// AppleSignInManager.m // 封装“用 Apple 登录”的实现与存储 #import "AppleSignInManager.h" #import #import static NSString * const kKBAppleUserIdentifierKey = @"com.company.keyboard.apple.user"; // 钥匙串键名 @interface AppleSignInManager () @property (nonatomic, weak) UIViewController *presentingVC; @property (nonatomic, copy) KBAppleSignInCompletion completion; @end @implementation AppleSignInManager + (instancetype)shared { static AppleSignInManager *instance; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ instance = [AppleSignInManager new]; }); return instance; } - (NSString *)storedUserIdentifier { return [self.class keychainLoad:kKBAppleUserIdentifierKey]; } - (void)signInFromViewController:(UIViewController *)presenting completion:(KBAppleSignInCompletion)completion { if (@available(iOS 13.0, *)) { self.presentingVC = presenting; self.completion = completion; ASAuthorizationAppleIDProvider *provider = [ASAuthorizationAppleIDProvider new]; ASAuthorizationAppleIDRequest *request = provider.createRequest; request.requestedScopes = @[ASAuthorizationScopeFullName, ASAuthorizationScopeEmail]; ASAuthorizationController *controller = [[ASAuthorizationController alloc] initWithAuthorizationRequests:@[request]]; controller.delegate = self; controller.presentationContextProvider = self; [controller performRequests]; } else { if (completion) { NSError *err = [NSError errorWithDomain:@"AppleSignIn" code:-1 userInfo:@{NSLocalizedDescriptionKey: @"Apple 登录需要 iOS 13 及以上版本"}]; completion(nil, err); } } } - (void)checkCredentialStateWithCompletion:(void(^)(ASAuthorizationAppleIDProviderCredentialState state))completion { if (!completion) return; if (@available(iOS 13.0, *)) { NSString *userID = self.storedUserIdentifier; if (!userID) { completion(ASAuthorizationAppleIDProviderCredentialNotFound); return; } ASAuthorizationAppleIDProvider *provider = [ASAuthorizationAppleIDProvider new]; [provider getCredentialStateForUserID:userID completion:^(ASAuthorizationAppleIDProviderCredentialState credentialState, NSError * _Nullable error) { dispatch_async(dispatch_get_main_queue(), ^{ completion(credentialState); }); }]; } else { completion(ASAuthorizationAppleIDProviderCredentialNotFound); } } #pragma mark - 授权回调 (ASAuthorizationControllerDelegate) - (void)authorizationController:(ASAuthorizationController *)controller didCompleteWithAuthorization:(ASAuthorization *)authorization API_AVAILABLE(ios(13.0)) { if (@available(iOS 13.0, *)) { ASAuthorizationAppleIDCredential *credential = authorization.credential; // 持久化保存 userIdentifier,便于后续校验凭证状态 NSString *userID = credential.user; [self.class keychainSave:kKBAppleUserIdentifierKey value:userID]; if (self.completion) { self.completion(credential, nil); } } self.completion = nil; self.presentingVC = nil; } - (void)authorizationController:(ASAuthorizationController *)controller didCompleteWithError:(NSError *)error API_AVAILABLE(ios(13.0)) { if (self.completion) { self.completion(nil, error); } self.completion = nil; self.presentingVC = nil; } #pragma mark - 授权界面展示锚点 (ASAuthorizationControllerPresentationContextProviding) - (ASPresentationAnchor)presentationAnchorForAuthorizationController:(ASAuthorizationController *)controller API_AVAILABLE(ios(13.0)) { return self.presentingVC.view.window ?: UIApplication.sharedApplication.keyWindow; } #pragma mark - Keychain 工具 // 本地登出:删除已存储的 userIdentifier,使 App 重新要求登录 - (void)signOut { NSDictionary *query = @{(__bridge id)kSecClass: (__bridge id)kSecClassGenericPassword, (__bridge id)kSecAttrService: kKBAppleUserIdentifierKey, (__bridge id)kSecAttrAccount: kKBAppleUserIdentifierKey}; SecItemDelete((__bridge CFDictionaryRef)query); } + (BOOL)keychainSave:(NSString *)key value:(NSString *)value { if (!key) return NO; NSData *data = [value dataUsingEncoding:NSUTF8StringEncoding]; NSDictionary *query = @{(__bridge id)kSecClass: (__bridge id)kSecClassGenericPassword, (__bridge id)kSecAttrService: key, (__bridge id)kSecAttrAccount: key}; SecItemDelete((__bridge CFDictionaryRef)query); NSMutableDictionary *attributes = [query mutableCopy]; attributes[(__bridge id)kSecValueData] = data ?: [NSData data]; OSStatus status = SecItemAdd((__bridge CFDictionaryRef)attributes, NULL); return (status == errSecSuccess); } + (NSString *)keychainLoad:(NSString *)key { if (!key) return nil; NSDictionary *query = @{(__bridge id)kSecClass: (__bridge id)kSecClassGenericPassword, (__bridge id)kSecAttrService: key, (__bridge id)kSecAttrAccount: key, (__bridge id)kSecReturnData: @YES, (__bridge id)kSecMatchLimit: (__bridge id)kSecMatchLimitOne}; CFTypeRef dataRef = NULL; OSStatus status = SecItemCopyMatching((__bridge CFDictionaryRef)query, &dataRef); if (status != errSecSuccess || !dataRef) return nil; NSData *data = (__bridge_transfer NSData *)dataRef; return [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]; } @end