处理ios18从其他app用自己键盘 拉起主app的bug
其他问题
This commit is contained in:
@@ -2,6 +2,10 @@
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>com.apple.developer.associated-domains</key>
|
||||
<array>
|
||||
<string>applinks:app.tknb.net</string>
|
||||
</array>
|
||||
<key>com.apple.security.application-groups</key>
|
||||
<array>
|
||||
<string>group.com.loveKey.nyx</string>
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
#import "KBFullAccessManager.h"
|
||||
#import "KBSkinManager.h"
|
||||
#import "KBSkinInstallBridge.h"
|
||||
#import "KBExtensionAppLauncher.h"
|
||||
#import "KBHostAppLauncher.h"
|
||||
|
||||
// 提前声明一个类别,使编译器在 static 回调中识别 kb_consumePendingShopSkin 方法。
|
||||
@interface KeyboardViewController (KBSkinShopBridge)
|
||||
@@ -213,21 +213,26 @@ static void KBSkinInstallNotificationCallback(CFNotificationCenterRef center,
|
||||
[self showFunctionPanel:NO];
|
||||
return;
|
||||
}
|
||||
|
||||
// // 1. 构造充值入口的通用链接(UL)和自定义 Scheme:
|
||||
// // - 当前复用 /login 路径,通过 entry=recharge 区分场景:
|
||||
// // https://app.tknb.net/ul/login?src=keyboard&entry=recharge
|
||||
// // - 若宿主拒绝或 UL 配置异常,则回退到 kbkeyboardAppExtension://recharge?src=keyboard
|
||||
// NSString *ulStr = [NSString stringWithFormat:@"%@?src=keyboard&entry=recharge", KB_UL_RECHARGE];
|
||||
// NSURL *ul = [NSURL URLWithString:ulStr];
|
||||
//
|
||||
NSString *schemeStr = [NSString stringWithFormat:@"%@://recharge?src=keyboard", KB_APP_SCHEME];
|
||||
NSURL *scheme = [NSURL URLWithString:schemeStr];
|
||||
//
|
||||
// if (!ul && !scheme) { return; }
|
||||
//
|
||||
// 从当前视图作为起点,通过响应链找到 UIApplication 再调起主 App
|
||||
BOOL ok = [KBHostAppLauncher openHostAppURL:scheme fromResponder:self.view];
|
||||
|
||||
// 1. 构造自定义 scheme:kbkeyboardAppExtension://recharge?src=keyboard
|
||||
NSString *urlStr = [NSString stringWithFormat:@"%@://recharge?src=keyboard", KB_APP_SCHEME];
|
||||
NSURL *scheme = [NSURL URLWithString:urlStr];
|
||||
if (!scheme) return;
|
||||
|
||||
// 2. 通过统一工具封装:extensionContext + 响应链兜底
|
||||
[KBExtensionAppLauncher openScheme:scheme
|
||||
usingInputController:self
|
||||
source:self.view
|
||||
completion:^(BOOL success) {
|
||||
if (!success) {
|
||||
[KBHUD showInfo:KBLocalized(@"请切换到主App完成充值")];
|
||||
}
|
||||
}];
|
||||
if (!ok) {
|
||||
// 失败兜底:给个文案提示
|
||||
// 比如:请回到桌面手动打开 XXX App 进行设置/充值
|
||||
}
|
||||
}
|
||||
|
||||
- (void)keyBoardMainViewDidTapSettings:(KBKeyBoardMainView *)keyBoardMainView {
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
#import "Masonry.h"
|
||||
#import "KBResponderUtils.h" // 统一查找 UIInputViewController 的工具
|
||||
#import "KBHUD.h"
|
||||
#import "KBExtensionAppLauncher.h"
|
||||
#import "KBHostAppLauncher.h"
|
||||
|
||||
@interface KBFullAccessGuideView ()
|
||||
@property (nonatomic, strong) UIControl *backdrop;
|
||||
@@ -157,31 +157,20 @@
|
||||
#pragma mark - Actions
|
||||
|
||||
// 工具方法已提取到 KBResponderUtils.h
|
||||
// 打开主 App,引导用户去系统设置开启完全访问:优先 Scheme,失败再试 UL;仍失败则提示手动路径。
|
||||
// 打开主 App,引导用户去系统设置开启完全访问:通过宿主 UIApplication + 自定义 Scheme 拉起。
|
||||
- (void)onTapGoEnable {
|
||||
UIInputViewController *ivc = KBFindInputViewController(self);
|
||||
if (!ivc) { [self dismiss]; return; }
|
||||
// 找不到键盘控制器也可以尝试从自身 responder 链出发
|
||||
UIResponder *start = ivc.view ?: (UIResponder *)self;
|
||||
|
||||
// 自定义 Scheme(App 里在 openURL 中转到设置页)
|
||||
// 统一使用主 App 的自定义 Scheme
|
||||
// 自定义 Scheme(AppDelegate 中处理 kbkeyboardAppExtension://settings)
|
||||
NSURL *scheme = [NSURL URLWithString:[NSString stringWithFormat:@"%@@//settings?src=kb_extension", KB_APP_SCHEME]];
|
||||
// Universal Link(需 AASA/Associated Domains 配置且 KB_UL_BASE 与域名一致)
|
||||
NSURL *ul = [NSURL URLWithString:[NSString stringWithFormat:@"%@?src=kb_extension", KB_UL_SETTINGS]];
|
||||
|
||||
__weak typeof(self) weakSelf = self;
|
||||
[KBExtensionAppLauncher openPrimaryURL:scheme
|
||||
fallbackURL:ul
|
||||
usingInputController:ivc
|
||||
source:self
|
||||
completion:^(BOOL ok) {
|
||||
__strong typeof(weakSelf) strongSelf = weakSelf;
|
||||
if (!strongSelf) return;
|
||||
if (ok) {
|
||||
[strongSelf dismiss];
|
||||
} else {
|
||||
NSString *showInfo = [NSString stringWithFormat:KBLocalized(@"Follow: Settings → General → Keyboard → Keyboards → %@ → Allow Full Access"),AppName];
|
||||
[KBHUD showInfo:showInfo];
|
||||
}
|
||||
}];
|
||||
BOOL ok = [KBHostAppLauncher openHostAppURL:scheme fromResponder:start];
|
||||
if (ok) {
|
||||
[self dismiss];
|
||||
} else {
|
||||
NSString *showInfo = [NSString stringWithFormat:KBLocalized(@"Follow: Settings → General → Keyboard → Keyboards → %@ → Allow Full Access"),AppName];
|
||||
[KBHUD showInfo:showInfo];
|
||||
}
|
||||
}
|
||||
@end
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
#import "KBSkinManager.h"
|
||||
#import "KBAuthManager.h" // 登录态判断(共享钥匙串)
|
||||
#import "KBULBridgeNotification.h" // Darwin 通知常量(UL 已处理)
|
||||
#import "KBExtensionAppLauncher.h"
|
||||
#import "KBHostAppLauncher.h"
|
||||
#import "KBStreamTextView.h" // 流式文本视图
|
||||
#import "KBStreamOverlayView.h" // 带关闭按钮的流式层
|
||||
#import "KBFunctionTagListView.h"
|
||||
@@ -321,7 +321,7 @@ static NSString * const kKBStreamDemoURL = @"http://192.168.1.144:7529/api/demo/
|
||||
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.05 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
|
||||
[ivc.extensionContext openURL:ul completionHandler:^(__unused BOOL ok) {}];
|
||||
});
|
||||
// 双路兜底:500ms 内未收到主 App 确认,则回退到自定义 Scheme
|
||||
// 双路兜底:500ms 内未收到主 App 确认,则回退到自定义 Scheme(通过宿主 UIApplication 打开)
|
||||
self.kb_ulHandledFlag = NO;
|
||||
NSUInteger token = ++self.kb_ulSeq;
|
||||
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.5 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
|
||||
@@ -329,14 +329,15 @@ static NSString * const kKBStreamDemoURL = @"http://192.168.1.144:7529/api/demo/
|
||||
if (self.kb_ulHandledFlag) return; // 主 App 已确认处理
|
||||
NSURL *scheme = [NSURL URLWithString:[NSString stringWithFormat:@"%@://login?src=functionView&index=%ld&title=%@", KB_APP_SCHEME, (long)index, encodedTitle]];
|
||||
if (!scheme) return;
|
||||
[KBExtensionAppLauncher openScheme:scheme
|
||||
usingInputController:ivc
|
||||
source:self
|
||||
completion:^(BOOL success) {
|
||||
if (!success) {
|
||||
[KBHUD showInfo:KBLocalized(@"请切换到主App完成登录")];
|
||||
}
|
||||
}];
|
||||
UIResponder *start = ivc.view ?: (UIResponder *)self;
|
||||
// 让键盘失去焦点
|
||||
[ivc dismissKeyboard];
|
||||
BOOL ok = [KBHostAppLauncher openHostAppURL:scheme fromResponder:start];
|
||||
if (!ok) {
|
||||
[KBHUD showInfo:KBLocalized(@"请切换到主App完成登录")];
|
||||
}else{
|
||||
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
@@ -378,13 +379,16 @@ static void KBULDarwinCallback(CFNotificationCenterRef center, void *observer, C
|
||||
if (!ul) return;
|
||||
|
||||
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.05 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
|
||||
NSURL *scheme = [NSURL URLWithString:[NSString stringWithFormat:@"%@@//login?src=functionView&index=%ld&title=%@", KB_APP_SCHEME, (long)indexPath.item, encodedTitle]];
|
||||
[KBExtensionAppLauncher openPrimaryURL:ul
|
||||
fallbackURL:scheme
|
||||
usingInputController:ivc
|
||||
source:self
|
||||
completion:^(BOOL success) {
|
||||
if (!success) {
|
||||
// 先尝试通过 extensionContext 打开 UL
|
||||
[ivc.extensionContext openURL:ul completionHandler:^(BOOL ok) {
|
||||
if (ok) {
|
||||
return;
|
||||
}
|
||||
// UL 失败时,再通过宿主 UIApplication + 自定义 Scheme 兜底
|
||||
NSURL *scheme = [NSURL URLWithString:[NSString stringWithFormat:@"%@@//login?src=functionView&index=%ld&title=%@", KB_APP_SCHEME, (long)indexPath.item, encodedTitle]];
|
||||
UIResponder *start = ivc.view ?: (UIResponder *)self;
|
||||
BOOL ok2 = [KBHostAppLauncher openHostAppURL:scheme fromResponder:start];
|
||||
if (!ok2) {
|
||||
// 两条路都失败:大概率未开完全访问或宿主拦截。统一交由 Manager 引导。
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
[[KBFullAccessManager shared] ensureFullAccessOrGuideInView:self];
|
||||
@@ -402,6 +406,12 @@ static void KBULDarwinCallback(CFNotificationCenterRef center, void *observer, C
|
||||
// - iOS16+ 会在跨 App 首次读取时自动弹出系统权限弹窗;
|
||||
// - iOS15 及以下不会弹窗,直接返回内容;
|
||||
// 注意:不要在非用户触发的时机主动读取(如 viewDidLoad),否则会造成“立刻弹窗”的体验。
|
||||
// 权限全部打开(键盘已启用 + 完全访问)。在扩展进程中仅需判断“完全访问”。
|
||||
if (![[KBFullAccessManager shared] hasFullAccess]) {
|
||||
// 未开启完全访问:保持原有引导路径
|
||||
[[KBFullAccessManager shared] ensureFullAccessOrGuideInView:self];
|
||||
return;
|
||||
}
|
||||
UIPasteboard *pb = [UIPasteboard generalPasteboard];
|
||||
NSString *text = pb.string; // 读取纯文本(可能触发系统粘贴权限弹窗)
|
||||
|
||||
|
||||
@@ -38,8 +38,10 @@
|
||||
#define KB_UL_BASE @"https://app.tknb.net/ul"
|
||||
#endif
|
||||
|
||||
#define KB_UL_LOGIN KB_UL_BASE @"/login"
|
||||
#define KB_UL_SETTINGS KB_UL_BASE @"/settings"
|
||||
#define KB_UL_LOGIN KB_UL_BASE @"/login"
|
||||
#define KB_UL_SETTINGS KB_UL_BASE @"/settings"
|
||||
// 充值入口的通用链接:当前复用 /login 路径,通过 query 区分(避免额外配置 AASA 路径)
|
||||
#define KB_UL_RECHARGE KB_UL_BASE @"/recharge"
|
||||
|
||||
#endif /* KBConfig_h */
|
||||
|
||||
|
||||
18
Shared/KBHostAppLauncher.h
Normal file
18
Shared/KBHostAppLauncher.h
Normal file
@@ -0,0 +1,18 @@
|
||||
//
|
||||
// KBHostAppLauncher.h
|
||||
// keyBoard
|
||||
//
|
||||
// Created by Mac on 2025/11/24.
|
||||
// 从扩展拉起主app
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface KBHostAppLauncher : NSObject
|
||||
/// 从某个 responder 出发,尝试通过 UIApplication 打开宿主 app 的 URL
|
||||
+ (BOOL)openHostAppURL:(NSURL *)url fromResponder:(UIResponder *)startResponder;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
52
Shared/KBHostAppLauncher.m
Normal file
52
Shared/KBHostAppLauncher.m
Normal file
@@ -0,0 +1,52 @@
|
||||
//
|
||||
// KBHostAppLauncher.m
|
||||
// keyBoard
|
||||
//
|
||||
// Created by Mac on 2025/11/24.
|
||||
//
|
||||
|
||||
// KBHostAppLauncher.m
|
||||
#import "KBHostAppLauncher.h"
|
||||
#import <objc/message.h>
|
||||
|
||||
@implementation KBHostAppLauncher
|
||||
|
||||
+ (BOOL)openHostAppURL:(NSURL *)url fromResponder:(UIResponder *)startResponder {
|
||||
if (!url || !startResponder) return NO;
|
||||
|
||||
UIResponder *responder = startResponder;
|
||||
while (responder) {
|
||||
if ([responder isKindOfClass:[UIApplication class]]) {
|
||||
UIApplication *app = (UIApplication *)responder;
|
||||
|
||||
if (@available(iOS 18.0, *)) {
|
||||
// iOS 18+:用新的 open:options:completionHandler:
|
||||
SEL sel = @selector(openURL:options:completionHandler:);
|
||||
if ([app respondsToSelector:sel]) {
|
||||
// 等价于 [app openURL:url options:@{} completionHandler:nil];
|
||||
void (*func)(id, SEL, NSURL *, NSDictionary *, void(^)(BOOL)) = (void *)objc_msgSend;
|
||||
if (func) {
|
||||
func(app, sel, url, @{}, nil);
|
||||
return YES;
|
||||
}
|
||||
}
|
||||
return NO;
|
||||
} else {
|
||||
// iOS 17-:兼容老的 openURL:
|
||||
#pragma clang diagnostic push
|
||||
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
|
||||
if ([app respondsToSelector:@selector(openURL:)]) {
|
||||
return [app openURL:url];
|
||||
}
|
||||
#pragma clang diagnostic pop
|
||||
return NO;
|
||||
}
|
||||
}
|
||||
responder = responder.nextResponder;
|
||||
}
|
||||
|
||||
return NO;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@@ -25,6 +25,9 @@ typedef NS_ENUM(NSInteger, KBFARecord) {
|
||||
/// 最后一次由扩展上报的“完全访问”状态(来源:扩展运行后写入共享钥匙串)
|
||||
- (KBFARecord)lastKnownFullAccess;
|
||||
|
||||
/// 重置“完全访问”记录为 Unknown(删除共享钥匙串中的记录,用于 App 首次安装/升级时清理旧状态)
|
||||
- (void)resetFullAccessRecord;
|
||||
|
||||
/// 扩展侧:上报“完全访问”状态(写入共享钥匙串,以便 App 读取)
|
||||
- (void)reportFullAccessFromExtension:(BOOL)granted;
|
||||
|
||||
@@ -34,4 +37,3 @@ typedef NS_ENUM(NSInteger, KBFARecord) {
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@ static NSString * const kKBPermAccount = @"full_access"; // 保存一个字节/
|
||||
#pragma mark - App side
|
||||
|
||||
- (BOOL)isKeyboardEnabled {
|
||||
// 与 AppDelegate 中同思路:遍历 activeInputModes,匹配自家扩展 bundle id
|
||||
// 与 AppDelegate 中同思路:遍历 activeInputModes,匹配自家扩展 bundle id 方法2:网上看也有判断Bundle拿到keyboards的方法
|
||||
for (UITextInputMode *mode in [UITextInputMode activeInputModes]) {
|
||||
NSString *identifier = nil;
|
||||
@try { identifier = [mode valueForKey:@"identifier"]; } @catch (__unused NSException *e) { identifier = nil; }
|
||||
@@ -63,6 +63,14 @@ static NSString * const kKBPermAccount = @"full_access"; // 保存一个字节/
|
||||
[self keychainWrite:data];
|
||||
}
|
||||
|
||||
#pragma mark - Reset
|
||||
|
||||
- (void)resetFullAccessRecord {
|
||||
// 删除共享钥匙串中的记录,让 lastKnownFullAccess 回退为 Unknown
|
||||
NSMutableDictionary *query = [self baseKCQuery];
|
||||
SecItemDelete((__bridge CFDictionaryRef)query);
|
||||
}
|
||||
|
||||
#pragma mark - Keychain shared blob
|
||||
|
||||
- (NSMutableDictionary *)baseKCQuery {
|
||||
|
||||
@@ -116,6 +116,13 @@
|
||||
"Apply failed" = "Apply failed";
|
||||
"Open agreement" = "Open agreement";
|
||||
"Shop Mall" = "Shop Mall";
|
||||
"My skin" = "My skin";
|
||||
"my_skin_selected_count" = "Selected: %ld Skins";
|
||||
"Editor" = "Editor";
|
||||
"Cancel" = "Cancel";
|
||||
"Delete" = "Delete";
|
||||
|
||||
|
||||
|
||||
// Skin sample names
|
||||
"极光" = "Aurora";
|
||||
|
||||
@@ -117,6 +117,11 @@
|
||||
"Apply failed" = "应用失败";
|
||||
"Open agreement" = "跳转协议";
|
||||
"Shop Mall" = "购物商城";
|
||||
"My skin" = "我的皮肤";
|
||||
"my_skin_selected_count" = "已选择:%ld 个皮肤";
|
||||
"Editor" = "编辑";
|
||||
"Cancel" = "取消";
|
||||
"Delete" = "删除";
|
||||
|
||||
// 皮肤示例名称
|
||||
"极光" = "极光";
|
||||
|
||||
@@ -38,7 +38,6 @@
|
||||
0459D1B82EBA287900F2D189 /* KBSkinManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 0459D1B62EBA287900F2D189 /* KBSkinManager.m */; };
|
||||
046131112ECF3A6E00A6FADF /* fense.zip in Resources */ = {isa = PBXBuildFile; fileRef = 046131102ECF3A6E00A6FADF /* fense.zip */; };
|
||||
046131142ECF454500A6FADF /* KBKeyPreviewView.m in Sources */ = {isa = PBXBuildFile; fileRef = 046131132ECF454500A6FADF /* KBKeyPreviewView.m */; };
|
||||
046131172ED06FC800A6FADF /* KBExtensionAppLauncher.m in Sources */ = {isa = PBXBuildFile; fileRef = 046131162ED06FC800A6FADF /* KBExtensionAppLauncher.m */; };
|
||||
0477BDF02EBB76E30055D639 /* HomeSheetVC.m in Sources */ = {isa = PBXBuildFile; fileRef = 0477BDEF2EBB76E30055D639 /* HomeSheetVC.m */; };
|
||||
0477BDF32EBB7B850055D639 /* KBDirectionIndicatorView.m in Sources */ = {isa = PBXBuildFile; fileRef = 0477BDF22EBB7B850055D639 /* KBDirectionIndicatorView.m */; };
|
||||
0477BDF72EBC63A80055D639 /* KBTestVC.m in Sources */ = {isa = PBXBuildFile; fileRef = 0477BDF62EBC63A80055D639 /* KBTestVC.m */; };
|
||||
@@ -47,6 +46,8 @@
|
||||
0477BE002EBC6A330055D639 /* HomeRankVC.m in Sources */ = {isa = PBXBuildFile; fileRef = 0477BDFF2EBC6A330055D639 /* HomeRankVC.m */; };
|
||||
0477BE042EBC83130055D639 /* HomeMainVC.m in Sources */ = {isa = PBXBuildFile; fileRef = 0477BE032EBC83130055D639 /* HomeMainVC.m */; };
|
||||
0477BEA22EBCF0000055D639 /* KBTopImageButton.m in Sources */ = {isa = PBXBuildFile; fileRef = 0477BEA12EBCF0000055D639 /* KBTopImageButton.m */; };
|
||||
04791F8E2ED469C0004E8522 /* KBHostAppLauncher.m in Sources */ = {isa = PBXBuildFile; fileRef = 04791F8D2ED469C0004E8522 /* KBHostAppLauncher.m */; };
|
||||
04791F8F2ED469C0004E8522 /* KBHostAppLauncher.m in Sources */ = {isa = PBXBuildFile; fileRef = 04791F8D2ED469C0004E8522 /* KBHostAppLauncher.m */; };
|
||||
047C650D2EBC8A840035E841 /* KBPanModalView.m in Sources */ = {isa = PBXBuildFile; fileRef = 047C650C2EBC8A840035E841 /* KBPanModalView.m */; };
|
||||
047C65102EBCA8DD0035E841 /* HomeRankContentVC.m in Sources */ = {isa = PBXBuildFile; fileRef = 047C650F2EBCA8DD0035E841 /* HomeRankContentVC.m */; };
|
||||
047C65502EBCBA9E0035E841 /* KBShopVC.m in Sources */ = {isa = PBXBuildFile; fileRef = 047C654F2EBCBA9E0035E841 /* KBShopVC.m */; };
|
||||
@@ -233,8 +234,6 @@
|
||||
046131102ECF3A6E00A6FADF /* fense.zip */ = {isa = PBXFileReference; lastKnownFileType = archive.zip; path = fense.zip; sourceTree = "<group>"; };
|
||||
046131122ECF454500A6FADF /* KBKeyPreviewView.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KBKeyPreviewView.h; sourceTree = "<group>"; };
|
||||
046131132ECF454500A6FADF /* KBKeyPreviewView.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KBKeyPreviewView.m; sourceTree = "<group>"; };
|
||||
046131152ED06FC800A6FADF /* KBExtensionAppLauncher.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KBExtensionAppLauncher.h; sourceTree = "<group>"; };
|
||||
046131162ED06FC800A6FADF /* KBExtensionAppLauncher.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KBExtensionAppLauncher.m; sourceTree = "<group>"; };
|
||||
0477BDEE2EBB76E30055D639 /* HomeSheetVC.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = HomeSheetVC.h; sourceTree = "<group>"; };
|
||||
0477BDEF2EBB76E30055D639 /* HomeSheetVC.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = HomeSheetVC.m; sourceTree = "<group>"; };
|
||||
0477BDF12EBB7B850055D639 /* KBDirectionIndicatorView.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KBDirectionIndicatorView.h; sourceTree = "<group>"; };
|
||||
@@ -251,6 +250,8 @@
|
||||
0477BE032EBC83130055D639 /* HomeMainVC.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = HomeMainVC.m; sourceTree = "<group>"; };
|
||||
0477BEA02EBCF0000055D639 /* KBTopImageButton.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KBTopImageButton.h; sourceTree = "<group>"; };
|
||||
0477BEA12EBCF0000055D639 /* KBTopImageButton.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KBTopImageButton.m; sourceTree = "<group>"; };
|
||||
04791F8C2ED469C0004E8522 /* KBHostAppLauncher.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KBHostAppLauncher.h; sourceTree = "<group>"; };
|
||||
04791F8D2ED469C0004E8522 /* KBHostAppLauncher.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KBHostAppLauncher.m; sourceTree = "<group>"; };
|
||||
047C650B2EBC8A840035E841 /* KBPanModalView.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KBPanModalView.h; sourceTree = "<group>"; };
|
||||
047C650C2EBC8A840035E841 /* KBPanModalView.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KBPanModalView.m; sourceTree = "<group>"; };
|
||||
047C650E2EBCA8DD0035E841 /* HomeRankContentVC.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = HomeRankContentVC.h; sourceTree = "<group>"; };
|
||||
@@ -587,8 +588,6 @@
|
||||
0477BD942EBAFF4E0055D639 /* Utils */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
046131152ED06FC800A6FADF /* KBExtensionAppLauncher.h */,
|
||||
046131162ED06FC800A6FADF /* KBExtensionAppLauncher.m */,
|
||||
);
|
||||
path = Utils;
|
||||
sourceTree = "<group>";
|
||||
@@ -1276,6 +1275,8 @@
|
||||
049FB23E2EC4B6EF00FAB05D /* KBULBridgeNotification.m */,
|
||||
04D1F6B02EDFF10A00B12345 /* KBSkinInstallBridge.h */,
|
||||
04D1F6B12EDFF10A00B12345 /* KBSkinInstallBridge.m */,
|
||||
04791F8C2ED469C0004E8522 /* KBHostAppLauncher.h */,
|
||||
04791F8D2ED469C0004E8522 /* KBHostAppLauncher.m */,
|
||||
);
|
||||
path = Shared;
|
||||
sourceTree = "<group>";
|
||||
@@ -1540,7 +1541,7 @@
|
||||
049FB2352EC45C6A00FAB05D /* NetworkStreamHandler.m in Sources */,
|
||||
04FC956A2EB05497007BD342 /* KBKeyButton.m in Sources */,
|
||||
04FC95B22EB0B2CC007BD342 /* KBSettingView.m in Sources */,
|
||||
046131172ED06FC800A6FADF /* KBExtensionAppLauncher.m in Sources */,
|
||||
04791F8E2ED469C0004E8522 /* KBHostAppLauncher.m in Sources */,
|
||||
049FB23B2EC4766700FAB05D /* KBFunctionTagListView.m in Sources */,
|
||||
049FB23C2EC4766700FAB05D /* KBStreamOverlayView.m in Sources */,
|
||||
049FB22F2EC34EB900FAB05D /* KBStreamTextView.m in Sources */,
|
||||
@@ -1573,6 +1574,7 @@
|
||||
04FC95E92EB23B67007BD342 /* KBNetworkManager.m in Sources */,
|
||||
04FC95D22EB1E7AE007BD342 /* MyVC.m in Sources */,
|
||||
04286A032ECB0A1600CE730C /* KBSexSelVC.m in Sources */,
|
||||
04791F8F2ED469C0004E8522 /* KBHostAppLauncher.m in Sources */,
|
||||
047C65582EBCC06D0035E841 /* HomeRankCardCell.m in Sources */,
|
||||
04D1F6B32EDFF10A00B12345 /* KBSkinInstallBridge.m in Sources */,
|
||||
04122F912EC73AF700EF7AB3 /* KBVipPay.m in Sources */,
|
||||
@@ -1710,6 +1712,7 @@
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = B8CA018AB878499327504AAD /* Pods-CustomKeyboard.debug.xcconfig */;
|
||||
buildSettings = {
|
||||
APPLICATION_EXTENSION_API_ONLY = YES;
|
||||
CODE_SIGN_ENTITLEMENTS = CustomKeyboard/CustomKeyboard.entitlements;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
@@ -1743,6 +1746,7 @@
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = B12EC429812407B9F0E67565 /* Pods-CustomKeyboard.release.xcconfig */;
|
||||
buildSettings = {
|
||||
APPLICATION_EXTENSION_API_ONLY = YES;
|
||||
CODE_SIGN_ENTITLEMENTS = CustomKeyboard/CustomKeyboard.entitlements;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
#import "IAPVerifyTransactionObj.h"
|
||||
#import "FGIAPManager.h"
|
||||
#import "KBSexSelVC.h"
|
||||
#import "KBKeyboardPermissionManager.h"
|
||||
|
||||
// 注意:用于判断系统已启用本输入法扩展的 bundle id 需与扩展 target 的
|
||||
// PRODUCT_BUNDLE_IDENTIFIER 完全一致。
|
||||
@@ -37,6 +38,15 @@
|
||||
/// 2:配置国际化
|
||||
[KBLocalizationManager shared].supportedLanguageCodes = @[ @"en", @"zh-Hans"];
|
||||
|
||||
// 首次安装/升级:重置“完全访问”记录,避免继承旧安装遗留在 Keychain 中的状态
|
||||
static NSString *const kKBFullAccessRecordInitializedKey = @"KBFullAccessRecordInitialized";
|
||||
NSUserDefaults *ud = [NSUserDefaults standardUserDefaults];
|
||||
if (![ud boolForKey:kKBFullAccessRecordInitializedKey]) {
|
||||
[[KBKeyboardPermissionManager shared] resetFullAccessRecord];
|
||||
[ud setBool:YES forKey:kKBFullAccessRecordInitializedKey];
|
||||
[ud synchronize];
|
||||
}
|
||||
|
||||
// 首次安装先进入性别选择页;点击 Skip 或确认后再进入主 TabBar
|
||||
BOOL hasShownSexVC = [[NSUserDefaults standardUserDefaults] boolForKey:KBSexSelectShownKey];
|
||||
if (hasShownSexVC) {
|
||||
@@ -110,13 +120,31 @@
|
||||
NSString *host = url.host.lowercaseString ?: @"";
|
||||
if ([host hasSuffix:@"app.tknb.net"]) {
|
||||
NSString *path = url.path.lowercaseString ?: @"";
|
||||
if ([path hasPrefix:@"/ul/settings"]) { [self kb_openAppSettings]; return YES; }
|
||||
if ([path hasPrefix:@"/ul/settings"]) {
|
||||
[self kb_openAppSettings];
|
||||
return YES;
|
||||
}
|
||||
if ([path hasPrefix:@"/ul/login"]) {
|
||||
// 区分普通登录与“充值”场景:通过 query 中的 entry=recharge 来判断
|
||||
NSURLComponents *components = [NSURLComponents componentsWithURL:url resolvingAgainstBaseURL:NO];
|
||||
NSString *entry = nil;
|
||||
for (NSURLQueryItem *item in components.queryItems ?: @[]) {
|
||||
if ([item.name isEqualToString:@"entry"]) {
|
||||
entry = item.value;
|
||||
break;
|
||||
}
|
||||
}
|
||||
// 通知扩展:UL 已被主 App 接收,扩展可取消回退到 Scheme
|
||||
CFNotificationCenterPostNotification(CFNotificationCenterGetDarwinNotifyCenter(),
|
||||
(__bridge CFStringRef)KBDarwinULHandled,
|
||||
NULL, NULL, true);
|
||||
[self kb_presentLoginSheetIfNeeded];
|
||||
if ([entry isEqualToString:@"recharge"]) {
|
||||
// 充值入口:拉起主 App 后进入充值相关页面(目前先做占位提示)
|
||||
[KBHUD showInfo:@"去充值"];
|
||||
} else {
|
||||
// 默认逻辑:登录
|
||||
[self kb_presentLoginSheetIfNeeded];
|
||||
}
|
||||
return YES;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -61,7 +61,7 @@
|
||||
|
||||
// 测试储存Token
|
||||
// [[KBAuthManager shared] saveAccessToken:@"TEST" refreshToken:nil expiryDate:[NSDate dateWithTimeIntervalSinceNow:3600] userIdentifier:nil];
|
||||
[[KBAuthManager shared] signOut];
|
||||
// [[KBAuthManager shared] signOut];
|
||||
}
|
||||
|
||||
- (void)setupTabbarAppearance{
|
||||
|
||||
@@ -138,6 +138,10 @@
|
||||
_q1Label.font = [UIFont systemFontOfSize:16 weight:UIFontWeightSemibold];
|
||||
_q1Label.textColor = [UIColor blackColor];
|
||||
_q1Label.text = KBLocalized(@"What are you doing?");
|
||||
// 支持点击复制
|
||||
_q1Label.userInteractionEnabled = YES;
|
||||
UITapGestureRecognizer *tap = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(kb_onTapQ1)];
|
||||
[_q1Label addGestureRecognizer:tap];
|
||||
}
|
||||
return _q1Label;
|
||||
}
|
||||
@@ -148,8 +152,29 @@
|
||||
_q2Label.font = [UIFont systemFontOfSize:16 weight:UIFontWeightSemibold];
|
||||
_q2Label.textColor = [UIColor blackColor];
|
||||
_q2Label.text = KBLocalized(@"I'm going to take a shower.");
|
||||
// 支持点击复制
|
||||
_q2Label.userInteractionEnabled = YES;
|
||||
UITapGestureRecognizer *tap = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(kb_onTapQ2)];
|
||||
[_q2Label addGestureRecognizer:tap];
|
||||
}
|
||||
return _q2Label;
|
||||
}
|
||||
|
||||
/// 复制统一处理
|
||||
- (void)kb_copyTextToPasteboard:(NSString *)text {
|
||||
if (text.length == 0) { return; }
|
||||
UIPasteboard.generalPasteboard.string = text;
|
||||
[KBHUD showInfo:KBLocalized(@"Copy Success")];
|
||||
}
|
||||
|
||||
/// 点击第一条示例文案
|
||||
- (void)kb_onTapQ1 {
|
||||
[self kb_copyTextToPasteboard:self.q1Label.text];
|
||||
}
|
||||
|
||||
/// 点击第二条示例文案
|
||||
- (void)kb_onTapQ2 {
|
||||
[self kb_copyTextToPasteboard:self.q2Label.text];
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@@ -50,7 +50,7 @@
|
||||
|
||||
[self.titleLabel mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||
make.centerX.equalTo(self.cardImageView);
|
||||
make.top.equalTo(self.cardImageView).offset(84);
|
||||
make.top.equalTo(self.cardImageView).offset(KBFit(95));
|
||||
}];
|
||||
|
||||
}
|
||||
|
||||
@@ -44,7 +44,7 @@
|
||||
make.centerY.equalTo(self.titleLabel);
|
||||
make.right.equalTo(self).offset(-20);
|
||||
make.height.mas_equalTo(34);
|
||||
make.width.mas_greaterThanOrEqualTo(115);
|
||||
make.width.mas_greaterThanOrEqualTo(110);
|
||||
}];
|
||||
[self.avatarView mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||
make.left.equalTo(self).offset(26);
|
||||
@@ -118,7 +118,7 @@
|
||||
if (!_keyboardBtn) {
|
||||
_keyboardBtn = [UIButton buttonWithType:UIButtonTypeCustom];
|
||||
[_keyboardBtn setTitle:KBLocalized(@"My Keyboard") forState:UIControlStateNormal];
|
||||
_keyboardBtn.titleLabel.font = [UIFont systemFontOfSize:10 weight:UIFontWeightSemibold];
|
||||
_keyboardBtn.titleLabel.font = [UIFont systemFontOfSize:12 weight:UIFontWeightSemibold];
|
||||
[_keyboardBtn setTitleColor:[UIColor whiteColor] forState:UIControlStateNormal];
|
||||
_keyboardBtn.backgroundColor = [UIColor colorWithHex:KBColorValue];
|
||||
_keyboardBtn.layer.cornerRadius = 17;
|
||||
|
||||
@@ -32,10 +32,10 @@ static NSString * const kMySkinCellId = @"kMySkinCellId";
|
||||
self.view.backgroundColor = [UIColor whiteColor];
|
||||
|
||||
// self.title = @"My Skin"; // 标题
|
||||
|
||||
self.kb_titleLabel.text = KBLocalized(@"My skin");
|
||||
// 右上角 Editor/Cancel 使用 BaseViewController 自定义导航栏的 kb_rightButton
|
||||
self.kb_rightButton.hidden = NO;
|
||||
[self.kb_rightButton setTitle:@"Editor" forState:UIControlStateNormal];
|
||||
[self.kb_rightButton setTitle:KBLocalized(@"Editor") forState:UIControlStateNormal];
|
||||
[self.kb_rightButton removeTarget:nil action:NULL forControlEvents:UIControlEventAllEvents];
|
||||
[self.kb_rightButton addTarget:self action:@selector(onToggleEdit) forControlEvents:UIControlEventTouchUpInside];
|
||||
|
||||
@@ -49,13 +49,14 @@ static NSString * const kMySkinCellId = @"kMySkinCellId";
|
||||
[self.collectionView mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||
make.top.equalTo(self.view).offset(KB_NAV_TOTAL_HEIGHT);
|
||||
make.left.right.equalTo(self.view);
|
||||
make.bottom.equalTo(self.view.mas_bottom);
|
||||
make.bottom.equalTo(self.bottomView.mas_top);
|
||||
}];
|
||||
|
||||
[self.bottomView mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||
make.left.right.equalTo(self.view);
|
||||
make.bottom.equalTo(self.view.mas_safeAreaLayoutGuideBottom);
|
||||
make.height.mas_equalTo(64);
|
||||
make.bottom.equalTo(self.view.mas_bottom);
|
||||
// 初始未编辑状态:高度为 0,不占据空间
|
||||
make.height.mas_equalTo(0);
|
||||
}];
|
||||
|
||||
// 空态视图(LYEmptyView)统一样式 + 重试按钮
|
||||
@@ -108,11 +109,21 @@ static NSString * const kMySkinCellId = @"kMySkinCellId";
|
||||
self.editingMode = !self.editingMode;
|
||||
|
||||
// 更新顶部按钮
|
||||
[self.kb_rightButton setTitle:(self.isEditingMode ? @"Cancel" : @"Editor")
|
||||
[self.kb_rightButton setTitle:(self.isEditingMode ? KBLocalized(@"Cancel") : KBLocalized(@"Editor"))
|
||||
forState:UIControlStateNormal];
|
||||
|
||||
// 控制底部栏显隐
|
||||
self.bottomView.hidden = !self.isEditingMode;
|
||||
// 根据编辑态更新底部栏高度,并保持列表底部始终贴着 bottomView 顶部
|
||||
CGFloat targetHeight = self.isEditingMode ? (KB_SAFE_BOTTOM + 44.0) : 0.0;
|
||||
// 展开前先确保可见,动画结束后再根据状态隐藏
|
||||
self.bottomView.hidden = NO;
|
||||
[self.bottomView mas_updateConstraints:^(MASConstraintMaker *make) {
|
||||
make.height.mas_equalTo(targetHeight);
|
||||
}];
|
||||
[UIView animateWithDuration:0.25 animations:^{
|
||||
[self.view layoutIfNeeded];
|
||||
} completion:^(BOOL finished) {
|
||||
self.bottomView.hidden = !self.isEditingMode;
|
||||
}];
|
||||
|
||||
// 列表进入/退出多选
|
||||
self.collectionView.allowsMultipleSelection = self.isEditingMode;
|
||||
@@ -155,7 +166,8 @@ static NSString * const kMySkinCellId = @"kMySkinCellId";
|
||||
|
||||
- (void)updateBottomUI {
|
||||
NSInteger count = self.collectionView.indexPathsForSelectedItems.count;
|
||||
self.selectedLabel.text = [NSString stringWithFormat:@"Selected: %ld Skins", (long)count];
|
||||
NSString *format = KBLocalized(@"my_skin_selected_count");
|
||||
self.selectedLabel.text = [NSString stringWithFormat:format, (long)count];
|
||||
|
||||
BOOL enable = count > 0;
|
||||
self.deleteButton.enabled = enable;
|
||||
@@ -253,11 +265,12 @@ static NSString * const kMySkinCellId = @"kMySkinCellId";
|
||||
|
||||
[self.selectedLabel mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||
make.left.equalTo(_bottomView).offset(16);
|
||||
make.centerY.equalTo(_bottomView);
|
||||
// 固定在安全区内的 64 高度中居中(顶部向下 32)
|
||||
make.centerY.equalTo(_bottomView.mas_top).offset(32);
|
||||
}];
|
||||
[self.deleteButton mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||
make.right.equalTo(_bottomView).offset(-16);
|
||||
make.centerY.equalTo(_bottomView);
|
||||
make.centerY.equalTo(_bottomView.mas_top).offset(32);
|
||||
make.width.mas_equalTo(92);
|
||||
make.height.mas_equalTo(36);
|
||||
}];
|
||||
@@ -270,7 +283,7 @@ static NSString * const kMySkinCellId = @"kMySkinCellId";
|
||||
_selectedLabel = [UILabel new];
|
||||
_selectedLabel.textColor = [UIColor colorWithHex:0x666666];
|
||||
_selectedLabel.font = [UIFont systemFontOfSize:14 weight:UIFontWeightMedium];
|
||||
_selectedLabel.text = @"Selected: 0 Skins";
|
||||
// _selectedLabel.text = @"Selected: 0 Skins";
|
||||
}
|
||||
return _selectedLabel;
|
||||
}
|
||||
@@ -278,7 +291,7 @@ static NSString * const kMySkinCellId = @"kMySkinCellId";
|
||||
- (UIButton *)deleteButton {
|
||||
if (!_deleteButton) {
|
||||
_deleteButton = [UIButton buttonWithType:UIButtonTypeSystem];
|
||||
[_deleteButton setTitle:@"Delete" forState:UIControlStateNormal];
|
||||
[_deleteButton setTitle:KBLocalized(@"Delete") forState:UIControlStateNormal];
|
||||
_deleteButton.titleLabel.font = [UIFont systemFontOfSize:16 weight:UIFontWeightSemibold];
|
||||
_deleteButton.layer.cornerRadius = 18;
|
||||
_deleteButton.layer.borderWidth = 1;
|
||||
|
||||
@@ -18,8 +18,10 @@
|
||||
#define KB_UL_BASE @"https://your.domain/ul"
|
||||
#endif
|
||||
|
||||
#define KB_UL_LOGIN KB_UL_BASE @"/login"
|
||||
#define KB_UL_SETTINGS KB_UL_BASE @"/settings"
|
||||
#define KB_UL_LOGIN KB_UL_BASE @"/login"
|
||||
#define KB_UL_SETTINGS KB_UL_BASE @"/settings"
|
||||
// 充值入口的通用链接:当前复用 /login 路径,通过 query 区分(避免额外配置 AASA 路径)
|
||||
#define KB_UL_RECHARGE KB_UL_LOGIN
|
||||
|
||||
#endif /* KBConfig_h */
|
||||
|
||||
|
||||
@@ -18,8 +18,10 @@
|
||||
#define KB_UL_BASE @"https://your.domain/ul"
|
||||
#endif
|
||||
|
||||
#define KB_UL_LOGIN KB_UL_BASE @"/login"
|
||||
#define KB_UL_SETTINGS KB_UL_BASE @"/settings"
|
||||
#define KB_UL_LOGIN KB_UL_BASE @"/login"
|
||||
#define KB_UL_SETTINGS KB_UL_BASE @"/settings"
|
||||
// 充值入口的通用链接:当前复用 /login 路径,通过 query 区分(避免额外配置 AASA 路径)
|
||||
#define KB_UL_RECHARGE KB_UL_LOGIN
|
||||
|
||||
#endif /* KBConfig_h */
|
||||
|
||||
|
||||
Reference in New Issue
Block a user