2025-10-30 14:29:11 +08:00
// AppleSignInManager . m
// 封 装 “ 用 Apple 登 录 ” 的 实 现 与 存 储
# import "AppleSignInManager.h"
# import < UIKit / UIKit . h >
# import < Security / Security . h >
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 {
2025-11-13 14:11:44 +08:00
if ( ! NSThread . isMainThread ) {
// 确 保 在 主 线 程 发 起 , 否 则 可 能 得 到 Unknown ( 1000 )
dispatch_async ( dispatch_get _main _queue ( ) , ^ { [ self signInFromViewController : presenting completion : completion ] ; } ) ;
return ;
}
2025-10-30 14:29:11 +08:00
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 ) {
2025-11-17 20:07:39 +08:00
NSError * err = [ NSError errorWithDomain : @ "AppleSignIn" code : -1 userInfo : @ { NSLocalizedDescriptionKey : KBLocalized ( @ "Sign in with Apple requires iOS 13 or later" ) } ] ;
2025-10-30 14:29:11 +08:00
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 ) ;
}
}
2025-10-30 14:35:06 +08:00
// 使 用 指 定 的 userIdentifier 检 查 凭 证 状 态 ( 无 需 本 地 存 储 )
- ( void ) checkCredentialStateForUserID : ( NSString * ) userID completion : ( void ( ^ ) ( ASAuthorizationAppleIDProviderCredentialState state ) ) completion {
if ( ! completion ) return ;
if ( @ available ( iOS 13.0 , * ) ) {
if ( userID . length = = 0 ) {
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 ) ;
}
}
2025-10-30 14:29:11 +08:00
# pragma mark - 授 权 回 调 ( ASAuthorizationControllerDelegate )
- ( void ) authorizationController : ( ASAuthorizationController * ) controller didCompleteWithAuthorization : ( ASAuthorization * ) authorization API_AVAILABLE ( ios ( 13.0 ) ) {
if ( @ available ( iOS 13.0 , * ) ) {
ASAuthorizationAppleIDCredential * credential = authorization . credential ;
2025-10-30 20:23:34 +08:00
// 为 了 让 键 盘 扩 展 / 主 App 能 识 别 “ 是 否 已 登 录 ” , 在 本 地 持 久 化 userIdentifier 到 钥 匙 串 。
// 仅 保 存 一 个 标 记 ( 不 包 含 敏 感 信 息 ) , 业 务 登 录 态 仍 以 服 务 端 为 准 。
NSString * userID = credential . user ? : @ "" ;
if ( userID . length > 0 ) {
[ self . class keychainSave : kKBAppleUserIdentifierKey value : userID ] ;
}
2025-10-30 14:29:11 +08:00
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 ) ) {
2025-11-13 14:11:44 +08:00
// 优 先 用 传 入 VC 的 window
UIWindow * win = self . presentingVC . view . window ;
if ( win ) return win ;
// iOS13 + 从 前 台 激 活 的 scene 中 取 keyWindow
for ( UIScene * scene in UIApplication . sharedApplication . connectedScenes ) {
if ( scene . activationState = = UISceneActivationStateForegroundActive && [ scene isKindOfClass : [ UIWindowScene class ] ] ) {
UIWindowScene * ws = ( UIWindowScene * ) scene ;
for ( UIWindow * w in ws . windows ) { if ( w . isKeyWindow ) return w ; }
if ( ws . windows . firstObject ) return ws . windows . firstObject ;
}
}
# pragma clang diagnostic push
# pragma clang diagnostic ignored "-Wdeprecated-declarations"
return UIApplication . sharedApplication . keyWindow ? : UIApplication . sharedApplication . windows . firstObject ;
# pragma clang diagnostic pop
2025-10-30 14:29:11 +08:00
}
# 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