Compare commits

...

10 Commits

Author SHA1 Message Date
5af2612ff7 fix 2025-11-03 20:02:11 +08:00
cac2f13b88 修改引导逻辑 2025-11-03 19:00:47 +08:00
edf88721da 调整逻辑 2025-11-03 18:45:06 +08:00
915b329805 1 2025-11-03 16:57:24 +08:00
1673a2f4be 添加多语言 2025-11-03 16:37:28 +08:00
e4cebeac85 处理再次进入弹起权限弹窗 2025-11-03 15:04:19 +08:00
c7021e382e fixUI 2025-11-03 13:25:41 +08:00
ffea9d2022 修改KBAuthSession主程序添加token extension没有拿到的情况 2025-10-31 16:50:15 +08:00
90c1e7ff6c 添加token管理 2025-10-31 16:06:54 +08:00
59d04bb33c extension添加提示 2025-10-31 15:08:30 +08:00
47 changed files with 2120 additions and 170 deletions

View 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>

View File

@@ -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];
}
}
}

View 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

View 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

View File

@@ -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]; }];

View File

@@ -18,6 +18,8 @@
// 公共配置
#import "KBConfig.h"
#import "Masonry.h"
#import "KBHUD.h" // 复用 App 内的 HUD 封装
#import "KBLocalizationManager.h" // 复用多语言封装(可在扩展内使用)
// 通用链接Universal Links统一配置
// 配置好 AASA 与 Associated Domains 后,只需修改这里即可切换域名/path。

View File

@@ -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
View 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
View 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

View File

@@ -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

View 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

View 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

View 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

View 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];
// TargetApp 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

View 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";

View 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" = "返回";

View File

@@ -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";

View File

@@ -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
}

View 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
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 388 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 674 B

View File

@@ -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];
}

View File

@@ -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];
}
/*

View File

@@ -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

View File

@@ -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

View 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

View 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

View File

@@ -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];
}];

View File

@@ -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];
}
}
/*

View File

@@ -0,0 +1,14 @@
//
// KBLangTestVC.h
// 多语言测试页:点击按钮在中英文之间切换,并刷新文案。
//
#import <UIKit/UIKit.h>
NS_ASSUME_NONNULL_BEGIN
@interface KBLangTestVC : UIViewController
@end
NS_ASSUME_NONNULL_END

View 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

View File

@@ -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

View File

@@ -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];

View File

@@ -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);
}];
}

View File

@@ -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]; }];

View 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

View 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

View 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

View File

@@ -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>

View File

@@ -25,9 +25,11 @@
/// 项目
#import "UIViewController+Extension.h"
#import "BaseNavigationController.h"
#import "BaseTableView.h"
#import "BaseCell.h"
#import "KBLocalizationManager.h" // 全局多语言封装
//-----------------------------------------------宏定义全局----------------------------------------------------------/

View 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

View 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

View 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

View File

@@ -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

View File

@@ -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

View File

@@ -6,5 +6,9 @@
<array>
<string>Default</string>
</array>
<key>keychain-access-groups</key>
<array>
<string>$(AppIdentifierPrefix)com.keyBoardst.shared</string>
</array>
</dict>
</plist>