【IOS】Firebase(Google、Facebook、Apple、Guest)登录,FCM,Apple In-App,Kakao

写在开头

记录自己接入SDK的过程。请各位指正。

最好提前做的工作

工欲善其事,必先利其器。

1.Mac电脑因Xcode而内存越来越大

弄到一半突然提示我内存不足,而且xcode还越来越卡。也是醉了。

2.Xcode卡顿解决方案

一.xcode项目处理

1.Cocoapods

Mac 下 安装Cocoapods
CocoaPods 换源 git 安装 与 使用
mac 安装homebrew 报错 curl: (7) Failed to connect to raw.githubusercontent.com port 443: Connection refused

1).ruby

Mac OS 新系统安装Cocoapods[不要直接使用系统默认ruby]
IOS 解决安装POD报You don’t have write permissions for the /usr/bin directory的错误
在这里插入图片描述
是的,我们得用自己的ruby,mac自带的不让写入了,连升级都不行,干!!!

a.如何让shell里优先用自己下的ruby

https://www.shuzhiduo.com/A/qVdeEK1gdP/

我使用了方法二
在这里插入图片描述

b.更换ruby源
更换 Ruby 源
// 查看现有的源
gem source -l  

// 移除
gem sources --remove  https://rubygems.org/

// 添加 ruby-china 的源
gem sources -a https://gems.ruby-china.com/

这篇里的ruby国内镜像,请网上查找ruby国内官方镜像网站gems.ruby-china.com

2).repo

Cocoapods 加速指南

3).加快github下载速度

pod install / pod update 速度慢的终极解决方案

解决git下载出现:Failed to connect to 127.0.0.1 port 1080: Connection refused拒绝连接错误
Git - SSL_ERROR_SYSCALL 问题解决
https://www.noxxxx.com/mac-下终端走-ss-代理.html

最后,最简易且我成功的方法是下面这篇
Mac OS 常用 Terminal Proxy 方法

打开关闭proxy

1.打开 .zshrc

// 也可以在 ~/.bashrc 编辑
$ vi ~/.zshrc 

2.增加 proxy | unproxy

alias proxy=""
alias unproxy=""

3.使用 .zshrc

// 让 proxy 和 unproxy 生效
$ source ~/.zshrc 

这样我们就能在命令行里,使用 proxy 和 unproxy 打开和关闭 proxy了。

通过查询系统环境有没有使用proxy(成功)
//可以显示当前是否有使用proxy
env|grep -I proxy
git 打开关闭proxy
// 打开 http https proxy.小飞机一般是 1086 端口
git config --global http.https://github.com.proxy socks5://127.0.0.1:1086

// 关闭 http https proxy
git config --global --unset http.https://github.com.proxy
4).中级方案直接下载替换

软件】CocoaPods “pod install” 慢

2.iOS 一个项目添加多个TARGET

3.我的新增项目流程

这次打包真是要了我的命了,一直找问题,一直改,耗费时间超过了1个月。而且Xcode变得极其卡顿,动不动就卡。真是服了。

说几个要点:

  • 跟着官方文档走
    图方便,网上找了一些写好的博客文章来copy,后面和官方文档一对比,导入的pod库是不一样的。一定要按官方文档的走,其余的博客文章之类都是辅助。
  • 版本一定要对比好
    对版本这个东西实在不敏感,而且有点抵触。本来就很多恶心的东西了,还要给你一个个对比版本?
    但是,版本不对会对你造成很多不明所以的问题,这一步一定不能省,而且也真花不了多少时间。
    注意文档里的提供版本信息!
  • 从头再来
    我写好了一大堆代码,觉得很nice,一泻千里。运行的时候直接报错。在这里耗费了很多时间。
    (不是说这些耗费的时间是完全无用的,也了解到了哪些该弄,哪些在哪个步骤弄,记得总结)
    最后我重新新建了一个项目,一步一步的走,走一步运行一下,最后成功了。

1).清理pod

cocoapods 删除已导入项目的第三方库和移除项目中的cocoapods

  1. .xcworkspace,Podfile.lock,Pods文件夹删掉。Podfile里的配置记得备份一下,删掉。

  2. 在target的 General -> Frameworks,Libraries and embeded content-> Pods_xxx.framework 删掉。

  3. xcode 左侧,整个项目结构下,Pods-> Pos_xxx_debug.xccconfig 和 Pos_xxx_release.xccconfig 删除

  4.  //会把和pod相关的删掉,虽然上面几步基本以及删光了	
     pod deintegrate
     
     //下面命令行按需要使用
     //如果想要重新下载所有的pod库,那就使用它吧。
     //pod cache clean --all
     		
     //它会把所有下好的pod库缓存都删掉,再次install就得重新下载。
     //deintegrate 不会删除缓存里下好的库。
    
  5.  //重新install
     pod install --no-repo-update
    
  6. 打开 .xworkspace
    注意,install之后第一次一定打开的是 .xworkspace ,而不是 .xcodeproj
    具体为什么,我暂时也不清楚

2).search path

在这里插入图片描述
以上部分网图。

在 install 完成之后,有很多警告。大致内容就是pod的xxconfig文件里的路径,会覆盖这些路径的内容,让我们最好继承 ${inherited} 一下

以下都在 build setting里,请自行检查

  1. Header Search Paths
    在这里插入图片描述

  2. Library Search Paths
    是的,这里在console里面并没有提示让我们增加,具体为啥我也忘了。但是记得也添加一下。
    在这里插入图片描述

  3. Other Linker Flags

    这个 -ObjC 标志。文档里要求加才加。
    在这里插入图片描述

3).cocos2dx Other Linker Flags 设置成 -ObjC 真机编译报错

是的,如果你不是Cocos2dx的项目,这一步可以跳过了。

解决方法就是只需要增加这两项即可:

  • MediaPlayer.framework
  • GameController.framework
    在这里插入图片描述

4).以上这些走完之后,我的项目就可以生成了。我用的是xworkspace生成的项目。

5.OC语法

1).OC怎么正确的写单例

写单例写的报错,也是醉了。查了一下, 原来是这么写的= =。。。

6.杂项

xcode左侧不显示工程文件目录,提示NO Filter Results

XCode更换Info.plist位置

xcode 新特性的 一点理解 enable module 和 link frameworks automatically

解决ios - use of @import when modules are disabled问题
虽然最后还是没解决,把 @import 改成了 #import

ios pod install成功后 文件里#import不提示 解决方法

二.Firebase登录 + FCM

官方文档
Firebase IOS 文档
将 Firebase 添加到您的 Apple 项目中

Google IOS 文档
iOS 版 Facebook 登录 — 快速入门

登录我都写在一起了。以下是所有代码集合

//
//  AppFirebase.h
//
//  Created by Mac on 2022/1/18.
//

#import <Foundation/Foundation.h>
#import <CommonCrypto/CommonHMAC.h>
#import <AuthenticationServices/AuthenticationServices.h>
#import <UIKit/UIKit.h>

#import <Firebase.h>
#import <FirebaseCore.h>
#import <FirebaseMessaging.h>
#import <GoogleSignIn.h>
#import <FBSDKCoreKit/FBSDKCoreKit.h>
#import <FBSDKLoginKit/FBSDKLoginKit.h>

@interface AppFirbase : NSObject <
    FBSDKLoginButtonDelegate,
    ASAuthorizationControllerDelegate,
    ASAuthorizationControllerDelegate,
    ASAuthorizationControllerPresentationContextProviding,
    FIRMessagingDelegate,
    UNUserNotificationCenterDelegate> {

    @public
    GIDConfiguration* signInConfig;
    bool canReceivePush;
    
    @private
    NSString* _way;
    NSString* _appleNonce;
    NSString* _fbNonce;
    bool _isAppleLoginOK;
    // btn
    GIDSignInButton* _googleBtn;
    ASAuthorizationAppleIDButton* _appleBtn;
    UIButton* _fbBtn;
}

+(instancetype) getInstance;
-(void) dealloc;
-(AppFirbase*) initSelf;

-(bool) handleURL
    :(NSURL *)url
    :(nonnull NSDictionary<UIApplicationOpenURLOptionsKey, id> *)options;
-(void) onViewDidLoad;
-(void) didFinishLaunchingWithOptions;

// my process
-(void) onLogin:(NSString*) way;
-(IBAction)firebaseLoginWithGoogle:(id)sender;
-(void) firebaseLoginWithGuest;

-(void) firebaseGetAuthLogin:(NSString*)behavior;

// firebase apple login
- (NSString *) randomNonce:(NSInteger)length;
- (NSString *) stringBySha256HashingString:(NSString *)input;
// apple login
- (void)firebaseLoginWithAppleID API_AVAILABLE(ios(13.0));

-(void)authorizationController:(ASAuthorizationController *)controller
   didCompleteWithAuthorization:(ASAuthorization *)authorization API_AVAILABLE(ios(13.0));

- (void)authorizationController:(ASAuthorizationController *)controller
           didCompleteWithError:(NSError *)error API_AVAILABLE(ios(13.0));

-(ASPresentationAnchor) presentationAnchorForAuthorizationController:(ASAuthorizationController *)controller API_AVAILABLE(ios(13.0));
-(void) handleSignInWithAppleStateChanged:(NSNotification *)notification;

-(void) onLogout:(BOOL) doCB;
-(void) onDeleteAccount;

// --------------- fcm ----------------
- (void)userNotificationCenter:(UNUserNotificationCenter *)center
       willPresentNotification:(UNNotification *)notification
         withCompletionHandler:(void (^)(UNNotificationPresentationOptions))completionHandler;
- (void)userNotificationCenter:(UNUserNotificationCenter *)center
didReceiveNotificationResponse:(UNNotificationResponse *)response
         withCompletionHandler:(void(^)(void))completionHandler;
- (void)messaging:(FIRMessaging *)messaging didReceiveRegistrationToken:(NSString *)fcmToken;

- (void) didFailToRegisterForRemoteNotificationsWithError:(NSError *)error;
- (void) didRegisterForRemoteNotificationsWithDeviceToken:(NSData *)deviceToken;
- (void) didReceiveRemoteNotification:(NSDictionary *)userInfo;
- (void) didReceiveRemoteNotificationFetchCompletionHandler:(NSDictionary *)userInfo;

-(void) checkFCMEnabled;

@end

AppFirebase.mm

//
//  AppFirebase.mm
//
//  Created by Mac on 2022/1/18.
//

#import <Foundation/Foundation.h>
#import "AppFirebase.h"

// Implement UNUserNotificationCenterDelegate to receive display notification via APNS for devices
// running iOS 10 and above.
@interface AppFirbase () <UNUserNotificationCenterDelegate>
@end

@implementation AppFirbase

// ------------------------------------------------------------------
// 单例
+ (instancetype)getInstance {
    static AppFirbase* _instance = nil;
    
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        NSLog(@"dispatch_once");
        _instance = [[super allocWithZone:NULL] init];
        
        // 下面代码进行当前类的初始化操作
        [_instance initSelf];
    });
    
    NSLog(@"shared called");
    
    return _instance;
}

// 不能使用下面这个类进行初始化,否则会陷入死循环,或会使用得外部调用[[xx alloc] init] 会再次触发这个方法
//- (instancetype)init {
//}

+ (instancetype)allocWithZone:(struct _NSZone *)zone {
    NSLog(@"allocWithZone");
    return [AppFirbase getInstance];
}

- (instancetype)copyWithZone:(NSZone *)zone {
    NSLog(@"copyWithZone");
    return self;
}

- (instancetype)mutableCopyWithZone:(NSZone *)zone {
    NSLog(@"mutableCopyWithZone");
    return self;
}
// ------------------------------------------------------------------
- (void)dealloc{
    //[_person release];
    NSLog(@"AppFirbase dealloc");
    if (@available(iOS 13.0, *)) {
        [[NSNotificationCenter defaultCenter] removeObserver:self name:ASAuthorizationAppleIDProviderCredentialRevokedNotification object:nil];
    }
    
    [[NSNotificationCenter defaultCenter] removeObserver:self name:@"FCMToken" object:nil];
    
    [super dealloc];
}

- (void)onViewDidLoad{
    // --------------------- google ---------------------
    // 封装的 Google 登录按钮
    _googleBtn = [GIDSignInButton new];
    _googleBtn.hidden = YES;
    [_googleBtn addTarget:self action:@selector(firebaseLoginWithGoogle:) forControlEvents:UIControlEventTouchUpInside];
    [Window addSubview:_googleBtn];
    
    // --------------------- facebook ---------------------	
    _fbBtn = [[UIButton alloc]init];
    [_fbBtn addTarget:self action:@selector(firebaseLoginWithFacebook) forControlEvents:UIControlEventTouchUpInside];
    _fbBtn.hidden = YES;
    
    [Window addSubview:_fbBtn];

    // --------------------- apple ---------------------
    // 手机系统版本 不支持 时 隐藏苹果登录按钮
    if (@available(iOS 13.0, *)) {
        _isAppleLoginOK = true;
        
        _appleBtn = [ASAuthorizationAppleIDButton buttonWithType:ASAuthorizationAppleIDButtonTypeContinue style:ASAuthorizationAppleIDButtonStyleBlack];
        _appleBtn.hidden = YES;
        
        [_appleBtn addTarget:self
                   action:@selector(firebaseLoginWithAppleID)
         forControlEvents:UIControlEventTouchUpInside];
        [Window addSubview:_appleBtn];
        
        [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(handleSignInWithAppleStateChanged:) name:ASAuthorizationAppleIDProviderCredentialRevokedNotification object:nil];
    } else {
        _isAppleLoginOK = false;
    }
}

- (void)didFinishLaunchingWithOptions{
    // --------------------- firebase ---------------------
    // Use Firebase library to configure APIs
    [FIRApp configure];
    
    // --------------------- google ---------------------
    // 初始化
    signInConfig = [[GIDConfiguration alloc] initWithClientID:@"你的GoogleService-Info.json里面找"];
    
    // --------------------- facebook ---------------------
    [[FBSDKApplicationDelegate sharedInstance]
        UIApplication
        didFinishLaunchingWithOptions:launchOptions];
    
    // 注册 FacebookAppID
    [FBSDKSettings setAppID:@"找运营商要或者自己有注册"];
    // Set AdvertiserTrackingEnabled to YES if a device provides consent
    [FBSDKSettings setAdvertiserTrackingEnabled:YES];
 
    // --------------------- fcm ---------------------
    [FIRMessaging messaging].delegate = self;
}

// 初始化
-(AppFirbase*) initSelf{
    canReceivePush = NO;
    return self;
}

// handleURL
-(bool) handleURL
    :(NSURL *)url
    :(nonnull NSDictionary<UIApplicationOpenURLOptionsKey, id> *)options{
    
    NSLog(@"handleURL %@", _way);
    if(nil == _way) return NO;
    
    bool handled = NO;
    // google
    if([_way  isEqual: @"google"]){
        handled = [GIDSignIn.sharedInstance handleURL:url];
    // facebook
    }else if([_way  isEqual: @"facebook"]){
        handled = [[FBSDKApplicationDelegate sharedInstance]
            UIApplication
            openURL:url
            options:options];
    }
    
    if (handled) return YES;
    
    return NO;
}

// 登录入口
-(void) onLogin:(NSString*)way{
    NSLog(@"登录 [way]%@", way);
    
    _way = way;
    
    // 自动登录
    if ([FIRAuth auth].currentUser) {
        NSLog(@"auto login");
        [self firebaseGetAuthLogin:@"auto login"];
        return;
    }
    
    // google
    if([_way  isEqual: @"google"]){
        [_googleBtn sendActionsForControlEvents:UIControlEventTouchUpInside];
    // facebook
    }else if([_way  isEqual: @"facebook"]){
        [_fbBtn sendActionsForControlEvents:UIControlEventTouchUpInside];
    // apple
    }else if([_way  isEqual: @"ios"]){
        //这个版本不能登录
        if(!_isAppleLoginOK) return;

        [_appleBtn sendActionsForControlEvents:UIControlEventTouchUpInside];
    // guest
    }else if([_way  isEqual: @"guest"]){
        [self firebaseLoginWithGuest];
    }
}

/// //
///                                  Firebase
/// /
// firebase 要的
// Adapted from https://auth0.com/docs/api-auth/tutorials/nonce#generate-a-cryptographically-random-nonce
- (NSString *)randomNonce:(NSInteger)length {
    NSAssert(length > 0, @"Expected nonce to have positive length");
    NSString *characterSet = @"0123456789ABCDEFGHIJKLMNOPQRSTUVXYZabcdefghijklmnopqrstuvwxyz-._";
    NSMutableString *result = [NSMutableString string];
    NSInteger remainingLength = length;

    while (remainingLength > 0) {
        NSMutableArray *randoms = [NSMutableArray arrayWithCapacity:16];
        for (NSInteger i = 0; i < 16; i++) {
          uint8_t random = 0;
          int errorCode = SecRandomCopyBytes(kSecRandomDefault, 1, &random);
          NSAssert(errorCode == errSecSuccess, @"Unable to generate nonce: OSStatus %i", errorCode);

          [randoms addObject:@(random)];
        }

        for (NSNumber *random in randoms) {
          if (remainingLength == 0) {
            break;
          }

          if (random.unsignedIntValue < characterSet.length) {
            unichar character = [characterSet characterAtIndex:random.unsignedIntValue];
            [result appendFormat:@"%C", character];
            remainingLength--;
          }
        }
    }

    return [result copy];
}

- (NSString *)stringBySha256HashingString:(NSString *)input {
    const char *string = [input UTF8String];
    unsigned char result[CC_SHA256_DIGEST_LENGTH];
    CC_SHA256(string, (CC_LONG)strlen(string), result);

    NSMutableString *hashed = [NSMutableString stringWithCapacity:CC_SHA256_DIGEST_LENGTH * 2];
    for (NSInteger i = 0; i < CC_SHA256_DIGEST_LENGTH; i++) {
    [hashed appendFormat:@"%02x", result[i]];
    }
    return hashed;
}

// 获取token给服务端,在登录之后调用
-(void) firebaseGetAuthLogin:(NSString*)behavior{
    NSLog(@"[firebaseGetAuthLogin] %@", behavior);
    // 用于获取登录用户 Firebase token 信息交给服务端校验
    [[FIRAuth auth].currentUser getIDTokenWithCompletion:^(NSString * _Nullable token, NSError * _Nullable error) {
        if (error) {
            NSLog(@"获取当前token出现错误:%@", error);
            return;
        }
        // Send token to your backend via HTTPS
        NSLog(@"Firebase当前用户 token 信息:%@", token);
        
        // 发送获取到的数据给你的服务端
    }];
}

/// //
///                                  Google
/// /
// 谷歌登录入口
- (IBAction)firebaseLoginWithGoogle:(id)sender {
    NSLog(@"[firebaseLoginWithGoogle] 谷歌登录 ");
    [GIDSignIn.sharedInstance signInWithConfiguration:signInConfig
                             presentingViewController:UIController
                                             callback:^(GIDGoogleUser * _Nullable user,
                                                        NSError * _Nullable error) {
        if (error) {
            //登录失败
            NSLog(@"谷歌登录失败 %@", error.debugDescription);
            return;
        }

        GIDAuthentication *authentication = user.authentication;
        FIRAuthCredential *credential =
        [FIRGoogleAuthProvider credentialWithIDToken:authentication.idToken
                                       accessToken:authentication.accessToken];
        
        [[FIRAuth auth] signInWithCredential:credential completion:^(FIRAuthDataResult * _Nullable authResult, NSError * _Nullable error) {
            if (error) {
                NSLog(@"谷歌->firebase 登录失败 %@", error.debugDescription);
           
            }
            if (!authResult) {
                NSLog(@"谷歌->firebase 登录失败 %@", error.debugDescription);

                return;
            }
            NSLog(@"谷歌 -> firebase successs uid:%@", authResult.user.uid);

            [self firebaseGetAuthLogin:@"login"];
        }];
    }];
}

// 自动登录
-(bool) FirebaseGoogleAutoLogin{
    [GIDSignIn.sharedInstance restorePreviousSignInWithCallback:^(GIDGoogleUser * _Nullable user,
                                                                  NSError * _Nullable error) {
        if (error) {
            //登录失败
            return;
        }

        GIDAuthentication *authentication = user.authentication;
        FIRAuthCredential *credential =
        [FIRGoogleAuthProvider credentialWithIDToken:authentication.idToken
                                       accessToken:authentication.accessToken];
        
        [[FIRAuth auth] signInWithCredential:credential completion:^(FIRAuthDataResult * _Nullable authResult, NSError * _Nullable error) {
            if (error) {
                NSLog(@"错误信息:%@", error.debugDescription);
            }
            if (!authResult) {
                NSLog(@"授权结果为空");
                return;
            }
            NSLog(@"Firebase uid:%@", authResult.user.uid);
            //todo
            [self firebaseGetAuthLogin:@"login"];
        }];
    }];
    return YES;
}

/// //
///                                  fabcebook
/// /
- (void) firebaseLoginWithFacebook{
    FBSDKLoginManager *loginManager = [[FBSDKLoginManager alloc] init];
    [loginManager logInWithPermissions:@[@"public_profile"] fromViewController:UIController handler:^(FBSDKLoginManagerLoginResult * _Nullable result, NSError * _Nullable error) {
        
        if (error) {
            NSLog(@"fb login failed %@", error.localizedDescription);
            return;
        }

        if (result && result.isCancelled) {
            return;
        }

        FIRAuthCredential *credential =
          [FIRFacebookAuthProvider credentialWithAccessToken:result.token.tokenString];

        [[FIRAuth auth] signInWithCredential:credential
                                completion:^(FIRAuthDataResult * _Nullable authResult,
                                             NSError * _Nullable error) {

          if (error) {
              NSLog(@"fb->firebase 登录失败 %@", error.debugDescription);
              return;
          }
          if (!authResult) {
              NSLog(@"fb->firebase 登录失败 %@", error.debugDescription);
              return;
          }

          NSLog(@"fb -> firebase successs uid:%@", authResult.user.uid);
          [self firebaseGetAuthLogin:@"login"];
        }];
          
    }];
}

// 当点击 Facebook Log out 按钮的时候会调用这个proxy方法
- (void)loginButtonDidLogOut:(FBSDKLoginButton *)loginButton {
    NSLog(@"fb 退出登录");
}

/// //
///                                  Apple
/// /

// 这里需要登录按钮来接收请求
#pragma mark- 授权苹果ID
- (void)firebaseLoginWithAppleID API_AVAILABLE(ios(13.0)) {
    NSString *nonce = [self randomNonce:32];
    self->_appleNonce = nonce;
    ASAuthorizationAppleIDProvider *appleIDProvider = [[ASAuthorizationAppleIDProvider alloc] init];
    ASAuthorizationAppleIDRequest *request = [appleIDProvider createRequest];
    request.requestedScopes = @[ASAuthorizationScopeFullName, ASAuthorizationScopeEmail];
    request.nonce = [self stringBySha256HashingString:nonce];

    ASAuthorizationController *authorizationController =
      [[ASAuthorizationController alloc] initWithAuthorizationRequests:@[request]];
    authorizationController.delegate = self;
    authorizationController.presentationContextProvider = self;
    [authorizationController performRequests];
}

// apple授权成功
#pragma mark- ASAuthorizationControllerDelegate
- (void)authorizationController:(ASAuthorizationController *)controller
   didCompleteWithAuthorization:(ASAuthorization *)authorization API_AVAILABLE(ios(13.0)) {
    NSLog(@"Apple 登录成功");
    if ([authorization.credential isKindOfClass:[ASAuthorizationAppleIDCredential class]]) {
        
        ASAuthorizationAppleIDCredential *appleIDCredential = authorization.credential;
        NSString *rawNonce = self->_appleNonce;
        NSAssert(rawNonce != nil, @"Invalid state: A login callback was received, but no login request was sent.");

        if (appleIDCredential.identityToken == nil) {
            NSLog(@"Unable to fetch identity token.");
            return;
        }

        NSString *idToken = [[NSString alloc] initWithData:appleIDCredential.identityToken
                                                  encoding:NSUTF8StringEncoding];
        if (idToken == nil) {
            NSLog(@"Unable to serialize id token from data: %@", appleIDCredential.identityToken);
            return;
        }

        // Initialize a Firebase credential.
        FIROAuthCredential *credential = [FIROAuthProvider credentialWithProviderID:@"apple.com"
                                                                            IDToken:idToken
                                                                           rawNonce:rawNonce];

        // Sign in with Firebase.
        [[FIRAuth auth] signInWithCredential:credential
                                  completion:^(FIRAuthDataResult * _Nullable authResult,
                                               NSError * _Nullable error) {
            if (error != nil) {
            // Error. If error.code == FIRAuthErrorCodeMissingOrInvalidNonce,
            // make sure you're sending the SHA256-hashed nonce as a hex string
            // with your request to Apple.
              return;
            }
            
            NSLog(@"apple -> firebase successs uid:%@", authResult.user.uid);
            [self firebaseGetAuthLogin:@"login"];
        }];
    }
}

// apple 授权失败
- (void)authorizationController:(ASAuthorizationController *)controller
           didCompleteWithError:(NSError *)error API_AVAILABLE(ios(13.0)) {

    NSLog(@"Sign in with Apple errored: %@", error);

    NSString *errorMsg = nil;
    switch (error.code) {
        case ASAuthorizationErrorCanceled:
            errorMsg = @"用户取消了授权请求";
            break;
        case ASAuthorizationErrorFailed:
            errorMsg = @"授权请求失败";
            break;
        case ASAuthorizationErrorInvalidResponse:
            errorMsg = @"授权请求响应无效";
            break;
        case ASAuthorizationErrorNotHandled:
            errorMsg = @"未能处理授权请求";
            break;
        case ASAuthorizationErrorUnknown:
            errorMsg = @"授权请求失败未知原因";
            break;
    }
    NSLog(@"%@", errorMsg);
}

#pragma mark- ASAuthorizationControllerPresentationContextProviding
- (ASPresentationAnchor)presentationAnchorForAuthorizationController:(ASAuthorizationController *)controller  API_AVAILABLE(ios(13.0)){
    return 你的Window;
}
#pragma mark- apple授权状态 更改通知
- (void)handleSignInWithAppleStateChanged:(NSNotification *)notification{
    NSLog(@"Apple 授权变了! %@", notification.userInfo);
}

/// //
///                                  游客登录
/// /
-(void) firebaseLoginWithGuest{
    NSLog(@"游客登录");
    [[FIRAuth auth] signInAnonymouslyWithCompletion:^(FIRAuthDataResult * _Nullable authResult,
                                                      NSError * _Nullable error) {
        if (error) {
            NSLog(@"游客登录失败:%@", error.debugDescription);
            return;
        }
        if (!authResult) {
            NSLog(@"游客登录失败 %@", error.debugDescription);
            return;
        }
        
        NSLog(@"游客登录成功 uid:%@", authResult.user.uid);
        [self firebaseGetAuthLogin:@"login"];
     }];
}

/// //
///                                   其它
/// /

// 登出入口
-(void) onLogout:(BOOL) doCB{
    NSLog(@"firebase 登出");
    //facebook
    if ([FBSDKAccessToken currentAccessToken]) {
        NSLog(@"facebook logout");
        [[[FBSDKLoginManager alloc] init] logOut];
    }
    
    //google
    [GIDSignIn.sharedInstance signOut];
    
    //apple
    
    //firebase
    NSString* result = @"success";
    NSError *signOutError;
    BOOL status = [[FIRAuth auth] signOut:&signOutError];
    if (!status) {
        NSLog(@"Error signing out: %@", signOutError);
        result = @"failed";
        return;
    }
    
    if(doCB == NO) return;
}

// 删除账号
-(void) onDeleteAccount{
    FIRUser *user = [FIRAuth auth].currentUser;

    [user deleteWithCompletion:^(NSError *_Nullable error) {
        if (error) {
            NSLog(@"删除账户失败 %@", error.debugDescription);
            return;
        }
        [self onLogout:NO];
    }];
}

/// //
///                                  			 FCM
/// /

// 不知道这干啥用的
NSString *const kGCMMessageIDKey = @"gcm.message_id";

// [START ios_10_message_handling]
// Receive displayed notifications for iOS 10 devices.
// Handle incoming notification messages while app is in the foreground.
- (void)userNotificationCenter:(UNUserNotificationCenter *)center
       willPresentNotification:(UNNotification *)notification
         withCompletionHandler:(void (^)(UNNotificationPresentationOptions))completionHandler {
  NSDictionary *userInfo = notification.request.content.userInfo;

  // With swizzling disabled you must let Messaging know about the message, for Analytics
  // [[FIRMessaging messaging] appDidReceiveMessage:userInfo];

  // [START_EXCLUDE]
  // Print message ID.
  if (userInfo[kGCMMessageIDKey]) {
    NSLog(@"Message ID: %@", userInfo[kGCMMessageIDKey]);
  }
  // [END_EXCLUDE]

  // Print full message.
  NSLog(@"%@", userInfo);

  // Change this to your preferred presentation option
  completionHandler(UNNotificationPresentationOptionBadge | UNNotificationPresentationOptionAlert);
}

// Handle notification messages after display notification is tapped by the user.
- (void)userNotificationCenter:(UNUserNotificationCenter *)center
didReceiveNotificationResponse:(UNNotificationResponse *)response
         withCompletionHandler:(void(^)(void))completionHandler {
  NSDictionary *userInfo = response.notification.request.content.userInfo;
  if (userInfo[kGCMMessageIDKey]) {
    NSLog(@"Message ID: %@", userInfo[kGCMMessageIDKey]);
  }

  // With swizzling disabled you must let Messaging know about the message, for Analytics
  // [[FIRMessaging messaging] appDidReceiveMessage:userInfo];

  // Print full message.
//  NSLog(@"%@", userInfo);

  completionHandler();
}

// [END ios_10_message_handling]

// [START refresh_token]
- (void)messaging:(FIRMessaging *)messaging didReceiveRegistrationToken:(NSString *)fcmToken {
    NSLog(@"FCM registration token: %@", fcmToken);
    if(NULL == fcmToken) return;
    
    // Notify about received token.
    NSDictionary *dataDict = [NSDictionary dictionaryWithObject:fcmToken forKey:@"token"];
    
    [[NSNotificationCenter defaultCenter] postNotificationName:
     @"FCMToken" object:nil userInfo:dataDict];
    // TODO: If necessary send token to application server.
    // Note: This callback is fired at each app startup and whenever a new token is generated.
}
// [END refresh_token]

- (void) didFailToRegisterForRemoteNotificationsWithError:(NSError *)error {
  NSLog(@"Unable to register for remote notifications: %@", error);
}

// This function is added here only for debugging purposes, and can be removed if swizzling is enabled.
// If swizzling is disabled then this function must be implemented so that the APNs device token can be paired to
// the FCM registration token.
- (void) didRegisterForRemoteNotificationsWithDeviceToken:(NSData *)deviceToken {
  NSLog(@"APNs device token retrieved: %@", deviceToken);

  // With swizzling disabled you must set the APNs device token here.
   [FIRMessaging messaging].APNSToken = deviceToken;
}

- (void) didReceiveRemoteNotification:(NSDictionary *)userInfo {
  // If you are receiving a notification message while your app is in the background,
  // this callback will not be fired till the user taps on the notification launching the application.
  // TODO: Handle data of notification

  // With swizzling disabled you must let Messaging know about the message, for Analytics
  // [[FIRMessaging messaging] appDidReceiveMessage:userInfo];

  // [START_EXCLUDE]
  // Print message ID.
  if (userInfo[kGCMMessageIDKey]) {
    NSLog(@"Message ID: %@", userInfo[kGCMMessageIDKey]);
  }
  // [END_EXCLUDE]

  // Print full message.
  NSLog(@"[didReceiveRemoteNotification]%@", userInfo);
}

// [START receive_message]
- (void) didReceiveRemoteNotificationFetchCompletionHandler:(NSDictionary *)userInfo {
  // If you are receiving a notification message while your app is in the background,
  // this callback will not be fired till the user taps on the notification launching the application.
  // TODO: Handle data of notification

  // With swizzling disabled you must let Messaging know about the message, for Analytics
  // [[FIRMessaging messaging] appDidReceiveMessage:userInfo];

  // [START_EXCLUDE]
  // Print message ID.
  if (userInfo[kGCMMessageIDKey]) {
    NSLog(@"Message ID: %@", userInfo[kGCMMessageIDKey]);
  }
  // [END_EXCLUDE]

  // Print full message.
    NSLog(@"[didReceiveRemoteNotificationFetchCompletionHandler]%@", userInfo);

//  completionHandler(UIBackgroundFetchResultNewData);
}
// [END receive_message]

// --------------------- 以下功能为自己新增的 ---------------------
// 开关 FCM
-(void) checkFCMEnabled{
    if(canReceivePush){
        [self openFCM];
        [FIRMessaging messaging].autoInitEnabled = YES;
    }else{
        [FIRMessaging messaging].autoInitEnabled = NO;
        [[FIRMessaging messaging] deleteTokenWithCompletion:^(NSError *_Nullable error) {
            if (error) {
                NSLog(@"删除token失败");
                return;
            }
            
            NSLog(@"删除token成功");
          }];
    }
}

-(void) openFCM{
    //注册远程通知。这显示了第一次运行时的权限对话框to
    //在更合适的时间显示对话框,相应移动此注册。
    if (@available(iOS 10.0, *)) {
        if ([UNUserNotificationCenter class] != nil) {
            // iOS 10 or later
            // For iOS 10 display notification (sent via APNS)
            [UNUserNotificationCenter currentNotificationCenter].delegate = self;
            UNAuthorizationOptions authOptions = UNAuthorizationOptionAlert |
            UNAuthorizationOptionSound | UNAuthorizationOptionBadge;
            [[UNUserNotificationCenter currentNotificationCenter]
            requestAuthorizationWithOptions:authOptions
            completionHandler:^(BOOL granted, NSError * _Nullable error) {
              // ...
            }];
         } else {
            // iOS 10 notifications aren't available; fall back to iOS 8-9 notifications.
            UIUserNotificationType allNotificationTypes =
            (UIUserNotificationTypeSound | UIUserNotificationTypeAlert | UIUserNotificationTypeBadge);
            UIUserNotificationSettings *settings =
            [UIUserNotificationSettings settingsForTypes:allNotificationTypes categories:nil];
            
         }
    } else {
     // Fallback on earlier versions
    }
}

@end

1.Google

iOS Google 登录
iOS 接入 Google、Facebook 登录(一)

2.Facebook

a.添加FB相关引用后运行报错

在增加了Facebook相关的sdk后,运行报错:Cannot create __weak reference in file using manual reference counting

Xcode的build setting
不能使用手动引用计数文件中__weak引用 -fobjc-weak is not supported on the current deployment target

在这里插入图片描述
这个问题,我们把上面这个选项改为 YES 就可以了。
似乎是因为版本修改的问题,因为Firebase目前要求版本为 ios10.0 以上,而我原来的项目的是 ios9.0

b.facebook限制登录?

在这里插入图片描述
官方文档提供了两种登录方式,一种是叫限制登录?这个不太明白,希望大牛指教。
我没有接这个,只是把正常方式的接入了。

4.Guest

5.其他问题

a.代码触发按钮

  1. 项目中,使用的是内部设计的界面和按钮,并不是使用xcode来生成按钮。
  2. 所有(google,facebook,apple)都是建议使用他们自己的按钮,然后通过对应的点击函数来触发登录。
  3. 隐藏Xcode生成的按钮,点击我们的按钮,用xcode的代码触发按钮点击。

iOS开发-objective-c使用代码触发按钮的点击事件

b.IOS多个GoogleService.json

官方文档

多个项目,单个GoogleServic.json

这个简单,选中你的GoogleService.json。然后看xcode的最右边,有个 Target Membership,目标就只勾选对应的项目即可。
在这里插入图片描述

某个项目,多个GoogleServic.json

官方文档写的很清楚,其实就是再初始化的时候传个路径。
官方文档

c.isMFAEnabled?

firebase google signin authentication AppDelegate- Use of unresolved identifier ‘isMFAEnabled’
在这里插入图片描述
这个问题不是很明白,firebase的ios其他渠道登录的示例,都有这方面的代码,但是我都省略了。

三.FCM

FCM 代码我也放在上面了,这样做是不好的,但是我的Xcode卡成屎了,就懒的再搞了。

四.Apple In-App Pay

1.文档和参考blog

苹果登录集成 Sign in with Apple
iOS 第三方登录之苹果登录(sign in with Apple)
Unity 苹果IAP和微信支付接入

iOS IAP基本流程.md
这篇文章讲的很精简,可以作为流程来先看一下。

2018iOS内购详细教程&iOS内购我踩过的坑
IOS内购(IAP)的那些事

2.具体逻辑

它的流程和Google Play Pay没啥区别。

但是,最坑人的地方就在这里了,Apple的支付,它没有透传参数来存储我们的订单号!也许你会说不是有applicationUsername 可以用吗?

applicationUsername 它在某个ios的版本是一定为nil的,而且其他班的的ios里有概率取出为nil。为啥?因为开发者说了,这个本来也不是为了让我们存这些东西用的。狗基8苹果啊。

AppIAPHelper.h

//
//  AppIAPHelper.h
//
//  Created by Mac on 2022/2/22.
//

#import <Foundation/Foundation.h>
#import <StoreKit/StoreKit.h>

#import "KeychainTool.h"


@interface AppIAPHelper : NSObject <SKPaymentTransactionObserver, SKProductsRequestDelegate> {
    int buyType;
    
    @private
    NSString* productId;
    NSString* orderId;
    NSArray<SKPaymentTransaction *>* _transactions;
}

//不能局部创建,需要被持有,否则收不到苹果回调
@property (nonatomic, strong) SKProductsRequest *productsRequest;

+(instancetype) getInstance;

-(void)dealloc;

-(AppIAPHelper*_Nullable)initSelf;
-(void)onViewDidLoad;

// ------------------ app调用 ------------------
-(void) onPay:(NSString* _Nullable) productId :(NSString* _Nullable) orderId;
-(void) completeTransactionAfterChecking:(NSString*) transactionIdentifier;
-(void) checkOrder;
-(void) finishAllTransaction;

// ------------------ in-app purchase ------------------
-(void)paymentQueue:(nonnull SKPaymentQueue *)queue updatedTransactions:(nonnull NSArray<SKPaymentTransaction *> *)transactions;

// 通过 productId 获取 product
-(void) getProductInfoById:(NSString*) productID;
//1.接口成功:
-(void) productsRequest:(nonnull SKProductsRequest *)request didReceiveResponse:(nonnull SKProductsResponse *)response;
//2.接口失败
-(void) request:(SKRequest *)request didFailWithError:(NSError *)error;
//3.接口结束
-(void) requestDidFinish:(SKRequest *)request;

-(void) completeTransactionByIdentifier:(NSString*)transactionIdentifier;

-(void) deleteKeychain:(SKPaymentTransaction*) transaction:(BOOL) all;

@end

AppIAPHelper.mm

//
//  AppIAPHelper.m
//  LuaGame-mobile-monawa
//
//  Created by Mac on 2022/2/22.
//

#import "AppIAPHelper.h"

# define KEYCHAIN_MONAWA @"xxx"

@implementation AppIAPHelper

// ------------------------------------------------------------------
// 单例
+ (instancetype)getInstance {
    static AppIAPHelper* _instance = nil;
    
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        NSLog(@"dispatch_once");
        _instance = [[super allocWithZone:NULL] init];
        
        // 下面代码进行当前类的初始化操作
        [_instance initSelf];
    });
    
    NSLog(@"shared called");
    
    return _instance;
}

// 不能使用下面这个类进行初始化,否则会陷入死循环,或会使用得外部调用[[xx alloc] init] 会再次触发这个方法
//- (instancetype)init {
//}

+ (instancetype)allocWithZone:(struct _NSZone *)zone {
    NSLog(@"allocWithZone");
    return [AppIAPHelper getInstance];
}

- (instancetype)copyWithZone:(NSZone *)zone {
    NSLog(@"copyWithZone");
    return self;
}

- (instancetype)mutableCopyWithZone:(NSZone *)zone {
    NSLog(@"mutableCopyWithZone");
    return self;
}
// ------------------------------------------------------------------

- (void)dealloc{
    //[_person release];
    NSLog(@"AppIAPHelper dealloc");
    [[SKPaymentQueue defaultQueue] removeTransactionObserver:self];
    [super dealloc];
}

- (AppIAPHelper*)initSelf{
    return self;
}

- (void)onViewDidLoad{
    [[SKPaymentQueue defaultQueue] addTransactionObserver:self];
}

// --------------------------------------------------------
// 支付入口
- (void) onPay:(NSString*) productId:(NSString*) orderId{
    if([SKPaymentQueue canMakePayments] == NO){
        NSLog(@"不允许使用内购功能");
        [self performSelectorOnMainThread:@selector(onPayFailed:) withObject:nil waitUntilDone:NO];;
        return;
    };
    
    if(NULL == productId || NULL == orderId){
        NSLog(@"没有productId 或 orderId");
        [self performSelectorOnMainThread:@selector(onPayFailed:) withObject:nil waitUntilDone:NO];
        return;
    }
    
    NSLog(@"productId %@ orderId %@", productId, orderId);
    self->productId = productId;
    self->orderId = orderId;
    [self getProductInfoById:productId];
}

//服务端确认后结束交易
-(void) completeTransactionAfterChecking:(NSString*) transactionIdentifier{
    [self completeTransactionByIdentifier:transactionIdentifier];
    return;
}

//补单
-(void) checkOrder{
    NSLog(@"开始补单");
    NSDictionary* dictionary = [NSDictionary dictionaryWithObject:@"success" forKey:@"state"];
    NSMutableDictionary* mDictionary = [NSMutableDictionary dictionary];
    
    //获取交易凭证
    NSString *reciptString = [self getReceiptData:nil];
    
    int len = 0;
    NSArray<SKPaymentTransaction *> * transactions = [[SKPaymentQueue defaultQueue] transactions];
    NSLog(@"订单数量 %d", [transactions count]);
    
    NSString* orderId;
    for (SKPaymentTransaction* transaction in transactions){
        NSLog(@"单子 %@ ", transaction.transactionIdentifier);
        if(transaction.transactionState == SKPaymentTransactionStatePurchased){
            len++;
            orderId = [self getOrderId:transaction];
            if(nil == orderId || orderId.length <= 0){
                NSLog(@"丢单, id:@%", transaction.transactionIdentifier);
                continue;
            }
            [mDictionary setValue:orderId forKey:[NSString stringWithFormat:@"orderId%i", len] ];
            [mDictionary setValue:transaction.transactionIdentifier forKey:[NSString stringWithFormat:@"sign%i", len] ];
            [mDictionary setValue:reciptString forKey:[NSString stringWithFormat:@"original%i", len] ];
        }
    }

    
    if(len > 0){
        [mDictionary setValue:[NSString stringWithFormat:@"%i", len] forKey:@"count"];
    }else{
        NSLog(@"没有单子需要补");
    }
    
}

//------------------------ in-app purchase ----------------------
// 通过 productId 获取 product
- (void)getProductInfoById:(NSString*)productID{
    NSLog(@"--productId: %@", productID);
    
    //========使用,开始请求========
    if(_productsRequest){
       [_productsRequest cancel];
       _productsRequest.delegate = nil;
       _productsRequest = nil;
    }
    
    NSArray *product = [ [NSArray alloc] initWithObjects:productID, nil];
    NSSet *productIdentifiers = [NSSet setWithArray:product];
    
     //productIdentifiers是一个NSSet对象,是无序的
     _productsRequest = [[SKProductsRequest alloc] initWithProductIdentifiers:productIdentifiers];
    _productsRequest.delegate = self;
    [_productsRequest start];
}

/*
* 注意:这些回调和自己的网络设计不一样.请求和结束可能不在同一个线程.
* 比如:请求是主线程,下面这些回调可能是在主线程、也可能是在子线程.
*/
 
//这个函数是getProductInfoById查询订单的回调函数
//1.接口成功:
    //一般查询不到商品信息有三种情况:
    //1.没有在iTunes中没有配置对应商品
    //2.没有使用Appstore Developer证书
    //3.使用了越狱的机器
- (void)productsRequest:(SKProductsRequest *)request didReceiveResponse:(SKProductsResponse *)response{
    
    NSArray *myProduct = response.products;
    if (myProduct.count == 0){
//        UnitySendMessage(self.mCallBackObjectName.UTF8String, "PreBuyProductFailed", "ProductNotExist");
        [self performSelectorOnMainThread:@selector(onPayFailed:) withObject:nil waitUntilDone:NO];
        return;
    }
    
    SKProduct* product = nil;
    for(SKProduct * pro in myProduct){
        if ([pro.productIdentifier isEqualToString:self->productId]) {
            product = pro;
            break;
        }
    }

    if (product) {
        NSLog(@"开始购买");
        //如果iOS已经登录了[iTunes Store与App Store]账号,则这一步会失败
        SKMutablePayment *payment = [SKMutablePayment paymentWithProduct:product];
        payment.applicationUsername = self->orderId;

        BOOL saveResult = [KeychainTool.getInstance keychainSaveData:self->orderId
                                               withAccountIdentifier:self->productId
                                                andServiceIdentifier:KEYCHAIN_MONAWA];
        
        if(saveResult){
            [[SKPaymentQueue defaultQueue] addPayment:payment];
        }else{
            NSLog(@"keychain 保存未成功");
            [self performSelectorOnMainThread:@selector(onPayFailed:) withObject:nil waitUntilDone:NO];
        }
    }else{
        NSLog(@"没有此商品");
        [self performSelectorOnMainThread:@selector(onPayFailed:) withObject:nil waitUntilDone:NO];
    }
}

//2.接口失败
- (void)request:(SKRequest *)request didFailWithError:(NSError *)error{
    NSLog(@"[2.请求商品失败] @%", error.debugDescription);
    [self performSelectorOnMainThread:@selector(onPayFailed:) withObject:nil waitUntilDone:NO];
}
//3.接口结束
- (void)requestDidFinish:(SKRequest *)request{
    NSLog(@"[3.接口结束] @%", request);
//    [self performSelectorOnMainThread:@selector(onPayFailed:) withObject:nil waitUntilDone:NO];
}

// 购买商品后的callback
// 补单也是走这里
- (void)paymentQueue:(nonnull SKPaymentQueue *)queue updatedTransactions:(nonnull NSArray<SKPaymentTransaction *> *)transactions {

    _transactions = transactions;
    for (SKPaymentTransaction* transaction in transactions) {
//        [self completeTransaction:transaction];s
        switch (transaction.transactionState) {
            //loading处理
            case SKPaymentTransactionStatePurchasing:
                NSLog(@"购买中。。。");
                break;
            //交易在队列中,但是最终的状态需要确定.比如:家庭共享购买,购买需要主账号的同意
            case SKPaymentTransactionStateDeferred:
                NSLog(@"购买状态等待确认");
                break;
            //购买失败后的处理:比如用户取消
            case SKPaymentTransactionStateFailed:
                NSLog(@"购买失败");
                [self performSelectorOnMainThread:@selector(onPayFailed:) withObject:nil waitUntilDone:NO];
                [self deleteKeychain:transaction :YES];
                [self completeTransaction:transaction];
                break;
            //购买成功后的处理
            //这个函数是支付成功之后App Store服务器会回调的函数,里面包含了Receipt数据
            //服务器会把Receipt数据发往App Store服务器进行验证,再把验证结果返回给客户端,如果验证成功
            //这次支付才算完成
            case SKPaymentTransactionStatePurchased:
                [self buyProductSuccess:transaction];
                break;
            //恢复购买.
            //遇到的情况:购买成功了,由于某种原因苹果一直没回调,后来再次购买一个商品,苹果又把这个交易返回了
            case SKPaymentTransactionStateRestored:
                NSLog(@"恢复购买");
//                [self restoreTransaction:transaction];
                break;
            default:
                NSLog(@"购买其它问题");
                // For debugging
                NSLog(@"Unexpected transaction state %@", @(transaction.transactionState));
                [self deleteKeychain:transaction :YES];
                [self performSelectorOnMainThread:@selector(onPayFailed:) withObject:nil waitUntilDone:NO];
                break;
        }
    }
}

-(void) buyProductSuccess:(SKPaymentTransaction*) transaction{
    NSLog(@"购买成功 %@", transaction);

    //从 productId -> orderId 的关联中获取 orderId
    NSString* orderId = [KeychainTool.getInstance
               keychainGetDataWithAccountIdentifier:transaction.payment.productIdentifier
                               andServiceIdentifier:KEYCHAIN_MONAWA];

    //将orderId和transactionIdentifier关联
    BOOL saveResult = [KeychainTool.getInstance keychainSaveData:orderId
                                           withAccountIdentifier:transaction.transactionIdentifier
                                            andServiceIdentifier:KEYCHAIN_MONAWA];
    // 删除 productId -> orderId 的关联
    if(saveResult){
        [KeychainTool.getInstance
                    keychainDeleteWithAccountIdentifier:transaction.payment.productIdentifier
                                   andServiceIdentifier:KEYCHAIN_MONAWA];
    }else{
        NSLog(@"订单丢失! @%", transaction.transactionIdentifier);
        [self performSelectorOnMainThread:@selector(onPayFailed:) withObject:nil waitUntilDone:NO];
        return;
    }
    
    //获取透传字段
    orderId = [self getOrderId:transaction];
    
    // 没有orderId订单就是丢失的
    if(orderId == nil || orderId.length <= 0){
        NSLog(@"订单丢失! @%", transaction.transactionIdentifier);
        [self performSelectorOnMainThread:@selector(onPayFailed:) withObject:nil waitUntilDone:NO];
        return;
    }
    
    //transactionIdentifier:相当于Apple的订单号
    NSString *transationId = transaction.transactionIdentifier;
    //base64 交易凭据
    NSString *reciptString = [self getReceiptData:transaction];
    
    NSLog(@"orderNo = %@, 交易ID = %@, base64 = %@", orderId, transationId, reciptString);

    //传给后台做二次验证
}

-(void) onPayFailed:(NSArray*)data{
}

// 获取交易凭据
-(NSString*) getReceiptData:(SKPaymentTransaction*) transaction{
    NSData *reciptData;
    if (NSFoundationVersionNumber_iOS_7_0) {
        // iOS 7 style app receipts
        //从沙盒中获取交易凭证
        reciptData = [NSData dataWithContentsOfURL:[[NSBundle mainBundle] appStoreReceiptURL]];
    }else {
        // iOS 6 style transaction receipt
        reciptData = transaction.transactionReceipt;
    }

    NSLog(@"receipt 交易凭据\n %@", reciptData);
    
    //转化成Base64字符串(用于校验
    return [reciptData base64EncodedStringWithOptions:0];
}

//这个函数是服务器验证成功之后必须调用的函数,
//作用就是将购买完成的订单从peymentQueue中移除,否则这个订单会在你的机器上一直
//保留,算作一个未完成的订单。就算你的App删除重装也没有用。
//transactionIdentifier是每个订单的唯一表示ID
- (void) completeTransactionByIdentifier:(NSString*)transactionIdentifier{
    NSLog(@"后台验证订单完成 %@", transactionIdentifier);
    NSArray<SKPaymentTransaction *> * transactions = [[SKPaymentQueue defaultQueue] transactions];
    
    for (SKPaymentTransaction *transaction in transactions){
        NSLog(@"apple 订单比较 %@ ---  %@", transaction.transactionIdentifier, transactionIdentifier);
        
        BOOL result = [transaction.transactionIdentifier compare:transactionIdentifier];
        
        if (NULL != transaction && !result){
            NSLog(@"找到了,消耗未finish交易");
            [self completeTransaction:transaction];
            return;
        }
    }
}

- (void) completeTransaction:(SKPaymentTransaction*)transaction {
    if(NULL == transaction) return;
    
    NSLog(@"completeTransaction: %@", transaction.transactionIdentifier);

    [self deleteKeychain:transaction :NO];
    
    // Remove the transaction from the payment queue.
    [[SKPaymentQueue defaultQueue] finishTransaction: transaction];
    
    // Your application should implement these two methods.
    NSString * productIdentifier = transaction.payment.productIdentifier;
    
    if([productIdentifier length] > 0){
        NSLog(@"productIdentifier : %@", productIdentifier);
    }
}

// 获取订单数据
-(NSString*) getOrderId:(SKPaymentTransaction*) transaction{
    if(transaction == nil) return nil;
    
    //获取透传字段
    NSString *orderId = transaction.payment.applicationUsername;
    NSLog(@"从applicationUsername中取出 orderId %@", orderId);
    //没有就去keychian里取
    if(NULL == orderId || orderId.length <= 0){
        orderId = [KeychainTool.getInstance keychainGetDataWithAccountIdentifier:transaction.transactionIdentifier andServiceIdentifier:KEYCHAIN_MONAWA];
        NSLog(@"从keychian中取出 orderId %@", orderId);
    }
    
    return orderId;
}

// 删除keychain数据
-(void) deleteKeychain:(SKPaymentTransaction*) transaction:(BOOL) all{
    NSString* orderId = [KeychainTool.getInstance keychainGetDataWithAccountIdentifier:transaction.transactionIdentifier andServiceIdentifier:KEYCHAIN_MONAWA];
    
    // transactionIdentifier -> orderId
    [KeychainTool.getInstance
        keychainDeleteWithAccountIdentifier:transaction.transactionIdentifier
                       andServiceIdentifier:KEYCHAIN_MONAWA];
    
    // productId -> orderId
    if(all){
        [KeychainTool.getInstance
                    keychainDeleteWithAccountIdentifier:transaction.payment.productIdentifier
                                   andServiceIdentifier:KEYCHAIN_MONAWA];
    }
    NSLog(@"删除keychian, apple:%@, orderId:%@", transaction.transactionIdentifier, orderId);
}

@end

3.performSelectorOnMainThread

我在使用lua回传给game的时候,发现app的弹框图片不见了,并且还会伴随随机的闪退。

看了后台发现 open GL 在报错,我想到了android的 runOnUI。我就百度查了一下,是否ios也有线程问题。

iOS简单学之6-performSelectorOnMainThread
apple不允许程序员在主线程以外的线程中对ui进行操作

在多线程操作中,有一个著名的错误,叫做“Tried to obtain the web lock from a thread other than the main thread or the web thread. This may be a result of calling to UIKit from a secondary thread”,一旦出现这个错误,程序会立即crashed。

4.[cocos2dx 3.0 (二)] 多线程std::thread的使用 以及performFunctionInCocosThread函数

介绍

有的时候很多操作如果在cocos2dx的主线程中来调用,可能会极大地占用主线程的时间,从而使游戏的不流畅。比如在获取网络文件数据或者在数据比较大的游戏存档时,就需要使用多线程了。

网上的一些教程上是使用pthread来创建新线程的,需要加入lib和头文件,但在cocos2dx 3.0中并未发现有pthread的支持文件,后来才发现在c++11中已经拥有了一个更好用的用于线程操作的类std::thread。cocos2dx 3.0的版本默认是在vs2012版本,支持c++11的新特性,使用std::thread来创建线程简直方便。

performFunctionInCocosThread

在cocos2dx中使用多线程,难免要考虑线程安全的问题。cocos2dx 3.0中新加入了一个专门处理线程安全的函数performFunctionInCocosThread()。他是Scheduler类的一个成员函数:

void Scheduler::performFunctionInCocosThread(const std::function<void ()> &function)

/** calls a function on the cocos2d thread. Useful when you need to call a cocos2d function from another thread.
This function is thread safe.
@since v3.0
*/

5.NSDictionary与NSMutableDictionary的转换

 NSDictionary   *dictionary = [NSDictionary dictionaryWithObject: @"String" forKey: @"Test"];

 NSMutableDictionary *anotherDict = [NSMutableDictionary dictionary];
 [anotherDict setValue:@"aa" forKey:@"bb"];

  //dictionary = [[NSDictionary alloc] initWithObjects:[anotherDict allValues] forKeys:[anotherDict allKeys]];	
 dictionary = [NSDictionary dictionaryWithDictionary:anotherDict];

6.OC中@Selector传参总结

我使用的就是最简单的第一种

[self performSelectorOnMainThread:@selector(testAA:) withObject:[NSArray arrayWithObjects:@"1",@"2", nil nil] waitUntilDone:NO];  
  
  
-(void) testAA:(NSArray*)data{  
   
    if (data==nil||data.count!=2) {  
        return;  
    }  
    NSInteger num=[(NSString*)data[0] intValue];  
    NSInteger index=[data[1] intValue];  
}  

7.iOS 内购如何验证订单,iOS7.0以后transaction.transactionReceipt被弃用,使用appStoreReceiptURL获取收据

咱们自己可以先验证一下订单,如果自己的服务器返回结果不一样,那就知道是哪里出了问题了。

8.苹果IAP receipt验单较佳实践

如果不需要展示商品信息,可以省去请求商品这一步

向苹果服务器请求商品信息,是为了展示商店UI。请求到的SKProduct,包含了商品的标题、描述、价格、货币符号等信息。在国内,一般都是服务器接口提供商品信息,客户端直接展示商店UI,用户点击购买的时候,才发起支付。所以,这种情况下,没必要向苹果服务器请求商品信息。因为请求商品信息时,苹果服务器在海外,国内延迟大,慢的约六七秒,甚至有可能跳不到SKProductsRequest的代理方法里面,造成支付失败。

解决办法:
直接省略掉SKProductsRequest这个请求的创建发起。支付时,使用paymentWithProductIdentifier来直接生成SKPayment。

SKPayment *payment = [SKPayment paymentWithProductIdentifier:productIdentifier];
[[SKPaymentQueue defaultQueue] addPayment:payment];
IOS 7 之后使用 receipt,不再从transaction里取

iOS 7风格的receipt,包含的信息是一个列表,里面包含了很多transaction的信息,如果返回status=0,那么只是表示整个App的receipt验证通过。

app端需要发receipt给服务端,服务端向苹果服务器验证receipt,然后返回status。

iOS 7风格的receipt包含了整个应用的所有的交易凭据,status=0时,应该分发该receipt中所有transaction的商品。苹果的验证结果只告诉我们receipt有效还是无效,并不知道哪些transaction分发过商品.

服务端需要根据从数据库里面查询,排重,记录,还要验证该笔transaction是否为退过款的订单,避免重复分发商品。

  • 误区:
    使用[[NSBundle mainBundle] appStoreReceiptURL]获得receipt,服务端却试图寻找最后一笔transaction信息。
  • 正确姿势:
    应该分发该receipt中所有transaction的商品(重复使用的、退款的除外)
  • 原因
    in_app 里记录了所有的订单信息,我们每次只取一次数据就可以了。
    在这里插入图片描述

9.使用keychain存储orderId

iOS内购中碰到的问题与解决方案13.3.1最新问题

既然没法存储订单号,那只好自己找地方存了。这个网上也有很多方案,在这里我选择了用 keychain来存。

keychain介绍
在这里插入图片描述
代码来自:iOS密码管理Keychain的使用

KeychainSave.h

 //
//  KeychainSave.h
//
//  Created by Mac on 2022/4/21.
//

#import <Foundation/Foundation.h>
#import <Security/Security.h>

@interface KeychainTool : NSObject{}

+(instancetype) getInstance;
-(void)dealloc;

-(KeychainTool*_Nullable)initSelf;
    
-(NSMutableDictionary *) keychainDicWithAccountId:(NSString *)accountId andServiceId:(NSString *)serviceId;
-(BOOL) keychainSaveData:(id)aData withAccountIdentifier:(NSString *)accountId andServiceIdentifier:(NSString *)serviceId;
-(id) keychainGetDataWithAccountIdentifier:(NSString *)accountId andServiceIdentifier:(NSString *)serviceId;
-(BOOL) keychainUpdataData:(id)data withAccountIdentifier:(NSString *)accountId andServiceIdentifier:(NSString *)serviceId;
-(void) keychainDeleteWithAccountIdentifier:(NSString *)accountId andServiceIdentifier:(NSString *)serviceId;

@end

KeychainSave.mm

 //
//  KeychainTool.m
//  Created by Mac on 2022/4/21.
//
#import "KeychainTool.h"

@implementation KeychainTool

// ------------------------------------------------------------------
// 单例
+ (instancetype)getInstance {
    static KeychainTool* _instance = nil;
    
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        NSLog(@"dispatch_once");
        _instance = [[super allocWithZone:NULL] init];
        
        // 下面代码进行当前类的初始化操作
        [_instance initSelf];
    });
    
    NSLog(@"shared called");
    
    return _instance;
}

// 不能使用下面这个类进行初始化,否则会陷入死循环,或会使用得外部调用[[xx alloc] init] 会再次触发这个方法
//- (instancetype)init {
//}

+ (instancetype)allocWithZone:(struct _NSZone *)zone {
    NSLog(@"allocWithZone");
    return [KeychainTool getInstance];
}

- (instancetype)copyWithZone:(NSZone *)zone {
    NSLog(@"copyWithZone");
    return self;
}

- (void)dealloc{
    //[_person release];
    NSLog(@"KeychainTool dealloc");
    [super dealloc];
}

- (KeychainTool*)initSelf{
    return self;
}

- (NSMutableDictionary *)keychainDicWithAccountId:(NSString *)accountId andServiceId:(NSString *)serviceId{
    //构建一个存取条件,实质是一个字典
    NSString *classKey = (__bridge NSString *)kSecClass;
    //指定服务类型是普通密码
    NSString *classValue = (__bridge NSString *)kSecClassGenericPassword;
    NSString *accessibleKey = (__bridge NSString *)kSecAttrAccessible;
    //指定安全类型是任何时候都可以访问
    NSString *accessibleValue = (__bridge NSString *)kSecAttrAccessibleAlways;
    NSString *accountKey = (__bridge NSString *)kSecAttrAccount;
    //指定服务的账户名 可以与服务名相同 账户名可以对应多个服务名
    NSString *accountValue = accountId;
    NSString *serviceKey = (__bridge NSString *)kSecAttrService;
    //指定服务的名字 可以与服务账户名相同
    NSString *serviceValue = serviceId;
    NSDictionary *keychainItems = @{classKey      : classValue,
                                    accessibleKey : accessibleValue,
                                    accountKey    : accountValue,
                                    serviceKey    : serviceValue};
    return keychainItems.mutableCopy;
}
 
- (BOOL)keychainSaveData:(id)aData withAccountIdentifier:(NSString *)accountId andServiceIdentifier:(NSString *)serviceId{
    // 获取存储的数据的条件
    NSMutableDictionary * saveQueryDic = [self keychainDicWithAccountId:accountId andServiceId:serviceId];
    // 删除旧的数据
    SecItemDelete((CFDictionaryRef)saveQueryDic);
    // 设置新的数据
    [saveQueryDic setObject:[NSKeyedArchiver archivedDataWithRootObject:aData] forKey:(id)kSecValueData];
    // 添加数据
    OSStatus saveState = SecItemAdd((CFDictionaryRef)saveQueryDic, nil);
    // 释放对象
    saveQueryDic = nil ;
    // 判断是否存储成功
    if (saveState == errSecSuccess) {
        return YES;
    }
    return NO;
}
 
- (id)keychainGetDataWithAccountIdentifier:(NSString *)accountId andServiceIdentifier:(NSString *)serviceId{
    id idObject = nil ;
    // 通过标记获取数据查询条件
    NSMutableDictionary * readQueryDic = [self keychainDicWithAccountId:accountId andServiceId:serviceId];
    // 查询结果返回到 kSecValueData (此项必选)
    [readQueryDic setObject:(id)kCFBooleanTrue forKey:(id)kSecReturnData];
    // 只返回搜索到的第一条数据 (此项必选)
    [readQueryDic setObject:(id)kSecMatchLimitOne forKey:(id)kSecMatchLimit];
    // 创建一个对象接受结果
    CFDataRef keyChainData = nil ;
    // 通过条件查询数据
    if (SecItemCopyMatching((CFDictionaryRef)readQueryDic , (CFTypeRef *)&keyChainData) == noErr){
        @try {
            //转换类型
            idObject = [NSKeyedUnarchiver unarchiveObjectWithData:(__bridge NSData *)(keyChainData)];
        } @catch (NSException * exception){
            NSLog(@"Unarchive of search data where %@ failed of %@ ",serviceId,exception);
        }
    }
    if (keyChainData) {
        CFRelease(keyChainData);
    }
    readQueryDic = nil;
    // 返回数据
    return idObject ;
}
 
- (BOOL)keychainUpdataData:(id)data withAccountIdentifier:(NSString *)accountId andServiceIdentifier:(NSString *)serviceId{
    // 通过标记获取数据更新的条件
    NSMutableDictionary * updataQueryDic = [self keychainDicWithAccountId:accountId andServiceId:serviceId];
    // 创建更新数据字典
    NSMutableDictionary * newDic = @{}.mutableCopy;
    // 存储数据
    [newDic setObject:[NSKeyedArchiver archivedDataWithRootObject:data] forKey:(id)kSecValueData];
    // 获取存储的状态
    OSStatus  updataStatus = SecItemUpdate((CFDictionaryRef)updataQueryDic, (CFDictionaryRef)newDic);
    updataQueryDic = nil;
    newDic = nil;
    // 判断是否更新成功
    if (updataStatus == errSecSuccess) {
        return  YES ;
    }
    return NO;
}

- (void)keychainDeleteWithAccountIdentifier:(NSString *)accountId andServiceIdentifier:(NSString *)serviceId{
    // 获取删除数据的查询条件
    NSMutableDictionary * deleteQueryDic = [self keychainDicWithAccountId:accountId andServiceId:serviceId];
    // 删除指定条件的数据
    SecItemDelete((CFDictionaryRef)deleteQueryDic);
    deleteQueryDic = nil ;
}

@end

五.Kakao

AppKakao.h

//
//  AppKakao.h
//
//  Created by Mac on 2022/3/26.
//

#import <Foundation/Foundation.h>
#import <KakaoAdSDK/KakaoAdSDK.h>


@interface AppKakao: NSObject{}

+(instancetype) getInstance;

-(void) dealloc;

//需要app一些周期调用来的函数
-(void) onViewDidLoad;
-(void) didFinishLaunchingWithOptions;
-(void) applicationDidBecomeActive;

-(void) onSignUp:(NSString*) playerId;

-(bool) handleURL
    :(NSURL *)url
    :(nonnull NSDictionary<UIApplicationOpenURLOptionsKey, id> *)options;

@end

AppKakao.mm

//
//  AppKakao.m
//
//  Created by Mac on 2022/3/26.
//

#import "AppKakao.h"

@implementation AppKakao

// ------------------------------------------------------------------
// 单例
+ (instancetype)getInstance {
    static AppKakao* _instance = nil;
    
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        NSLog(@"dispatch_once");
        _instance = [[super allocWithZone:NULL] init];
        
        // 下面代码进行当前类的初始化操作
        [_instance initSelf];
    });
    
    NSLog(@"shared called");
    
    return _instance;
}

// 不能使用下面这个类进行初始化,否则会陷入死循环,或会使用得外部调用[[xx alloc] init] 会再次触发这个方法
//- (instancetype)init {
//}

+ (instancetype)allocWithZone:(struct _NSZone *)zone {
    NSLog(@"allocWithZone");
    return [AppKakao getInstance];
}

- (instancetype)copyWithZone:(NSZone *)zone {
    NSLog(@"copyWithZone");
    return self;
}

- (instancetype)mutableCopyWithZone:(NSZone *)zone {
    NSLog(@"mutableCopyWithZone");
    return self;
}
// ------------------------------------------------------------------

- (void)dealloc{
    [super dealloc];
}

// 初始化
-(AppKakao*) initSelf{
    return self;
}

// ------------------------------------------------------------------
//
-(void) onViewDidLoad{
}

-(void) didFinishLaunchingWithOptions{
    // Override point for customization after application launch.
    KakaoAdTracker.trackId = @"你的id";
}

-(void) applicationDidBecomeActive{
// Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface.
    [KakaoAdTracker activate];
}

-(bool) handleURL
    :(NSURL *)url
    :(nonnull NSDictionary<UIApplicationOpenURLOptionsKey, id> *)options{
}

-(void) onSignUp:(NSString*) playerId{
    NSString* tag = @"tag";
    [KakaoAdTracker sendCompleteRegisterEventWithTag:tag];
}

@end

运营商给的文档太老了,要分dynamic和static两种,文档里还要混合用。
但是我看它的github的demo,项目就直接分成了Dynamic和Static,直接选它demo中一种搞就行了。

六.其他

1.出包的时候报错,IPA processing failed

Xcode11打包不成功 IPA processing failed(一)
Xcode11打包导出不成功 IPA processing failed(续)
Xcode11打包失敗IPA processing failed
xcode11 打包IPA process failed 解决办法

解决的方法奇奇怪怪的。
目前引起的原因似乎是某个库是 x86 的,而 xcode13 之后只支持 64 位了。

2.App Tracking Transparency (iOS 14.5+)

How to get IDFA in iOS 14
iOS14-AppTrackingTransparency(idfa适配)
在这里插入图片描述
以下两个错误就是要加上这两个库

XCode12编译出错:Undefined symbol: OBJC_CLASSKaTeX parse error: Expected group after '_' at position 105: …题之 “_OBJC_CLASS_̲_ASIdentifierManager”, referenced from: objc-class-ref in