Compare commits
10 Commits
eb0d3aaa71
...
5af2612ff7
| Author | SHA1 | Date | |
|---|---|---|---|
| 5af2612ff7 | |||
| cac2f13b88 | |||
| edf88721da | |||
| 915b329805 | |||
| 1673a2f4be | |||
| e4cebeac85 | |||
| c7021e382e | |||
| ffea9d2022 | |||
| 90c1e7ff6c | |||
| 59d04bb33c |
11
CustomKeyboard/CustomKeyboard.entitlements
Normal file
11
CustomKeyboard/CustomKeyboard.entitlements
Normal file
@@ -0,0 +1,11 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>keychain-access-groups</key>
|
||||
<array>
|
||||
<string>$(AppIdentifierPrefix)com.keyBoardst.shared</string>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||
|
||||
@@ -12,6 +12,8 @@
|
||||
#import "KBFunctionView.h"
|
||||
#import "KBSettingView.h"
|
||||
#import "Masonry.h"
|
||||
#import "KBAuthManager.h"
|
||||
#import "KBFullAccessManager.h"
|
||||
|
||||
static CGFloat KEYBOARDHEIGHT = 256 + 20;
|
||||
|
||||
@@ -31,6 +33,13 @@ static CGFloat KEYBOARDHEIGHT = 256 + 20;
|
||||
- (void)viewDidLoad {
|
||||
[super viewDidLoad];
|
||||
[self setupUI];
|
||||
// 指定 HUD 的承载视图(扩展里无法取到 App 的 KeyWindow)
|
||||
[KBHUD setContainerView:self.view];
|
||||
// 绑定完全访问管理器,便于统一感知和联动网络开关
|
||||
[[KBFullAccessManager shared] bindInputController:self];
|
||||
__unused id token = [[NSNotificationCenter defaultCenter] addObserverForName:KBFullAccessChangedNotification object:nil queue:[NSOperationQueue mainQueue] usingBlock:^(__unused NSNotification * _Nonnull note) {
|
||||
// 如需,可在此刷新与完全访问相关的 UI
|
||||
}];
|
||||
}
|
||||
|
||||
|
||||
@@ -192,7 +201,10 @@ static CGFloat KEYBOARDHEIGHT = 256 + 20;
|
||||
[super viewDidAppear:animated];
|
||||
if (!_kb_didTriggerLoginDeepLinkOnce) {
|
||||
_kb_didTriggerLoginDeepLinkOnce = YES;
|
||||
[self kb_tryOpenContainerForLoginIfNeeded];
|
||||
// 仅在未登录时尝试拉起主App登录
|
||||
if (!KBAuthManager.shared.isLoggedIn) {
|
||||
[self kb_tryOpenContainerForLoginIfNeeded];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
43
CustomKeyboard/Manager/KBFullAccessManager.h
Normal file
43
CustomKeyboard/Manager/KBFullAccessManager.h
Normal file
@@ -0,0 +1,43 @@
|
||||
//
|
||||
// KBFullAccessManager.h
|
||||
// 统一封装:检测并管理键盘扩展的“允许完全访问”状态
|
||||
//
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
#import <UIKit/UIKit.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
typedef NS_ENUM(NSInteger, KBFullAccessState) {
|
||||
KBFullAccessStateUnknown = 0, // 无法确定(降级处理为未开启)
|
||||
KBFullAccessStateDenied, // 未开启完全访问
|
||||
KBFullAccessStateGranted // 已开启完全访问
|
||||
};
|
||||
|
||||
/// 状态变更通知(仅扩展进程内广播)
|
||||
extern NSNotificationName const KBFullAccessChangedNotification;
|
||||
|
||||
/// 键盘扩展“完全访问”状态管理
|
||||
@interface KBFullAccessManager : NSObject
|
||||
|
||||
+ (instancetype)shared;
|
||||
|
||||
/// 绑定当前的 UIInputViewController(用于调用系统私有选择器 hasFullAccess;按字符串反射,避免编译期引用)
|
||||
- (void)bindInputController:(UIInputViewController *)ivc;
|
||||
|
||||
/// 当前状态(内部做缓存;如需强制刷新,调用 refresh)
|
||||
- (KBFullAccessState)currentState;
|
||||
|
||||
/// 便捷判断
|
||||
- (BOOL)hasFullAccess;
|
||||
|
||||
/// 立即刷新一次状态(若状态有变化会发送 KBFullAccessChangedNotification)
|
||||
- (void)refresh;
|
||||
|
||||
/// 若未开启,则在传入视图上展示引导弹层(使用现有的 KBFullAccessGuideView);返回是否已开启
|
||||
- (BOOL)ensureFullAccessOrGuideInView:(UIView *)parent;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
|
||||
100
CustomKeyboard/Manager/KBFullAccessManager.m
Normal file
100
CustomKeyboard/Manager/KBFullAccessManager.m
Normal file
@@ -0,0 +1,100 @@
|
||||
//
|
||||
// KBFullAccessManager.m
|
||||
//
|
||||
// 统一封装“允许完全访问”检测:
|
||||
// 1) 首选:反射调用 UIInputViewController 的 hasFullAccess(避免直接引用私有 API 标识)
|
||||
// 2) 兜底:无法判断时返回 Unknown(上层可按需降级为 Denied 并提示)
|
||||
//
|
||||
|
||||
#import "KBFullAccessManager.h"
|
||||
#import <objc/message.h>
|
||||
#if __has_include("KBNetworkManager.h")
|
||||
#import "KBNetworkManager.h"
|
||||
#endif
|
||||
#if __has_include("KBKeyboardPermissionManager.h")
|
||||
#import "KBKeyboardPermissionManager.h"
|
||||
#endif
|
||||
|
||||
NSNotificationName const KBFullAccessChangedNotification = @"KBFullAccessChangedNotification";
|
||||
|
||||
@interface KBFullAccessManager ()
|
||||
@property (nonatomic, weak) UIInputViewController *ivc;
|
||||
@property (nonatomic, assign) KBFullAccessState state;
|
||||
@end
|
||||
|
||||
@implementation KBFullAccessManager
|
||||
|
||||
+ (instancetype)shared {
|
||||
static KBFullAccessManager *m; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ m = [KBFullAccessManager new]; });
|
||||
return m;
|
||||
}
|
||||
|
||||
- (instancetype)init {
|
||||
if (self = [super init]) {
|
||||
_state = KBFullAccessStateUnknown;
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)bindInputController:(UIInputViewController *)ivc {
|
||||
self.ivc = ivc;
|
||||
[self refresh];
|
||||
}
|
||||
|
||||
- (KBFullAccessState)currentState { return _state; }
|
||||
|
||||
- (BOOL)hasFullAccess { return self.state == KBFullAccessStateGranted; }
|
||||
|
||||
- (void)refresh {
|
||||
KBFullAccessState newState = [self p_detectFullAccessState];
|
||||
if (newState != self.state) {
|
||||
self.state = newState;
|
||||
[[NSNotificationCenter defaultCenter] postNotificationName:KBFullAccessChangedNotification object:nil];
|
||||
[self p_applySideEffects];
|
||||
}
|
||||
}
|
||||
|
||||
- (BOOL)ensureFullAccessOrGuideInView:(UIView *)parent {
|
||||
[self refresh];
|
||||
if (self.state == KBFullAccessStateGranted) return YES;
|
||||
#if __has_include("KBFullAccessGuideView.h")
|
||||
// 动态引入,避免主 App 编译引用
|
||||
Class guideCls = NSClassFromString(@"KBFullAccessGuideView");
|
||||
if (guideCls && [guideCls respondsToSelector:NSSelectorFromString(@"showInView:")]) {
|
||||
SEL sel = NSSelectorFromString(@"showInView:");
|
||||
((void (*)(id, SEL, UIView *))objc_msgSend)(guideCls, sel, parent);
|
||||
}
|
||||
#endif
|
||||
return NO;
|
||||
}
|
||||
|
||||
#pragma mark - Detect
|
||||
|
||||
// 通过反射调用 hasFullAccess(若系统提供),否则返回 Unknown
|
||||
- (KBFullAccessState)p_detectFullAccessState {
|
||||
UIInputViewController *ivc = self.ivc;
|
||||
if (!ivc) return KBFullAccessStateUnknown;
|
||||
|
||||
SEL sel = NSSelectorFromString(@"hasFullAccess");
|
||||
if ([ivc respondsToSelector:sel]) {
|
||||
BOOL granted = ((BOOL (*)(id, SEL))objc_msgSend)(ivc, sel);
|
||||
return granted ? KBFullAccessStateGranted : KBFullAccessStateDenied;
|
||||
}
|
||||
// 无法判断时标记 Unknown(上层可按需处理为未开启)
|
||||
return KBFullAccessStateUnknown;
|
||||
}
|
||||
|
||||
#pragma mark - Side Effects
|
||||
|
||||
- (void)p_applySideEffects {
|
||||
#if __has_include("KBNetworkManager.h")
|
||||
// 根据完全访问状态切换网络总开关
|
||||
[KBNetworkManager shared].enabled = (self.state == KBFullAccessStateGranted);
|
||||
#endif
|
||||
#if __has_include("KBKeyboardPermissionManager.h")
|
||||
// 上报给主 App:记录最近一次“完全访问”状态(App 将据此决定是否展示引导页)
|
||||
[[KBKeyboardPermissionManager shared] reportFullAccessFromExtension:(self.state == KBFullAccessStateGranted)];
|
||||
#endif
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -5,6 +5,7 @@
|
||||
|
||||
#import "KBNetworkManager.h"
|
||||
#import "AFNetworking.h"
|
||||
#import "KBAuthManager.h"
|
||||
|
||||
NSErrorDomain const KBNetworkErrorDomain = @"com.company.keyboard.network";
|
||||
|
||||
@@ -95,8 +96,10 @@ NSErrorDomain const KBNetworkErrorDomain = @"com.company.keyboard.network";
|
||||
}
|
||||
|
||||
- (void)applyHeaders:(NSDictionary<NSString *,NSString *> *)headers toMutableRequest:(NSMutableURLRequest *)req contentType:(NSString *)contentType {
|
||||
// 合并默认头与局部头,局部覆盖
|
||||
// 合并默认头与局部头,并注入授权头(若可用)。局部覆盖优先。
|
||||
NSMutableDictionary *all = [self.defaultHeaders mutableCopy] ?: [NSMutableDictionary new];
|
||||
NSDictionary *auth = [[KBAuthManager shared] authorizationHeader];
|
||||
[auth enumerateKeysAndObjectsUsingBlock:^(NSString *key, NSString *obj, BOOL *stop) { all[key] = obj; }];
|
||||
if (contentType) all[@"Content-Type"] = contentType;
|
||||
[headers enumerateKeysAndObjectsUsingBlock:^(NSString *key, NSString *obj, BOOL *stop) { all[key] = obj; }];
|
||||
[all enumerateKeysAndObjectsUsingBlock:^(NSString *key, NSString *obj, BOOL *stop) { [req setValue:obj forHTTPHeaderField:key]; }];
|
||||
|
||||
@@ -18,6 +18,8 @@
|
||||
// 公共配置
|
||||
#import "KBConfig.h"
|
||||
#import "Masonry.h"
|
||||
#import "KBHUD.h" // 复用 App 内的 HUD 封装
|
||||
#import "KBLocalizationManager.h" // 复用多语言封装(可在扩展内使用)
|
||||
|
||||
// 通用链接(Universal Links)统一配置
|
||||
// 配置好 AASA 与 Associated Domains 后,只需修改这里即可切换域名/path。
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
#import "Masonry.h"
|
||||
#import <MBProgressHUD.h>
|
||||
#import "KBFullAccessGuideView.h"
|
||||
#import "KBFullAccessManager.h"
|
||||
|
||||
static NSString * const kKBFunctionTagCellId = @"KBFunctionTagCellId";
|
||||
|
||||
@@ -164,6 +165,10 @@ static NSString * const kKBFunctionTagCellId = @"KBFunctionTagCellId";
|
||||
|
||||
// 用户点击功能标签:优先 UL 拉起主App,失败再 Scheme;两次都失败则提示开启完全访问
|
||||
- (void)collectionView:(UICollectionView *)collectionView didSelectItemAtIndexPath:(NSIndexPath *)indexPath {
|
||||
|
||||
[KBHUD showInfo:@"处理中…"];
|
||||
// return;
|
||||
|
||||
UIInputViewController *ivc = [self findInputViewController];
|
||||
if (!ivc) return;
|
||||
|
||||
@@ -180,8 +185,8 @@ static NSString * const kKBFunctionTagCellId = @"KBFunctionTagCellId";
|
||||
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]; });
|
||||
// 两条路都失败:大概率未开完全访问或宿主拦截。统一交由 Manager 引导。
|
||||
dispatch_async(dispatch_get_main_queue(), ^{ [[KBFullAccessManager shared] ensureFullAccessOrGuideInView:self]; });
|
||||
}
|
||||
}];
|
||||
}];
|
||||
|
||||
56
Shared/KBAuthManager.h
Normal file
56
Shared/KBAuthManager.h
Normal file
@@ -0,0 +1,56 @@
|
||||
//
|
||||
// KBAuthManager.h
|
||||
// 主 App 与键盘扩展共享使用
|
||||
//
|
||||
// 通过 Keychain Sharing 统一管理用户登录态(access/refresh token)。
|
||||
// 线程安全;在保存/清空时同时发送进程内通知与 Darwin 跨进程通知,
|
||||
// 以便键盘扩展正运行在其他 App 时也能及时感知变更。
|
||||
//
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
/// Darwin 跨进程通知名称:当令牌更新或清除时发送,用于提示 App/扩展刷新缓存。
|
||||
extern NSString * const kKBDarwinAuthChanged;
|
||||
|
||||
/// 进程内通知(NSNotificationCenter):令牌更新或清除时发送。
|
||||
extern NSNotificationName const KBAuthChangedNotification;
|
||||
|
||||
/// 简单的会话容器;可按需扩展字段。
|
||||
@interface KBAuthSession : NSObject <NSSecureCoding>
|
||||
@property (nonatomic, copy, nullable) NSString *accessToken;
|
||||
@property (nonatomic, copy, nullable) NSString *refreshToken;
|
||||
@property (nonatomic, strong, nullable) NSDate *expiryDate; // 可选:过期时间
|
||||
@property (nonatomic, copy, nullable) NSString *userIdentifier; // 可选:如“用 Apple 登录”的 userIdentifier
|
||||
@end
|
||||
|
||||
/// 基于“共享钥匙串”的鉴权管理器(使用 Keychain Sharing 访问组)。
|
||||
@interface KBAuthManager : NSObject
|
||||
|
||||
+ (instancetype)shared;
|
||||
|
||||
/// 当前会话(内存缓存),在加载/保存/清除后更新。
|
||||
@property (atomic, strong, readonly, nullable) KBAuthSession *current;
|
||||
|
||||
/// 是否已登录:存在 accessToken 且未明显过期(未设置过期时间则只要有 token 即视为已登录)。
|
||||
- (BOOL)isLoggedIn;
|
||||
|
||||
/// 从钥匙串加载到内存;通常首次访问时会自动加载。
|
||||
- (void)reloadFromKeychain;
|
||||
|
||||
/// 保存令牌到“共享钥匙串”并通知观察者。
|
||||
- (BOOL)saveAccessToken:(NSString *)accessToken
|
||||
refreshToken:(nullable NSString *)refreshToken
|
||||
expiryDate:(nullable NSDate *)expiryDate
|
||||
userIdentifier:(nullable NSString *)userIdentifier;
|
||||
|
||||
/// 从钥匙串与内存中清除令牌,并通知观察者。
|
||||
- (void)signOut;
|
||||
|
||||
/// 便捷方法:若存在有效令牌,返回 `Authorization` 请求头;否则返回空字典。
|
||||
- (NSDictionary<NSString *, NSString *> *)authorizationHeader;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
211
Shared/KBAuthManager.m
Normal file
211
Shared/KBAuthManager.m
Normal file
@@ -0,0 +1,211 @@
|
||||
//
|
||||
// KBAuthManager.m
|
||||
//
|
||||
// 关键点:
|
||||
// - 使用固定的 service/account 将 KBAuthSession 序列化后保存到钥匙串;
|
||||
// - 通过 kSecAttrAccessGroup 指定 Keychain Sharing 访问组,实现 App 与扩展共享;
|
||||
// - 保存/清除时发送 Darwin 跨进程通知,便于对端刷新缓存;
|
||||
//
|
||||
|
||||
#import "KBAuthManager.h"
|
||||
#import <Security/Security.h>
|
||||
#import "KBConfig.h" // 需要共享钥匙串访问组常量,见 KBConfig.h 中的说明
|
||||
|
||||
NSString * const kKBDarwinAuthChanged = @"com.keyBoardst.auth.changed";
|
||||
NSNotificationName const KBAuthChangedNotification = @"KBAuthChangedNotification";
|
||||
|
||||
static NSString * const kKBKCService = @"com.keyBoardst.auth"; // 钥匙串 service 名
|
||||
static NSString * const kKBKCAccount = @"session"; // 钥匙串 account 键
|
||||
|
||||
// 用于 Keychain Sharing 的访问组;必须与两个 target 的 entitlements 配置一致。
|
||||
// 示例(Capabilities 中勾选 Keychain Sharing 后的值):
|
||||
// $(AppIdentifierPrefix)com.keyBoardst.shared
|
||||
// 运行时会被展开为:TN6HHV45BB.com.keyBoardst.shared
|
||||
#ifndef KB_KEYCHAIN_ACCESS_GROUP
|
||||
#define KB_KEYCHAIN_ACCESS_GROUP @"TN6HHV45BB.com.keyBoardst.shared"
|
||||
#endif
|
||||
|
||||
// 过期宽限:若过期时间距离当前 <= 该阈值,则视为已过期。
|
||||
static const NSTimeInterval kKBExpiryGrace = 5.0; // 秒
|
||||
|
||||
@implementation KBAuthSession
|
||||
|
||||
+ (BOOL)supportsSecureCoding { return YES; }
|
||||
|
||||
- (void)encodeWithCoder:(NSCoder *)coder {
|
||||
[coder encodeObject:self.accessToken forKey:@"accessToken"];
|
||||
[coder encodeObject:self.refreshToken forKey:@"refreshToken"];
|
||||
[coder encodeObject:self.expiryDate forKey:@"expiryDate"];
|
||||
[coder encodeObject:self.userIdentifier forKey:@"userIdentifier"];
|
||||
}
|
||||
|
||||
- (instancetype)initWithCoder:(NSCoder *)coder {
|
||||
if (self = [super init]) {
|
||||
_accessToken = [coder decodeObjectOfClass:NSString.class forKey:@"accessToken"];
|
||||
_refreshToken = [coder decodeObjectOfClass:NSString.class forKey:@"refreshToken"];
|
||||
_expiryDate = [coder decodeObjectOfClass:NSDate.class forKey:@"expiryDate"];
|
||||
_userIdentifier = [coder decodeObjectOfClass:NSString.class forKey:@"userIdentifier"];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@interface KBAuthManager ()
|
||||
@property (atomic, strong, readwrite, nullable) KBAuthSession *current;
|
||||
@end
|
||||
|
||||
@implementation KBAuthManager
|
||||
|
||||
+ (instancetype)shared {
|
||||
static KBAuthManager *m; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ m = [KBAuthManager new]; });
|
||||
return m;
|
||||
}
|
||||
|
||||
#if DEBUG
|
||||
static inline void KBLog(NSString *fmt, ...) {
|
||||
va_list args; va_start(args, fmt);
|
||||
NSString *msg = [[NSString alloc] initWithFormat:fmt arguments:args];
|
||||
va_end(args);
|
||||
NSLog(@"[KBAuth] %@", msg);
|
||||
}
|
||||
#endif
|
||||
|
||||
- (instancetype)init {
|
||||
if (self = [super init]) {
|
||||
[self reloadFromKeychain];
|
||||
// 监听 Darwin 跨进程通知(App 与扩展之间)
|
||||
CFNotificationCenterAddObserver(CFNotificationCenterGetDarwinNotifyCenter(),
|
||||
(__bridge const void *)(self),
|
||||
KBAuthDarwinCallback,
|
||||
(__bridge CFStringRef)kKBDarwinAuthChanged,
|
||||
NULL,
|
||||
CFNotificationSuspensionBehaviorDeliverImmediately);
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
static void KBAuthDarwinCallback(CFNotificationCenterRef center, void *observer, CFStringRef name, const void *object, CFDictionaryRef userInfo) {
|
||||
KBAuthManager *self = (__bridge KBAuthManager *)observer;
|
||||
[self reloadFromKeychain];
|
||||
}
|
||||
|
||||
- (void)dealloc {
|
||||
CFNotificationCenterRemoveObserver(CFNotificationCenterGetDarwinNotifyCenter(), (__bridge const void *)(self), (__bridge CFStringRef)kKBDarwinAuthChanged, NULL);
|
||||
}
|
||||
|
||||
- (BOOL)isLoggedIn {
|
||||
KBAuthSession *s = self.current;
|
||||
if (s.accessToken.length == 0) return NO;
|
||||
if (!s.expiryDate) return YES; // 未设置过期时间时,只要有 token 即视为已登录
|
||||
return ([s.expiryDate timeIntervalSinceNow] > kKBExpiryGrace);
|
||||
}
|
||||
|
||||
#pragma mark - Public
|
||||
|
||||
- (void)reloadFromKeychain {
|
||||
NSData *data = [self keychainRead];
|
||||
KBAuthSession *session = nil;
|
||||
if (data.length > 0) {
|
||||
@try {
|
||||
session = [NSKeyedUnarchiver unarchivedObjectOfClass:KBAuthSession.class fromData:data error:NULL];
|
||||
} @catch (__unused NSException *e) { session = nil; }
|
||||
}
|
||||
self.current = session;
|
||||
[[NSNotificationCenter defaultCenter] postNotificationName:KBAuthChangedNotification object:nil]; // 进程内通知
|
||||
}
|
||||
|
||||
- (BOOL)saveAccessToken:(NSString *)accessToken
|
||||
refreshToken:(NSString *)refreshToken
|
||||
expiryDate:(NSDate *)expiryDate
|
||||
userIdentifier:(NSString *)userIdentifier {
|
||||
KBAuthSession *s = [KBAuthSession new];
|
||||
s.accessToken = accessToken ?: @"";
|
||||
s.refreshToken = refreshToken;
|
||||
s.expiryDate = expiryDate;
|
||||
s.userIdentifier = userIdentifier;
|
||||
|
||||
NSError *err = nil;
|
||||
NSData *data = [NSKeyedArchiver archivedDataWithRootObject:s requiringSecureCoding:YES error:&err];
|
||||
if (err || data.length == 0) return NO;
|
||||
|
||||
BOOL ok = [self keychainWrite:data];
|
||||
if (ok) {
|
||||
self.current = s;
|
||||
// 进程内通知
|
||||
[[NSNotificationCenter defaultCenter] postNotificationName:KBAuthChangedNotification object:nil];
|
||||
// 跨进程通知(App <-> 扩展)
|
||||
CFNotificationCenterPostNotification(CFNotificationCenterGetDarwinNotifyCenter(), (__bridge CFStringRef)kKBDarwinAuthChanged, NULL, NULL, true);
|
||||
}
|
||||
return ok;
|
||||
}
|
||||
|
||||
- (void)signOut {
|
||||
[self keychainDelete];
|
||||
self.current = nil;
|
||||
[[NSNotificationCenter defaultCenter] postNotificationName:KBAuthChangedNotification object:nil];
|
||||
CFNotificationCenterPostNotification(CFNotificationCenterGetDarwinNotifyCenter(), (__bridge CFStringRef)kKBDarwinAuthChanged, NULL, NULL, true);
|
||||
}
|
||||
|
||||
- (NSDictionary<NSString *,NSString *> *)authorizationHeader {
|
||||
NSString *t = self.current.accessToken;
|
||||
if (t.length == 0) return @{}; // 未登录返回空头部
|
||||
return @{ @"Authorization": [@"Bearer " stringByAppendingString:t] };
|
||||
}
|
||||
|
||||
#pragma mark - Keychain (shared)
|
||||
|
||||
- (NSMutableDictionary *)baseKCQuery {
|
||||
NSMutableDictionary *q = [@{ (__bridge id)kSecClass: (__bridge id)kSecClassGenericPassword,
|
||||
(__bridge id)kSecAttrService: kKBKCService,
|
||||
(__bridge id)kSecAttrAccount: kKBKCAccount } mutableCopy];
|
||||
// 指定共享访问组(App 与扩展共用同一组)
|
||||
q[(__bridge id)kSecAttrAccessGroup] = KB_KEYCHAIN_ACCESS_GROUP;
|
||||
return q;
|
||||
}
|
||||
|
||||
- (BOOL)keychainWrite:(NSData *)data {
|
||||
if (!data) return NO;
|
||||
NSMutableDictionary *query = [self baseKCQuery];
|
||||
SecItemDelete((__bridge CFDictionaryRef)query);
|
||||
|
||||
// 设置属性
|
||||
query[(__bridge id)kSecValueData] = data;
|
||||
// 访问控制:设备首次解锁后可读,不随备份迁移
|
||||
query[(__bridge id)kSecAttrAccessible] = (__bridge id)kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly;
|
||||
|
||||
OSStatus status = SecItemAdd((__bridge CFDictionaryRef)query, NULL);
|
||||
#if DEBUG
|
||||
if (status != errSecSuccess) {
|
||||
KBLog(@"SecItemAdd failed status=%ld group=%@", (long)status, KB_KEYCHAIN_ACCESS_GROUP);
|
||||
} else {
|
||||
KBLog(@"SecItemAdd ok group=%@", KB_KEYCHAIN_ACCESS_GROUP);
|
||||
}
|
||||
#endif
|
||||
return (status == errSecSuccess);
|
||||
}
|
||||
|
||||
- (NSData *)keychainRead {
|
||||
NSMutableDictionary *query = [self baseKCQuery];
|
||||
query[(__bridge id)kSecReturnData] = @YES;
|
||||
query[(__bridge id)kSecMatchLimit] = (__bridge id)kSecMatchLimitOne;
|
||||
|
||||
CFTypeRef dataRef = NULL;
|
||||
OSStatus status = SecItemCopyMatching((__bridge CFDictionaryRef)query, &dataRef);
|
||||
#if DEBUG
|
||||
if (status != errSecSuccess) {
|
||||
KBLog(@"SecItemCopyMatching status=%ld group=%@ (item not found or no entitlement)", (long)status, KB_KEYCHAIN_ACCESS_GROUP);
|
||||
} else {
|
||||
KBLog(@"SecItemCopyMatching ok group=%@", KB_KEYCHAIN_ACCESS_GROUP);
|
||||
}
|
||||
#endif
|
||||
if (status != errSecSuccess || !dataRef) return nil;
|
||||
return (__bridge_transfer NSData *)dataRef;
|
||||
}
|
||||
|
||||
- (void)keychainDelete {
|
||||
NSDictionary *query = [self baseKCQuery];
|
||||
SecItemDelete((__bridge CFDictionaryRef)query);
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -1,8 +1,8 @@
|
||||
//
|
||||
// KBConfig.h
|
||||
// Shared config/macros for both main app and keyboard extension.
|
||||
// 主 App 与键盘扩展共用的配置/宏。
|
||||
//
|
||||
// Edit these values once and both targets pick them up via PCH import.
|
||||
// 在此处修改后,会通过 PCH 被两个 target 同步引用。
|
||||
//
|
||||
|
||||
#ifndef KBConfig_h
|
||||
@@ -23,3 +23,22 @@
|
||||
|
||||
#endif /* KBConfig_h */
|
||||
|
||||
// --- 认证/共享钥匙串 配置 ---
|
||||
// 若已在 Capabilities 中启用 Keychain Sharing,并添加访问组:
|
||||
// $(AppIdentifierPrefix)com.keyBoardst.shared
|
||||
// 运行时会展开为:TN6HHV45BB.com.keyBoardst.shared
|
||||
// KBAuthManager 通过下面的宏定位访问组;如需修改,可在 Build Settings 或前缀头中覆盖该宏。
|
||||
#ifndef KB_KEYCHAIN_ACCESS_GROUP
|
||||
#define KB_KEYCHAIN_ACCESS_GROUP @"TN6HHV45BB.com.keyBoardst.shared"
|
||||
#endif
|
||||
|
||||
// 键盘扩展的 Bundle Identifier(用于 App 侧检测是否已添加该键盘)
|
||||
#ifndef KB_KEYBOARD_EXTENSION_BUNDLE_ID
|
||||
#define KB_KEYBOARD_EXTENSION_BUNDLE_ID @"com.keyBoardst.CustomKeyboard"
|
||||
#endif
|
||||
|
||||
// --- 常用宏 ---
|
||||
// 弱引用 self(在 block 中避免循环引用):使用处直接写 KBWeakSelf;
|
||||
#ifndef KBWeakSelf
|
||||
#define KBWeakSelf __weak __typeof(self) weakSelf = self;
|
||||
#endif
|
||||
|
||||
37
Shared/KBKeyboardPermissionManager.h
Normal file
37
Shared/KBKeyboardPermissionManager.h
Normal file
@@ -0,0 +1,37 @@
|
||||
//
|
||||
// KBKeyboardPermissionManager.h
|
||||
// 主 App/键盘扩展 共用的“键盘启用 + 完全访问”权限管理
|
||||
//
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
#import <UIKit/UIKit.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
typedef NS_ENUM(NSInteger, KBFARecord) {
|
||||
KBFARecordUnknown = 0,
|
||||
KBFARecordDenied = 1,
|
||||
KBFARecordGranted = 2,
|
||||
};
|
||||
|
||||
/// 统一权限管理(App 与扩展均可使用)
|
||||
@interface KBKeyboardPermissionManager : NSObject
|
||||
|
||||
+ (instancetype)shared;
|
||||
|
||||
/// App 侧:是否已添加并启用了自定义键盘(通过遍历 activeInputModes 粗略判断)
|
||||
- (BOOL)isKeyboardEnabled;
|
||||
|
||||
/// 最后一次由扩展上报的“完全访问”状态(来源:扩展运行后写入共享钥匙串)
|
||||
- (KBFARecord)lastKnownFullAccess;
|
||||
|
||||
/// 扩展侧:上报“完全访问”状态(写入共享钥匙串,以便 App 读取)
|
||||
- (void)reportFullAccessFromExtension:(BOOL)granted;
|
||||
|
||||
/// App 侧:若未满足“已启用键盘 + 完全访问(或未知)”则展示引导页(KBPermissionViewController)
|
||||
- (void)presentPermissionIfNeededFrom:(UIViewController *)presenting;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
|
||||
94
Shared/KBKeyboardPermissionManager.m
Normal file
94
Shared/KBKeyboardPermissionManager.m
Normal file
@@ -0,0 +1,94 @@
|
||||
//
|
||||
// KBKeyboardPermissionManager.m
|
||||
//
|
||||
|
||||
#import "KBKeyboardPermissionManager.h"
|
||||
#import <Security/Security.h>
|
||||
#import "KBConfig.h"
|
||||
|
||||
// Keychain 存储:记录上次扩展上报的“完全访问”状态
|
||||
static NSString * const kKBPermService = @"com.keyBoardst.perm";
|
||||
static NSString * const kKBPermAccount = @"full_access"; // 保存一个字节/数字:0/1/2
|
||||
|
||||
@implementation KBKeyboardPermissionManager
|
||||
|
||||
+ (instancetype)shared {
|
||||
static KBKeyboardPermissionManager *m; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ m = [KBKeyboardPermissionManager new]; });
|
||||
return m;
|
||||
}
|
||||
|
||||
#pragma mark - App side
|
||||
|
||||
- (BOOL)isKeyboardEnabled {
|
||||
// 与 AppDelegate 中同思路:遍历 activeInputModes,匹配自家扩展 bundle id
|
||||
for (UITextInputMode *mode in [UITextInputMode activeInputModes]) {
|
||||
NSString *identifier = nil;
|
||||
@try { identifier = [mode valueForKey:@"identifier"]; } @catch (__unused NSException *e) { identifier = nil; }
|
||||
if ([identifier isKindOfClass:NSString.class] && [identifier rangeOfString:KB_KEYBOARD_EXTENSION_BUNDLE_ID].location != NSNotFound) {
|
||||
return YES;
|
||||
}
|
||||
}
|
||||
return NO;
|
||||
}
|
||||
|
||||
- (KBFARecord)lastKnownFullAccess {
|
||||
NSData *data = [self keychainRead];
|
||||
if (data.length == 0) return KBFARecordUnknown;
|
||||
uint8_t v = 0; [data getBytes:&v length:1];
|
||||
if (v > KBFARecordGranted) v = KBFARecordUnknown;
|
||||
return (KBFARecord)v;
|
||||
}
|
||||
|
||||
- (void)presentPermissionIfNeededFrom:(UIViewController *)presenting {
|
||||
BOOL enabled = [self isKeyboardEnabled];
|
||||
KBFARecord fa = [self lastKnownFullAccess];
|
||||
// 策略:
|
||||
// - 未启用键盘:一定引导;
|
||||
// - 已启用键盘:仅当明确知道“完全访问被拒绝”才引导;Unknown 不打扰(等待扩展上报)。
|
||||
BOOL needGuide = (!enabled) || (enabled && fa == KBFARecordDenied);
|
||||
if (!needGuide || !presenting) return;
|
||||
|
||||
Class cls = NSClassFromString(@"KBPermissionViewController");
|
||||
if (!cls) return; // 主 App 才存在该类
|
||||
UIViewController *guide = [cls new];
|
||||
guide.modalPresentationStyle = UIModalPresentationFullScreen;
|
||||
[presenting presentViewController:guide animated:YES completion:nil];
|
||||
}
|
||||
|
||||
#pragma mark - Extension side
|
||||
|
||||
- (void)reportFullAccessFromExtension:(BOOL)granted {
|
||||
uint8_t v = granted ? KBFARecordGranted : KBFARecordDenied;
|
||||
NSData *data = [NSData dataWithBytes:&v length:1];
|
||||
[self keychainWrite:data];
|
||||
}
|
||||
|
||||
#pragma mark - Keychain shared blob
|
||||
|
||||
- (NSMutableDictionary *)baseKCQuery {
|
||||
NSMutableDictionary *q = [@{ (__bridge id)kSecClass: (__bridge id)kSecClassGenericPassword,
|
||||
(__bridge id)kSecAttrService: kKBPermService,
|
||||
(__bridge id)kSecAttrAccount: kKBPermAccount } mutableCopy];
|
||||
q[(__bridge id)kSecAttrAccessGroup] = KB_KEYCHAIN_ACCESS_GROUP;
|
||||
return q;
|
||||
}
|
||||
|
||||
- (BOOL)keychainWrite:(NSData *)data {
|
||||
NSMutableDictionary *query = [self baseKCQuery];
|
||||
SecItemDelete((__bridge CFDictionaryRef)query);
|
||||
query[(__bridge id)kSecValueData] = data ?: [NSData data];
|
||||
query[(__bridge id)kSecAttrAccessible] = (__bridge id)kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly;
|
||||
OSStatus st = SecItemAdd((__bridge CFDictionaryRef)query, NULL);
|
||||
return (st == errSecSuccess);
|
||||
}
|
||||
|
||||
- (NSData *)keychainRead {
|
||||
NSMutableDictionary *query = [self baseKCQuery];
|
||||
query[(__bridge id)kSecReturnData] = @YES;
|
||||
query[(__bridge id)kSecMatchLimit] = (__bridge id)kSecMatchLimitOne;
|
||||
CFTypeRef dataRef = NULL; OSStatus st = SecItemCopyMatching((__bridge CFDictionaryRef)query, &dataRef);
|
||||
if (st != errSecSuccess || !dataRef) return nil;
|
||||
return (__bridge_transfer NSData *)dataRef;
|
||||
}
|
||||
|
||||
@end
|
||||
57
Shared/KBLocalizationManager.h
Normal file
57
Shared/KBLocalizationManager.h
Normal file
@@ -0,0 +1,57 @@
|
||||
//
|
||||
// KBLocalizationManager.h
|
||||
// 多语言管理(App 与键盘扩展共用)
|
||||
// 功能:
|
||||
// - 运行时切换语言(不依赖系统设置)
|
||||
// - 可选跨 Target 同步(共享钥匙串),让 App 与扩展语言一致
|
||||
// - 提供便捷宏 KBLocalized(key)
|
||||
//
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
/// 当前语言变更通知(不附带 userInfo)
|
||||
extern NSString * const KBLocalizationDidChangeNotification;
|
||||
|
||||
/// 轻量多语言管理器:支持运行时切换、跨 Target 同步
|
||||
@interface KBLocalizationManager : NSObject
|
||||
|
||||
/// 单例
|
||||
+ (instancetype)shared;
|
||||
|
||||
/// 当前语言代码(如:"en"、"zh-Hans"、"ja")。
|
||||
/// 默认会在受支持语言中,按系统首选语言择优匹配。
|
||||
@property (nonatomic, copy, readonly) NSString *currentLanguageCode;
|
||||
|
||||
/// 支持的语言代码列表。默认 @[@"en", @"zh-Hans"]。
|
||||
/// 建议在启动早期设置;或设置后再调用 `-setCurrentLanguageCode:persist:` 以刷新。
|
||||
@property (nonatomic, copy) NSArray<NSString *> *supportedLanguageCodes;
|
||||
|
||||
/// 设置当前语言。
|
||||
/// @param code 语言代码
|
||||
/// @param persist 是否持久化到共享钥匙串(以便 App 与扩展共享该选择)
|
||||
- (void)setCurrentLanguageCode:(NSString *)code persist:(BOOL)persist;
|
||||
|
||||
/// 清除用户选择,恢复为系统最佳匹配。
|
||||
- (void)resetToSystemLanguage;
|
||||
|
||||
/// 从默认表(Localizable.strings)取文案。
|
||||
- (NSString *)localizedStringForKey:(NSString *)key;
|
||||
|
||||
/// 指定表名(不含扩展名)取文案。
|
||||
- (NSString *)localizedStringForKey:(NSString *)key
|
||||
table:(nullable NSString *)table
|
||||
value:(nullable NSString *)value;
|
||||
|
||||
/// 基于一组“偏好语言”计算最佳支持语言。
|
||||
- (NSString *)bestSupportedLanguageForPreferred:(NSArray<NSString *> *)preferred;
|
||||
|
||||
@end
|
||||
|
||||
/// 便捷宏:与 NSLocalizedString 类似,但遵循 KBLocalizationManager 当前语言
|
||||
#ifndef KBLocalized
|
||||
#define KBLocalized(key) [[KBLocalizationManager shared] localizedStringForKey:(key)]
|
||||
#endif
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
180
Shared/KBLocalizationManager.m
Normal file
180
Shared/KBLocalizationManager.m
Normal file
@@ -0,0 +1,180 @@
|
||||
//
|
||||
// KBLocalizationManager.m
|
||||
// 多语言管理实现
|
||||
//
|
||||
|
||||
#import "KBLocalizationManager.h"
|
||||
#import <Security/Security.h>
|
||||
#import "KBConfig.h"
|
||||
|
||||
/// 语言变更通知名称
|
||||
NSString * const KBLocalizationDidChangeNotification = @"KBLocalizationDidChangeNotification";
|
||||
|
||||
// 通过共享钥匙串跨 Target 持久化语言选择
|
||||
static NSString * const kKBLocService = @"com.keyBoardst.loc";
|
||||
static NSString * const kKBLocAccount = @"lang"; // 保存 UTF8 的语言代码
|
||||
|
||||
@interface KBLocalizationManager ()
|
||||
@property (nonatomic, copy, readwrite) NSString *currentLanguageCode; // 当前语言代码
|
||||
@property (nonatomic, strong) NSBundle *langBundle; // 对应语言的 .lproj 资源包
|
||||
@end
|
||||
|
||||
// 避免 +shared 初始化阶段递归触发自身:
|
||||
// 这里提供一个 C 级别的工具函数,构建钥匙串查询,不依赖实例或 +shared。
|
||||
static inline NSMutableDictionary *KBLocBaseKCQuery(void) {
|
||||
NSMutableDictionary *q = [@{ (__bridge id)kSecClass: (__bridge id)kSecClassGenericPassword,
|
||||
(__bridge id)kSecAttrService: kKBLocService,
|
||||
(__bridge id)kSecAttrAccount: kKBLocAccount } mutableCopy];
|
||||
if (KB_KEYCHAIN_ACCESS_GROUP.length > 0) {
|
||||
q[(__bridge id)kSecAttrAccessGroup] = KB_KEYCHAIN_ACCESS_GROUP;
|
||||
}
|
||||
return q;
|
||||
}
|
||||
|
||||
@implementation KBLocalizationManager
|
||||
|
||||
+ (instancetype)shared {
|
||||
static KBLocalizationManager *m; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{
|
||||
m = [KBLocalizationManager new];
|
||||
// 默认支持语言;可在启动时由外部重置
|
||||
m.supportedLanguageCodes = @[ @"en", @"zh-Hans" ];
|
||||
// 启动读取:先取共享钥匙串,再按系统偏好回退
|
||||
NSString *saved = [[self class] kc_read];
|
||||
if (saved.length == 0) {
|
||||
saved = [m bestSupportedLanguageForPreferred:[NSLocale preferredLanguages]] ?: @"en";
|
||||
}
|
||||
[m applyLanguage:saved];
|
||||
});
|
||||
return m;
|
||||
}
|
||||
|
||||
#pragma mark - 对外 API
|
||||
|
||||
- (void)setSupportedLanguageCodes:(NSArray<NSString *> *)supportedLanguageCodes {
|
||||
// 归一化:去重、去空
|
||||
NSMutableOrderedSet *set = [NSMutableOrderedSet orderedSet];
|
||||
for (NSString *c in supportedLanguageCodes) {
|
||||
if (c.length) { [set addObject:c]; }
|
||||
}
|
||||
_supportedLanguageCodes = set.array.count ? set.array : @[ @"en" ];
|
||||
// 若当前语言不再受支持,则按最佳匹配切回(不持久化,仅内存),并广播变更
|
||||
if (self.currentLanguageCode.length && ![set containsObject:self.currentLanguageCode]) {
|
||||
NSString *best = [self bestSupportedLanguageForPreferred:@[self.currentLanguageCode]];
|
||||
[self applyLanguage:best ?: _supportedLanguageCodes.firstObject];
|
||||
[[NSNotificationCenter defaultCenter] postNotificationName:KBLocalizationDidChangeNotification object:nil];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)setCurrentLanguageCode:(NSString *)code persist:(BOOL)persist {
|
||||
if (code.length == 0) return; // 忽略空值
|
||||
if ([code isEqualToString:self.currentLanguageCode]) return; // 无变更
|
||||
[self applyLanguage:code];
|
||||
if (persist) { [[self class] kc_write:code]; } // 需同步到 App/扩展
|
||||
[[NSNotificationCenter defaultCenter] postNotificationName:KBLocalizationDidChangeNotification object:nil];
|
||||
}
|
||||
|
||||
- (void)resetToSystemLanguage {
|
||||
NSString *best = [self bestSupportedLanguageForPreferred:[NSLocale preferredLanguages]] ?: @"en";
|
||||
[self setCurrentLanguageCode:best persist:NO];
|
||||
}
|
||||
|
||||
- (NSString *)localizedStringForKey:(NSString *)key {
|
||||
return [self localizedStringForKey:key table:nil value:key];
|
||||
}
|
||||
|
||||
- (NSString *)localizedStringForKey:(NSString *)key table:(NSString *)table value:(NSString *)value {
|
||||
if (key.length == 0) return @"";
|
||||
NSBundle *bundle = self.langBundle ?: NSBundle.mainBundle;
|
||||
NSString *tbl = table ?: @"Localizable";
|
||||
// 使用 bundle API,避免 NSLocalizedString 被系统语言钉死
|
||||
NSString *str = [bundle localizedStringForKey:key value:value table:tbl];
|
||||
return str ?: (value ?: key);
|
||||
}
|
||||
|
||||
- (NSString *)bestSupportedLanguageForPreferred:(NSArray<NSString *> *)preferred {
|
||||
if (self.supportedLanguageCodes.count == 0) return @"en";
|
||||
// 1) 完全匹配
|
||||
for (NSString *p in preferred) {
|
||||
NSString *pLC = p.lowercaseString;
|
||||
for (NSString *s in self.supportedLanguageCodes) {
|
||||
if ([pLC isEqualToString:s.lowercaseString]) { return s; }
|
||||
}
|
||||
}
|
||||
// 2) 前缀匹配:如 zh-Hans-CN -> zh-Hans, en-GB -> en
|
||||
for (NSString *p in preferred) {
|
||||
NSString *pLC = p.lowercaseString;
|
||||
for (NSString *s in self.supportedLanguageCodes) {
|
||||
NSString *sLC = s.lowercaseString;
|
||||
if ([pLC hasPrefix:[sLC stringByAppendingString:@"-"]] || [pLC hasPrefix:[sLC stringByAppendingString:@"_"]]) {
|
||||
return s;
|
||||
}
|
||||
// also allow reverse: when supported is regional (rare)
|
||||
if ([sLC hasPrefix:[pLC stringByAppendingString:@"-"]] || [sLC hasPrefix:[pLC stringByAppendingString:@"_"]]) {
|
||||
return s;
|
||||
}
|
||||
}
|
||||
}
|
||||
// 3) 特殊处理中文:将 zh-Hant/zh-TW/zh-HK 映射到 zh-Hant(若受支持)
|
||||
for (NSString *p in preferred) {
|
||||
NSString *pLC = p.lowercaseString;
|
||||
if ([pLC hasPrefix:@"zh-hant"] || [pLC hasPrefix:@"zh-tw"] || [pLC hasPrefix:@"zh-hk"]) {
|
||||
for (NSString *s in self.supportedLanguageCodes) {
|
||||
if ([s.lowercaseString isEqualToString:@"zh-hant"]) { return s; }
|
||||
}
|
||||
}
|
||||
if ([pLC hasPrefix:@"zh-hans"] || [pLC hasPrefix:@"zh-cn"]) {
|
||||
for (NSString *s in self.supportedLanguageCodes) {
|
||||
if ([s.lowercaseString isEqualToString:@"zh-hans"]) { return s; }
|
||||
}
|
||||
}
|
||||
}
|
||||
// 4) 兜底:取第一个受支持语言
|
||||
return self.supportedLanguageCodes.firstObject ?: @"en";
|
||||
}
|
||||
|
||||
#pragma mark - 内部实现
|
||||
|
||||
- (void)applyLanguage:(NSString *)code {
|
||||
_currentLanguageCode = [code copy];
|
||||
// 基于当前 Target(App 或扩展)的主 bundle 加载 .lproj 资源
|
||||
NSString *path = [NSBundle.mainBundle pathForResource:code ofType:@"lproj"];
|
||||
if (!path) {
|
||||
// 尝试去区域后缀:如 en-GB -> en
|
||||
NSString *shortCode = [[code componentsSeparatedByCharactersInSet:[NSCharacterSet characterSetWithCharactersInString:@"-_"]] firstObject];
|
||||
if (shortCode.length > 0) {
|
||||
path = [NSBundle.mainBundle pathForResource:shortCode ofType:@"lproj"];
|
||||
}
|
||||
}
|
||||
if (path) {
|
||||
self.langBundle = [NSBundle bundleWithPath:path];
|
||||
} else {
|
||||
self.langBundle = NSBundle.mainBundle; // 兜底
|
||||
}
|
||||
}
|
||||
|
||||
#pragma mark - 钥匙串读写(App/扩展共享)
|
||||
|
||||
+ (BOOL)kc_write:(NSString *)lang {
|
||||
NSMutableDictionary *q = KBLocBaseKCQuery();
|
||||
SecItemDelete((__bridge CFDictionaryRef)q);
|
||||
if (lang.length == 0) return YES; // 等价于删除
|
||||
NSData *data = [lang dataUsingEncoding:NSUTF8StringEncoding];
|
||||
q[(__bridge id)kSecValueData] = data ?: [NSData data];
|
||||
q[(__bridge id)kSecAttrAccessible] = (__bridge id)kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly;
|
||||
OSStatus st = SecItemAdd((__bridge CFDictionaryRef)q, NULL);
|
||||
return (st == errSecSuccess);
|
||||
}
|
||||
|
||||
+ (NSString *)kc_read {
|
||||
NSMutableDictionary *q = KBLocBaseKCQuery();
|
||||
q[(__bridge id)kSecReturnData] = @YES;
|
||||
q[(__bridge id)kSecMatchLimit] = (__bridge id)kSecMatchLimitOne;
|
||||
CFTypeRef dataRef = NULL; OSStatus st = SecItemCopyMatching((__bridge CFDictionaryRef)q, &dataRef);
|
||||
if (st != errSecSuccess || !dataRef) return nil;
|
||||
NSData *data = (__bridge_transfer NSData *)dataRef;
|
||||
if (data.length == 0) return nil;
|
||||
NSString *lang = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
|
||||
return lang;
|
||||
}
|
||||
|
||||
@end
|
||||
20
Shared/Localization/en.lproj/Localizable.strings
Normal file
20
Shared/Localization/en.lproj/Localizable.strings
Normal file
@@ -0,0 +1,20 @@
|
||||
/*
|
||||
Localizable.strings (English)
|
||||
Keys shared by App and Keyboard extension
|
||||
*/
|
||||
|
||||
"perm_title_enable" = "Enable Keyboard";
|
||||
"perm_steps" = "1 Enable Keyboard > 2 Allow Full Access";
|
||||
"perm_open_settings" = "Open in Settings";
|
||||
"perm_help" = "Can't find the keyboard? Go to Settings > General > Keyboard > Keyboards > Add New Keyboard";
|
||||
|
||||
// Home page & language test
|
||||
"home_title" = "Home";
|
||||
"home_input_placeholder" = "Type here to test the keyboard";
|
||||
"home_item_lang_test" = "Language Test";
|
||||
"home_item_keyboard_permission" = "Keyboard Permission Guide";
|
||||
|
||||
"lang_test_title" = "Language Test";
|
||||
"lang_toggle" = "Toggle Language";
|
||||
"current_lang" = "Current: %@";
|
||||
"common_back" = "Back";
|
||||
20
Shared/Localization/zh-Hans.lproj/Localizable.strings
Normal file
20
Shared/Localization/zh-Hans.lproj/Localizable.strings
Normal file
@@ -0,0 +1,20 @@
|
||||
/*
|
||||
Localizable.strings (简体中文)
|
||||
App 与键盘扩展共用的文案 Key
|
||||
*/
|
||||
|
||||
"perm_title_enable" = "启用输入法";
|
||||
"perm_steps" = "1 开启键盘 > 2 允许完全访问";
|
||||
"perm_open_settings" = "去设置中开启";
|
||||
"perm_help" = "没有找到键盘? 请前往 设置 > 通用 > 键盘 > 键盘 > 添加新键盘";
|
||||
|
||||
// 首页与多语言测试
|
||||
"home_title" = "首页";
|
||||
"home_input_placeholder" = "在此输入,测试键盘";
|
||||
"home_item_lang_test" = "多语言测试";
|
||||
"home_item_keyboard_permission" = "键盘权限引导";
|
||||
|
||||
"lang_test_title" = "多语言测试";
|
||||
"lang_toggle" = "切换语言";
|
||||
"current_lang" = "当前:%@";
|
||||
"common_back" = "返回";
|
||||
@@ -8,6 +8,13 @@
|
||||
|
||||
/* Begin PBXBuildFile section */
|
||||
043FBCD22EAF97630036AFE1 /* KBPermissionViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 04C6EAE12EAF940F0089C901 /* KBPermissionViewController.m */; };
|
||||
04A9FE0F2EB481100020DB6D /* KBHUD.m in Sources */ = {isa = PBXBuildFile; fileRef = 04FC97082EB31B14007BD342 /* KBHUD.m */; };
|
||||
04A9FE132EB4D0D20020DB6D /* KBFullAccessManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 04A9FE112EB4D0D20020DB6D /* KBFullAccessManager.m */; };
|
||||
04A9FE162EB873C80020DB6D /* UIViewController+Extension.m in Sources */ = {isa = PBXBuildFile; fileRef = 04A9FE152EB873C80020DB6D /* UIViewController+Extension.m */; };
|
||||
04A9FE1A2EB892460020DB6D /* KBLocalizationManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 04A9FE192EB892460020DB6D /* KBLocalizationManager.m */; };
|
||||
04A9FE1B2EB892460020DB6D /* KBLocalizationManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 04A9FE192EB892460020DB6D /* KBLocalizationManager.m */; };
|
||||
04A9FE202EB893F10020DB6D /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 04A9FE1E2EB893F10020DB6D /* Localizable.strings */; };
|
||||
04A9FE212EB893F10020DB6D /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 04A9FE1E2EB893F10020DB6D /* Localizable.strings */; };
|
||||
04C6EABA2EAF86530089C901 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 04C6EAAE2EAF86530089C901 /* Assets.xcassets */; };
|
||||
04C6EABC2EAF86530089C901 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 04C6EAB12EAF86530089C901 /* LaunchScreen.storyboard */; };
|
||||
04C6EABD2EAF86530089C901 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 04C6EAB42EAF86530089C901 /* Main.storyboard */; };
|
||||
@@ -47,6 +54,11 @@
|
||||
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 */; };
|
||||
A1B2C4002EB4A0A100000003 /* KBAuthManager.m in Sources */ = {isa = PBXBuildFile; fileRef = A1B2C4002EB4A0A100000002 /* KBAuthManager.m */; };
|
||||
A1B2C4002EB4A0A100000004 /* KBAuthManager.m in Sources */ = {isa = PBXBuildFile; fileRef = A1B2C4002EB4A0A100000002 /* KBAuthManager.m */; };
|
||||
A1B2C4202EB4B7A100000001 /* KBKeyboardPermissionManager.m in Sources */ = {isa = PBXBuildFile; fileRef = A1B2C4222EB4B7A100000001 /* KBKeyboardPermissionManager.m */; };
|
||||
A1B2C4212EB4B7A100000001 /* KBKeyboardPermissionManager.m in Sources */ = {isa = PBXBuildFile; fileRef = A1B2C4222EB4B7A100000001 /* KBKeyboardPermissionManager.m */; };
|
||||
A1B2D7022EB8C00100000001 /* KBLangTestVC.m in Sources */ = {isa = PBXBuildFile; fileRef = A1B2D7012EB8C00100000001 /* KBLangTestVC.m */; };
|
||||
ECC9EE02174D86E8D792472F /* Pods_keyBoard.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 967065BB5230E43F293B3AF9 /* Pods_keyBoard.framework */; };
|
||||
/* End PBXBuildFile section */
|
||||
|
||||
@@ -75,6 +87,14 @@
|
||||
/* End PBXCopyFilesBuildPhase section */
|
||||
|
||||
/* Begin PBXFileReference section */
|
||||
04A9FE102EB4D0D20020DB6D /* KBFullAccessManager.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KBFullAccessManager.h; sourceTree = "<group>"; };
|
||||
04A9FE112EB4D0D20020DB6D /* KBFullAccessManager.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KBFullAccessManager.m; sourceTree = "<group>"; };
|
||||
04A9FE142EB873C80020DB6D /* UIViewController+Extension.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "UIViewController+Extension.h"; sourceTree = "<group>"; };
|
||||
04A9FE152EB873C80020DB6D /* UIViewController+Extension.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "UIViewController+Extension.m"; sourceTree = "<group>"; };
|
||||
04A9FE182EB892460020DB6D /* KBLocalizationManager.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KBLocalizationManager.h; sourceTree = "<group>"; };
|
||||
04A9FE192EB892460020DB6D /* KBLocalizationManager.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KBLocalizationManager.m; sourceTree = "<group>"; };
|
||||
04A9FE1C2EB893F10020DB6D /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Localizable.strings; sourceTree = "<group>"; };
|
||||
04A9FE1D2EB893F10020DB6D /* zh-Hans */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hans"; path = "zh-Hans.lproj/Localizable.strings"; sourceTree = "<group>"; };
|
||||
04C6EAAC2EAF86530089C901 /* AppDelegate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = AppDelegate.h; sourceTree = "<group>"; };
|
||||
04C6EAAD2EAF86530089C901 /* AppDelegate.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = AppDelegate.m; sourceTree = "<group>"; };
|
||||
04C6EAAE2EAF86530089C901 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
|
||||
@@ -147,17 +167,23 @@
|
||||
04FC970B2EB334F8007BD342 /* UIImageView+KBWebImage.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "UIImageView+KBWebImage.m"; sourceTree = "<group>"; };
|
||||
04FC970C2EB334F8007BD342 /* KBWebImageManager.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KBWebImageManager.h; sourceTree = "<group>"; };
|
||||
04FC970D2EB334F8007BD342 /* KBWebImageManager.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KBWebImageManager.m; sourceTree = "<group>"; };
|
||||
04FC98012EB36AAB007BD342 /* KBConfig.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KBConfig.h; sourceTree = "<group>"; };
|
||||
2C1092FB2B452F95B15D4263 /* Pods_CustomKeyboard.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_CustomKeyboard.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
51FE7C4C42C2255B3C1C4128 /* Pods-keyBoard.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-keyBoard.release.xcconfig"; path = "Target Support Files/Pods-keyBoard/Pods-keyBoard.release.xcconfig"; sourceTree = "<group>"; };
|
||||
727EC7532EAF848B00B36487 /* keyBoard.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = keyBoard.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
967065BB5230E43F293B3AF9 /* Pods_keyBoard.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_keyBoard.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
04FC98012EB36AAB007BD342 /* KBConfig.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KBConfig.h; sourceTree = "<group>"; };
|
||||
A1B2C3D22EB0A0A100000001 /* KBFunctionTagCell.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KBFunctionTagCell.h; sourceTree = "<group>"; };
|
||||
A1B2C3D32EB0A0A100000001 /* KBFunctionTagCell.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KBFunctionTagCell.m; sourceTree = "<group>"; };
|
||||
A1B2C3E02EB0C0A100000001 /* KBNetworkManager.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KBNetworkManager.h; sourceTree = "<group>"; };
|
||||
A1B2C3E12EB0C0A100000001 /* KBNetworkManager.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KBNetworkManager.m; sourceTree = "<group>"; };
|
||||
A1B2C3F12EB35A9900000001 /* KBFullAccessGuideView.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KBFullAccessGuideView.h; sourceTree = "<group>"; };
|
||||
A1B2C3F22EB35A9900000001 /* KBFullAccessGuideView.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KBFullAccessGuideView.m; sourceTree = "<group>"; };
|
||||
A1B2C4002EB4A0A100000001 /* KBAuthManager.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KBAuthManager.h; sourceTree = "<group>"; };
|
||||
A1B2C4002EB4A0A100000002 /* KBAuthManager.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KBAuthManager.m; sourceTree = "<group>"; };
|
||||
A1B2C4222EB4B7A100000001 /* KBKeyboardPermissionManager.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KBKeyboardPermissionManager.m; sourceTree = "<group>"; };
|
||||
A1B2C4232EB4B7A100000001 /* KBKeyboardPermissionManager.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KBKeyboardPermissionManager.h; sourceTree = "<group>"; };
|
||||
A1B2D7002EB8C00100000001 /* KBLangTestVC.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KBLangTestVC.h; sourceTree = "<group>"; };
|
||||
A1B2D7012EB8C00100000001 /* KBLangTestVC.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KBLangTestVC.m; sourceTree = "<group>"; };
|
||||
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 = "<group>"; };
|
||||
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 = "<group>"; };
|
||||
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 = "<group>"; };
|
||||
@@ -183,15 +209,23 @@
|
||||
/* End PBXFrameworksBuildPhase section */
|
||||
|
||||
/* Begin PBXGroup section */
|
||||
04FC98002EB36AAB007BD342 /* Shared */ = {
|
||||
04A9FE122EB4D0D20020DB6D /* Manager */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
04FC98012EB36AAB007BD342 /* KBConfig.h */,
|
||||
04A9FE102EB4D0D20020DB6D /* KBFullAccessManager.h */,
|
||||
04A9FE112EB4D0D20020DB6D /* KBFullAccessManager.m */,
|
||||
);
|
||||
path = Shared;
|
||||
path = Manager;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
04A9FE1F2EB893F10020DB6D /* Localization */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
04A9FE1E2EB893F10020DB6D /* Localizable.strings */,
|
||||
);
|
||||
path = Localization;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
|
||||
04C6EAB92EAF86530089C901 /* keyBoard */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
@@ -215,6 +249,7 @@
|
||||
04C6EAD72EAF870B0089C901 /* CustomKeyboard */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
04A9FE122EB4D0D20020DB6D /* Manager */,
|
||||
04FC95662EB0546C007BD342 /* Model */,
|
||||
04C6EADA2EAF8C7B0089C901 /* View */,
|
||||
A1B2C3E52EB0C0A100000001 /* Network */,
|
||||
@@ -244,9 +279,9 @@
|
||||
04FC95742EB095DE007BD342 /* KBFunctionPasteView.h */,
|
||||
04FC95752EB095DE007BD342 /* KBFunctionPasteView.m */,
|
||||
A1B2C3D22EB0A0A100000001 /* KBFunctionTagCell.h */,
|
||||
A1B2C3D32EB0A0A100000001 /* KBFunctionTagCell.m */,
|
||||
A1B2C3F12EB35A9900000001 /* KBFullAccessGuideView.h */,
|
||||
A1B2C3F22EB35A9900000001 /* KBFullAccessGuideView.m */,
|
||||
A1B2C3D32EB0A0A100000001 /* KBFunctionTagCell.m */,
|
||||
A1B2C3F12EB35A9900000001 /* KBFullAccessGuideView.h */,
|
||||
A1B2C3F22EB35A9900000001 /* KBFullAccessGuideView.m */,
|
||||
04FC95B02EB0B2CC007BD342 /* KBSettingView.h */,
|
||||
04FC95B12EB0B2CC007BD342 /* KBSettingView.m */,
|
||||
);
|
||||
@@ -290,6 +325,8 @@
|
||||
children = (
|
||||
04FC95CD2EB1E7A1007BD342 /* HomeVC.h */,
|
||||
04FC95CE2EB1E7A1007BD342 /* HomeVC.m */,
|
||||
A1B2D7002EB8C00100000001 /* KBLangTestVC.h */,
|
||||
A1B2D7012EB8C00100000001 /* KBLangTestVC.m */,
|
||||
);
|
||||
path = VC;
|
||||
sourceTree = "<group>";
|
||||
@@ -321,8 +358,6 @@
|
||||
04FC95B92EB1E3B1007BD342 /* VC */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
04FC95CA2EB1E780007BD342 /* BaseTabBarController.h */,
|
||||
04FC95CB2EB1E780007BD342 /* BaseTabBarController.m */,
|
||||
);
|
||||
path = VC;
|
||||
sourceTree = "<group>";
|
||||
@@ -409,6 +444,8 @@
|
||||
children = (
|
||||
04FC95C72EB1E4C9007BD342 /* BaseNavigationController.h */,
|
||||
04FC95C82EB1E4C9007BD342 /* BaseNavigationController.m */,
|
||||
04FC95CA2EB1E780007BD342 /* BaseTabBarController.h */,
|
||||
04FC95CB2EB1E780007BD342 /* BaseTabBarController.m */,
|
||||
);
|
||||
path = VC;
|
||||
sourceTree = "<group>";
|
||||
@@ -473,6 +510,8 @@
|
||||
04FC970D2EB334F8007BD342 /* KBWebImageManager.m */,
|
||||
04FC97072EB31B14007BD342 /* KBHUD.h */,
|
||||
04FC97082EB31B14007BD342 /* KBHUD.m */,
|
||||
04A9FE142EB873C80020DB6D /* UIViewController+Extension.h */,
|
||||
04A9FE152EB873C80020DB6D /* UIViewController+Extension.m */,
|
||||
);
|
||||
path = Categories;
|
||||
sourceTree = "<group>";
|
||||
@@ -530,6 +569,21 @@
|
||||
path = Manager;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
04FC98002EB36AAB007BD342 /* Shared */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
04A9FE1F2EB893F10020DB6D /* Localization */,
|
||||
04FC98012EB36AAB007BD342 /* KBConfig.h */,
|
||||
A1B2C4002EB4A0A100000001 /* KBAuthManager.h */,
|
||||
A1B2C4002EB4A0A100000002 /* KBAuthManager.m */,
|
||||
A1B2C4232EB4B7A100000001 /* KBKeyboardPermissionManager.h */,
|
||||
A1B2C4222EB4B7A100000001 /* KBKeyboardPermissionManager.m */,
|
||||
04A9FE182EB892460020DB6D /* KBLocalizationManager.h */,
|
||||
04A9FE192EB892460020DB6D /* KBLocalizationManager.m */,
|
||||
);
|
||||
path = Shared;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
2C53A0856097DCFBE7B55649 /* Pods */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
@@ -646,6 +700,7 @@
|
||||
knownRegions = (
|
||||
en,
|
||||
Base,
|
||||
"zh-Hans",
|
||||
);
|
||||
mainGroup = 727EC74A2EAF848B00B36487;
|
||||
minimizedProjectReferenceProxies = 1;
|
||||
@@ -665,6 +720,7 @@
|
||||
isa = PBXResourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
04A9FE202EB893F10020DB6D /* Localizable.strings in Resources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
@@ -673,6 +729,7 @@
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
04C6EABA2EAF86530089C901 /* Assets.xcassets in Resources */,
|
||||
04A9FE212EB893F10020DB6D /* Localizable.strings in Resources */,
|
||||
04C6EABC2EAF86530089C901 /* LaunchScreen.storyboard in Resources */,
|
||||
04C6EABD2EAF86530089C901 /* Main.storyboard in Resources */,
|
||||
);
|
||||
@@ -753,19 +810,24 @@
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
04A9FE0F2EB481100020DB6D /* KBHUD.m in Sources */,
|
||||
04C6EADD2EAF8CEB0089C901 /* KBToolBar.m in Sources */,
|
||||
04FC95792EB09BC8007BD342 /* KBKeyBoardMainView.m in Sources */,
|
||||
04FC95732EB09570007BD342 /* KBFunctionBarView.m in Sources */,
|
||||
04C6EAD82EAF870B0089C901 /* KeyboardViewController.m in Sources */,
|
||||
04FC95762EB095DE007BD342 /* KBFunctionPasteView.m in Sources */,
|
||||
A1B2C3D42EB0A0A100000001 /* KBFunctionTagCell.m in Sources */,
|
||||
04A9FE1A2EB892460020DB6D /* KBLocalizationManager.m in Sources */,
|
||||
A1B2C3E22EB0C0A100000001 /* KBNetworkManager.m in Sources */,
|
||||
04FC956A2EB05497007BD342 /* KBKeyButton.m in Sources */,
|
||||
04FC95B22EB0B2CC007BD342 /* KBSettingView.m in Sources */,
|
||||
04FC95702EB09516007BD342 /* KBFunctionView.m in Sources */,
|
||||
04FC956D2EB054B7007BD342 /* KBKeyboardView.m in Sources */,
|
||||
04FC95672EB0546C007BD342 /* KBKey.m in Sources */,
|
||||
A1B2C3F42EB35A9900000001 /* KBFullAccessGuideView.m in Sources */,
|
||||
04FC95672EB0546C007BD342 /* KBKey.m in Sources */,
|
||||
A1B2C3F42EB35A9900000001 /* KBFullAccessGuideView.m in Sources */,
|
||||
A1B2C4002EB4A0A100000003 /* KBAuthManager.m in Sources */,
|
||||
04A9FE132EB4D0D20020DB6D /* KBFullAccessManager.m in Sources */,
|
||||
A1B2C4202EB4B7A100000001 /* KBKeyboardPermissionManager.m in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
@@ -776,9 +838,11 @@
|
||||
04FC95E92EB23B67007BD342 /* KBNetworkManager.m in Sources */,
|
||||
04FC95D22EB1E7AE007BD342 /* MyVC.m in Sources */,
|
||||
043FBCD22EAF97630036AFE1 /* KBPermissionViewController.m in Sources */,
|
||||
04A9FE162EB873C80020DB6D /* UIViewController+Extension.m in Sources */,
|
||||
04C6EABE2EAF86530089C901 /* AppDelegate.m in Sources */,
|
||||
04FC95F12EB339A7007BD342 /* LoginViewController.m in Sources */,
|
||||
04FC96142EB34E00007BD342 /* KBLoginSheetViewController.m in Sources */,
|
||||
04A9FE1B2EB892460020DB6D /* KBLocalizationManager.m in Sources */,
|
||||
04FC95D72EB1EA16007BD342 /* BaseTableView.m in Sources */,
|
||||
04FC95D82EB1EA16007BD342 /* BaseCell.m in Sources */,
|
||||
04FC95C92EB1E4C9007BD342 /* BaseNavigationController.m in Sources */,
|
||||
@@ -791,10 +855,13 @@
|
||||
04FC970E2EB334F8007BD342 /* UIImageView+KBWebImage.m in Sources */,
|
||||
04FC970F2EB334F8007BD342 /* KBWebImageManager.m in Sources */,
|
||||
04FC95CF2EB1E7A1007BD342 /* HomeVC.m in Sources */,
|
||||
A1B2D7022EB8C00100000001 /* KBLangTestVC.m in Sources */,
|
||||
04C6EABF2EAF86530089C901 /* main.m in Sources */,
|
||||
04FC95CC2EB1E780007BD342 /* BaseTabBarController.m in Sources */,
|
||||
04FC95F42EB339C1007BD342 /* AppleSignInManager.m in Sources */,
|
||||
04C6EAC12EAF86530089C901 /* ViewController.m in Sources */,
|
||||
A1B2C4002EB4A0A100000004 /* KBAuthManager.m in Sources */,
|
||||
A1B2C4212EB4B7A100000001 /* KBKeyboardPermissionManager.m in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
@@ -809,6 +876,15 @@
|
||||
/* End PBXTargetDependency section */
|
||||
|
||||
/* Begin PBXVariantGroup section */
|
||||
04A9FE1E2EB893F10020DB6D /* Localizable.strings */ = {
|
||||
isa = PBXVariantGroup;
|
||||
children = (
|
||||
04A9FE1C2EB893F10020DB6D /* en */,
|
||||
04A9FE1D2EB893F10020DB6D /* zh-Hans */,
|
||||
);
|
||||
name = Localizable.strings;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
04C6EAB12EAF86530089C901 /* LaunchScreen.storyboard */ = {
|
||||
isa = PBXVariantGroup;
|
||||
children = (
|
||||
@@ -832,11 +908,16 @@
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = B8CA018AB878499327504AAD /* Pods-CustomKeyboard.debug.xcconfig */;
|
||||
buildSettings = {
|
||||
CODE_SIGN_ENTITLEMENTS = CustomKeyboard/CustomKeyboard.entitlements;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
DEVELOPMENT_TEAM = TN6HHV45BB;
|
||||
DEVELOPMENT_TEAM = UFX79H8H66;
|
||||
ENABLE_USER_SCRIPT_SANDBOXING = NO;
|
||||
GCC_PREFIX_HEADER = CustomKeyboard/PrefixHeader.pch;
|
||||
GCC_PREPROCESSOR_DEFINITIONS = (
|
||||
"$(inherited)",
|
||||
"KB_KEYCHAIN_ACCESS_GROUP=@\\\"$(AppIdentifierPrefix)com.keyBoardst.shared\\\"",
|
||||
);
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
INFOPLIST_FILE = CustomKeyboard/Info.plist;
|
||||
INFOPLIST_KEY_CFBundleDisplayName = "我的输入法";
|
||||
@@ -848,7 +929,7 @@
|
||||
"@executable_path/../../Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 1.0;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.keyBoardst.CustomKeyboard;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.loveKey.nyx.CustomKeyboard;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SKIP_INSTALL = YES;
|
||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||
@@ -860,11 +941,16 @@
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = B12EC429812407B9F0E67565 /* Pods-CustomKeyboard.release.xcconfig */;
|
||||
buildSettings = {
|
||||
CODE_SIGN_ENTITLEMENTS = CustomKeyboard/CustomKeyboard.entitlements;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
DEVELOPMENT_TEAM = TN6HHV45BB;
|
||||
DEVELOPMENT_TEAM = UFX79H8H66;
|
||||
ENABLE_USER_SCRIPT_SANDBOXING = NO;
|
||||
GCC_PREFIX_HEADER = CustomKeyboard/PrefixHeader.pch;
|
||||
GCC_PREPROCESSOR_DEFINITIONS = (
|
||||
"$(inherited)",
|
||||
"KB_KEYCHAIN_ACCESS_GROUP=@\\\"$(AppIdentifierPrefix)com.keyBoardst.shared\\\"",
|
||||
);
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
INFOPLIST_FILE = CustomKeyboard/Info.plist;
|
||||
INFOPLIST_KEY_CFBundleDisplayName = "我的输入法";
|
||||
@@ -876,7 +962,7 @@
|
||||
"@executable_path/../../Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 1.0;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.keyBoardst.CustomKeyboard;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.loveKey.nyx.CustomKeyboard;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SKIP_INSTALL = YES;
|
||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||
@@ -891,21 +977,19 @@
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||
CODE_SIGN_ENTITLEMENTS = keyBoard/keyBoard.entitlements;
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
DEVELOPMENT_TEAM = TN6HHV45BB;
|
||||
DEVELOPMENT_TEAM = UFX79H8H66;
|
||||
GCC_PREFIX_HEADER = keyBoard/KeyBoardPrefixHeader.pch;
|
||||
GCC_PREPROCESSOR_DEFINITIONS = (
|
||||
"$(inherited)",
|
||||
"KB_KEYCHAIN_ACCESS_GROUP=@\\\"$(AppIdentifierPrefix)com.keyBoardst.shared\\\"",
|
||||
);
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
INFOPLIST_FILE = keyBoard/Info.plist;
|
||||
INFOPLIST_KEY_CFBundleURLTypes = (
|
||||
{
|
||||
CFBundleURLName = com.keyBoardst.keyboard;
|
||||
CFBundleURLSchemes = (
|
||||
kbkeyboard,
|
||||
);
|
||||
},
|
||||
);
|
||||
INFOPLIST_KEY_CFBundleDisplayName = "YOLO输入法";
|
||||
INFOPLIST_KEY_CFBundleURLTypes = "{\n CFBundleURLName = \"com.keyBoardst.keyboard\";\n CFBundleURLSchemes = (\n kbkeyboard\n );\n}";
|
||||
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
|
||||
INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen;
|
||||
INFOPLIST_KEY_UIMainStoryboardFile = Main;
|
||||
@@ -917,14 +1001,15 @@
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 1.0;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.keyBoardst;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.loveKey.nyx;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||
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 */;
|
||||
@@ -932,21 +1017,19 @@
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||
CODE_SIGN_ENTITLEMENTS = keyBoard/keyBoard.entitlements;
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
DEVELOPMENT_TEAM = TN6HHV45BB;
|
||||
DEVELOPMENT_TEAM = UFX79H8H66;
|
||||
GCC_PREFIX_HEADER = keyBoard/KeyBoardPrefixHeader.pch;
|
||||
GCC_PREPROCESSOR_DEFINITIONS = (
|
||||
"$(inherited)",
|
||||
"KB_KEYCHAIN_ACCESS_GROUP=@\\\"$(AppIdentifierPrefix)com.keyBoardst.shared\\\"",
|
||||
);
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
INFOPLIST_FILE = keyBoard/Info.plist;
|
||||
INFOPLIST_KEY_CFBundleURLTypes = (
|
||||
{
|
||||
CFBundleURLName = com.keyBoardst.keyboard;
|
||||
CFBundleURLSchemes = (
|
||||
kbkeyboard,
|
||||
);
|
||||
},
|
||||
);
|
||||
INFOPLIST_KEY_CFBundleDisplayName = "YOLO输入法";
|
||||
INFOPLIST_KEY_CFBundleURLTypes = "{\n CFBundleURLName = \"com.keyBoardst.keyboard\";\n CFBundleURLSchemes = (\n kbkeyboard\n );\n}";
|
||||
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
|
||||
INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen;
|
||||
INFOPLIST_KEY_UIMainStoryboardFile = Main;
|
||||
@@ -958,8 +1041,9 @@
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 1.0;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.keyBoardst;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.loveKey.nyx;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||
STRING_CATALOG_GENERATE_SYMBOLS = YES;
|
||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
#import "LoginViewController.h"
|
||||
#import "KBLoginSheetViewController.h"
|
||||
#import "AppleSignInManager.h"
|
||||
#import <objc/message.h>
|
||||
|
||||
// 注意:用于判断系统已启用本输入法扩展的 bundle id 需与扩展 target 的
|
||||
// PRODUCT_BUNDLE_IDENTIFIER 完全一致。
|
||||
@@ -34,13 +35,12 @@ static NSString * const kKBKeyboardExtensionBundleId = @"com.keyBoardst.CustomKe
|
||||
/// 设置GroupID进行配置
|
||||
// buglyConfig.applicationGroupIdentifier = @"";
|
||||
[Bugly startWithAppId:BuglyId config:buglyConfig];
|
||||
/// 判断获取键盘权限
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
[self kb_presentPermissionIfNeeded];
|
||||
});
|
||||
// 键盘权限引导改由 KBGuideVC 内部负责;此处不主动弹出。
|
||||
return YES;
|
||||
}
|
||||
|
||||
- (void)applicationDidBecomeActive:(UIApplication *)application{}
|
||||
|
||||
|
||||
- (void)setupRootVC{
|
||||
self.window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]];
|
||||
@@ -52,17 +52,6 @@ static NSString * const kKBKeyboardExtensionBundleId = @"com.keyBoardst.CustomKe
|
||||
|
||||
#pragma mark - Permission presentation
|
||||
|
||||
- (UIViewController *)kb_topMostViewController
|
||||
{
|
||||
UIViewController *root = self.window.rootViewController;
|
||||
if (!root) return nil;
|
||||
UIViewController *top = root;
|
||||
while (top.presentedViewController) {
|
||||
top = top.presentedViewController;
|
||||
}
|
||||
return top;
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - Deep Link
|
||||
|
||||
@@ -88,7 +77,7 @@ static NSString * const kKBKeyboardExtensionBundleId = @"com.keyBoardst.CustomKe
|
||||
// 已登录则不提示
|
||||
BOOL loggedIn = ([AppleSignInManager shared].storedUserIdentifier.length > 0);
|
||||
if (loggedIn) return;
|
||||
UIViewController *top = [self kb_topMostViewController];
|
||||
UIViewController *top = [UIViewController kb_topMostViewController];
|
||||
if (!top) return;
|
||||
[KBLoginSheetViewController presentIfNeededFrom:top];
|
||||
}
|
||||
@@ -107,20 +96,7 @@ static NSString * const kKBKeyboardExtensionBundleId = @"com.keyBoardst.CustomKe
|
||||
|
||||
- (void)kb_presentPermissionIfNeeded
|
||||
{
|
||||
BOOL enabled = KBIsKeyboardEnabled();
|
||||
UIViewController *top = [self kb_topMostViewController];
|
||||
if (!top) return;
|
||||
if ([top isKindOfClass:[KBPermissionViewController class]]) {
|
||||
if (enabled) {
|
||||
[top dismissViewControllerAnimated:YES completion:nil];
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (!enabled) {
|
||||
KBPermissionViewController *guide = [KBPermissionViewController new];
|
||||
guide.modalPresentationStyle = UIModalPresentationFullScreen;
|
||||
[top presentViewController:guide animated:YES completion:nil];
|
||||
}
|
||||
// 该逻辑已迁移到 KBGuideVC,保留占位以兼容旧调用,但不再执行任何操作
|
||||
}
|
||||
|
||||
|
||||
|
||||
22
keyBoard/Assets.xcassets/back_black_icon.imageset/Contents.json
vendored
Normal file
22
keyBoard/Assets.xcassets/back_black_icon.imageset/Contents.json
vendored
Normal file
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"filename" : "back@2x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"filename" : "back@3x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
BIN
keyBoard/Assets.xcassets/back_black_icon.imageset/back@2x.png
vendored
Normal file
BIN
keyBoard/Assets.xcassets/back_black_icon.imageset/back@2x.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 388 B |
BIN
keyBoard/Assets.xcassets/back_black_icon.imageset/back@3x.png
vendored
Normal file
BIN
keyBoard/Assets.xcassets/back_black_icon.imageset/back@3x.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 674 B |
@@ -15,12 +15,28 @@
|
||||
|
||||
- (void)viewDidLoad {
|
||||
[super viewDidLoad];
|
||||
// Do any additional setup after loading the view.
|
||||
// 统一返回箭头样式与颜色
|
||||
UIImage *backImg = [UIImage imageNamed:@"back_black_icon"];
|
||||
if (backImg) {
|
||||
self.navigationBar.backIndicatorImage = backImg;
|
||||
self.navigationBar.backIndicatorTransitionMaskImage = backImg;
|
||||
}
|
||||
self.navigationBar.tintColor = [UIColor blackColor]; // 箭头/按钮的着色
|
||||
|
||||
if (@available(iOS 14.0, *)) {
|
||||
self.navigationBar.topItem.backButtonDisplayMode = UINavigationItemBackButtonDisplayModeMinimal;
|
||||
}
|
||||
}
|
||||
|
||||
- (void)pushViewController:(UIViewController *)viewController animated:(BOOL)animated{
|
||||
if (self.viewControllers.count > 0) {
|
||||
viewController.hidesBottomBarWhenPushed = true;
|
||||
UIViewController *prev = self.topViewController;
|
||||
if (@available(iOS 14.0, *)) {
|
||||
prev.navigationItem.backButtonDisplayMode = UINavigationItemBackButtonDisplayModeMinimal;
|
||||
} else {
|
||||
prev.navigationItem.backBarButtonItem = [[UIBarButtonItem alloc] initWithTitle:@"" style:UIBarButtonItemStylePlain target:nil action:nil];
|
||||
}
|
||||
}
|
||||
[super pushViewController:viewController animated:animated];
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
#import "HomeVC.h"
|
||||
#import "MyVC.h"
|
||||
#import "BaseNavigationController.h"
|
||||
|
||||
#import "KBAuthManager.h"
|
||||
@interface BaseTabBarController ()
|
||||
|
||||
@end
|
||||
@@ -30,6 +30,8 @@
|
||||
navMy.tabBarItem = [[UITabBarItem alloc] initWithTitle:@"我的" image:nil selectedImage:nil];
|
||||
|
||||
self.viewControllers = @[navHome, navMy];
|
||||
|
||||
[[KBAuthManager shared] saveAccessToken:@"TEST" refreshToken:nil expiryDate:[NSDate dateWithTimeIntervalSinceNow:3600] userIdentifier:nil];
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -11,6 +11,7 @@
|
||||
//
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
@class UIView; // forward declare to avoid importing UIKit in header
|
||||
|
||||
typedef NS_ENUM(NSUInteger, KBHUDMaskType) {
|
||||
/// 不加遮罩,事件可透传到后面的视图(与 SVProgressHUDMaskTypeNone 类似)
|
||||
@@ -59,6 +60,13 @@ NS_ASSUME_NONNULL_BEGIN
|
||||
/// 设置自动隐藏的时长(success/error/info),默认 1.2s
|
||||
+ (void)setAutoDismissInterval:(NSTimeInterval)interval;
|
||||
|
||||
/// 设置缺省承载视图(App Extension 环境下必须设置,例如在键盘扩展里传入 self.view)
|
||||
/// 注意:内部弱引用,不会形成循环引用。
|
||||
/// 若不设置,在非 Extension 的 App 内默认加到 KeyWindow;在 Extension 内将不会显示。
|
||||
/// 可在 viewDidLoad 或 viewDidAppear 调用一次即可。
|
||||
/// @param view 作为 HUD 的承载父视图
|
||||
+ (void)setContainerView:(nullable UIView *)view;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
|
||||
#import "KBHUD.h"
|
||||
#import <MBProgressHUD/MBProgressHUD.h>
|
||||
#import <UIKit/UIKit.h>
|
||||
|
||||
#ifndef KBSCREEN
|
||||
#define KBSCREEN [UIScreen mainScreen].bounds.size
|
||||
@@ -16,6 +17,7 @@ static __weak MBProgressHUD *sHUD = nil;
|
||||
static KBHUDMaskType sMaskType = KBHUDMaskTypeClear; // 全局默认遮罩
|
||||
static BOOL sDefaultTapToDismiss = NO; // 全局默认:不允许点击关闭
|
||||
static NSTimeInterval sAutoDismiss = 1.2;
|
||||
static __weak UIView *sContainerView = nil; // 缺省承载视图(扩展里必须设置)
|
||||
|
||||
#pragma mark - Private Helpers
|
||||
|
||||
@@ -24,11 +26,37 @@ static NSTimeInterval sAutoDismiss = 1.2;
|
||||
}
|
||||
|
||||
+ (MBProgressHUD *)ensureHUDWithMask:(KBHUDMaskType)mask tap:(BOOL)tap {
|
||||
// 先尝试使用外部指定的容器视图(扩展环境推荐)
|
||||
UIView *hostView = sContainerView;
|
||||
#ifndef KB_APP_EXTENSION
|
||||
// App 内退回到 KeyWindow
|
||||
if (!hostView) {
|
||||
// KB_KeyWindow 在 App 目标的 PrefixHeader 中定义;在扩展内不依赖它
|
||||
UIWindow *win = nil;
|
||||
// 避免强依赖某个前缀:这里以运行时方式访问 UIApplication 以规避编译期的 App Extension 限制
|
||||
Class uiAppClass = NSClassFromString(@"UIApplication");
|
||||
if (uiAppClass && [uiAppClass respondsToSelector:@selector(sharedApplication)]) {
|
||||
#pragma clang diagnostic push
|
||||
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
|
||||
id app = [uiAppClass performSelector:@selector(sharedApplication)];
|
||||
if ([app respondsToSelector:@selector(keyWindow)]) {
|
||||
win = [app keyWindow];
|
||||
}
|
||||
if (!win && [app respondsToSelector:@selector(windows)]) {
|
||||
NSArray *wins = [app windows];
|
||||
win = wins.firstObject;
|
||||
}
|
||||
#pragma clang diagnostic pop
|
||||
}
|
||||
hostView = win;
|
||||
}
|
||||
#endif
|
||||
|
||||
if (!hostView) { return nil; }
|
||||
|
||||
MBProgressHUD *hud = sHUD;
|
||||
if (!hud) {
|
||||
UIWindow *win = KB_KeyWindow();
|
||||
if (!win) { win = UIApplication.sharedApplication.windows.firstObject; }
|
||||
hud = [MBProgressHUD showHUDAddedTo:win animated:YES];
|
||||
hud = [MBProgressHUD showHUDAddedTo:hostView animated:YES];
|
||||
sHUD = hud;
|
||||
// 外观:深色圆角,白色文字,模仿 SVProgressHUD 默认
|
||||
hud.removeFromSuperViewOnHide = YES;
|
||||
@@ -92,6 +120,7 @@ static NSTimeInterval sAutoDismiss = 1.2;
|
||||
+ (void)_showText:(NSString *)text icon:(nullable UIImage *)icon {
|
||||
[self onMain:^{
|
||||
MBProgressHUD *hud = [self ensureHUDWithMask:sMaskType tap:sDefaultTapToDismiss];
|
||||
if (!hud) { return; }
|
||||
hud.mode = icon ? MBProgressHUDModeCustomView : MBProgressHUDModeText;
|
||||
hud.label.text = text ?: @"";
|
||||
hud.detailsLabel.text = nil;
|
||||
@@ -114,6 +143,7 @@ static NSTimeInterval sAutoDismiss = 1.2;
|
||||
+ (void)showWithStatus:(NSString *)status allowTapToDismiss:(BOOL)allow {
|
||||
[self onMain:^{
|
||||
MBProgressHUD *hud = [self ensureHUDWithMask:sMaskType tap:allow];
|
||||
if (!hud) { return; }
|
||||
hud.mode = MBProgressHUDModeIndeterminate;
|
||||
hud.label.text = status ?: @"";
|
||||
}];
|
||||
@@ -126,6 +156,7 @@ static NSTimeInterval sAutoDismiss = 1.2;
|
||||
+ (void)showProgress:(float)progress status:(NSString *)status allowTapToDismiss:(BOOL)allow {
|
||||
[self onMain:^{
|
||||
MBProgressHUD *hud = [self ensureHUDWithMask:sMaskType tap:allow];
|
||||
if (!hud) { return; }
|
||||
hud.mode = MBProgressHUDModeDeterminate;
|
||||
hud.progress = progress;
|
||||
hud.label.text = status ?: @"";
|
||||
@@ -162,4 +193,8 @@ static NSTimeInterval sAutoDismiss = 1.2;
|
||||
[self showWithStatus:nil allowTapToDismiss:allow];
|
||||
}
|
||||
|
||||
+ (void)setContainerView:(UIView *)view {
|
||||
sContainerView = view;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
18
keyBoard/Class/Categories/UIViewController+Extension.h
Normal file
18
keyBoard/Class/Categories/UIViewController+Extension.h
Normal file
@@ -0,0 +1,18 @@
|
||||
//
|
||||
// UIViewController+Extension.h
|
||||
// keyBoard
|
||||
//
|
||||
// Created by Mac on 2025/11/3.
|
||||
//
|
||||
|
||||
#import <UIKit/UIKit.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface UIViewController (Extension)
|
||||
/// Returns the top-most presented view controller from the app's active window.
|
||||
/// This mirrors the prior logic in AppDelegate (walks presentedViewController chain).
|
||||
+ (nullable UIViewController *)kb_topMostViewController;
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
46
keyBoard/Class/Categories/UIViewController+Extension.m
Normal file
46
keyBoard/Class/Categories/UIViewController+Extension.m
Normal file
@@ -0,0 +1,46 @@
|
||||
//
|
||||
// UIViewController+Extension.m
|
||||
// keyBoard
|
||||
//
|
||||
// Created by Mac on 2025/11/3.
|
||||
//
|
||||
|
||||
#import "UIViewController+Extension.h"
|
||||
|
||||
@implementation UIViewController (Extension)
|
||||
|
||||
/// Find the app's active window in a scene-friendly way (iOS 13+ safe)
|
||||
static inline __kindof UIWindow *KBActiveWindow(void) {
|
||||
UIWindow *window = [UIApplication sharedApplication].keyWindow;
|
||||
if (window) return window;
|
||||
// Fallbacks when keyWindow is nil (e.g. iOS 13+ with scenes)
|
||||
if (@available(iOS 13.0, *)) {
|
||||
for (UIScene *scene in [UIApplication sharedApplication].connectedScenes) {
|
||||
if (scene.activationState != UISceneActivationStateForegroundActive) { continue; }
|
||||
if (![scene isKindOfClass:[UIWindowScene class]]) { continue; }
|
||||
UIWindowScene *ws = (UIWindowScene *)scene;
|
||||
for (UIWindow *w in ws.windows) {
|
||||
if (w.isKeyWindow) { return w; }
|
||||
}
|
||||
if (ws.windows.firstObject) { return ws.windows.firstObject; }
|
||||
}
|
||||
}
|
||||
// iOS 12 and earlier fallback
|
||||
for (UIWindow *w in [UIApplication sharedApplication].windows) {
|
||||
if (w.isKeyWindow) { return w; }
|
||||
}
|
||||
return [UIApplication sharedApplication].windows.firstObject;
|
||||
}
|
||||
|
||||
+ (UIViewController *)kb_topMostViewController {
|
||||
UIWindow *window = KBActiveWindow();
|
||||
UIViewController *root = window.rootViewController;
|
||||
if (!root) return nil;
|
||||
UIViewController *top = root;
|
||||
while (top.presentedViewController) {
|
||||
top = top.presentedViewController;
|
||||
}
|
||||
return top;
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -9,6 +9,8 @@
|
||||
#import "KBGuideTopCell.h"
|
||||
#import "KBGuideKFCell.h"
|
||||
#import "KBGuideUserCell.h"
|
||||
#import "KBPermissionViewController.h"
|
||||
#import "KBKeyboardPermissionManager.h"
|
||||
|
||||
typedef NS_ENUM(NSInteger, KBGuideItemType) {
|
||||
KBGuideItemTypeTop = 0, // 顶部固定卡片
|
||||
@@ -25,6 +27,8 @@ typedef NS_ENUM(NSInteger, KBGuideItemType) {
|
||||
@property (nonatomic, strong) UITapGestureRecognizer *bgTap;// 点击空白收起键盘
|
||||
|
||||
@property (nonatomic, strong) NSMutableArray<NSDictionary *> *items; // 数据源 [{type, text}]
|
||||
/// 权限引导页作为子控制器(用于“同时隐藏”)
|
||||
@property (nonatomic, strong, nullable) KBPermissionViewController *permVC;
|
||||
|
||||
@end
|
||||
|
||||
@@ -42,7 +46,7 @@ typedef NS_ENUM(NSInteger, KBGuideItemType) {
|
||||
|
||||
[self.tableView mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||
make.top.left.right.equalTo(self.view);
|
||||
make.bottom.equalTo(self.inputBar.mas_top);
|
||||
make.bottom.equalTo(self.view);
|
||||
}];
|
||||
|
||||
[self.inputBar mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||
@@ -75,12 +79,56 @@ typedef NS_ENUM(NSInteger, KBGuideItemType) {
|
||||
self.bgTap.cancelsTouchesInView = NO;
|
||||
self.bgTap.delegate = self;
|
||||
[self.tableView addGestureRecognizer:self.bgTap];
|
||||
|
||||
// 监听应用回到前台/变为活跃:用于从设置返回时再次校验权限
|
||||
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(kb_checkKeyboardPermission) name:UIApplicationDidBecomeActiveNotification object:nil];
|
||||
|
||||
// 提前创建并铺满权限引导页(默认隐藏),避免后续显示时出现布局进场感
|
||||
[self kb_preparePermissionOverlayIfNeeded];
|
||||
}
|
||||
|
||||
- (void)dealloc {
|
||||
[[NSNotificationCenter defaultCenter] removeObserver:self];
|
||||
}
|
||||
|
||||
- (void)viewWillAppear:(BOOL)animated {
|
||||
[super viewWillAppear:animated];
|
||||
// 每次进入页面都校验一次(包括从其它页面返回)
|
||||
[self kb_checkKeyboardPermission];
|
||||
}
|
||||
|
||||
/// 校验键盘权限:
|
||||
/// - 未启用或已启用但拒绝完全访问 => 弹出引导页
|
||||
/// - 已满足条件且正在展示引导页 => 关闭引导页
|
||||
- (void)kb_checkKeyboardPermission {
|
||||
KBKeyboardPermissionManager *mgr = [KBKeyboardPermissionManager shared];
|
||||
BOOL enabled = [mgr isKeyboardEnabled];
|
||||
KBFARecord fa = [mgr lastKnownFullAccess];
|
||||
BOOL needGuide = (!enabled) || (enabled && fa == KBFARecordDenied);
|
||||
|
||||
[self kb_preparePermissionOverlayIfNeeded];
|
||||
BOOL show = needGuide;
|
||||
// [UIView performWithoutAnimation:^{
|
||||
self.permVC.view.hidden = !show;
|
||||
// }];
|
||||
}
|
||||
|
||||
/// 提前创建权限引导页覆盖层(仅一次)
|
||||
- (void)kb_preparePermissionOverlayIfNeeded {
|
||||
if (self.permVC) return;
|
||||
KBPermissionViewController *guide = [KBPermissionViewController new];
|
||||
// guide.modalPresentationStyle = UIModalPresentationFullScreen; // 仅用于内部布局,不会真正 present
|
||||
KBWeakSelf;
|
||||
guide.backHandler = ^{ [weakSelf.navigationController popViewControllerAnimated:YES]; };
|
||||
self.permVC = guide;
|
||||
guide.backButton.hidden = true;
|
||||
[self addChildViewController:guide];
|
||||
[self.view addSubview:guide.view];
|
||||
// [guide.view mas_makeConstraints:^(MASConstraintMaker *make) { make.edges.equalTo(self.view); }];
|
||||
[guide didMoveToParentViewController:self];
|
||||
guide.view.hidden = YES; // 初始隐藏
|
||||
}
|
||||
|
||||
- (void)kb_didTapBackground {
|
||||
// 结束编辑,隐藏键盘
|
||||
[self.view endEditing:YES];
|
||||
@@ -125,12 +173,10 @@ typedef NS_ENUM(NSInteger, KBGuideItemType) {
|
||||
self.inputBarBottom.offset = offset;
|
||||
|
||||
[UIView animateWithDuration:duration delay:0 options:curve animations:^{
|
||||
[self.view layoutIfNeeded];
|
||||
// 留出输入栏高度的 contentInset,避免最后一行被遮挡
|
||||
UIEdgeInsets inset = self.tableView.contentInset;
|
||||
inset.bottom = 52 + MAX(kbHeight - safeBtm, 0);
|
||||
self.tableView.contentInset = inset;
|
||||
self.tableView.scrollIndicatorInsets = inset;
|
||||
// self.tableView.scrollIndicatorInsets = inset;
|
||||
} completion:^(BOOL finished) {
|
||||
[self scrollToBottomAnimated:YES];
|
||||
}];
|
||||
|
||||
@@ -7,10 +7,12 @@
|
||||
|
||||
#import "HomeVC.h"
|
||||
#import "KBGuideVC.h"
|
||||
#import "KBLangTestVC.h"
|
||||
|
||||
@interface HomeVC ()
|
||||
@property (nonatomic, strong) UITextView *textView;
|
||||
|
||||
@interface HomeVC () <UITableViewDelegate, UITableViewDataSource>
|
||||
@property (nonatomic, strong) UITextView *textView; // 作为表头,保持原有键盘测试
|
||||
@property (nonatomic, strong) BaseTableView *tableView; // 首页列表
|
||||
@property (nonatomic, copy) NSArray<NSString *> *items; // 简单数据源
|
||||
@end
|
||||
|
||||
@implementation HomeVC
|
||||
@@ -18,25 +20,72 @@
|
||||
- (void)viewDidLoad {
|
||||
[super viewDidLoad];
|
||||
self.view.backgroundColor = [UIColor whiteColor];
|
||||
CGRect frame = CGRectMake(([UIScreen mainScreen].bounds.size.width - 200)/2, 150, 200, 200);
|
||||
|
||||
CGFloat width = [UIScreen mainScreen].bounds.size.width;
|
||||
UIView *header = [[UIView alloc] initWithFrame:CGRectMake(0, 0, width, 220)];
|
||||
CGRect frame = CGRectMake(16, 16, width - 32, 188);
|
||||
self.textView = [[UITextView alloc] initWithFrame:frame];
|
||||
self.textView.text = @"测试";
|
||||
self.textView.text = KBLocalized(@"home_input_placeholder");
|
||||
self.textView.layer.borderColor = [UIColor blackColor].CGColor;
|
||||
self.textView.layer.borderWidth = 0.5;
|
||||
[self.view addSubview:self.textView];
|
||||
[self.textView becomeFirstResponder];
|
||||
|
||||
[header addSubview:self.textView];
|
||||
|
||||
// 列表
|
||||
self.tableView = [[BaseTableView alloc] initWithFrame:self.view.bounds style:UITableViewStyleInsetGrouped];
|
||||
self.tableView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
|
||||
self.tableView.delegate = self;
|
||||
self.tableView.dataSource = self;
|
||||
self.tableView.tableHeaderView = header;
|
||||
[self.view addSubview:self.tableView];
|
||||
|
||||
self.items = @[ KBLocalized(@"home_item_lang_test"), KBLocalized(@"home_item_keyboard_permission") ];
|
||||
|
||||
dispatch_async(dispatch_get_main_queue(), ^{ [self.textView becomeFirstResponder]; });
|
||||
|
||||
[[KBNetworkManager shared] GET:@"app/config" parameters:nil headers:nil completion:^(id _Nullable jsonOrData, NSURLResponse * _Nullable response, NSError * _Nullable error) {
|
||||
NSLog(@"====");
|
||||
}];
|
||||
}
|
||||
|
||||
- (void)viewWillAppear:(BOOL)animated{
|
||||
[super viewWillAppear:animated];
|
||||
self.title = KBLocalized(@"home_title");
|
||||
self.items = @[ KBLocalized(@"home_item_lang_test"), KBLocalized(@"home_item_keyboard_permission") ];
|
||||
[self.tableView reloadData];
|
||||
}
|
||||
|
||||
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
|
||||
// KBGuideVC *vc = [[KBGuideVC alloc] init];
|
||||
// [self.navigationController pushViewController:vc animated:true];
|
||||
// [KBHUD showInfo:@"加载中..."];
|
||||
[KBHUD show];
|
||||
[KBHUD showAllowTapToDismiss:true];
|
||||
// [KBHUD show];
|
||||
// [KBHUD showAllowTapToDismiss:true];
|
||||
}
|
||||
|
||||
#pragma mark - UITableViewDataSource
|
||||
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView { return 1; }
|
||||
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { return self.items.count; }
|
||||
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
|
||||
static NSString *cellId = @"home.cell";
|
||||
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:cellId];
|
||||
if (!cell) { cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:cellId]; }
|
||||
cell.textLabel.text = self.items[indexPath.row];
|
||||
cell.accessoryType = UITableViewCellAccessoryDisclosureIndicator;
|
||||
return cell;
|
||||
}
|
||||
|
||||
#pragma mark - UITableViewDelegate
|
||||
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
|
||||
[tableView deselectRowAtIndexPath:indexPath animated:YES];
|
||||
if (indexPath.row == 0) {
|
||||
// 多语言测试页
|
||||
KBLangTestVC *vc = [KBLangTestVC new];
|
||||
[self.navigationController pushViewController:vc animated:YES];
|
||||
} else if (indexPath.row == 1) {
|
||||
// 键盘权限引导页
|
||||
KBGuideVC *vc = [KBGuideVC new];
|
||||
[self.navigationController pushViewController:vc animated:YES];
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
|
||||
14
keyBoard/Class/Home/VC/KBLangTestVC.h
Normal file
14
keyBoard/Class/Home/VC/KBLangTestVC.h
Normal file
@@ -0,0 +1,14 @@
|
||||
//
|
||||
// KBLangTestVC.h
|
||||
// 多语言测试页:点击按钮在中英文之间切换,并刷新文案。
|
||||
//
|
||||
|
||||
#import <UIKit/UIKit.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface KBLangTestVC : UIViewController
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
|
||||
60
keyBoard/Class/Home/VC/KBLangTestVC.m
Normal file
60
keyBoard/Class/Home/VC/KBLangTestVC.m
Normal file
@@ -0,0 +1,60 @@
|
||||
//
|
||||
// KBLangTestVC.m
|
||||
//
|
||||
|
||||
#import "KBLangTestVC.h"
|
||||
|
||||
@interface KBLangTestVC ()
|
||||
@property (nonatomic, strong) UILabel *label;
|
||||
@property (nonatomic, strong) UIButton *toggleBtn;
|
||||
@end
|
||||
|
||||
@implementation KBLangTestVC
|
||||
|
||||
- (void)viewDidLoad {
|
||||
[super viewDidLoad];
|
||||
self.view.backgroundColor = UIColor.whiteColor;
|
||||
[self buildUI];
|
||||
[self refreshTexts];
|
||||
|
||||
// 监听语言变更,实时刷新
|
||||
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(refreshTexts) name:KBLocalizationDidChangeNotification object:nil];
|
||||
}
|
||||
|
||||
- (void)dealloc {
|
||||
[[NSNotificationCenter defaultCenter] removeObserver:self];
|
||||
}
|
||||
|
||||
- (void)buildUI {
|
||||
CGFloat w = UIScreen.mainScreen.bounds.size.width;
|
||||
|
||||
self.label = [[UILabel alloc] initWithFrame:CGRectMake(24, 120, w - 48, 60)];
|
||||
self.label.textAlignment = NSTextAlignmentCenter;
|
||||
self.label.numberOfLines = 0;
|
||||
[self.view addSubview:self.label];
|
||||
|
||||
self.toggleBtn = [UIButton buttonWithType:UIButtonTypeSystem];
|
||||
self.toggleBtn.frame = CGRectMake(40, CGRectGetMaxY(self.label.frame) + 24, w - 80, 48);
|
||||
self.toggleBtn.layer.cornerRadius = 8;
|
||||
self.toggleBtn.backgroundColor = [UIColor colorWithRed:0.22 green:0.49 blue:0.96 alpha:1];
|
||||
[self.toggleBtn setTitleColor:UIColor.whiteColor forState:UIControlStateNormal];
|
||||
[self.toggleBtn addTarget:self action:@selector(onToggle) forControlEvents:UIControlEventTouchUpInside];
|
||||
[self.view addSubview:self.toggleBtn];
|
||||
}
|
||||
|
||||
- (void)refreshTexts {
|
||||
self.title = KBLocalized(@"lang_test_title");
|
||||
NSString *code = [KBLocalizationManager shared].currentLanguageCode ?: @"";
|
||||
NSString *fmt = KBLocalized(@"current_lang");
|
||||
self.label.text = [NSString stringWithFormat:fmt.length ? fmt : @"当前:%@", code];
|
||||
[self.toggleBtn setTitle:KBLocalized(@"lang_toggle") forState:UIControlStateNormal];
|
||||
}
|
||||
|
||||
- (void)onToggle {
|
||||
KBLocalizationManager *mgr = [KBLocalizationManager shared];
|
||||
NSString *next = [mgr.currentLanguageCode.lowercaseString hasPrefix:@"zh"] ? @"en" : @"zh-Hans";
|
||||
[mgr setCurrentLanguageCode:next persist:YES];
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@@ -11,10 +11,8 @@ 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
|
||||
|
||||
@@ -35,7 +35,6 @@
|
||||
}
|
||||
|
||||
- (void)buildUI {
|
||||
// 半透明背景,点击可关闭
|
||||
self.backdrop = [[UIControl alloc] init];
|
||||
self.backdrop.backgroundColor = [[UIColor blackColor] colorWithAlphaComponent:0.15];
|
||||
[self.backdrop addTarget:self action:@selector(dismissSelf) forControlEvents:UIControlEventTouchUpInside];
|
||||
@@ -44,11 +43,9 @@
|
||||
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) {
|
||||
@@ -57,7 +54,6 @@
|
||||
make.height.mas_equalTo(160);
|
||||
}];
|
||||
|
||||
// 复选框 + 文案
|
||||
self.checkButton = [UIButton buttonWithType:UIButtonTypeSystem];
|
||||
self.checkButton.layer.cornerRadius = 16;
|
||||
self.checkButton.layer.borderWidth = 2;
|
||||
@@ -84,7 +80,6 @@
|
||||
make.right.lessThanOrEqualTo(self.sheet).offset(-20);
|
||||
}];
|
||||
|
||||
// Continue 按钮
|
||||
self.continueButton = [UIButton buttonWithType:UIButtonTypeSystem];
|
||||
self.continueButton.backgroundColor = [UIColor whiteColor];
|
||||
self.continueButton.layer.cornerRadius = 10;
|
||||
@@ -136,12 +131,10 @@
|
||||
}
|
||||
|
||||
- (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];
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
#import "AppleSignInManager.h"
|
||||
#import <AuthenticationServices/AuthenticationServices.h>
|
||||
#import <Masonry/Masonry.h>
|
||||
#import "KBAuthManager.h"
|
||||
|
||||
@interface LoginViewController ()
|
||||
// 容器视图(用于居中摆放内容)
|
||||
@@ -12,7 +13,7 @@
|
||||
@property (nonatomic, strong) UILabel *titleLabel;
|
||||
// Apple 登录按钮容器(懒加载)
|
||||
@property (nonatomic, strong) UIView *appleContainer;
|
||||
// iOS13+ 的 Apple 登录按钮(懒加载)
|
||||
// Apple 登录按钮(懒加载)
|
||||
@property (nonatomic, strong) ASAuthorizationAppleIDButton *appleButton API_AVAILABLE(ios(13.0));
|
||||
// iOS13 以下的占位按钮(懒加载)
|
||||
@property (nonatomic, strong) UIButton *compatHintButton;
|
||||
@@ -147,6 +148,12 @@
|
||||
if (code.length) info[@"authorizationCode"] = code;
|
||||
}
|
||||
|
||||
// 将示例中的 identityToken 暂存为访问令牌(实际项目应调用服务端换取业务 token)
|
||||
NSString *accessToken = info[@"identityToken"];
|
||||
NSString *uid = info[@"userIdentifier"]; // 不变
|
||||
if (accessToken.length > 0) {
|
||||
[[KBAuthManager shared] saveAccessToken:accessToken refreshToken:nil expiryDate:nil userIdentifier:uid];
|
||||
}
|
||||
if (selfStrong.onLoginSuccess) selfStrong.onLoginSuccess(info);
|
||||
}];
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
|
||||
#import "KBNetworkManager.h"
|
||||
#import "AFNetworking.h"
|
||||
#import "KBAuthManager.h"
|
||||
|
||||
NSErrorDomain const KBNetworkErrorDomain = @"com.company.keyboard.network";
|
||||
|
||||
@@ -95,8 +96,10 @@ NSErrorDomain const KBNetworkErrorDomain = @"com.company.keyboard.network";
|
||||
}
|
||||
|
||||
- (void)applyHeaders:(NSDictionary<NSString *,NSString *> *)headers toMutableRequest:(NSMutableURLRequest *)req contentType:(NSString *)contentType {
|
||||
// 合并默认头与局部头,局部覆盖
|
||||
// 合并默认头与局部头,并注入授权头(若可用)。局部覆盖优先。
|
||||
NSMutableDictionary *all = [self.defaultHeaders mutableCopy] ?: [NSMutableDictionary new];
|
||||
NSDictionary *auth = [[KBAuthManager shared] authorizationHeader];
|
||||
[auth enumerateKeysAndObjectsUsingBlock:^(NSString *key, NSString *obj, BOOL *stop) { all[key] = obj; }];
|
||||
if (contentType) all[@"Content-Type"] = contentType;
|
||||
[headers enumerateKeysAndObjectsUsingBlock:^(NSString *key, NSString *obj, BOOL *stop) { all[key] = obj; }];
|
||||
[all enumerateKeysAndObjectsUsingBlock:^(NSString *key, NSString *obj, BOOL *stop) { [req setValue:obj forHTTPHeaderField:key]; }];
|
||||
|
||||
56
keyBoard/Class/Shared/KBAuthManager.h
Normal file
56
keyBoard/Class/Shared/KBAuthManager.h
Normal file
@@ -0,0 +1,56 @@
|
||||
//
|
||||
// KBAuthManager.h
|
||||
// 主 App 与键盘扩展共享使用
|
||||
//
|
||||
// 通过 Keychain Sharing 统一管理用户登录态(access/refresh token)。
|
||||
// 线程安全;在保存/清空时同时发送进程内通知与 Darwin 跨进程通知,
|
||||
// 以便键盘扩展正运行在其他 App 时也能及时感知变更。
|
||||
//
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
/// Darwin 跨进程通知名称:当令牌更新或清除时发送,用于提示 App/扩展刷新缓存。
|
||||
extern NSString * const kKBDarwinAuthChanged;
|
||||
|
||||
/// 进程内通知(NSNotificationCenter):令牌更新或清除时发送。
|
||||
extern NSNotificationName const KBAuthChangedNotification;
|
||||
|
||||
/// 简单的会话容器;可按需扩展字段。
|
||||
@interface KBAuthSession : NSObject <NSSecureCoding>
|
||||
@property (nonatomic, copy, nullable) NSString *accessToken;
|
||||
@property (nonatomic, copy, nullable) NSString *refreshToken;
|
||||
@property (nonatomic, strong, nullable) NSDate *expiryDate; // 可选:过期时间
|
||||
@property (nonatomic, copy, nullable) NSString *userIdentifier; // 可选:如“用 Apple 登录”的 userIdentifier
|
||||
@end
|
||||
|
||||
/// 基于“共享钥匙串”的鉴权管理器(使用 Keychain Sharing 访问组)。
|
||||
@interface KBAuthManager : NSObject
|
||||
|
||||
+ (instancetype)shared;
|
||||
|
||||
/// 当前会话(内存缓存),在加载/保存/清除后更新。
|
||||
@property (atomic, strong, readonly, nullable) KBAuthSession *current;
|
||||
|
||||
/// 是否已登录:存在 accessToken 且未明显过期(未设置过期时间则只要有 token 即视为已登录)。
|
||||
- (BOOL)isLoggedIn;
|
||||
|
||||
/// 从钥匙串加载到内存;通常首次访问时会自动加载。
|
||||
- (void)reloadFromKeychain;
|
||||
|
||||
/// 保存令牌到“共享钥匙串”并通知观察者。
|
||||
- (BOOL)saveAccessToken:(NSString *)accessToken
|
||||
refreshToken:(nullable NSString *)refreshToken
|
||||
expiryDate:(nullable NSDate *)expiryDate
|
||||
userIdentifier:(nullable NSString *)userIdentifier;
|
||||
|
||||
/// 从钥匙串与内存中清除令牌,并通知观察者。
|
||||
- (void)signOut;
|
||||
|
||||
/// 便捷方法:若存在有效令牌,返回 `Authorization` 请求头;否则返回空字典。
|
||||
- (NSDictionary<NSString *, NSString *> *)authorizationHeader;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
211
keyBoard/Class/Shared/KBAuthManager.m
Normal file
211
keyBoard/Class/Shared/KBAuthManager.m
Normal file
@@ -0,0 +1,211 @@
|
||||
//
|
||||
// KBAuthManager.m
|
||||
//
|
||||
// 关键点:
|
||||
// - 使用固定的 service/account 将 KBAuthSession 序列化后保存到钥匙串;
|
||||
// - 通过 kSecAttrAccessGroup 指定 Keychain Sharing 访问组,实现 App 与扩展共享;
|
||||
// - 保存/清除时发送 Darwin 跨进程通知,便于对端刷新缓存;
|
||||
//
|
||||
|
||||
#import "KBAuthManager.h"
|
||||
#import <Security/Security.h>
|
||||
#import "KBConfig.h" // 需要共享钥匙串访问组常量,见 KBConfig.h 中的说明
|
||||
|
||||
NSString * const kKBDarwinAuthChanged = @"com.keyBoardst.auth.changed";
|
||||
NSNotificationName const KBAuthChangedNotification = @"KBAuthChangedNotification";
|
||||
|
||||
static NSString * const kKBKCService = @"com.keyBoardst.auth"; // 钥匙串 service 名
|
||||
static NSString * const kKBKCAccount = @"session"; // 钥匙串 account 键
|
||||
|
||||
// 用于 Keychain Sharing 的访问组;必须与两个 target 的 entitlements 配置一致。
|
||||
// 示例(Capabilities 中勾选 Keychain Sharing 后的值):
|
||||
// $(AppIdentifierPrefix)com.keyBoardst.shared
|
||||
// 运行时会被展开为:TN6HHV45BB.com.keyBoardst.shared
|
||||
#ifndef KB_KEYCHAIN_ACCESS_GROUP
|
||||
#define KB_KEYCHAIN_ACCESS_GROUP @"TN6HHV45BB.com.keyBoardst.shared"
|
||||
#endif
|
||||
|
||||
// 过期宽限:若过期时间距离当前 <= 该阈值,则视为已过期。
|
||||
static const NSTimeInterval kKBExpiryGrace = 5.0; // 秒
|
||||
|
||||
@implementation KBAuthSession
|
||||
|
||||
+ (BOOL)supportsSecureCoding { return YES; }
|
||||
|
||||
- (void)encodeWithCoder:(NSCoder *)coder {
|
||||
[coder encodeObject:self.accessToken forKey:@"accessToken"];
|
||||
[coder encodeObject:self.refreshToken forKey:@"refreshToken"];
|
||||
[coder encodeObject:self.expiryDate forKey:@"expiryDate"];
|
||||
[coder encodeObject:self.userIdentifier forKey:@"userIdentifier"];
|
||||
}
|
||||
|
||||
- (instancetype)initWithCoder:(NSCoder *)coder {
|
||||
if (self = [super init]) {
|
||||
_accessToken = [coder decodeObjectOfClass:NSString.class forKey:@"accessToken"];
|
||||
_refreshToken = [coder decodeObjectOfClass:NSString.class forKey:@"refreshToken"];
|
||||
_expiryDate = [coder decodeObjectOfClass:NSDate.class forKey:@"expiryDate"];
|
||||
_userIdentifier = [coder decodeObjectOfClass:NSString.class forKey:@"userIdentifier"];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@interface KBAuthManager ()
|
||||
@property (atomic, strong, readwrite, nullable) KBAuthSession *current;
|
||||
@end
|
||||
|
||||
@implementation KBAuthManager
|
||||
|
||||
+ (instancetype)shared {
|
||||
static KBAuthManager *m; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ m = [KBAuthManager new]; });
|
||||
return m;
|
||||
}
|
||||
|
||||
#if DEBUG
|
||||
static inline void KBLog(NSString *fmt, ...) {
|
||||
va_list args; va_start(args, fmt);
|
||||
NSString *msg = [[NSString alloc] initWithFormat:fmt arguments:args];
|
||||
va_end(args);
|
||||
NSLog(@"[KBAuth] %@", msg);
|
||||
}
|
||||
#endif
|
||||
|
||||
- (instancetype)init {
|
||||
if (self = [super init]) {
|
||||
[self reloadFromKeychain];
|
||||
// 监听 Darwin 跨进程通知(App 与扩展之间)
|
||||
CFNotificationCenterAddObserver(CFNotificationCenterGetDarwinNotifyCenter(),
|
||||
(__bridge const void *)(self),
|
||||
KBAuthDarwinCallback,
|
||||
(__bridge CFStringRef)kKBDarwinAuthChanged,
|
||||
NULL,
|
||||
CFNotificationSuspensionBehaviorDeliverImmediately);
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
static void KBAuthDarwinCallback(CFNotificationCenterRef center, void *observer, CFStringRef name, const void *object, CFDictionaryRef userInfo) {
|
||||
KBAuthManager *self = (__bridge KBAuthManager *)observer;
|
||||
[self reloadFromKeychain];
|
||||
}
|
||||
|
||||
- (void)dealloc {
|
||||
CFNotificationCenterRemoveObserver(CFNotificationCenterGetDarwinNotifyCenter(), (__bridge const void *)(self), (__bridge CFStringRef)kKBDarwinAuthChanged, NULL);
|
||||
}
|
||||
|
||||
- (BOOL)isLoggedIn {
|
||||
KBAuthSession *s = self.current;
|
||||
if (s.accessToken.length == 0) return NO;
|
||||
if (!s.expiryDate) return YES; // 未设置过期时间时,只要有 token 即视为已登录
|
||||
return ([s.expiryDate timeIntervalSinceNow] > kKBExpiryGrace);
|
||||
}
|
||||
|
||||
#pragma mark - Public
|
||||
|
||||
- (void)reloadFromKeychain {
|
||||
NSData *data = [self keychainRead];
|
||||
KBAuthSession *session = nil;
|
||||
if (data.length > 0) {
|
||||
@try {
|
||||
session = [NSKeyedUnarchiver unarchivedObjectOfClass:KBAuthSession.class fromData:data error:NULL];
|
||||
} @catch (__unused NSException *e) { session = nil; }
|
||||
}
|
||||
self.current = session;
|
||||
[[NSNotificationCenter defaultCenter] postNotificationName:KBAuthChangedNotification object:nil]; // 进程内通知
|
||||
}
|
||||
|
||||
- (BOOL)saveAccessToken:(NSString *)accessToken
|
||||
refreshToken:(NSString *)refreshToken
|
||||
expiryDate:(NSDate *)expiryDate
|
||||
userIdentifier:(NSString *)userIdentifier {
|
||||
KBAuthSession *s = [KBAuthSession new];
|
||||
s.accessToken = accessToken ?: @"";
|
||||
s.refreshToken = refreshToken;
|
||||
s.expiryDate = expiryDate;
|
||||
s.userIdentifier = userIdentifier;
|
||||
|
||||
NSError *err = nil;
|
||||
NSData *data = [NSKeyedArchiver archivedDataWithRootObject:s requiringSecureCoding:YES error:&err];
|
||||
if (err || data.length == 0) return NO;
|
||||
|
||||
BOOL ok = [self keychainWrite:data];
|
||||
if (ok) {
|
||||
self.current = s;
|
||||
// 进程内通知
|
||||
[[NSNotificationCenter defaultCenter] postNotificationName:KBAuthChangedNotification object:nil];
|
||||
// 跨进程通知(App <-> 扩展)
|
||||
CFNotificationCenterPostNotification(CFNotificationCenterGetDarwinNotifyCenter(), (__bridge CFStringRef)kKBDarwinAuthChanged, NULL, NULL, true);
|
||||
}
|
||||
return ok;
|
||||
}
|
||||
|
||||
- (void)signOut {
|
||||
[self keychainDelete];
|
||||
self.current = nil;
|
||||
[[NSNotificationCenter defaultCenter] postNotificationName:KBAuthChangedNotification object:nil];
|
||||
CFNotificationCenterPostNotification(CFNotificationCenterGetDarwinNotifyCenter(), (__bridge CFStringRef)kKBDarwinAuthChanged, NULL, NULL, true);
|
||||
}
|
||||
|
||||
- (NSDictionary<NSString *,NSString *> *)authorizationHeader {
|
||||
NSString *t = self.current.accessToken;
|
||||
if (t.length == 0) return @{}; // 未登录返回空头部
|
||||
return @{ @"Authorization": [@"Bearer " stringByAppendingString:t] };
|
||||
}
|
||||
|
||||
#pragma mark - Keychain (shared)
|
||||
|
||||
- (NSMutableDictionary *)baseKCQuery {
|
||||
NSMutableDictionary *q = [@{ (__bridge id)kSecClass: (__bridge id)kSecClassGenericPassword,
|
||||
(__bridge id)kSecAttrService: kKBKCService,
|
||||
(__bridge id)kSecAttrAccount: kKBKCAccount } mutableCopy];
|
||||
// 指定共享访问组(App 与扩展共用同一组)
|
||||
q[(__bridge id)kSecAttrAccessGroup] = KB_KEYCHAIN_ACCESS_GROUP;
|
||||
return q;
|
||||
}
|
||||
|
||||
- (BOOL)keychainWrite:(NSData *)data {
|
||||
if (!data) return NO;
|
||||
NSMutableDictionary *query = [self baseKCQuery];
|
||||
SecItemDelete((__bridge CFDictionaryRef)query);
|
||||
|
||||
// 设置属性
|
||||
query[(__bridge id)kSecValueData] = data;
|
||||
// 访问控制:设备首次解锁后可读,不随备份迁移
|
||||
query[(__bridge id)kSecAttrAccessible] = (__bridge id)kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly;
|
||||
|
||||
OSStatus status = SecItemAdd((__bridge CFDictionaryRef)query, NULL);
|
||||
#if DEBUG
|
||||
if (status != errSecSuccess) {
|
||||
KBLog(@"SecItemAdd failed status=%ld group=%@", (long)status, KB_KEYCHAIN_ACCESS_GROUP);
|
||||
} else {
|
||||
KBLog(@"SecItemAdd ok group=%@", KB_KEYCHAIN_ACCESS_GROUP);
|
||||
}
|
||||
#endif
|
||||
return (status == errSecSuccess);
|
||||
}
|
||||
|
||||
- (NSData *)keychainRead {
|
||||
NSMutableDictionary *query = [self baseKCQuery];
|
||||
query[(__bridge id)kSecReturnData] = @YES;
|
||||
query[(__bridge id)kSecMatchLimit] = (__bridge id)kSecMatchLimitOne;
|
||||
|
||||
CFTypeRef dataRef = NULL;
|
||||
OSStatus status = SecItemCopyMatching((__bridge CFDictionaryRef)query, &dataRef);
|
||||
#if DEBUG
|
||||
if (status != errSecSuccess) {
|
||||
KBLog(@"SecItemCopyMatching status=%ld group=%@ (item not found or no entitlement)", (long)status, KB_KEYCHAIN_ACCESS_GROUP);
|
||||
} else {
|
||||
KBLog(@"SecItemCopyMatching ok group=%@", KB_KEYCHAIN_ACCESS_GROUP);
|
||||
}
|
||||
#endif
|
||||
if (status != errSecSuccess || !dataRef) return nil;
|
||||
return (__bridge_transfer NSData *)dataRef;
|
||||
}
|
||||
|
||||
- (void)keychainDelete {
|
||||
NSDictionary *query = [self baseKCQuery];
|
||||
SecItemDelete((__bridge CFDictionaryRef)query);
|
||||
}
|
||||
|
||||
@end
|
||||
33
keyBoard/Class/Shared/KBConfig.h
Normal file
33
keyBoard/Class/Shared/KBConfig.h
Normal file
@@ -0,0 +1,33 @@
|
||||
//
|
||||
// KBConfig.h
|
||||
// 主 App 与键盘扩展共用的配置/宏。
|
||||
//
|
||||
// 在此处修改后,会通过 PCH 被两个 target 同步引用。
|
||||
//
|
||||
|
||||
#ifndef KBConfig_h
|
||||
#define KBConfig_h
|
||||
|
||||
// 基础baseUrl
|
||||
#ifndef KB_BASE_URL
|
||||
#define KB_BASE_URL @"https://m1.apifoxmock.com/m1/5438099-5113192-default/"
|
||||
#endif
|
||||
|
||||
// Universal Links 通用链接
|
||||
#ifndef KB_UL_BASE
|
||||
#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"
|
||||
|
||||
#endif /* KBConfig_h */
|
||||
|
||||
// --- 认证/共享钥匙串 配置 ---
|
||||
// 若已在 Capabilities 中启用 Keychain Sharing,并添加访问组:
|
||||
// $(AppIdentifierPrefix)com.keyBoardst.shared
|
||||
// 运行时会展开为:TN6HHV45BB.com.keyBoardst.shared
|
||||
// KBAuthManager 通过下面的宏定位访问组;如需修改,可在 Build Settings 或前缀头中覆盖该宏。
|
||||
#ifndef KB_KEYCHAIN_ACCESS_GROUP
|
||||
#define KB_KEYCHAIN_ACCESS_GROUP @"TN6HHV45BB.com.keyBoardst.shared"
|
||||
#endif
|
||||
@@ -2,21 +2,21 @@
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CFBundleURLTypes</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>CFBundleURLName</key>
|
||||
<string>com.keyBoardst.keyboard</string>
|
||||
<key>CFBundleURLSchemes</key>
|
||||
<array>
|
||||
<string>kbkeyboard</string>
|
||||
</array>
|
||||
</dict>
|
||||
</array>
|
||||
<key>NSAppTransportSecurity</key>
|
||||
<dict>
|
||||
<key>NSAllowsArbitraryLoads</key>
|
||||
<true/>
|
||||
</dict>
|
||||
<key>CFBundleURLTypes</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>CFBundleURLName</key>
|
||||
<string>com.keyBoardst.keyboard</string>
|
||||
<key>CFBundleURLSchemes</key>
|
||||
<array>
|
||||
<string>kbkeyboard</string>
|
||||
</array>
|
||||
</dict>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||
|
||||
@@ -25,9 +25,11 @@
|
||||
|
||||
|
||||
/// 项目
|
||||
#import "UIViewController+Extension.h"
|
||||
#import "BaseNavigationController.h"
|
||||
#import "BaseTableView.h"
|
||||
#import "BaseCell.h"
|
||||
#import "KBLocalizationManager.h" // 全局多语言封装
|
||||
|
||||
|
||||
//-----------------------------------------------宏定义全局----------------------------------------------------------/
|
||||
|
||||
56
keyBoard/Shared/KBAuthManager.h
Normal file
56
keyBoard/Shared/KBAuthManager.h
Normal file
@@ -0,0 +1,56 @@
|
||||
//
|
||||
// KBAuthManager.h
|
||||
// 主 App 与键盘扩展共享使用
|
||||
//
|
||||
// 通过 Keychain Sharing 统一管理用户登录态(access/refresh token)。
|
||||
// 线程安全;在保存/清空时同时发送进程内通知与 Darwin 跨进程通知,
|
||||
// 以便键盘扩展正运行在其他 App 时也能及时感知变更。
|
||||
//
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
/// Darwin 跨进程通知名称:当令牌更新或清除时发送,用于提示 App/扩展刷新缓存。
|
||||
extern NSString * const kKBDarwinAuthChanged;
|
||||
|
||||
/// 进程内通知(NSNotificationCenter):令牌更新或清除时发送。
|
||||
extern NSNotificationName const KBAuthChangedNotification;
|
||||
|
||||
/// 简单的会话容器;可按需扩展字段。
|
||||
@interface KBAuthSession : NSObject <NSSecureCoding>
|
||||
@property (nonatomic, copy, nullable) NSString *accessToken;
|
||||
@property (nonatomic, copy, nullable) NSString *refreshToken;
|
||||
@property (nonatomic, strong, nullable) NSDate *expiryDate; // 可选:过期时间
|
||||
@property (nonatomic, copy, nullable) NSString *userIdentifier; // 可选:如“用 Apple 登录”的 userIdentifier
|
||||
@end
|
||||
|
||||
/// 基于“共享钥匙串”的鉴权管理器(使用 Keychain Sharing 访问组)。
|
||||
@interface KBAuthManager : NSObject
|
||||
|
||||
+ (instancetype)shared;
|
||||
|
||||
/// 当前会话(内存缓存),在加载/保存/清除后更新。
|
||||
@property (atomic, strong, readonly, nullable) KBAuthSession *current;
|
||||
|
||||
/// 是否已登录:存在 accessToken 且未明显过期(未设置过期时间则只要有 token 即视为已登录)。
|
||||
- (BOOL)isLoggedIn;
|
||||
|
||||
/// 从钥匙串加载到内存;通常首次访问时会自动加载。
|
||||
- (void)reloadFromKeychain;
|
||||
|
||||
/// 保存令牌到“共享钥匙串”并通知观察者。
|
||||
- (BOOL)saveAccessToken:(NSString *)accessToken
|
||||
refreshToken:(nullable NSString *)refreshToken
|
||||
expiryDate:(nullable NSDate *)expiryDate
|
||||
userIdentifier:(nullable NSString *)userIdentifier;
|
||||
|
||||
/// 从钥匙串与内存中清除令牌,并通知观察者。
|
||||
- (void)signOut;
|
||||
|
||||
/// 便捷方法:若存在有效令牌,返回 `Authorization` 请求头;否则返回空字典。
|
||||
- (NSDictionary<NSString *, NSString *> *)authorizationHeader;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
211
keyBoard/Shared/KBAuthManager.m
Normal file
211
keyBoard/Shared/KBAuthManager.m
Normal file
@@ -0,0 +1,211 @@
|
||||
//
|
||||
// KBAuthManager.m
|
||||
//
|
||||
// 关键点:
|
||||
// - 使用固定的 service/account 将 KBAuthSession 序列化后保存到钥匙串;
|
||||
// - 通过 kSecAttrAccessGroup 指定 Keychain Sharing 访问组,实现 App 与扩展共享;
|
||||
// - 保存/清除时发送 Darwin 跨进程通知,便于对端刷新缓存;
|
||||
//
|
||||
|
||||
#import "KBAuthManager.h"
|
||||
#import <Security/Security.h>
|
||||
#import "KBConfig.h" // 需要共享钥匙串访问组常量,见 KBConfig.h 中的说明
|
||||
|
||||
NSString * const kKBDarwinAuthChanged = @"com.keyBoardst.auth.changed";
|
||||
NSNotificationName const KBAuthChangedNotification = @"KBAuthChangedNotification";
|
||||
|
||||
static NSString * const kKBKCService = @"com.keyBoardst.auth"; // 钥匙串 service 名
|
||||
static NSString * const kKBKCAccount = @"session"; // 钥匙串 account 键
|
||||
|
||||
// 用于 Keychain Sharing 的访问组;必须与两个 target 的 entitlements 配置一致。
|
||||
// 示例(Capabilities 中勾选 Keychain Sharing 后的值):
|
||||
// $(AppIdentifierPrefix)com.keyBoardst.shared
|
||||
// 运行时会被展开为:TN6HHV45BB.com.keyBoardst.shared
|
||||
#ifndef KB_KEYCHAIN_ACCESS_GROUP
|
||||
#define KB_KEYCHAIN_ACCESS_GROUP @"TN6HHV45BB.com.keyBoardst.shared"
|
||||
#endif
|
||||
|
||||
// 过期宽限:若过期时间距离当前 <= 该阈值,则视为已过期。
|
||||
static const NSTimeInterval kKBExpiryGrace = 5.0; // 秒
|
||||
|
||||
@implementation KBAuthSession
|
||||
|
||||
+ (BOOL)supportsSecureCoding { return YES; }
|
||||
|
||||
- (void)encodeWithCoder:(NSCoder *)coder {
|
||||
[coder encodeObject:self.accessToken forKey:@"accessToken"];
|
||||
[coder encodeObject:self.refreshToken forKey:@"refreshToken"];
|
||||
[coder encodeObject:self.expiryDate forKey:@"expiryDate"];
|
||||
[coder encodeObject:self.userIdentifier forKey:@"userIdentifier"];
|
||||
}
|
||||
|
||||
- (instancetype)initWithCoder:(NSCoder *)coder {
|
||||
if (self = [super init]) {
|
||||
_accessToken = [coder decodeObjectOfClass:NSString.class forKey:@"accessToken"];
|
||||
_refreshToken = [coder decodeObjectOfClass:NSString.class forKey:@"refreshToken"];
|
||||
_expiryDate = [coder decodeObjectOfClass:NSDate.class forKey:@"expiryDate"];
|
||||
_userIdentifier = [coder decodeObjectOfClass:NSString.class forKey:@"userIdentifier"];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@interface KBAuthManager ()
|
||||
@property (atomic, strong, readwrite, nullable) KBAuthSession *current;
|
||||
@end
|
||||
|
||||
@implementation KBAuthManager
|
||||
|
||||
+ (instancetype)shared {
|
||||
static KBAuthManager *m; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ m = [KBAuthManager new]; });
|
||||
return m;
|
||||
}
|
||||
|
||||
#if DEBUG
|
||||
static inline void KBLog(NSString *fmt, ...) {
|
||||
va_list args; va_start(args, fmt);
|
||||
NSString *msg = [[NSString alloc] initWithFormat:fmt arguments:args];
|
||||
va_end(args);
|
||||
NSLog(@"[KBAuth] %@", msg);
|
||||
}
|
||||
#endif
|
||||
|
||||
- (instancetype)init {
|
||||
if (self = [super init]) {
|
||||
[self reloadFromKeychain];
|
||||
// 监听 Darwin 跨进程通知(App 与扩展之间)
|
||||
CFNotificationCenterAddObserver(CFNotificationCenterGetDarwinNotifyCenter(),
|
||||
(__bridge const void *)(self),
|
||||
KBAuthDarwinCallback,
|
||||
(__bridge CFStringRef)kKBDarwinAuthChanged,
|
||||
NULL,
|
||||
CFNotificationSuspensionBehaviorDeliverImmediately);
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
static void KBAuthDarwinCallback(CFNotificationCenterRef center, void *observer, CFStringRef name, const void *object, CFDictionaryRef userInfo) {
|
||||
KBAuthManager *self = (__bridge KBAuthManager *)observer;
|
||||
[self reloadFromKeychain];
|
||||
}
|
||||
|
||||
- (void)dealloc {
|
||||
CFNotificationCenterRemoveObserver(CFNotificationCenterGetDarwinNotifyCenter(), (__bridge const void *)(self), (__bridge CFStringRef)kKBDarwinAuthChanged, NULL);
|
||||
}
|
||||
|
||||
- (BOOL)isLoggedIn {
|
||||
KBAuthSession *s = self.current;
|
||||
if (s.accessToken.length == 0) return NO;
|
||||
if (!s.expiryDate) return YES; // 未设置过期时间时,只要有 token 即视为已登录
|
||||
return ([s.expiryDate timeIntervalSinceNow] > kKBExpiryGrace);
|
||||
}
|
||||
|
||||
#pragma mark - Public
|
||||
|
||||
- (void)reloadFromKeychain {
|
||||
NSData *data = [self keychainRead];
|
||||
KBAuthSession *session = nil;
|
||||
if (data.length > 0) {
|
||||
@try {
|
||||
session = [NSKeyedUnarchiver unarchivedObjectOfClass:KBAuthSession.class fromData:data error:NULL];
|
||||
} @catch (__unused NSException *e) { session = nil; }
|
||||
}
|
||||
self.current = session;
|
||||
[[NSNotificationCenter defaultCenter] postNotificationName:KBAuthChangedNotification object:nil]; // 进程内通知
|
||||
}
|
||||
|
||||
- (BOOL)saveAccessToken:(NSString *)accessToken
|
||||
refreshToken:(NSString *)refreshToken
|
||||
expiryDate:(NSDate *)expiryDate
|
||||
userIdentifier:(NSString *)userIdentifier {
|
||||
KBAuthSession *s = [KBAuthSession new];
|
||||
s.accessToken = accessToken ?: @"";
|
||||
s.refreshToken = refreshToken;
|
||||
s.expiryDate = expiryDate;
|
||||
s.userIdentifier = userIdentifier;
|
||||
|
||||
NSError *err = nil;
|
||||
NSData *data = [NSKeyedArchiver archivedDataWithRootObject:s requiringSecureCoding:YES error:&err];
|
||||
if (err || data.length == 0) return NO;
|
||||
|
||||
BOOL ok = [self keychainWrite:data];
|
||||
if (ok) {
|
||||
self.current = s;
|
||||
// 进程内通知
|
||||
[[NSNotificationCenter defaultCenter] postNotificationName:KBAuthChangedNotification object:nil];
|
||||
// 跨进程通知(App <-> 扩展)
|
||||
CFNotificationCenterPostNotification(CFNotificationCenterGetDarwinNotifyCenter(), (__bridge CFStringRef)kKBDarwinAuthChanged, NULL, NULL, true);
|
||||
}
|
||||
return ok;
|
||||
}
|
||||
|
||||
- (void)signOut {
|
||||
[self keychainDelete];
|
||||
self.current = nil;
|
||||
[[NSNotificationCenter defaultCenter] postNotificationName:KBAuthChangedNotification object:nil];
|
||||
CFNotificationCenterPostNotification(CFNotificationCenterGetDarwinNotifyCenter(), (__bridge CFStringRef)kKBDarwinAuthChanged, NULL, NULL, true);
|
||||
}
|
||||
|
||||
- (NSDictionary<NSString *,NSString *> *)authorizationHeader {
|
||||
NSString *t = self.current.accessToken;
|
||||
if (t.length == 0) return @{}; // 未登录返回空头部
|
||||
return @{ @"Authorization": [@"Bearer " stringByAppendingString:t] };
|
||||
}
|
||||
|
||||
#pragma mark - Keychain (shared)
|
||||
|
||||
- (NSMutableDictionary *)baseKCQuery {
|
||||
NSMutableDictionary *q = [@{ (__bridge id)kSecClass: (__bridge id)kSecClassGenericPassword,
|
||||
(__bridge id)kSecAttrService: kKBKCService,
|
||||
(__bridge id)kSecAttrAccount: kKBKCAccount } mutableCopy];
|
||||
// 指定共享访问组(App 与扩展共用同一组)
|
||||
q[(__bridge id)kSecAttrAccessGroup] = KB_KEYCHAIN_ACCESS_GROUP;
|
||||
return q;
|
||||
}
|
||||
|
||||
- (BOOL)keychainWrite:(NSData *)data {
|
||||
if (!data) return NO;
|
||||
NSMutableDictionary *query = [self baseKCQuery];
|
||||
SecItemDelete((__bridge CFDictionaryRef)query);
|
||||
|
||||
// 设置属性
|
||||
query[(__bridge id)kSecValueData] = data;
|
||||
// 访问控制:设备首次解锁后可读,不随备份迁移
|
||||
query[(__bridge id)kSecAttrAccessible] = (__bridge id)kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly;
|
||||
|
||||
OSStatus status = SecItemAdd((__bridge CFDictionaryRef)query, NULL);
|
||||
#if DEBUG
|
||||
if (status != errSecSuccess) {
|
||||
KBLog(@"SecItemAdd failed status=%ld group=%@", (long)status, KB_KEYCHAIN_ACCESS_GROUP);
|
||||
} else {
|
||||
KBLog(@"SecItemAdd ok group=%@", KB_KEYCHAIN_ACCESS_GROUP);
|
||||
}
|
||||
#endif
|
||||
return (status == errSecSuccess);
|
||||
}
|
||||
|
||||
- (NSData *)keychainRead {
|
||||
NSMutableDictionary *query = [self baseKCQuery];
|
||||
query[(__bridge id)kSecReturnData] = @YES;
|
||||
query[(__bridge id)kSecMatchLimit] = (__bridge id)kSecMatchLimitOne;
|
||||
|
||||
CFTypeRef dataRef = NULL;
|
||||
OSStatus status = SecItemCopyMatching((__bridge CFDictionaryRef)query, &dataRef);
|
||||
#if DEBUG
|
||||
if (status != errSecSuccess) {
|
||||
KBLog(@"SecItemCopyMatching status=%ld group=%@ (item not found or no entitlement)", (long)status, KB_KEYCHAIN_ACCESS_GROUP);
|
||||
} else {
|
||||
KBLog(@"SecItemCopyMatching ok group=%@", KB_KEYCHAIN_ACCESS_GROUP);
|
||||
}
|
||||
#endif
|
||||
if (status != errSecSuccess || !dataRef) return nil;
|
||||
return (__bridge_transfer NSData *)dataRef;
|
||||
}
|
||||
|
||||
- (void)keychainDelete {
|
||||
NSDictionary *query = [self baseKCQuery];
|
||||
SecItemDelete((__bridge CFDictionaryRef)query);
|
||||
}
|
||||
|
||||
@end
|
||||
33
keyBoard/Shared/KBConfig.h
Normal file
33
keyBoard/Shared/KBConfig.h
Normal file
@@ -0,0 +1,33 @@
|
||||
//
|
||||
// KBConfig.h
|
||||
// 主 App 与键盘扩展共用的配置/宏。
|
||||
//
|
||||
// 在此处修改后,会通过 PCH 被两个 target 同步引用。
|
||||
//
|
||||
|
||||
#ifndef KBConfig_h
|
||||
#define KBConfig_h
|
||||
|
||||
// 基础baseUrl
|
||||
#ifndef KB_BASE_URL
|
||||
#define KB_BASE_URL @"https://m1.apifoxmock.com/m1/5438099-5113192-default/"
|
||||
#endif
|
||||
|
||||
// Universal Links 通用链接
|
||||
#ifndef KB_UL_BASE
|
||||
#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"
|
||||
|
||||
#endif /* KBConfig_h */
|
||||
|
||||
// --- 认证/共享钥匙串 配置 ---
|
||||
// 若已在 Capabilities 中启用 Keychain Sharing,并添加访问组:
|
||||
// $(AppIdentifierPrefix)com.keyBoardst.shared
|
||||
// 运行时会展开为:TN6HHV45BB.com.keyBoardst.shared
|
||||
// KBAuthManager 通过下面的宏定位访问组;如需修改,可在 Build Settings 或前缀头中覆盖该宏。
|
||||
#ifndef KB_KEYCHAIN_ACCESS_GROUP
|
||||
#define KB_KEYCHAIN_ACCESS_GROUP @"TN6HHV45BB.com.keyBoardst.shared"
|
||||
#endif
|
||||
@@ -11,6 +11,11 @@ NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface KBPermissionViewController : UIViewController
|
||||
|
||||
/// 点击页面左上角“返回”时回调。用于让调用方(如 KBGuideVC)一起退出等自定义行为。
|
||||
/// 注意:避免与按钮 action `onBack` 重名,故命名为 backHandler。
|
||||
@property (nonatomic, copy, nullable) void (^backHandler)(void);
|
||||
@property (nonatomic, strong) UIButton *backButton; // 左上角返回
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
|
||||
@@ -6,10 +6,13 @@
|
||||
//
|
||||
|
||||
#import "KBPermissionViewController.h"
|
||||
#import <TargetConditionals.h>
|
||||
|
||||
@interface KBPermissionViewController ()
|
||||
|
||||
@property (nonatomic, strong) UILabel *titleLabel; // 标题
|
||||
@property (nonatomic, strong) UILabel *tipsLabel; // 步骤提示
|
||||
@property (nonatomic, strong) UIView *cardView; // 中部卡片
|
||||
@property (nonatomic, strong) UIButton *openButton; // 去设置
|
||||
@property (nonatomic, strong) UILabel *helpLabel; // 底部帮助
|
||||
@end
|
||||
|
||||
@implementation KBPermissionViewController
|
||||
@@ -18,66 +21,83 @@
|
||||
[super viewDidLoad];
|
||||
self.view.backgroundColor = [UIColor colorWithWhite:0.96 alpha:1.0];
|
||||
|
||||
UIButton *close = [UIButton buttonWithType:UIButtonTypeSystem];
|
||||
[close setTitle:@"X" forState:UIControlStateNormal];
|
||||
close.titleLabel.font = [UIFont systemFontOfSize:20 weight:UIFontWeightSemibold];
|
||||
close.tintColor = [UIColor darkTextColor];
|
||||
close.frame = CGRectMake(self.view.bounds.size.width - 44, 44, 28, 28);
|
||||
close.autoresizingMask = UIViewAutoresizingFlexibleLeftMargin | UIViewAutoresizingFlexibleBottomMargin;
|
||||
[close addTarget:self action:@selector(dismissSelf) forControlEvents:UIControlEventTouchUpInside];
|
||||
[self.view addSubview:close];
|
||||
// 懒加载控件 + 添加到视图
|
||||
[self.view addSubview:self.backButton];
|
||||
[self.view addSubview:self.titleLabel];
|
||||
[self.view addSubview:self.tipsLabel];
|
||||
[self.view addSubview:self.cardView];
|
||||
[self.view addSubview:self.openButton];
|
||||
[self.view addSubview:self.helpLabel];
|
||||
|
||||
UILabel *title = [[UILabel alloc] init];
|
||||
title.text = @"启用输入法";
|
||||
title.font = [UIFont systemFontOfSize:22 weight:UIFontWeightSemibold];
|
||||
title.textColor = [UIColor blackColor];
|
||||
title.textAlignment = NSTextAlignmentCenter;
|
||||
title.frame = CGRectMake(24, 100, self.view.bounds.size.width - 48, 28);
|
||||
title.autoresizingMask = UIViewAutoresizingFlexibleWidth;
|
||||
[self.view addSubview:title];
|
||||
// Masonry 约束
|
||||
[self.backButton mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||
make.left.equalTo(self.view).offset(16);
|
||||
make.top.equalTo(self.view.mas_safeAreaLayoutGuideTop).offset(8);
|
||||
|
||||
make.width.mas_equalTo(60);
|
||||
make.height.mas_equalTo(32);
|
||||
}];
|
||||
|
||||
UILabel *tips = [[UILabel alloc] init];
|
||||
tips.text = @"1 开启键盘 > 2 允许完全访问";
|
||||
tips.font = [UIFont systemFontOfSize:14];
|
||||
tips.textColor = [UIColor darkGrayColor];
|
||||
tips.textAlignment = NSTextAlignmentCenter;
|
||||
tips.frame = CGRectMake(24, CGRectGetMaxY(title.frame) + 8, self.view.bounds.size.width - 48, 20);
|
||||
tips.autoresizingMask = UIViewAutoresizingFlexibleWidth;
|
||||
[self.view addSubview:tips];
|
||||
[self.titleLabel mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||
make.top.equalTo(self.view.mas_safeAreaLayoutGuideTop).offset(48);
|
||||
|
||||
make.left.equalTo(self.view).offset(24);
|
||||
make.right.equalTo(self.view).offset(-24);
|
||||
make.height.mas_equalTo(28);
|
||||
}];
|
||||
|
||||
UIView *card = [[UIView alloc] initWithFrame:CGRectMake(32, CGRectGetMaxY(tips.frame) + 28, self.view.bounds.size.width - 64, 260)];
|
||||
card.backgroundColor = [UIColor whiteColor];
|
||||
card.layer.cornerRadius = 16;
|
||||
card.layer.shadowColor = [UIColor colorWithWhite:0 alpha:0.1].CGColor;
|
||||
card.layer.shadowOpacity = 1;
|
||||
card.layer.shadowRadius = 12;
|
||||
[self.view addSubview:card];
|
||||
[self.tipsLabel mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||
make.top.equalTo(self.titleLabel.mas_bottom).offset(8);
|
||||
make.left.equalTo(self.view).offset(24);
|
||||
make.right.equalTo(self.view).offset(-24);
|
||||
}];
|
||||
|
||||
UIButton *open = [UIButton buttonWithType:UIButtonTypeSystem];
|
||||
[open setTitle:@"去设置中开启" forState:UIControlStateNormal];
|
||||
open.titleLabel.font = [UIFont systemFontOfSize:17 weight:UIFontWeightSemibold];
|
||||
[open setTitleColor:[UIColor whiteColor] forState:UIControlStateNormal];
|
||||
open.backgroundColor = [UIColor colorWithRed:0.22 green:0.49 blue:0.96 alpha:1.0];
|
||||
open.layer.cornerRadius = 8;
|
||||
CGFloat btnW = self.view.bounds.size.width - 64;
|
||||
open.frame = CGRectMake(32, CGRectGetMaxY(card.frame) + 32, btnW, 48);
|
||||
open.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleTopMargin;
|
||||
[open addTarget:self action:@selector(openSettings) forControlEvents:UIControlEventTouchUpInside];
|
||||
[self.view addSubview:open];
|
||||
[self.cardView mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||
make.top.equalTo(self.tipsLabel.mas_bottom).offset(28);
|
||||
make.left.equalTo(self.view).offset(32);
|
||||
make.right.equalTo(self.view).offset(-32);
|
||||
make.height.mas_equalTo(260);
|
||||
}];
|
||||
|
||||
UILabel *help = [[UILabel alloc] init];
|
||||
help.text = @"没有找到键盘? 请前往 设置 > 通用 > 键盘 > 键盘 > 添加新键盘";
|
||||
help.font = [UIFont systemFontOfSize:12];
|
||||
help.textColor = [UIColor grayColor];
|
||||
help.textAlignment = NSTextAlignmentCenter;
|
||||
help.numberOfLines = 2;
|
||||
help.frame = CGRectMake(24, CGRectGetMaxY(open.frame) + 12, self.view.bounds.size.width - 48, 36);
|
||||
help.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleTopMargin;
|
||||
[self.view addSubview:help];
|
||||
[self.openButton mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||
make.top.equalTo(self.cardView.mas_bottom).offset(32);
|
||||
make.left.equalTo(self.view).offset(32);
|
||||
make.right.equalTo(self.view).offset(-32);
|
||||
make.height.mas_equalTo(48);
|
||||
}];
|
||||
|
||||
[self.helpLabel mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||
make.top.equalTo(self.openButton.mas_bottom).offset(12);
|
||||
make.left.equalTo(self.view).offset(24);
|
||||
make.right.equalTo(self.view).offset(-24);
|
||||
}];
|
||||
}
|
||||
|
||||
- (void)dismissSelf {
|
||||
[self dismissViewControllerAnimated:YES completion:nil];
|
||||
#pragma mark - Actions
|
||||
|
||||
- (void)onBack {
|
||||
// 支持两种展示方式:
|
||||
// 1) 作为子控制器嵌入(无 presentingViewController)→ 交由回调让父 VC 处理(通常 pop)
|
||||
// 2) 作为模态弹出 → 按原策略先 pop 再 dismiss
|
||||
UIViewController *presenter = self.presentingViewController;
|
||||
if (!presenter) {
|
||||
if (self.backHandler) self.backHandler();
|
||||
return;
|
||||
}
|
||||
|
||||
UINavigationController *nav = nil;
|
||||
if ([presenter isKindOfClass:UINavigationController.class]) {
|
||||
nav = (UINavigationController *)presenter;
|
||||
} else if (presenter.navigationController) {
|
||||
nav = presenter.navigationController;
|
||||
}
|
||||
|
||||
if (nav) {
|
||||
[nav popViewControllerAnimated:NO];
|
||||
[nav dismissViewControllerAnimated:YES completion:^{ if (self.backHandler) self.backHandler(); }];
|
||||
} else {
|
||||
[self dismissViewControllerAnimated:YES completion:^{ if (self.backHandler) self.backHandler(); }];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)openSettings {
|
||||
@@ -92,4 +112,76 @@
|
||||
}
|
||||
}
|
||||
|
||||
#pragma mark - Lazy Subviews
|
||||
|
||||
- (UIButton *)backButton {
|
||||
if (!_backButton) {
|
||||
_backButton = [UIButton buttonWithType:UIButtonTypeSystem];
|
||||
[_backButton setTitle:KBLocalized(@"common_back") forState:UIControlStateNormal];
|
||||
_backButton.titleLabel.font = [UIFont systemFontOfSize:16 weight:UIFontWeightMedium];
|
||||
[_backButton setTitleColor:[UIColor darkTextColor] forState:UIControlStateNormal];
|
||||
[_backButton addTarget:self action:@selector(onBack) forControlEvents:UIControlEventTouchUpInside];
|
||||
}
|
||||
return _backButton;
|
||||
}
|
||||
|
||||
- (UILabel *)titleLabel {
|
||||
if (!_titleLabel) {
|
||||
_titleLabel = [UILabel new];
|
||||
_titleLabel.text = KBLocalized(@"perm_title_enable");
|
||||
_titleLabel.font = [UIFont systemFontOfSize:22 weight:UIFontWeightSemibold];
|
||||
_titleLabel.textColor = [UIColor blackColor];
|
||||
_titleLabel.textAlignment = NSTextAlignmentCenter;
|
||||
}
|
||||
return _titleLabel;
|
||||
}
|
||||
|
||||
- (UILabel *)tipsLabel {
|
||||
if (!_tipsLabel) {
|
||||
_tipsLabel = [UILabel new];
|
||||
_tipsLabel.text = KBLocalized(@"perm_steps");
|
||||
_tipsLabel.font = [UIFont systemFontOfSize:14];
|
||||
_tipsLabel.textColor = [UIColor darkGrayColor];
|
||||
_tipsLabel.textAlignment = NSTextAlignmentCenter;
|
||||
}
|
||||
return _tipsLabel;
|
||||
}
|
||||
|
||||
- (UIView *)cardView {
|
||||
if (!_cardView) {
|
||||
_cardView = [UIView new];
|
||||
_cardView.backgroundColor = [UIColor whiteColor];
|
||||
_cardView.layer.cornerRadius = 16;
|
||||
_cardView.layer.shadowColor = [UIColor colorWithWhite:0 alpha:0.1].CGColor;
|
||||
_cardView.layer.shadowOpacity = 1;
|
||||
_cardView.layer.shadowRadius = 12;
|
||||
}
|
||||
return _cardView;
|
||||
}
|
||||
|
||||
- (UIButton *)openButton {
|
||||
if (!_openButton) {
|
||||
_openButton = [UIButton buttonWithType:UIButtonTypeSystem];
|
||||
[_openButton setTitle:KBLocalized(@"perm_open_settings") forState:UIControlStateNormal];
|
||||
_openButton.titleLabel.font = [UIFont systemFontOfSize:17 weight:UIFontWeightSemibold];
|
||||
[_openButton setTitleColor:[UIColor whiteColor] forState:UIControlStateNormal];
|
||||
_openButton.backgroundColor = [UIColor colorWithRed:0.22 green:0.49 blue:0.96 alpha:1.0];
|
||||
_openButton.layer.cornerRadius = 8;
|
||||
[_openButton addTarget:self action:@selector(openSettings) forControlEvents:UIControlEventTouchUpInside];
|
||||
}
|
||||
return _openButton;
|
||||
}
|
||||
|
||||
- (UILabel *)helpLabel {
|
||||
if (!_helpLabel) {
|
||||
_helpLabel = [UILabel new];
|
||||
_helpLabel.text = KBLocalized(@"perm_help");
|
||||
_helpLabel.font = [UIFont systemFontOfSize:12];
|
||||
_helpLabel.textColor = [UIColor grayColor];
|
||||
_helpLabel.textAlignment = NSTextAlignmentCenter;
|
||||
_helpLabel.numberOfLines = 2;
|
||||
}
|
||||
return _helpLabel;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@@ -6,5 +6,9 @@
|
||||
<array>
|
||||
<string>Default</string>
|
||||
</array>
|
||||
<key>keychain-access-groups</key>
|
||||
<array>
|
||||
<string>$(AppIdentifierPrefix)com.keyBoardst.shared</string>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||
|
||||
Reference in New Issue
Block a user