diff --git a/CustomKeyboard/View/KBFullAccessGuideView.h b/CustomKeyboard/View/KBFullAccessGuideView.h new file mode 100644 index 0000000..dc8c8e4 --- /dev/null +++ b/CustomKeyboard/View/KBFullAccessGuideView.h @@ -0,0 +1,19 @@ +// +// KBFullAccessGuideView.h +// CustomKeyboard +// +// A lightweight overlay prompting user to enable "Allow Full Access". +// + +#import + +@interface KBFullAccessGuideView : UIView + +/// Present the guide overlay inside a given parent view (or its window). ++ (void)showInView:(UIView *)parent; + +/// Dismiss if shown. ++ (void)dismissFromView:(UIView *)parent; + +@end + diff --git a/CustomKeyboard/View/KBFullAccessGuideView.m b/CustomKeyboard/View/KBFullAccessGuideView.m new file mode 100644 index 0000000..ad10716 --- /dev/null +++ b/CustomKeyboard/View/KBFullAccessGuideView.m @@ -0,0 +1,185 @@ +// +// KBFullAccessGuideView.m +// CustomKeyboard +// + +#import "KBFullAccessGuideView.h" +#import "Masonry.h" + +@interface KBFullAccessGuideView () +@property (nonatomic, strong) UIControl *backdrop; +@property (nonatomic, strong) UIView *card; +@end + +@implementation KBFullAccessGuideView + ++ (instancetype)build { + KBFullAccessGuideView *v = [[KBFullAccessGuideView alloc] initWithFrame:CGRectZero]; + [v setupUI]; + return v; +} + +- (void)setupUI { + self.backgroundColor = [UIColor clearColor]; + + self.backdrop = [[UIControl alloc] init]; + self.backdrop.backgroundColor = [[UIColor blackColor] colorWithAlphaComponent:0.35]; + // [self.backdrop addTarget:self action:@selector(dismiss) forControlEvents:UIControlEventTouchUpInside]; + [self addSubview:self.backdrop]; + [self.backdrop mas_makeConstraints:^(MASConstraintMaker *make) { + make.edges.equalTo(self); + }]; + + self.card = [[UIView alloc] init]; + self.card.backgroundColor = [UIColor whiteColor]; + self.card.layer.cornerRadius = 16; + self.card.layer.masksToBounds = YES; + [self addSubview:self.card]; + [self.card mas_makeConstraints:^(MASConstraintMaker *make) { + make.center.equalTo(self); + make.left.equalTo(self).offset(28); + make.right.equalTo(self).offset(-28); + }]; + + UILabel *title = [UILabel new]; + title.text = @"开启【允许完全访问】,体验完整功能"; + title.font = [UIFont boldSystemFontOfSize:16]; + title.textColor = [UIColor blackColor]; + title.textAlignment = NSTextAlignmentCenter; + [self.card addSubview:title]; + [title mas_makeConstraints:^(MASConstraintMaker *make) { + make.top.equalTo(self.card).offset(16); + make.left.right.equalTo(self.card).insets(UIEdgeInsetsMake(0, 16, 0, 16)); + }]; + + // 模拟两行开关(纯展示,不真实控制) + UIView *box = [UIView new]; + box.backgroundColor = [UIColor colorWithWhite:0.98 alpha:1.0]; + box.layer.cornerRadius = 12; + [self.card addSubview:box]; + [box mas_makeConstraints:^(MASConstraintMaker *make) { + make.top.equalTo(title.mas_bottom).offset(12); + make.left.equalTo(self.card).offset(16); + make.right.equalTo(self.card).offset(-16); + make.height.mas_equalTo(100); + }]; + + UILabel *row1 = [UILabel new]; row1.text = @"恋爱键盘"; row1.textColor = [UIColor blackColor]; + UILabel *row2 = [UILabel new]; row2.text = @"允许完全访问"; row2.textColor = [UIColor blackColor]; + [box addSubview:row1]; [box addSubview:row2]; + [row1 mas_makeConstraints:^(MASConstraintMaker *make) { make.left.equalTo(box).offset(16); make.top.equalTo(box).offset(14); }]; + UIView *line = [UIView new]; line.backgroundColor = [UIColor colorWithWhite:0.9 alpha:1.0]; + [box addSubview:line]; + [line mas_makeConstraints:^(MASConstraintMaker *make) { make.left.equalTo(box).offset(12); make.right.equalTo(box).offset(-12); make.centerY.equalTo(box); make.height.mas_equalTo(1); }]; + [row2 mas_makeConstraints:^(MASConstraintMaker *make) { make.left.equalTo(box).offset(16); make.bottom.equalTo(box).offset(-14); }]; + + // 右侧绿色开关的装饰 + UIView* (^switchView)(void) = ^UIView *{ + UIView *sw = [UIView new]; + sw.backgroundColor = [UIColor colorWithRed:0.12 green:0.75 blue:0.35 alpha:1.0]; + sw.layer.cornerRadius = 15; + UIView *dot = [UIView new]; + dot.backgroundColor = [UIColor whiteColor]; + dot.layer.cornerRadius = 12; + [sw addSubview:dot]; + [dot mas_makeConstraints:^(MASConstraintMaker *make) { make.centerY.equalTo(sw); make.right.equalTo(sw).offset(-3); make.width.height.mas_equalTo(24); }]; + [sw mas_makeConstraints:^(MASConstraintMaker *make) { make.width.mas_equalTo(52); make.height.mas_equalTo(30); }]; + return sw; + }; + UIView *sw1 = switchView(); UIView *sw2 = switchView(); + [box addSubview:sw1]; [box addSubview:sw2]; + [sw1 mas_makeConstraints:^(MASConstraintMaker *make) { make.right.equalTo(box).offset(-12); make.centerY.equalTo(row1); }]; + [sw2 mas_makeConstraints:^(MASConstraintMaker *make) { make.right.equalTo(box).offset(-12); make.centerY.equalTo(row2); }]; + + UIButton *go = [UIButton buttonWithType:UIButtonTypeSystem]; + go.backgroundColor = [UIColor blackColor]; + [go setTitle:@"去开启" forState:UIControlStateNormal]; + [go setTitleColor:[UIColor whiteColor] forState:UIControlStateNormal]; + go.titleLabel.font = [UIFont boldSystemFontOfSize:18]; + go.layer.cornerRadius = 12; + [go addTarget:self action:@selector(onTapGoEnable) forControlEvents:UIControlEventTouchUpInside]; + [self.card addSubview:go]; + [go mas_makeConstraints:^(MASConstraintMaker *make) { + make.top.equalTo(box.mas_bottom).offset(16); + make.left.equalTo(self.card).offset(16); + make.right.equalTo(self.card).offset(-16); + make.height.mas_equalTo(48); + make.bottom.equalTo(self.card).offset(-16); + }]; +} + +- (void)presentIn:(UIView *)parent { + UIView *container = parent.window ?: parent; + self.frame = container.bounds; + self.alpha = 0; + [container addSubview:self]; + [self mas_makeConstraints:^(MASConstraintMaker *make) { make.edges.equalTo(container); }]; + [UIView animateWithDuration:0.2 animations:^{ self.alpha = 1; }]; +} + +- (void)dismiss { + [UIView animateWithDuration:0.18 animations:^{ self.alpha = 0; } completion:^(BOOL finished) { + [self removeFromSuperview]; + }]; +} + ++ (void)showInView:(UIView *)parent { + if (!parent) return; + // 避免重复 + for (UIView *v in (parent.window ?: parent).subviews) { + if ([v isKindOfClass:[KBFullAccessGuideView class]]) return; + } + [[KBFullAccessGuideView build] presentIn:parent]; +} + ++ (void)dismissFromView:(UIView *)parent { + UIView *container = parent.window ?: parent; + for (UIView *v in container.subviews) { + if ([v isKindOfClass:[KBFullAccessGuideView class]]) { + [(KBFullAccessGuideView *)v dismiss]; + break; + } + } +} + + + +#pragma mark - Actions + +- (UIInputViewController *)kb_findInputController { + UIResponder *res = self; + while (res) { + if ([res isKindOfClass:[UIInputViewController class]]) { + return (UIInputViewController *)res; + } + res = res.nextResponder; + } + return nil; +} + +- (void)onTapGoEnable { + // 在扩展中无法使用 UIApplication。改为委托宿主打开链接: + // 方案:优先拉起主 App 并由主 App 打开设置页,避免宿主拦截。 + UIInputViewController *ivc = [self kb_findInputController]; + if (!ivc) { [self dismiss]; return; } + + // 先尝试 Universal Link(如未配置可改为你的域名),失败再用自定义 scheme。 + NSURL *ul = [NSURL URLWithString:@"https://your.domain/ul/settings?src=kb_extension"]; + void (^fallback)(void) = ^{ + NSURL *scheme = [NSURL URLWithString:@"kbkeyboard://settings?src=kb_extension"]; // 主App在 openURL 中处理 + [ivc.extensionContext openURL:scheme completionHandler:^(__unused BOOL ok2) { + // 无论成功与否,都收起当前提示层,避免遮挡 + [self dismiss]; + }]; + }; + + if (ul) { + [ivc.extensionContext openURL:ul completionHandler:^(BOOL ok) { + if (ok) { [self dismiss]; } + else { fallback(); } + }]; + } else { + fallback(); + } +} +@end diff --git a/CustomKeyboard/View/KBFunctionView.m b/CustomKeyboard/View/KBFunctionView.m index 0166904..08f50a0 100644 --- a/CustomKeyboard/View/KBFunctionView.m +++ b/CustomKeyboard/View/KBFunctionView.m @@ -11,8 +11,11 @@ #import "KBFunctionTagCell.h" #import "Masonry.h" #import +#import "KBFullAccessGuideView.h" static NSString * const kKBFunctionTagCellId = @"KBFunctionTagCellId"; +// 通用链接(替换为你的真实域名 + 路径,并在主App配置 Associated Domains 与 AASA) +static NSString * const kKBUniversalLinkLogin = @"https://your.domain/ul/login"; @interface KBFunctionView () // UI @@ -161,21 +164,28 @@ static NSString * const kKBFunctionTagCellId = @"KBFunctionTagCellId"; return 12.0; } -// 用户点击功能标签:拉起主 App,并由主 App 侧弹出登录浮层 +// 用户点击功能标签:优先 UL 拉起主App,失败再 Scheme;两次都失败则提示开启完全访问 - (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; + + NSURL *ul = [NSURL URLWithString:[NSString stringWithFormat:@"%@?src=functionView&index=%ld&title=%@", kKBUniversalLinkLogin, (long)indexPath.item, encodedTitle]]; + if (!ul) 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. 请到 设置>通用>键盘>键盘>你的键盘 开启“允许完全访问”。"); - } + [ivc.extensionContext openURL:ul completionHandler:^(BOOL ok) { + if (ok) return; // Universal Link 成功 + + NSURL *scheme = [NSURL URLWithString:[NSString stringWithFormat:@"kbkeyboard://login?src=functionView&index=%ld&title=%@", (long)indexPath.item, encodedTitle]]; + [ivc.extensionContext openURL:scheme completionHandler:^(BOOL ok2) { + if (!ok2) { + // 两条路都失败:大概率未开完全访问或宿主拦截。给出指引层。 + dispatch_async(dispatch_get_main_queue(), ^{ [KBFullAccessGuideView showInView:self]; }); + } + }]; }]; }); } diff --git a/keyBoard.xcodeproj/project.pbxproj b/keyBoard.xcodeproj/project.pbxproj index f361945..698d4a4 100644 --- a/keyBoard.xcodeproj/project.pbxproj +++ b/keyBoard.xcodeproj/project.pbxproj @@ -46,6 +46,7 @@ 7A36414DFDA5BEC9B7D2E318 /* Pods_CustomKeyboard.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 2C1092FB2B452F95B15D4263 /* Pods_CustomKeyboard.framework */; }; A1B2C3D42EB0A0A100000001 /* KBFunctionTagCell.m in Sources */ = {isa = PBXBuildFile; fileRef = A1B2C3D32EB0A0A100000001 /* KBFunctionTagCell.m */; }; A1B2C3E22EB0C0A100000001 /* KBNetworkManager.m in Sources */ = {isa = PBXBuildFile; fileRef = A1B2C3E12EB0C0A100000001 /* KBNetworkManager.m */; }; + A1B2C3F42EB35A9900000001 /* KBFullAccessGuideView.m in Sources */ = {isa = PBXBuildFile; fileRef = A1B2C3F22EB35A9900000001 /* KBFullAccessGuideView.m */; }; ECC9EE02174D86E8D792472F /* Pods_keyBoard.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 967065BB5230E43F293B3AF9 /* Pods_keyBoard.framework */; }; /* End PBXBuildFile section */ @@ -154,6 +155,8 @@ A1B2C3D32EB0A0A100000001 /* KBFunctionTagCell.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KBFunctionTagCell.m; sourceTree = ""; }; A1B2C3E02EB0C0A100000001 /* KBNetworkManager.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KBNetworkManager.h; sourceTree = ""; }; A1B2C3E12EB0C0A100000001 /* KBNetworkManager.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KBNetworkManager.m; sourceTree = ""; }; + A1B2C3F12EB35A9900000001 /* KBFullAccessGuideView.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KBFullAccessGuideView.h; sourceTree = ""; }; + A1B2C3F22EB35A9900000001 /* KBFullAccessGuideView.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KBFullAccessGuideView.m; sourceTree = ""; }; B12EC429812407B9F0E67565 /* Pods-CustomKeyboard.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-CustomKeyboard.release.xcconfig"; path = "Target Support Files/Pods-CustomKeyboard/Pods-CustomKeyboard.release.xcconfig"; sourceTree = ""; }; B8CA018AB878499327504AAD /* Pods-CustomKeyboard.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-CustomKeyboard.debug.xcconfig"; path = "Target Support Files/Pods-CustomKeyboard/Pods-CustomKeyboard.debug.xcconfig"; sourceTree = ""; }; F67DDBD716E4E616D8CC2C9C /* Pods-keyBoard.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-keyBoard.debug.xcconfig"; path = "Target Support Files/Pods-keyBoard/Pods-keyBoard.debug.xcconfig"; sourceTree = ""; }; @@ -231,7 +234,9 @@ 04FC95742EB095DE007BD342 /* KBFunctionPasteView.h */, 04FC95752EB095DE007BD342 /* KBFunctionPasteView.m */, A1B2C3D22EB0A0A100000001 /* KBFunctionTagCell.h */, - A1B2C3D32EB0A0A100000001 /* KBFunctionTagCell.m */, + A1B2C3D32EB0A0A100000001 /* KBFunctionTagCell.m */, + A1B2C3F12EB35A9900000001 /* KBFullAccessGuideView.h */, + A1B2C3F22EB35A9900000001 /* KBFullAccessGuideView.m */, 04FC95B02EB0B2CC007BD342 /* KBSettingView.h */, 04FC95B12EB0B2CC007BD342 /* KBSettingView.m */, ); @@ -748,7 +753,8 @@ 04FC95B22EB0B2CC007BD342 /* KBSettingView.m in Sources */, 04FC95702EB09516007BD342 /* KBFunctionView.m in Sources */, 04FC956D2EB054B7007BD342 /* KBKeyboardView.m in Sources */, - 04FC95672EB0546C007BD342 /* KBKey.m in Sources */, + 04FC95672EB0546C007BD342 /* KBKey.m in Sources */, + A1B2C3F42EB35A9900000001 /* KBFullAccessGuideView.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/keyBoard/AppDelegate.m b/keyBoard/AppDelegate.m index 3f842ff..95b54c6 100644 --- a/keyBoard/AppDelegate.m +++ b/keyBoard/AppDelegate.m @@ -70,10 +70,14 @@ static NSString * const kKBKeyboardExtensionBundleId = @"com.keyBoardst.CustomKe - (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"]) { + NSString *urlHost = url.host ?: @""; + NSString *host = [urlHost lowercaseString]; + if ([host isEqualToString:@"login"]) { // kbkeyboard://login [self kb_presentLoginSheetIfNeeded]; return YES; + } else if ([host isEqualToString:@"settings"]) { // kbkeyboard://settings + [self kb_openAppSettings]; + return YES; } return NO; } @@ -89,6 +93,18 @@ static NSString * const kKBKeyboardExtensionBundleId = @"com.keyBoardst.CustomKe [KBLoginSheetViewController presentIfNeededFrom:top]; } +- (void)kb_openAppSettings { + NSURL *url = [NSURL URLWithString:UIApplicationOpenSettingsURLString]; + UIApplication *app = [UIApplication sharedApplication]; + if ([app canOpenURL:url]) { + if (@available(iOS 10.0, *)) { + [app openURL:url options:@{} completionHandler:nil]; + } else { + [app openURL:url]; + } + } +} + - (void)kb_presentPermissionIfNeeded { BOOL enabled = KBIsKeyboardEnabled();