diff --git a/CustomKeyboard/KeyboardViewController.m b/CustomKeyboard/KeyboardViewController.m index a259cbb..347b227 100644 --- a/CustomKeyboard/KeyboardViewController.m +++ b/CustomKeyboard/KeyboardViewController.m @@ -24,6 +24,9 @@ static CGFloat KEYBOARDHEIGHT = 256 + 20; @implementation KeyboardViewController +{ + BOOL _kb_didTriggerLoginDeepLinkOnce; +} - (void)viewDidLoad { [super viewDidLoad]; @@ -72,16 +75,16 @@ static CGFloat KEYBOARDHEIGHT = 256 + 20; /// 显示/隐藏设置页(高度与 keyBoardMainView 一致),右侧滑入/滑出 - (void)showSettingView:(BOOL)show { if (show) { -// if (!self.settingView) { - self.settingView = [[KBSettingView alloc] init]; - self.settingView.hidden = YES; - [self.view addSubview:self.settingView]; - [self.settingView mas_makeConstraints:^(MASConstraintMaker *make) { - // 与键盘主视图完全等同的区域,保证高度、宽度一致 - make.edges.equalTo(self.keyBoardMainView); - }]; - [self.settingView.backButton addTarget:self action:@selector(onTapSettingsBack) forControlEvents:UIControlEventTouchUpInside]; -// } + // if (!self.settingView) { + self.settingView = [[KBSettingView alloc] init]; + self.settingView.hidden = YES; + [self.view addSubview:self.settingView]; + [self.settingView mas_makeConstraints:^(MASConstraintMaker *make) { + // 与键盘主视图完全等同的区域,保证高度、宽度一致 + make.edges.equalTo(self.keyBoardMainView); + }]; + [self.settingView.backButton addTarget:self action:@selector(onTapSettingsBack) forControlEvents:UIControlEventTouchUpInside]; + // } [self.view bringSubviewToFront:self.settingView]; // 以 keyBoardMainView 的实际宽度为准,避免首次添加时 self.view 宽度尚未计算 [self.view layoutIfNeeded]; @@ -183,4 +186,23 @@ static CGFloat KEYBOARDHEIGHT = 256 + 20; [self showSettingView:NO]; } + +// 当键盘第一次显示时,尝试唤起主 App 以提示登录(由主 App 决定是否真的弹登录)。 +- (void)viewDidAppear:(BOOL)animated { + [super viewDidAppear:animated]; + if (!_kb_didTriggerLoginDeepLinkOnce) { + _kb_didTriggerLoginDeepLinkOnce = YES; + [self kb_tryOpenContainerForLoginIfNeeded]; + } +} + +- (void)kb_tryOpenContainerForLoginIfNeeded { + NSURL *url = [NSURL URLWithString:@"kbkeyboard://login?src=keyboard"]; + if (!url) return; + __weak typeof(self) weakSelf = self; + [self.extensionContext openURL:url completionHandler:^(__unused BOOL success) { + // 即使失败也不重复尝试;避免打扰。 + __unused typeof(weakSelf) selfStrong = weakSelf; + }]; +} @end diff --git a/CustomKeyboard/View/KBFunctionView.m b/CustomKeyboard/View/KBFunctionView.m index c961b80..0166904 100644 --- a/CustomKeyboard/View/KBFunctionView.m +++ b/CustomKeyboard/View/KBFunctionView.m @@ -10,6 +10,7 @@ #import "KBFunctionPasteView.h" #import "KBFunctionTagCell.h" #import "Masonry.h" +#import static NSString * const kKBFunctionTagCellId = @"KBFunctionTagCellId"; @@ -160,6 +161,26 @@ static NSString * const kKBFunctionTagCellId = @"KBFunctionTagCellId"; return 12.0; } +// 用户点击功能标签:拉起主 App,并由主 App 侧弹出登录浮层 +- (void)collectionView:(UICollectionView *)collectionView didSelectItemAtIndexPath:(NSIndexPath *)indexPath { + UIInputViewController *ivc = [self findInputViewController]; + if (!ivc) return; + + NSString *title = (indexPath.item < self.itemsInternal.count) ? self.itemsInternal[indexPath.item] : @""; + NSString *encodedTitle = [title stringByAddingPercentEncodingWithAllowedCharacters:[NSCharacterSet URLQueryAllowedCharacterSet]] ?: @""; + NSURL *url = [NSURL URLWithString:[NSString stringWithFormat:@"kbkeyboard://login?src=functionView&index=%ld&title=%@", (long)indexPath.item, encodedTitle]]; + if (!url) return; + + dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.05 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ + [ivc.extensionContext openURL:url completionHandler:^(BOOL success) { + if (!success) { + NSLog(@"[KB] openURL failed. Likely Full Access is OFF. 请到 设置>通用>键盘>键盘>你的键盘 开启“允许完全访问”。"); + } + }]; + }); +} + + #pragma mark - Button Actions - (void)onTapPaste { diff --git a/keyBoard.xcodeproj/project.pbxproj b/keyBoard.xcodeproj/project.pbxproj index 67f4203..f361945 100644 --- a/keyBoard.xcodeproj/project.pbxproj +++ b/keyBoard.xcodeproj/project.pbxproj @@ -36,6 +36,7 @@ 04FC95E92EB23B67007BD342 /* KBNetworkManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 04FC95E72EB23B67007BD342 /* KBNetworkManager.m */; }; 04FC95F12EB339A7007BD342 /* LoginViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 04FC95F02EB339A7007BD342 /* LoginViewController.m */; }; 04FC95F42EB339C1007BD342 /* AppleSignInManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 04FC95F32EB339C1007BD342 /* AppleSignInManager.m */; }; + 04FC96142EB34E00007BD342 /* KBLoginSheetViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 04FC96122EB34E00007BD342 /* KBLoginSheetViewController.m */; }; 04FC97002EB30A00007BD342 /* KBGuideTopCell.m in Sources */ = {isa = PBXBuildFile; fileRef = 04FC96FF2EB30A00007BD342 /* KBGuideTopCell.m */; }; 04FC97032EB30A00007BD342 /* KBGuideKFCell.m in Sources */ = {isa = PBXBuildFile; fileRef = 04FC97022EB30A00007BD342 /* KBGuideKFCell.m */; }; 04FC97062EB30A00007BD342 /* KBGuideUserCell.m in Sources */ = {isa = PBXBuildFile; fileRef = 04FC97052EB30A00007BD342 /* KBGuideUserCell.m */; }; @@ -131,6 +132,8 @@ 04FC95F22EB339C1007BD342 /* AppleSignInManager.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = AppleSignInManager.h; sourceTree = ""; }; 04FC95F32EB339C1007BD342 /* AppleSignInManager.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = AppleSignInManager.m; sourceTree = ""; }; 04FC95F52EB33B52007BD342 /* keyBoard.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = keyBoard.entitlements; sourceTree = ""; }; + 04FC96112EB34E00007BD342 /* KBLoginSheetViewController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KBLoginSheetViewController.h; sourceTree = ""; }; + 04FC96122EB34E00007BD342 /* KBLoginSheetViewController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KBLoginSheetViewController.m; sourceTree = ""; }; 04FC96FE2EB30A00007BD342 /* KBGuideTopCell.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KBGuideTopCell.h; sourceTree = ""; }; 04FC96FF2EB30A00007BD342 /* KBGuideTopCell.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KBGuideTopCell.m; sourceTree = ""; }; 04FC97012EB30A00007BD342 /* KBGuideKFCell.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KBGuideKFCell.h; sourceTree = ""; }; @@ -487,6 +490,8 @@ children = ( 04FC95EF2EB339A7007BD342 /* LoginViewController.h */, 04FC95F02EB339A7007BD342 /* LoginViewController.m */, + 04FC96112EB34E00007BD342 /* KBLoginSheetViewController.h */, + 04FC96122EB34E00007BD342 /* KBLoginSheetViewController.m */, ); path = VC; sourceTree = ""; @@ -756,6 +761,7 @@ 043FBCD22EAF97630036AFE1 /* KBPermissionViewController.m in Sources */, 04C6EABE2EAF86530089C901 /* AppDelegate.m in Sources */, 04FC95F12EB339A7007BD342 /* LoginViewController.m in Sources */, + 04FC96142EB34E00007BD342 /* KBLoginSheetViewController.m in Sources */, 04FC95D72EB1EA16007BD342 /* BaseTableView.m in Sources */, 04FC95D82EB1EA16007BD342 /* BaseCell.m in Sources */, 04FC95C92EB1E4C9007BD342 /* BaseNavigationController.m in Sources */, @@ -874,7 +880,15 @@ GCC_PREFIX_HEADER = keyBoard/KeyBoardPrefixHeader.pch; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = keyBoard/Info.plist; - INFOPLIST_KEY_CFBundleDisplayName = "我的输入法"; + INFOPLIST_KEY_CFBundleURLTypes = ( + { + CFBundleURLName = com.keyBoardst.keyboard; + CFBundleURLSchemes = ( + kbkeyboard, + ); + }, + ); + INFOPLIST_KEY_CFBundleDisplayName = "YOLO输入法"; INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen; INFOPLIST_KEY_UIMainStoryboardFile = Main; @@ -891,9 +905,9 @@ STRING_CATALOG_GENERATE_SYMBOLS = YES; SWIFT_EMIT_LOC_STRINGS = YES; TARGETED_DEVICE_FAMILY = "1,2"; - }; - name = Debug; }; + name = Debug; + }; 727EC76D2EAF848C00B36487 /* Release */ = { isa = XCBuildConfiguration; baseConfigurationReference = 51FE7C4C42C2255B3C1C4128 /* Pods-keyBoard.release.xcconfig */; @@ -907,7 +921,15 @@ GCC_PREFIX_HEADER = keyBoard/KeyBoardPrefixHeader.pch; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = keyBoard/Info.plist; - INFOPLIST_KEY_CFBundleDisplayName = "我的输入法"; + INFOPLIST_KEY_CFBundleURLTypes = ( + { + CFBundleURLName = com.keyBoardst.keyboard; + CFBundleURLSchemes = ( + kbkeyboard, + ); + }, + ); + INFOPLIST_KEY_CFBundleDisplayName = "YOLO输入法"; INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen; INFOPLIST_KEY_UIMainStoryboardFile = Main; diff --git a/keyBoard/AppDelegate.m b/keyBoard/AppDelegate.m index be66216..3f842ff 100644 --- a/keyBoard/AppDelegate.m +++ b/keyBoard/AppDelegate.m @@ -11,6 +11,8 @@ #import #import "BaseTabBarController.h" #import "LoginViewController.h" +#import "KBLoginSheetViewController.h" +#import "AppleSignInManager.h" // 注意:用于判断系统已启用本输入法扩展的 bundle id 需与扩展 target 的 // PRODUCT_BUNDLE_IDENTIFIER 完全一致。 @@ -44,7 +46,7 @@ static NSString * const kKBKeyboardExtensionBundleId = @"com.keyBoardst.CustomKe self.window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]]; self.window.backgroundColor = [UIColor whiteColor]; [self.window makeKeyAndVisible]; - LoginViewController *vc = [[LoginViewController alloc] init]; + BaseTabBarController *vc = [[BaseTabBarController alloc] init]; self.window.rootViewController = vc; } @@ -61,6 +63,32 @@ static NSString * const kKBKeyboardExtensionBundleId = @"com.keyBoardst.CustomKe return top; } + +#pragma mark - Deep Link + +// iOS 9+ +- (BOOL)application:(UIApplication *)app openURL:(NSURL *)url options:(NSDictionary *)options { + if (!url) return NO; + if ([[url.scheme lowercaseString] isEqualToString:@"kbkeyboard"]) { + // 路由:kbkeyboard://login + if ([[url.host lowercaseString] isEqualToString:@"login"]) { + [self kb_presentLoginSheetIfNeeded]; + return YES; + } + return NO; + } + return NO; +} + +- (void)kb_presentLoginSheetIfNeeded { + // 已登录则不提示 + BOOL loggedIn = ([AppleSignInManager shared].storedUserIdentifier.length > 0); + if (loggedIn) return; + UIViewController *top = [self kb_topMostViewController]; + if (!top) return; + [KBLoginSheetViewController presentIfNeededFrom:top]; +} + - (void)kb_presentPermissionIfNeeded { BOOL enabled = KBIsKeyboardEnabled(); diff --git a/keyBoard/Class/Login/VC/KBLoginSheetViewController.h b/keyBoard/Class/Login/VC/KBLoginSheetViewController.h new file mode 100644 index 0000000..101a497 --- /dev/null +++ b/keyBoard/Class/Login/VC/KBLoginSheetViewController.h @@ -0,0 +1,23 @@ +// +// KBLoginSheetViewController.h +// keyBoard +// +// A lightweight bottom-sheet style login prompt. +// + +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface KBLoginSheetViewController : UIViewController + +/// Called when the login finished successfully; sheet will dismiss itself as well. +@property (nonatomic, copy, nullable) void (^onLoginSuccess)(void); + +/// Present the sheet from a top most view controller. ++ (void)presentIfNeededFrom:(UIViewController *)presenting; + +@end + +NS_ASSUME_NONNULL_END + diff --git a/keyBoard/Class/Login/VC/KBLoginSheetViewController.m b/keyBoard/Class/Login/VC/KBLoginSheetViewController.m new file mode 100644 index 0000000..a4b69fa --- /dev/null +++ b/keyBoard/Class/Login/VC/KBLoginSheetViewController.m @@ -0,0 +1,154 @@ +// +// KBLoginSheetViewController.m +// keyBoard +// +// Bottom sheet asking user to login with Apple ID. Tapping Continue shows LoginViewController. +// + +#import "KBLoginSheetViewController.h" +#import "LoginViewController.h" +#import + +@interface KBLoginSheetViewController () +@property (nonatomic, strong) UIControl *backdrop; +@property (nonatomic, strong) UIView *sheet; +@property (nonatomic, strong) UIButton *checkButton; +@property (nonatomic, strong) UILabel *descLabel; +@property (nonatomic, strong) UIButton *continueButton; +@end + +@implementation KBLoginSheetViewController + ++ (void)presentIfNeededFrom:(UIViewController *)presenting { + if (!presenting) return; + // 避免重复弹多个 + if ([presenting.presentedViewController isKindOfClass:[self class]]) return; + KBLoginSheetViewController *vc = [KBLoginSheetViewController new]; + vc.modalPresentationStyle = UIModalPresentationOverFullScreen; + [presenting presentViewController:vc animated:NO completion:nil]; +} + +- (void)viewDidLoad { + [super viewDidLoad]; + self.view.backgroundColor = [UIColor clearColor]; + [self buildUI]; +} + +- (void)buildUI { + // 半透明背景,点击可关闭 + self.backdrop = [[UIControl alloc] init]; + self.backdrop.backgroundColor = [[UIColor blackColor] colorWithAlphaComponent:0.15]; + [self.backdrop addTarget:self action:@selector(dismissSelf) forControlEvents:UIControlEventTouchUpInside]; + [self.view addSubview:self.backdrop]; + [self.backdrop mas_makeConstraints:^(MASConstraintMaker *make) { + make.edges.equalTo(self.view); + }]; + + // 底部白色卡片 + self.sheet = [[UIView alloc] init]; + self.sheet.backgroundColor = [UIColor whiteColor]; + self.sheet.layer.cornerRadius = 12.0; + self.sheet.layer.maskedCorners = kCALayerMinXMinYCorner | kCALayerMaxXMinYCorner; // 顶部圆角 + self.sheet.layer.masksToBounds = YES; + [self.view addSubview:self.sheet]; + [self.sheet mas_makeConstraints:^(MASConstraintMaker *make) { + make.left.right.equalTo(self.view); + make.bottom.equalTo(self.view.mas_safeAreaLayoutGuideBottom); + make.height.mas_equalTo(160); + }]; + + // 复选框 + 文案 + self.checkButton = [UIButton buttonWithType:UIButtonTypeSystem]; + self.checkButton.layer.cornerRadius = 16; + self.checkButton.layer.borderWidth = 2; + self.checkButton.layer.borderColor = [UIColor colorWithWhite:0.8 alpha:1.0].CGColor; + self.checkButton.backgroundColor = [UIColor whiteColor]; + [self.checkButton setTitle:@"" forState:UIControlStateNormal]; + [self.checkButton addTarget:self action:@selector(toggleCheck) forControlEvents:UIControlEventTouchUpInside]; + + self.descLabel = [UILabel new]; + self.descLabel.text = @"allow log in with apple id?"; + self.descLabel.textColor = [UIColor blackColor]; + self.descLabel.font = [UIFont systemFontOfSize:16]; + + [self.sheet addSubview:self.checkButton]; + [self.sheet addSubview:self.descLabel]; + [self.checkButton mas_makeConstraints:^(MASConstraintMaker *make) { + make.left.equalTo(self.sheet).offset(28); + make.top.equalTo(self.sheet).offset(18); + make.width.height.mas_equalTo(32); + }]; + [self.descLabel mas_makeConstraints:^(MASConstraintMaker *make) { + make.left.equalTo(self.checkButton.mas_right).offset(12); + make.centerY.equalTo(self.checkButton.mas_centerY); + make.right.lessThanOrEqualTo(self.sheet).offset(-20); + }]; + + // Continue 按钮 + self.continueButton = [UIButton buttonWithType:UIButtonTypeSystem]; + self.continueButton.backgroundColor = [UIColor whiteColor]; + self.continueButton.layer.cornerRadius = 10; + self.continueButton.layer.borderWidth = 1.0; + self.continueButton.layer.borderColor = [UIColor colorWithWhite:0.8 alpha:1.0].CGColor; + [self.continueButton setTitle:@"Continue" forState:UIControlStateNormal]; + [self.continueButton setTitleColor:[UIColor blackColor] forState:UIControlStateNormal]; + self.continueButton.titleLabel.font = [UIFont boldSystemFontOfSize:20]; + [self.continueButton addTarget:self action:@selector(onContinue) forControlEvents:UIControlEventTouchUpInside]; + [self.sheet addSubview:self.continueButton]; + [self.continueButton mas_makeConstraints:^(MASConstraintMaker *make) { + make.left.equalTo(self.sheet).offset(28); + make.right.equalTo(self.sheet).offset(-28); + make.height.mas_equalTo(48); + make.top.equalTo(self.checkButton.mas_bottom).offset(18); + }]; +} + +- (void)viewDidAppear:(BOOL)animated { + [super viewDidAppear:animated]; + // 初始位置在屏幕下方,做一个上滑动画 + CGRect frame = self.sheet.frame; + self.sheet.transform = CGAffineTransformMakeTranslation(0, frame.size.height + 40); + self.backdrop.alpha = 0; + [UIView animateWithDuration:0.22 animations:^{ + self.sheet.transform = CGAffineTransformIdentity; + self.backdrop.alpha = 1; + }]; +} + +- (void)dismissSelf { + [UIView animateWithDuration:0.18 animations:^{ + self.sheet.transform = CGAffineTransformMakeTranslation(0, self.sheet.bounds.size.height + 40); + self.backdrop.alpha = 0; + } completion:^(BOOL finished) { + [self dismissViewControllerAnimated:NO completion:nil]; + }]; +} + +- (void)toggleCheck { + self.checkButton.selected = !self.checkButton.selected; + if (self.checkButton.selected) { + self.checkButton.backgroundColor = [UIColor colorWithRed:0.22 green:0.49 blue:0.96 alpha:1.0]; + self.checkButton.layer.borderColor = self.checkButton.backgroundColor.CGColor; + } else { + self.checkButton.backgroundColor = [UIColor whiteColor]; + self.checkButton.layer.borderColor = [UIColor colorWithWhite:0.8 alpha:1.0].CGColor; + } +} + +- (void)onContinue { + // 继续:展示内置的登录页(Apple 登录) + LoginViewController *login = [LoginViewController new]; + __weak typeof(self) weakSelf = self; + login.onLoginSuccess = ^(NSDictionary * _Nonnull userInfo) { + __strong typeof(weakSelf) self = weakSelf; if (!self) return; + // 先关闭登录页,再收起底部弹层 + [self dismissViewControllerAnimated:YES completion:^{ + if (self.onLoginSuccess) self.onLoginSuccess(); + [self dismissSelf]; + }]; + }; + login.modalPresentationStyle = UIModalPresentationPageSheet; + [self presentViewController:login animated:YES completion:nil]; +} + +@end diff --git a/keyBoard/Class/Manager/AppleSignInManager.m b/keyBoard/Class/Manager/AppleSignInManager.m index f92be26..3306926 100644 --- a/keyBoard/Class/Manager/AppleSignInManager.m +++ b/keyBoard/Class/Manager/AppleSignInManager.m @@ -85,8 +85,12 @@ static NSString * const kKBAppleUserIdentifierKey = @"com.company.keyboard.apple - (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; // 如需本地缓存,请自行决定是否保存 + // 为了让键盘扩展/主 App 能识别“是否已登录”,在本地持久化 userIdentifier 到钥匙串。 + // 仅保存一个标记(不包含敏感信息),业务登录态仍以服务端为准。 + NSString *userID = credential.user ?: @""; + if (userID.length > 0) { + [self.class keychainSave:kKBAppleUserIdentifierKey value:userID]; + } if (self.completion) { self.completion(credential, nil); } diff --git a/keyBoard/Info.plist b/keyBoard/Info.plist index 6a6654d..91491f9 100644 --- a/keyBoard/Info.plist +++ b/keyBoard/Info.plist @@ -7,5 +7,16 @@ NSAllowsArbitraryLoads + CFBundleURLTypes + + + CFBundleURLName + com.keyBoardst.keyboard + CFBundleURLSchemes + + kbkeyboard + + +