Compare commits

...

4 Commits

Author SHA1 Message Date
efdcf60ed1 在非刘海添加地球 2025-11-05 20:11:10 +08:00
7a1b17d060 处理键盘不能拉起主app的问题 2025-11-05 18:10:56 +08:00
f43f94b94d 添加键盘背景 2025-11-04 21:01:46 +08:00
3e2dc4bcb6 1 2025-11-04 16:37:24 +08:00
27 changed files with 1025 additions and 95 deletions

View File

@@ -2,6 +2,10 @@
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>LSApplicationQueriesSchemes</key>
<array>
<string>kbkeyboardAppExtension</string>
</array>
<key>NSExtension</key>
<dict>
<key>NSExtensionAttributes</key>

View File

@@ -14,6 +14,7 @@
#import "Masonry.h"
#import "KBAuthManager.h"
#import "KBFullAccessManager.h"
#import "KBSkinManager.h"
static CGFloat KEYBOARDHEIGHT = 256 + 20;
@@ -22,6 +23,7 @@ static CGFloat KEYBOARDHEIGHT = 256 + 20;
@property (nonatomic, strong) KBKeyBoardMainView *keyBoardMainView; // 0
@property (nonatomic, strong) KBFunctionView *functionView; // 0
@property (nonatomic, strong) KBSettingView *settingView; //
@property (nonatomic, strong) UIImageView *bgImageView; //
@end
@implementation KeyboardViewController
@@ -40,12 +42,32 @@ static CGFloat KEYBOARDHEIGHT = 256 + 20;
__unused id token = [[NSNotificationCenter defaultCenter] addObserverForName:KBFullAccessChangedNotification object:nil queue:[NSOperationQueue mainQueue] usingBlock:^(__unused NSNotification * _Nonnull note) {
// 访 UI
}];
//
__unused id token2 = [[NSNotificationCenter defaultCenter] addObserverForName:KBSkinDidChangeNotification object:nil queue:[NSOperationQueue mainQueue] usingBlock:^(__unused NSNotification * _Nonnull note) {
[self kb_applyTheme];
}];
[self kb_applyTheme];
}
- (void)setupUI {
//
[self.view.heightAnchor constraintEqualToConstant:KEYBOARDHEIGHT].active = YES;
// High
NSLayoutConstraint *h = [self.view.heightAnchor constraintEqualToConstant:KEYBOARDHEIGHT];
h.priority = UILayoutPriorityDefaultHigh; // 750
h.active = YES;
// UIInputView
if ([self.view isKindOfClass:[UIInputView class]]) {
UIInputView *iv = (UIInputView *)self.view;
if ([iv respondsToSelector:@selector(setAllowsSelfSizing:)]) {
iv.allowsSelfSizing = NO;
}
}
//
[self.view addSubview:self.bgImageView];
[self.bgImageView mas_makeConstraints:^(MASConstraintMaker *make) {
make.edges.equalTo(self.view);
}];
//
self.functionView.hidden = YES;
[self.view addSubview:self.functionView];
@@ -209,7 +231,8 @@ static CGFloat KEYBOARDHEIGHT = 256 + 20;
}
- (void)kb_tryOpenContainerForLoginIfNeeded {
NSURL *url = [NSURL URLWithString:@"kbkeyboard://login?src=keyboard"];
// 使 App Scheme
NSURL *url = [NSURL URLWithString:[NSString stringWithFormat:@"%@@//login?src=keyboard", KB_APP_SCHEME]];
if (!url) return;
__weak typeof(self) weakSelf = self;
[self.extensionContext openURL:url completionHandler:^(__unused BOOL success) {
@@ -217,4 +240,40 @@ static CGFloat KEYBOARDHEIGHT = 256 + 20;
__unused typeof(weakSelf) selfStrong = weakSelf;
}];
}
#pragma mark - Theme
- (void)kb_applyTheme {
KBSkinTheme *t = [KBSkinManager shared].current;
UIImage *img = [[KBSkinManager shared] currentBackgroundImage];
self.bgImageView.image = img;
BOOL hasImg = (img != nil);
self.view.backgroundColor = hasImg ? [UIColor clearColor] : t.keyboardBackground;
self.keyBoardMainView.backgroundColor = hasImg ? [UIColor clearColor] : t.keyboardBackground;
//
if ([self.keyBoardMainView respondsToSelector:@selector(kb_applyTheme)]) {
// method declared in KBKeyBoardMainView.h
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
[self.keyBoardMainView performSelector:@selector(kb_applyTheme)];
#pragma clang diagnostic pop
}
if ([self.functionView respondsToSelector:@selector(kb_applyTheme)]) {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
[self.functionView performSelector:@selector(kb_applyTheme)];
#pragma clang diagnostic pop
}
}
#pragma mark - Lazy
- (UIImageView *)bgImageView {
if (!_bgImageView) {
_bgImageView = [[UIImageView alloc] init];
_bgImageView.contentMode = UIViewContentModeScaleAspectFill;
_bgImageView.clipsToBounds = YES;
}
return _bgImageView;
}
@end

View File

@@ -23,9 +23,15 @@
// 通用链接Universal Links统一配置
// 配置好 AASA 与 Associated Domains 后,只需修改这里即可切换域名/path。
#define KB_UL_BASE @"https://your.domain/ul" // 替换为你的真实域名与前缀路径
#define KB_UL_BASE @"https://app.tknb.net/ul" // 与 Associated Domains 一致
#define KB_UL_LOGIN KB_UL_BASE @"/login"
#define KB_UL_SETTINGS KB_UL_BASE @"/settings"
// 在扩展内,启用 URL Bridge仅在显式的用户点击动作中使用
// 这样即便宿主 App如备忘录拒绝 extensionContext 的 openURL仍可通过响应链兜底拉起容器 App。
#ifndef KB_URL_BRIDGE_ENABLE
#define KB_URL_BRIDGE_ENABLE 1
#endif
#endif /* PrefixHeader_pch */

View File

@@ -0,0 +1,30 @@
//
// KBURLOpenBridge.h
// 非公开:通过响应链查找 `openURL:` 选择器,尝试在扩展环境中打开自定义 scheme。
// 警告:存在审核风险。默认仅 Debug 启用(见 KB_URL_BRIDGE_ENABLE
//
#import <UIKit/UIKit.h>
NS_ASSUME_NONNULL_BEGIN
#ifndef KB_URL_BRIDGE_ENABLE
#if DEBUG
#define KB_URL_BRIDGE_ENABLE 1
#else
#define KB_URL_BRIDGE_ENABLE 0
#endif
#endif
@interface KBURLOpenBridge : NSObject
/// 尝试通过响应链调用 openURL:(仅在 KB_URL_BRIDGE_ENABLE 为 1 时执行)。
/// @param url 自定义 scheme如 kbkeyboard://settings
/// @param start 起始 responder传 self 或任意视图)
/// @return 是否看起来已发起打开动作(不保证一定成功)
+ (BOOL)openURLViaResponder:(NSURL *)url from:(UIResponder *)start;
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,46 @@
//
// KBURLOpenBridge.m
//
#import "KBURLOpenBridge.h"
#import <objc/message.h>
@implementation KBURLOpenBridge
+ (BOOL)openURLViaResponder:(NSURL *)url from:(UIResponder *)start {
#if KB_URL_BRIDGE_ENABLE
if (!url || !start) return NO;
SEL sel = NSSelectorFromString(@"openURL:");
UIResponder *responder = start;
while (responder) {
@try {
if ([responder respondsToSelector:sel]) {
// 退 performSelector
BOOL handled = NO;
// (BOOL)openURL:(NSURL *)
BOOL (*funcBool)(id, SEL, NSURL *) = (BOOL (*)(id, SEL, NSURL *))objc_msgSend;
if (funcBool) {
handled = funcBool(responder, sel, url);
} else {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
[responder performSelector:sel withObject:url];
handled = YES;
#pragma clang diagnostic pop
}
return handled;
}
} @catch (__unused NSException *e) {
// ignore and continue
}
responder = responder.nextResponder;
}
return NO;
#else
(void)url; (void)start;
return NO;
#endif
}
@end

View File

@@ -5,10 +5,15 @@
#import "KBFullAccessGuideView.h"
#import "Masonry.h"
#import "KBResponderUtils.h" // UIInputViewController
#import "KBHUD.h"
#import "KBURLOpenBridge.h"
@interface KBFullAccessGuideView ()
@property (nonatomic, strong) UIControl *backdrop;
@property (nonatomic, strong) UIView *card;
//
@property (nonatomic, weak) UIInputViewController *ivc;
@end
@implementation KBFullAccessGuideView
@@ -109,7 +114,8 @@
}
- (void)presentIn:(UIView *)parent {
UIView *container = parent.window ?: parent;
if (!parent) return;
UIView *container = parent; // window
self.frame = container.bounds;
self.alpha = 0;
[container addSubview:self];
@@ -125,15 +131,19 @@
+ (void)showInView:(UIView *)parent {
if (!parent) return;
//
for (UIView *v in (parent.window ?: parent).subviews) {
// parent
for (UIView *v in parent.subviews) {
if ([v isKindOfClass:[KBFullAccessGuideView class]]) return;
}
[[KBFullAccessGuideView build] presentIn:parent];
KBFullAccessGuideView *view = [KBFullAccessGuideView build];
// ivc
view.ivc = KBFindInputViewController(parent);
[view presentIn:parent];
}
+ (void)dismissFromView:(UIView *)parent {
UIView *container = parent.window ?: parent;
UIView *container = parent;
if (!container) return;
for (UIView *v in container.subviews) {
if ([v isKindOfClass:[KBFullAccessGuideView class]]) {
[(KBFullAccessGuideView *)v dismiss];
@@ -146,40 +156,74 @@
#pragma mark - Actions
- (UIInputViewController *)kb_findInputController {
UIResponder *res = self;
while (res) {
if ([res isKindOfClass:[UIInputViewController class]]) {
return (UIInputViewController *)res;
}
res = res.nextResponder;
}
return nil;
}
// KBResponderUtils.h
// App访 Scheme UL
- (void)onTapGoEnable {
// 使 UIApplication宿
// App App 宿
UIInputViewController *ivc = [self kb_findInputController];
UIInputViewController *ivc = KBFindInputViewController(self);
if (!ivc) { [self dismiss]; return; }
// Universal Link scheme
// SchemeApp openURL
// 使 App Scheme
NSURL *scheme = [NSURL URLWithString:[NSString stringWithFormat:@"%@@//settings?src=kb_extension", KB_APP_SCHEME]];
// Universal Link AASA/Associated Domains KB_UL_BASE
NSURL *ul = [NSURL URLWithString:[NSString stringWithFormat:@"%@?src=kb_extension", KB_UL_SETTINGS]];
void (^fallback)(void) = ^{
NSURL *scheme = [NSURL URLWithString:@"kbkeyboard://settings?src=kb_extension"]; // App openURL
[ivc.extensionContext openURL:scheme completionHandler:^(__unused BOOL ok2) {
//
[self dismiss];
}];
void (^finish)(BOOL) = ^(BOOL ok){
if (ok) { [self dismiss]; }
else {
[KBHUD showInfo:@"无法自动打开,请按路径:设置→通用→键盘→键盘→恋爱键盘→允许完全访问"]; //
}
};
// Scheme宿 App
if (scheme) {
[ivc.extensionContext openURL:scheme completionHandler:^(BOOL ok) {
if (ok) { finish(YES); return; }
if (ul) {
[ivc.extensionContext openURL:ul completionHandler:^(BOOL ok2) {
if (ok2) { finish(YES); return; }
// openURL:
BOOL bridged = NO;
@try {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wunguarded-availability"
bridged = [KBURLOpenBridge openURLViaResponder:scheme from:self];
if (!bridged && ul) {
bridged = [KBURLOpenBridge openURLViaResponder:ul from:self];
}
#pragma clang diagnostic pop
} @catch (__unused NSException *e) { bridged = NO; }
finish(bridged);
}];
} else {
// UL Scheme
BOOL bridged = NO;
@try {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wunguarded-availability"
bridged = [KBURLOpenBridge openURLViaResponder:scheme from:self];
#pragma clang diagnostic pop
} @catch (__unused NSException *e) { bridged = NO; }
finish(bridged);
}
}];
return;
}
// scheme UL
if (ul) {
[ivc.extensionContext openURL:ul completionHandler:^(BOOL ok) {
if (ok) { [self dismiss]; }
else { fallback(); }
if (ok) { finish(YES); return; }
BOOL bridged = NO;
@try {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wunguarded-availability"
bridged = [KBURLOpenBridge openURLViaResponder:ul from:self];
#pragma clang diagnostic pop
} @catch (__unused NSException *e) { bridged = NO; }
finish(bridged);
}];
} else {
fallback();
finish(NO);
}
}
@end

View File

@@ -7,12 +7,14 @@
#import "KBFunctionBarView.h"
#import "Masonry.h"
#import "KBResponderUtils.h" // UIInputViewController
@interface KBFunctionBarView ()
@property (nonatomic, strong) UIView *leftContainer; //
@property (nonatomic, strong) UIView *rightContainer; //
@property (nonatomic, strong) NSArray<UIButton *> *leftButtonsInternal;
@property (nonatomic, strong) NSArray<UIButton *> *rightButtonsInternal;
@property (nonatomic, strong) UIButton *globeButtonInternal; //
@end
@implementation KBFunctionBarView
@@ -38,6 +40,7 @@
- (void)buildUI {
// 便
[self addSubview:self.leftContainer];
[self addSubview:self.globeButtonInternal];
[self addSubview:self.rightContainer];
[self.rightContainer mas_makeConstraints:^(MASConstraintMaker *make) {
@@ -46,8 +49,15 @@
make.height.mas_equalTo(36);
}];
[self.leftContainer mas_makeConstraints:^(MASConstraintMaker *make) {
//
[self.globeButtonInternal mas_makeConstraints:^(MASConstraintMaker *make) {
make.left.equalTo(self.mas_left).offset(12);
make.centerY.equalTo(self.mas_centerY);
make.width.height.mas_equalTo(32);
}];
[self.leftContainer mas_makeConstraints:^(MASConstraintMaker *make) {
make.left.equalTo(self.globeButtonInternal.mas_right).offset(8);
make.right.equalTo(self.rightContainer.mas_left).offset(-12);
make.centerY.equalTo(self.mas_centerY);
make.height.mas_equalTo(36);
@@ -113,6 +123,9 @@
}
self.rightButtonsInternal = rightBtns.copy;
//
[self kb_refreshGlobeVisibility];
}
#pragma mark - Actions
@@ -158,4 +171,56 @@
return _rightContainer;
}
- (UIButton *)globeButtonInternal {
if (!_globeButtonInternal) {
_globeButtonInternal = [UIButton buttonWithType:UIButtonTypeSystem];
_globeButtonInternal.layer.cornerRadius = 16;
_globeButtonInternal.layer.masksToBounds = YES;
_globeButtonInternal.backgroundColor = [UIColor colorWithWhite:1 alpha:0.9];
[_globeButtonInternal setTitle:@"🌐" forState:UIControlStateNormal];
[_globeButtonInternal setTitleColor:[UIColor blackColor] forState:UIControlStateNormal];
}
return _globeButtonInternal;
}
#pragma mark - Globe (Input Mode Switch)
- (void)kb_refreshGlobeVisibility {
UIInputViewController *ivc = KBFindInputViewController(self);
BOOL needSwitchKey = YES;
if (ivc && [ivc respondsToSelector:@selector(needsInputModeSwitchKey)]) {
needSwitchKey = ivc.needsInputModeSwitchKey;
}
self.globeButtonInternal.hidden = !needSwitchKey;
//
[self.leftContainer mas_remakeConstraints:^(MASConstraintMaker *make) {
if (needSwitchKey) {
make.left.equalTo(self.globeButtonInternal.mas_right).offset(8);
} else {
make.left.equalTo(self.mas_left).offset(12);
}
make.right.equalTo(self.rightContainer.mas_left).offset(-12);
make.centerY.equalTo(self.mas_centerY);
make.height.mas_equalTo(36);
}];
//
[self.globeButtonInternal removeTarget:nil action:NULL forControlEvents:UIControlEventAllEvents];
if (needSwitchKey && ivc) {
SEL sel = NSSelectorFromString(@"handleInputModeListFromView:withEvent:");
if ([ivc respondsToSelector:sel]) {
[self.globeButtonInternal addTarget:ivc action:sel forControlEvents:UIControlEventAllTouchEvents];
} else {
[self.globeButtonInternal addTarget:ivc action:@selector(advanceToNextInputMode) forControlEvents:UIControlEventTouchUpInside];
}
}
}
- (void)didMoveToWindow {
[super didMoveToWindow];
[self kb_refreshGlobeVisibility];
}
@end

View File

@@ -33,6 +33,9 @@ NS_ASSUME_NONNULL_BEGIN
@property (nonatomic, strong, readonly) UIButton *clearButton; // 右侧-清空
@property (nonatomic, strong, readonly) UIButton *sendButton; // 右侧-发送
/// 应用当前皮肤(更新背景/强调色)
- (void)kb_applyTheme;
@end
NS_ASSUME_NONNULL_END

View File

@@ -6,6 +6,7 @@
//
#import "KBFunctionView.h"
#import "KBResponderUtils.h" // UIInputViewController
#import "KBFunctionBarView.h"
#import "KBFunctionPasteView.h"
#import "KBFunctionTagCell.h"
@@ -13,6 +14,8 @@
#import <MBProgressHUD.h>
#import "KBFullAccessGuideView.h"
#import "KBFullAccessManager.h"
#import "KBSkinManager.h"
#import "KBURLOpenBridge.h" // openURL:
static NSString * const kKBFunctionTagCellId = @"KBFunctionTagCellId";
@@ -39,20 +42,33 @@ static NSString * const kKBFunctionTagCellId = @"KBFunctionTagCellId";
- (instancetype)initWithFrame:(CGRect)frame {
if (self = [super initWithFrame:frame]) {
// 绿
self.backgroundColor = [UIColor colorWithRed:0.77 green:0.93 blue:0.82 alpha:1.0];
// 使
[self kb_applyTheme];
[self setupUI];
[self reloadDemoData];
//
_lastHandledPBCount = [UIPasteboard generalPasteboard].changeCount;
// 访访 TCC/XPC
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(kb_fullAccessChanged) name:KBFullAccessChangedNotification object:nil];
}
return self;
}
#pragma mark - Theme
- (void)kb_applyTheme {
KBSkinManager *mgr = [KBSkinManager shared];
UIColor *accent = mgr.current.accentColor ?: [UIColor colorWithRed:0.77 green:0.93 blue:0.82 alpha:1.0];
BOOL hasImg = ([mgr currentBackgroundImage] != nil);
self.backgroundColor = hasImg ? [accent colorWithAlphaComponent:0.65] : accent;
}
- (void)dealloc {
[self stopPasteboardMonitor];
[[NSNotificationCenter defaultCenter] removeObserver:self];
}
#pragma mark - UI
@@ -81,10 +97,11 @@ static NSString * const kKBFunctionTagCellId = @"KBFunctionTagCellId";
[self.rightButtonContainer addSubview:self.clearButtonInternal];
[self.rightButtonContainer addSubview:self.sendButtonInternal];
//
//
CGFloat smallH = 44;
CGFloat bigH = 56;
CGFloat vSpace = 10;
// 10 276 8 AutoLayout
CGFloat vSpace = 8;
[self.pasteButtonInternal mas_makeConstraints:^(MASConstraintMaker *make) {
make.top.equalTo(self.rightButtonContainer.mas_top);
make.left.right.equalTo(self.rightButtonContainer);
@@ -103,8 +120,10 @@ static NSString * const kKBFunctionTagCellId = @"KBFunctionTagCellId";
[self.sendButtonInternal mas_makeConstraints:^(MASConstraintMaker *make) {
make.top.equalTo(self.clearButtonInternal.mas_bottom).offset(vSpace);
make.left.right.equalTo(self.rightButtonContainer);
make.height.mas_equalTo(bigH);
make.bottom.lessThanOrEqualTo(self.rightButtonContainer.mas_bottom); //
// smallH
make.height.greaterThanOrEqualTo(@(smallH));
make.height.lessThanOrEqualTo(@(bigH));
make.bottom.lessThanOrEqualTo(self.rightButtonContainer.mas_bottom);
}];
// 2.
@@ -169,7 +188,7 @@ static NSString * const kKBFunctionTagCellId = @"KBFunctionTagCellId";
[KBHUD showInfo:@"处理中…"];
// return;
UIInputViewController *ivc = [self findInputViewController];
UIInputViewController *ivc = KBFindInputViewController(self);
if (!ivc) return;
NSString *title = (indexPath.item < self.itemsInternal.count) ? self.itemsInternal[indexPath.item] : @"";
@@ -182,9 +201,22 @@ static NSString * const kKBFunctionTagCellId = @"KBFunctionTagCellId";
[ivc.extensionContext openURL:ul completionHandler:^(BOOL ok) {
if (ok) return; // Universal Link
NSURL *scheme = [NSURL URLWithString:[NSString stringWithFormat:@"kbkeyboard://login?src=functionView&index=%ld&title=%@", (long)indexPath.item, encodedTitle]];
// 使 App Scheme
NSURL *scheme = [NSURL URLWithString:[NSString stringWithFormat:@"%@@//login?src=functionView&index=%ld&title=%@", KB_APP_SCHEME, (long)indexPath.item, encodedTitle]];
[ivc.extensionContext openURL:scheme completionHandler:^(BOOL ok2) {
if (!ok2) {
if (ok2) return;
// openURL:
// 宿
BOOL bridged = NO;
@try {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wunguarded-availability"
bridged = [KBURLOpenBridge openURLViaResponder:scheme from:self];
#pragma clang diagnostic pop
} @catch (__unused NSException *e) { bridged = NO; }
if (!bridged) {
// 访宿 Manager
dispatch_async(dispatch_get_main_queue(), ^{ [[KBFullAccessManager shared] ensureFullAccessOrGuideInView:self]; });
}
@@ -224,6 +256,8 @@ static NSString * const kKBFunctionTagCellId = @"KBFunctionTagCellId";
// - / changeCount
- (void)startPasteboardMonitor {
// 访宿/
if (![[KBFullAccessManager shared] hasFullAccess]) return;
if (self.pasteboardTimer) return;
__weak typeof(self) weakSelf = self;
self.pasteboardTimer = [NSTimer scheduledTimerWithTimeInterval:0.5 repeats:YES block:^(NSTimer * _Nonnull timer) {
@@ -248,34 +282,40 @@ static NSString * const kKBFunctionTagCellId = @"KBFunctionTagCellId";
- (void)didMoveToWindow {
[super didMoveToWindow];
if (self.window && !self.isHidden) {
[self startPasteboardMonitor];
} else {
[self stopPasteboardMonitor];
}
[self kb_refreshPasteboardMonitor];
}
- (void)setHidden:(BOOL)hidden {
BOOL wasHidden = self.isHidden;
[super setHidden:hidden];
if (wasHidden != hidden) {
if (!hidden && self.window) {
[self startPasteboardMonitor];
} else {
[self stopPasteboardMonitor];
}
[self kb_refreshPasteboardMonitor];
}
}
// 访
- (void)kb_refreshPasteboardMonitor {
BOOL visible = (self.window && !self.isHidden);
if (visible && [[KBFullAccessManager shared] hasFullAccess]) {
[self startPasteboardMonitor];
} else {
[self stopPasteboardMonitor];
}
}
- (void)kb_fullAccessChanged {
dispatch_async(dispatch_get_main_queue(), ^{ [self kb_refreshPasteboardMonitor]; });
}
- (void)onTapDelete {
NSLog(@"点击:删除");
UIInputViewController *ivc = [self findInputViewController];
UIInputViewController *ivc = KBFindInputViewController(self);
id<UITextDocumentProxy> proxy = ivc.textDocumentProxy;
[proxy deleteBackward];
}
- (void)onTapClear {
NSLog(@"点击:清空");
// pasteView
UIInputViewController *ivc = [self findInputViewController];
UIInputViewController *ivc = KBFindInputViewController(self);
id<UITextDocumentProxy> proxy = ivc.textDocumentProxy;
// documentContextBeforeInput 50
NSInteger guard = 0; //
@@ -292,7 +332,7 @@ static NSString * const kKBFunctionTagCellId = @"KBFunctionTagCellId";
- (void)onTapSend {
NSLog(@"点击:发送");
// App
UIInputViewController *ivc = [self findInputViewController];
UIInputViewController *ivc = KBFindInputViewController(self);
id<UITextDocumentProxy> proxy = ivc.textDocumentProxy;
[proxy insertText:@"\n"];
}
@@ -417,16 +457,6 @@ static NSString * const kKBFunctionTagCellId = @"KBFunctionTagCellId";
#pragma mark - Find Owner Controller
// 宿 UIInputViewControllerKeyboardViewController
- (UIInputViewController *)findInputViewController {
UIResponder *responder = self;
while (responder) {
if ([responder isKindOfClass:[UIInputViewController class]]) {
return (UIInputViewController *)responder;
}
responder = responder.nextResponder;
}
return nil;
}
// KBResponderUtils.h
@end

View File

@@ -28,6 +28,9 @@ NS_ASSUME_NONNULL_BEGIN
@interface KBKeyBoardMainView : UIView
@property (nonatomic, weak) id<KBKeyBoardMainViewDelegate> delegate;
/// 应用当前皮肤(会触发键区重载以应用按键颜色)
- (void)kb_applyTheme;
@end
NS_ASSUME_NONNULL_END

View File

@@ -11,6 +11,7 @@
#import "KBFunctionView.h"
#import "KBKey.h"
#import "Masonry.h"
#import "KBSkinManager.h"
@interface KBKeyBoardMainView ()<KBToolBarDelegate, KBKeyboardViewDelegate>
@property (nonatomic, strong) KBToolBar *topBar;
@@ -21,7 +22,7 @@
- (instancetype)initWithFrame:(CGRect)frame {
if (self = [super initWithFrame:frame]) {
self.backgroundColor = [UIColor colorWithWhite:0.95 alpha:1.0];
self.backgroundColor = [KBSkinManager shared].current.keyboardBackground;
//
self.topBar = [[KBToolBar alloc] init];
self.topBar.delegate = self;
@@ -31,7 +32,7 @@
make.top.equalTo(self.mas_top).offset(6);
make.height.mas_equalTo(40);
}];
//
self.keyboardView = [[KBKeyboardView alloc] init];
self.keyboardView.delegate = self;
@@ -41,9 +42,9 @@
make.top.equalTo(self.topBar.mas_bottom).offset(4);
make.bottom.equalTo(self.mas_bottom).offset(-4);
}];
// /
}
return self;
}
@@ -114,5 +115,15 @@
//
// KeyboardViewController
#pragma mark - Theme
- (void)kb_applyTheme {
KBSkinManager *mgr = [KBSkinManager shared];
BOOL hasImg = ([mgr currentBackgroundImage] != nil);
UIColor *bg = mgr.current.keyboardBackground;
self.backgroundColor = hasImg ? [UIColor clearColor] : bg;
self.keyboardView.backgroundColor = hasImg ? [UIColor clearColor] : bg;
[self.keyboardView reloadKeys];
}
@end

View File

@@ -5,6 +5,7 @@
#import "KBKeyButton.h"
#import "KBKey.h"
#import "KBSkinManager.h"
@implementation KBKeyButton
@@ -16,10 +17,11 @@
}
- (void)applyDefaultStyle {
self.titleLabel.font = [UIFont systemFontOfSize:18 weight:UIFontWeightSemibold]; //
[self setTitleColor:[UIColor blackColor] forState:UIControlStateNormal];
[self setTitleColor:[UIColor blackColor] forState:UIControlStateHighlighted];
self.backgroundColor = [UIColor whiteColor];
self.titleLabel.font = [UIFont systemFontOfSize:18 weight:UIFontWeightSemibold];
KBSkinTheme *t = [KBSkinManager shared].current;
[self setTitleColor:t.keyTextColor forState:UIControlStateNormal];
[self setTitleColor:t.keyTextColor forState:UIControlStateHighlighted];
self.backgroundColor = t.keyBackground;
self.layer.cornerRadius = 6.0; //
self.layer.masksToBounds = NO;
self.layer.shadowColor = [UIColor colorWithWhite:0 alpha:0.1].CGColor; //
@@ -46,10 +48,11 @@
- (void)refreshStateAppearance {
// Shift/CapsLock
KBSkinTheme *t = [KBSkinManager shared].current;
if (self.isSelected) {
self.backgroundColor = [UIColor colorWithWhite:0.85 alpha:1.0];
self.backgroundColor = t.keyHighlightBackground ?: t.keyBackground;
} else {
self.backgroundColor = [UIColor whiteColor];
self.backgroundColor = t.keyBackground;
}
}

View File

@@ -6,6 +6,8 @@
#import "KBKeyboardView.h"
#import "KBKeyButton.h"
#import "KBKey.h"
#import "KBResponderUtils.h" //
#import "KBSkinManager.h"
@interface KBKeyboardView ()
@property (nonatomic, strong) UIView *row1;
@@ -13,13 +15,15 @@
@property (nonatomic, strong) UIView *row3;
@property (nonatomic, strong) UIView *row4;
@property (nonatomic, strong) NSArray<NSArray<KBKey *> *> *keysForRows;
// 退使 NSTimer GCD
@property (nonatomic, assign) BOOL backspaceHoldActive;
@end
@implementation KBKeyboardView
- (instancetype)initWithFrame:(CGRect)frame {
if (self = [super initWithFrame:frame]) {
self.backgroundColor = [UIColor colorWithWhite:0.95 alpha:1.0];
self.backgroundColor = [KBSkinManager shared].current.keyboardBackground;
_layoutStyle = KBKeyboardLayoutStyleLetters;
// Shift
_shiftOn = NO;
@@ -196,6 +200,16 @@
[btn addTarget:self action:@selector(onKeyTapped:) forControlEvents:UIControlEventTouchUpInside];
[row addSubview:btn];
// NSTimer使 UILongPressGestureRecognizer
// 1
if (key.type == KBKeyTypeBackspace) {
UILongPressGestureRecognizer *lp = [[UILongPressGestureRecognizer alloc] initWithTarget:self action:@selector(onBackspaceLongPress:)];
// 0.5s
lp.minimumPressDuration = 0.35;
lp.cancelsTouchesInView = YES; //
[btn addGestureRecognizer:lp];
}
// Shift
if (key.type == KBKeyTypeShift) {
btn.selected = self.shiftOn;
@@ -250,6 +264,9 @@
if (firstChar) {
for (KBKeyButton *b in row.subviews) {
if (![b isKindOfClass:[KBKeyButton class]]) continue;
// firstChar
// self == self * k
if (b == firstChar) continue;
if (b.key.type == KBKeyTypeCharacter) continue;
CGFloat multiplier = 1.5;
if (b.key.type == KBKeyTypeSpace) multiplier = 4.0;
@@ -293,6 +310,42 @@
}
}
// 退使 NSTimer/DisplayLink
- (void)onBackspaceLongPress:(UILongPressGestureRecognizer *)gr {
switch (gr.state) {
case UIGestureRecognizerStateBegan: {
self.backspaceHoldActive = YES;
[self kb_backspaceStep];
} break;
case UIGestureRecognizerStateEnded:
case UIGestureRecognizerStateCancelled:
case UIGestureRecognizerStateFailed: {
self.backspaceHoldActive = NO;
} break;
default: break;
}
}
#pragma mark - Helpers
//
- (void)kb_backspaceStep {
if (!self.backspaceHoldActive) { return; }
UIInputViewController *ivc = KBFindInputViewController(self);
if (!ivc) { self.backspaceHoldActive = NO; return; }
id<UITextDocumentProxy> proxy = ivc.textDocumentProxy;
NSString *before = proxy.documentContextBeforeInput ?: @"";
if (before.length <= 0) { self.backspaceHoldActive = NO; return; }
[proxy deleteBackward]; // 1
// 使 NSTimer
__weak typeof(self) weakSelf = self;
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.06 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
__strong typeof(weakSelf) selfStrong = weakSelf;
[selfStrong kb_backspaceStep];
});
}
#pragma mark - Lazy
- (UIView *)row1 { if (!_row1) _row1 = [UIView new]; return _row1; }

View File

@@ -0,0 +1,28 @@
//
// KBResponderUtils.h
// CustomKeyboard
//
// 统一封装:从任意 UIView/UIResponder 起,向响应链上查找 UIInputViewController。
// 作为 headeronly 的工具,便于多处直接引入使用。
//
#import <UIKit/UIKit.h>
#ifndef KBResponderUtils_h
#define KBResponderUtils_h
/// 从给定 responder 开始,沿响应链查找宿主 UIInputViewController。
/// 用法UIInputViewController *ivc = KBFindInputViewController(self);
static inline UIInputViewController *KBFindInputViewController(UIResponder *start) {
UIResponder *responder = start;
while (responder) {
if ([responder isKindOfClass:[UIInputViewController class]]) {
return (UIInputViewController *)responder;
}
responder = responder.nextResponder;
}
return nil;
}
#endif /* KBResponderUtils_h */

View File

@@ -6,11 +6,13 @@
//
#import "KBToolBar.h"
#import "KBResponderUtils.h" // UIInputViewController
@interface KBToolBar ()
@property (nonatomic, strong) UIView *leftContainer;
@property (nonatomic, strong) NSArray<UIButton *> *leftButtonsInternal;
@property (nonatomic, strong) UIButton *settingsButtonInternal;
@property (nonatomic, strong) UIButton *globeButtonInternal; //
@end
@implementation KBToolBar
@@ -50,6 +52,7 @@
- (void)setupUI {
[self addSubview:self.leftContainer];
[self addSubview:self.settingsButtonInternal];
[self addSubview:self.globeButtonInternal];
//
[self.settingsButtonInternal mas_makeConstraints:^(MASConstraintMaker *make) {
@@ -58,9 +61,16 @@
make.width.height.mas_equalTo(32);
}];
//
[self.globeButtonInternal mas_makeConstraints:^(MASConstraintMaker *make) {
make.left.equalTo(self.mas_left).offset(12);
make.centerY.equalTo(self.mas_centerY);
make.width.height.mas_equalTo(32);
}];
//
[self.leftContainer mas_makeConstraints:^(MASConstraintMaker *make) {
make.left.equalTo(self.mas_left).offset(12);
make.left.equalTo(self.globeButtonInternal.mas_right).offset(8);
make.right.equalTo(self.settingsButtonInternal.mas_left).offset(-12);
make.centerY.equalTo(self.mas_centerY);
make.height.mas_equalTo(32);
@@ -89,6 +99,9 @@
make.right.equalTo(self.leftContainer.mas_right);
}];
self.leftButtonsInternal = buttons.copy;
//
[self kb_refreshGlobeVisibility];
}
- (UIButton *)buildActionButtonAtIndex:(NSInteger)idx {
@@ -142,4 +155,59 @@
return _settingsButtonInternal;
}
- (UIButton *)globeButtonInternal {
if (!_globeButtonInternal) {
_globeButtonInternal = [UIButton buttonWithType:UIButtonTypeSystem];
_globeButtonInternal.layer.cornerRadius = 16;
_globeButtonInternal.layer.masksToBounds = YES;
_globeButtonInternal.backgroundColor = [UIColor colorWithWhite:1 alpha:0.9];
[_globeButtonInternal setTitle:@"🌐" forState:UIControlStateNormal];
[_globeButtonInternal setTitleColor:[UIColor blackColor] forState:UIControlStateNormal];
}
return _globeButtonInternal;
}
#pragma mark - Globe (Input Mode Switch)
// 宿
- (void)kb_refreshGlobeVisibility {
UIInputViewController *ivc = KBFindInputViewController(self);
BOOL needSwitchKey = YES;
if (ivc && [ivc respondsToSelector:@selector(needsInputModeSwitchKey)]) {
needSwitchKey = ivc.needsInputModeSwitchKey; // YES
}
self.globeButtonInternal.hidden = !needSwitchKey;
// leftContainer 12
[self.leftContainer mas_remakeConstraints:^(MASConstraintMaker *make) {
if (needSwitchKey) {
make.left.equalTo(self.globeButtonInternal.mas_right).offset(8);
} else {
make.left.equalTo(self.mas_left).offset(12);
}
make.right.equalTo(self.settingsButtonInternal.mas_left).offset(-12);
make.centerY.equalTo(self.mas_centerY);
make.height.mas_equalTo(32);
}];
//
//
[self.globeButtonInternal removeTarget:nil action:NULL forControlEvents:UIControlEventAllEvents];
if (needSwitchKey && ivc) {
SEL sel = NSSelectorFromString(@"handleInputModeListFromView:withEvent:");
if ([ivc respondsToSelector:sel]) {
[self.globeButtonInternal addTarget:ivc action:sel forControlEvents:UIControlEventAllTouchEvents];
} else {
// 退
[self.globeButtonInternal addTarget:ivc action:@selector(advanceToNextInputMode) forControlEvents:UIControlEventTouchUpInside];
}
}
}
- (void)didMoveToWindow {
[super didMoveToWindow];
[self kb_refreshGlobeVisibility];
}
@end

View File

@@ -15,7 +15,7 @@
// Universal Links 通用链接
#ifndef KB_UL_BASE
#define KB_UL_BASE @"https://your.domain/ul"
#define KB_UL_BASE @"https://app.tknb.net/ul"
#endif
#define KB_UL_LOGIN KB_UL_BASE @"/login"
@@ -37,6 +37,14 @@
#define KB_KEYBOARD_EXTENSION_BUNDLE_ID @"com.loveKey.nyx.CustomKeyboard"
#endif
// --- 应用自定义 Scheme ---
// 主 App 在 Info.plist 中注册的 URL Scheme用于从键盘扩展唤起容器 App。
// 注意AppDelegate 中对 scheme 做了小写化比较kbkeyboardappextensioniOS 对大小写不敏感;
// 这里统一通过宏引用,避免出现与 App 端不一致的字符串。
#ifndef KB_APP_SCHEME
#define KB_APP_SCHEME @"kbkeyboardAppExtension"
#endif
// --- 常用宏 ---
// 弱引用 self在 block 中避免循环引用):使用处直接写 KBWeakSelf;
#ifndef KBWeakSelf

58
Shared/KBSkinManager.h Normal file
View File

@@ -0,0 +1,58 @@
//
// KBSkinManager.h
// App & Keyboard Extension shared skin/theme manager.
//
// Stores a lightweight theme (colors, identifiers) to shared keychain so
// both targets see the same current skin. Cross-process updates are delivered
// via Darwin notification. Intended for immediate reflection in extension.
//
#import <Foundation/Foundation.h>
#import <UIKit/UIKit.h>
NS_ASSUME_NONNULL_BEGIN
extern NSString * const KBSkinDidChangeNotification; // in-process
extern NSString * const KBDarwinSkinChanged; // cross-process
/// Simple theme model (colors only; assets can be added later via App Group)
@interface KBSkinTheme : NSObject <NSSecureCoding>
@property (nonatomic, copy) NSString *skinId; // e.g. "mint"
@property (nonatomic, copy) NSString *name; // display name
@property (nonatomic, strong) UIColor *keyboardBackground;
@property (nonatomic, strong) UIColor *keyBackground;
@property (nonatomic, strong) UIColor *keyTextColor;
@property (nonatomic, strong) UIColor *keyHighlightBackground; // selected/highlighted
@property (nonatomic, strong) UIColor *accentColor; // function view accents
/// 可选:键盘背景图片的 PNG/JPEG 数据(若存在,优先显示图片)
@property (nonatomic, strong, nullable) NSData *backgroundImageData;
@end
/// Shared skin manager (Keychain Sharing based)
@interface KBSkinManager : NSObject
+ (instancetype)shared;
@property (atomic, strong, readonly) KBSkinTheme *current; // never nil (fallback to default)
/// Save theme from JSON dictionary (keys: id, name, background, key_bg, key_text, key_highlight, accent)
- (BOOL)applyThemeFromJSON:(NSDictionary *)json;
/// Save explicit theme
- (BOOL)applyTheme:(KBSkinTheme *)theme;
/// Reset to default theme
- (void)resetToDefault;
/// 直接应用图片皮肤(使用 JPEG/PNG 数据)。建议大小 < 512KB。
- (BOOL)applyImageSkinWithData:(NSData *)imageData skinId:(NSString *)skinId name:(NSString *)name;
/// 当前背景图片(若存在)
- (nullable UIImage *)currentBackgroundImage;
/// Parse a hex color string like "#RRGGBB"/"#RRGGBBAA"
+ (UIColor *)colorFromHexString:(NSString *)hex defaultColor:(UIColor *)fallback;
@end
NS_ASSUME_NONNULL_END

214
Shared/KBSkinManager.m Normal file
View File

@@ -0,0 +1,214 @@
//
// KBSkinManager.m
//
#import "KBSkinManager.h"
#import <Security/Security.h>
#import "KBConfig.h"
NSString * const KBSkinDidChangeNotification = @"KBSkinDidChangeNotification";
NSString * const KBDarwinSkinChanged = @"com.loveKey.nyx.skin.changed";
static NSString * const kKBSkinService = @"com.loveKey.nyx.skin"; // Keychain service
static NSString * const kKBSkinAccount = @"current"; // Keychain account
@implementation KBSkinTheme
+ (BOOL)supportsSecureCoding { return YES; }
- (void)encodeWithCoder:(NSCoder *)coder {
[coder encodeObject:self.skinId forKey:@"skinId"];
[coder encodeObject:self.name forKey:@"name"];
[coder encodeObject:self.keyboardBackground forKey:@"keyboardBackground"];
[coder encodeObject:self.keyBackground forKey:@"keyBackground"];
[coder encodeObject:self.keyTextColor forKey:@"keyTextColor"];
[coder encodeObject:self.keyHighlightBackground forKey:@"keyHighlightBackground"];
[coder encodeObject:self.accentColor forKey:@"accentColor"];
if (self.backgroundImageData) {
[coder encodeObject:self.backgroundImageData forKey:@"backgroundImageData"];
}
}
- (instancetype)initWithCoder:(NSCoder *)coder {
if (self = [super init]) {
_skinId = [coder decodeObjectOfClass:NSString.class forKey:@"skinId"] ?: @"default";
_name = [coder decodeObjectOfClass:NSString.class forKey:@"name"] ?: @"Default";
_keyboardBackground = [coder decodeObjectOfClass:UIColor.class forKey:@"keyboardBackground"] ?: [UIColor colorWithWhite:0.95 alpha:1.0];
_keyBackground = [coder decodeObjectOfClass:UIColor.class forKey:@"keyBackground"] ?: UIColor.whiteColor;
_keyTextColor = [coder decodeObjectOfClass:UIColor.class forKey:@"keyTextColor"] ?: UIColor.blackColor;
_keyHighlightBackground = [coder decodeObjectOfClass:UIColor.class forKey:@"keyHighlightBackground"] ?: [UIColor colorWithWhite:0.85 alpha:1.0];
_accentColor = [coder decodeObjectOfClass:UIColor.class forKey:@"accentColor"] ?: [UIColor colorWithRed:0.77 green:0.93 blue:0.82 alpha:1.0];
_backgroundImageData = [coder decodeObjectOfClass:NSData.class forKey:@"backgroundImageData"];
}
return self;
}
@end
@interface KBSkinManager ()
@property (atomic, strong, readwrite) KBSkinTheme *current;
@end
@implementation KBSkinManager
+ (instancetype)shared {
static KBSkinManager *m; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ m = [KBSkinManager new]; });
return m;
}
- (instancetype)init {
if (self = [super init]) {
_current = [self p_loadFromKeychain] ?: [self.class defaultTheme];
// Observe Darwin notification for cross-process updates
CFNotificationCenterAddObserver(CFNotificationCenterGetDarwinNotifyCenter(),
(__bridge const void *)(self),
KBSkinDarwinCallback,
(__bridge CFStringRef)KBDarwinSkinChanged,
NULL,
CFNotificationSuspensionBehaviorDeliverImmediately);
}
return self;
}
static void KBSkinDarwinCallback(CFNotificationCenterRef center, void *observer, CFStringRef name, const void *object, CFDictionaryRef userInfo) {
KBSkinManager *self = (__bridge KBSkinManager *)observer;
[self p_reloadFromKeychainAndBroadcast:YES];
}
- (void)dealloc {
CFNotificationCenterRemoveObserver(CFNotificationCenterGetDarwinNotifyCenter(), (__bridge const void *)(self), (__bridge CFStringRef)KBDarwinSkinChanged, NULL);
}
#pragma mark - Public
- (BOOL)applyThemeFromJSON:(NSDictionary *)json {
if (json.count == 0) return NO;
KBSkinTheme *t = [KBSkinTheme new];
t.skinId = [json[@"id"] isKindOfClass:NSString.class] ? json[@"id"] : @"custom";
t.name = [json[@"name"] isKindOfClass:NSString.class] ? json[@"name"] : t.skinId;
t.keyboardBackground = [self.class colorFromHexString:json[@"background"] defaultColor:[self.class defaultTheme].keyboardBackground];
t.keyBackground = [self.class colorFromHexString:json[@"key_bg"] defaultColor:[self.class defaultTheme].keyBackground];
t.keyTextColor = [self.class colorFromHexString:json[@"key_text"] defaultColor:[self.class defaultTheme].keyTextColor];
t.keyHighlightBackground = [self.class colorFromHexString:json[@"key_highlight"] defaultColor:[self.class defaultTheme].keyHighlightBackground];
t.accentColor = [self.class colorFromHexString:json[@"accent"] defaultColor:[self.class defaultTheme].accentColor];
return [self applyTheme:t];
}
- (BOOL)applyTheme:(KBSkinTheme *)theme {
if (!theme) return NO;
if ([self p_saveToKeychain:theme]) {
self.current = theme;
[[NSNotificationCenter defaultCenter] postNotificationName:KBSkinDidChangeNotification object:nil];
CFNotificationCenterPostNotification(CFNotificationCenterGetDarwinNotifyCenter(), (__bridge CFStringRef)KBDarwinSkinChanged, NULL, NULL, true);
return YES;
}
return NO;
}
- (void)resetToDefault {
[self applyTheme:[self.class defaultTheme]];
}
- (BOOL)applyImageSkinWithData:(NSData *)imageData skinId:(NSString *)skinId name:(NSString *)name {
if (imageData.length == 0) return NO;
// /
KBSkinTheme *base = self.current ?: [self.class defaultTheme];
KBSkinTheme *t = [KBSkinTheme new];
t.skinId = skinId ?: @"image";
t.name = name ?: t.skinId;
t.keyboardBackground = base.keyboardBackground ?: [self.class defaultTheme].keyboardBackground;
t.keyBackground = base.keyBackground ?: [self.class defaultTheme].keyBackground;
t.keyTextColor = base.keyTextColor ?: [self.class defaultTheme].keyTextColor;
t.keyHighlightBackground = base.keyHighlightBackground ?: [self.class defaultTheme].keyHighlightBackground;
t.accentColor = base.accentColor ?: [self.class defaultTheme].accentColor;
t.backgroundImageData = imageData;
return [self applyTheme:t];
}
- (UIImage *)currentBackgroundImage {
NSData *d = self.current.backgroundImageData;
if (d.length == 0) return nil;
return [UIImage imageWithData:d scale:[UIScreen mainScreen].scale] ?: nil;
}
+ (UIColor *)colorFromHexString:(NSString *)hex defaultColor:(UIColor *)fallback {
if (![hex isKindOfClass:NSString.class] || hex.length == 0) return fallback;
NSString *s = [[hex stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]] lowercaseString];
if ([s hasPrefix:@"#"]) s = [s substringFromIndex:1];
unsigned long long v = 0; NSScanner *scanner = [NSScanner scannerWithString:s];
if (![scanner scanHexLongLong:&v]) return fallback;
if (s.length == 6) { // RRGGBB
CGFloat r = ((v >> 16) & 0xFF) / 255.0;
CGFloat g = ((v >> 8) & 0xFF) / 255.0;
CGFloat b = (v & 0xFF) / 255.0;
return [UIColor colorWithRed:r green:g blue:b alpha:1.0];
} else if (s.length == 8) { // RRGGBBAA
CGFloat r = ((v >> 24) & 0xFF) / 255.0;
CGFloat g = ((v >> 16) & 0xFF) / 255.0;
CGFloat b = ((v >> 8) & 0xFF) / 255.0;
CGFloat a = (v & 0xFF) / 255.0;
return [UIColor colorWithRed:r green:g blue:b alpha:a];
}
return fallback;
}
#pragma mark - Defaults
+ (KBSkinTheme *)defaultTheme {
KBSkinTheme *t = [KBSkinTheme new];
t.skinId = @"default";
t.name = @"Default";
t.keyboardBackground = [UIColor colorWithWhite:0.95 alpha:1.0];
t.keyBackground = UIColor.whiteColor;
t.keyTextColor = UIColor.blackColor;
t.keyHighlightBackground = [UIColor colorWithWhite:0.85 alpha:1.0];
t.accentColor = [UIColor colorWithRed:0.77 green:0.93 blue:0.82 alpha:1.0];
t.backgroundImageData = nil;
return t;
}
#pragma mark - Keychain
- (NSMutableDictionary *)baseKCQuery {
NSMutableDictionary *q = [@{ (__bridge id)kSecClass: (__bridge id)kSecClassGenericPassword,
(__bridge id)kSecAttrService: kKBSkinService,
(__bridge id)kSecAttrAccount: kKBSkinAccount } mutableCopy];
q[(__bridge id)kSecAttrAccessGroup] = KB_KEYCHAIN_ACCESS_GROUP;
return q;
}
- (BOOL)p_saveToKeychain:(KBSkinTheme *)theme {
NSError *err = nil;
NSData *data = [NSKeyedArchiver archivedDataWithRootObject:theme requiringSecureCoding:YES error:&err];
if (err || data.length == 0) return NO;
NSMutableDictionary *q = [self baseKCQuery];
SecItemDelete((__bridge CFDictionaryRef)q);
q[(__bridge id)kSecValueData] = data;
q[(__bridge id)kSecAttrAccessible] = (__bridge id)kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly;
OSStatus st = SecItemAdd((__bridge CFDictionaryRef)q, NULL);
return (st == errSecSuccess);
}
- (KBSkinTheme *)p_loadFromKeychain {
NSMutableDictionary *q = [self baseKCQuery];
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;
@try {
KBSkinTheme *t = [NSKeyedUnarchiver unarchivedObjectOfClass:KBSkinTheme.class fromData:data error:NULL];
return t;
} @catch (__unused NSException *e) { return nil; }
}
- (void)p_reloadFromKeychainAndBroadcast:(BOOL)broadcast {
KBSkinTheme *t = [self p_loadFromKeychain] ?: [self.class defaultTheme];
self.current = t;
if (broadcast) {
[[NSNotificationCenter defaultCenter] postNotificationName:KBSkinDidChangeNotification object:nil];
}
}
@end

View File

@@ -8,6 +8,10 @@
/* Begin PBXBuildFile section */
043FBCD22EAF97630036AFE1 /* KBPermissionViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 04C6EAE12EAF940F0089C901 /* KBPermissionViewController.m */; };
0459D1B42EBA284C00F2D189 /* KBSkinCenterVC.m in Sources */ = {isa = PBXBuildFile; fileRef = 0459D1B32EBA284C00F2D189 /* KBSkinCenterVC.m */; };
0459D1B72EBA287900F2D189 /* KBSkinManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 0459D1B62EBA287900F2D189 /* KBSkinManager.m */; };
0459D1B82EBA287900F2D189 /* KBSkinManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 0459D1B62EBA287900F2D189 /* KBSkinManager.m */; };
0477BD952EBAFF4E0055D639 /* KBURLOpenBridge.m in Sources */ = {isa = PBXBuildFile; fileRef = 0477BD932EBAFF4E0055D639 /* KBURLOpenBridge.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 */; };
@@ -87,6 +91,13 @@
/* End PBXCopyFilesBuildPhase section */
/* Begin PBXFileReference section */
0459D1B22EBA284C00F2D189 /* KBSkinCenterVC.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KBSkinCenterVC.h; sourceTree = "<group>"; };
0459D1B32EBA284C00F2D189 /* KBSkinCenterVC.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KBSkinCenterVC.m; sourceTree = "<group>"; };
0459D1B52EBA287900F2D189 /* KBSkinManager.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KBSkinManager.h; sourceTree = "<group>"; };
0459D1B62EBA287900F2D189 /* KBSkinManager.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KBSkinManager.m; sourceTree = "<group>"; };
0477BD922EBAFF4E0055D639 /* KBURLOpenBridge.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KBURLOpenBridge.h; sourceTree = "<group>"; };
0477BD932EBAFF4E0055D639 /* KBURLOpenBridge.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KBURLOpenBridge.m; sourceTree = "<group>"; };
04A9A67D2EB9E1690023B8F4 /* KBResponderUtils.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KBResponderUtils.h; sourceTree = "<group>"; };
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>"; };
@@ -209,6 +220,15 @@
/* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */
0477BD942EBAFF4E0055D639 /* Utils */ = {
isa = PBXGroup;
children = (
0477BD922EBAFF4E0055D639 /* KBURLOpenBridge.h */,
0477BD932EBAFF4E0055D639 /* KBURLOpenBridge.m */,
);
path = Utils;
sourceTree = "<group>";
};
04A9FE122EB4D0D20020DB6D /* Manager */ = {
isa = PBXGroup;
children = (
@@ -249,6 +269,7 @@
04C6EAD72EAF870B0089C901 /* CustomKeyboard */ = {
isa = PBXGroup;
children = (
0477BD942EBAFF4E0055D639 /* Utils */,
04A9FE122EB4D0D20020DB6D /* Manager */,
04FC95662EB0546C007BD342 /* Model */,
04C6EADA2EAF8C7B0089C901 /* View */,
@@ -266,6 +287,7 @@
children = (
04C6EADB2EAF8CEB0089C901 /* KBToolBar.h */,
04C6EADC2EAF8CEB0089C901 /* KBToolBar.m */,
04A9A67D2EB9E1690023B8F4 /* KBResponderUtils.h */,
04FC95682EB05497007BD342 /* KBKeyButton.h */,
04FC95692EB05497007BD342 /* KBKeyButton.m */,
04FC956B2EB054B7007BD342 /* KBKeyboardView.h */,
@@ -327,6 +349,8 @@
04FC95CE2EB1E7A1007BD342 /* HomeVC.m */,
A1B2D7002EB8C00100000001 /* KBLangTestVC.h */,
A1B2D7012EB8C00100000001 /* KBLangTestVC.m */,
0459D1B22EBA284C00F2D189 /* KBSkinCenterVC.h */,
0459D1B32EBA284C00F2D189 /* KBSkinCenterVC.m */,
);
path = VC;
sourceTree = "<group>";
@@ -580,6 +604,8 @@
A1B2C4222EB4B7A100000001 /* KBKeyboardPermissionManager.m */,
04A9FE182EB892460020DB6D /* KBLocalizationManager.h */,
04A9FE192EB892460020DB6D /* KBLocalizationManager.m */,
0459D1B52EBA287900F2D189 /* KBSkinManager.h */,
0459D1B62EBA287900F2D189 /* KBSkinManager.m */,
);
path = Shared;
sourceTree = "<group>";
@@ -815,6 +841,7 @@
04FC95792EB09BC8007BD342 /* KBKeyBoardMainView.m in Sources */,
04FC95732EB09570007BD342 /* KBFunctionBarView.m in Sources */,
04C6EAD82EAF870B0089C901 /* KeyboardViewController.m in Sources */,
0459D1B82EBA287900F2D189 /* KBSkinManager.m in Sources */,
04FC95762EB095DE007BD342 /* KBFunctionPasteView.m in Sources */,
A1B2C3D42EB0A0A100000001 /* KBFunctionTagCell.m in Sources */,
04A9FE1A2EB892460020DB6D /* KBLocalizationManager.m in Sources */,
@@ -828,6 +855,7 @@
A1B2C4002EB4A0A100000003 /* KBAuthManager.m in Sources */,
04A9FE132EB4D0D20020DB6D /* KBFullAccessManager.m in Sources */,
A1B2C4202EB4B7A100000001 /* KBKeyboardPermissionManager.m in Sources */,
0477BD952EBAFF4E0055D639 /* KBURLOpenBridge.m in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -858,10 +886,12 @@
A1B2D7022EB8C00100000001 /* KBLangTestVC.m in Sources */,
04C6EABF2EAF86530089C901 /* main.m in Sources */,
04FC95CC2EB1E780007BD342 /* BaseTabBarController.m in Sources */,
0459D1B72EBA287900F2D189 /* KBSkinManager.m in Sources */,
04FC95F42EB339C1007BD342 /* AppleSignInManager.m in Sources */,
04C6EAC12EAF86530089C901 /* ViewController.m in Sources */,
A1B2C4002EB4A0A100000004 /* KBAuthManager.m in Sources */,
A1B2C4212EB4B7A100000001 /* KBKeyboardPermissionManager.m in Sources */,
0459D1B42EBA284C00F2D189 /* KBSkinCenterVC.m in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -989,7 +1019,7 @@
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = keyBoard/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = "YOLO输入法";
INFOPLIST_KEY_CFBundleURLTypes = "{\n CFBundleURLName = \"com.loveKey.nyx.keyboard\";\n CFBundleURLSchemes = (\n kbkeyboard\n );\n}";
INFOPLIST_KEY_CFBundleURLTypes = "{\n CFBundleURLName = \"com.loveKey.nyx.keyboard\";\n CFBundleURLSchemes = (\n kbkeyboardAppExtension\n );\n}";
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen;
INFOPLIST_KEY_UIMainStoryboardFile = Main;
@@ -1028,7 +1058,7 @@
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = keyBoard/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = "YOLO输入法";
INFOPLIST_KEY_CFBundleURLTypes = "{\n CFBundleURLName = \"com.loveKey.nyx.keyboard\";\n CFBundleURLSchemes = (\n kbkeyboard\n );\n}";
INFOPLIST_KEY_CFBundleURLTypes = "{\n CFBundleURLName = \"com.loveKey.nyx.keyboard\";\n CFBundleURLSchemes = (\n kbkeyboardAppExtension\n );\n}";
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen;
INFOPLIST_KEY_UIMainStoryboardFile = Main;

View File

@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "2600"
version = "1.7">
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES">
@@ -27,8 +27,6 @@
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES">
<Testables>
</Testables>
<MacroExpansion>
<BuildableReference
BuildableIdentifier = "primary"
@@ -38,18 +36,20 @@
ReferencedContainer = "container:keyBoard.xcodeproj">
</BuildableReference>
</MacroExpansion>
<Testables>
</Testables>
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
enableGPUFrameCaptureMode = "3"
enableGPUValidationMode = "1"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
enableGPUFrameCaptureMode = "3"
enableGPUValidationMode = "1"
allowLocationSimulation = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
@@ -87,4 +87,3 @@
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>

View File

@@ -61,10 +61,25 @@ static NSString * const kKBKeyboardExtensionBundleId = @"com.loveKey.nyx.CustomK
#pragma mark - Deep Link
- (BOOL)application:(UIApplication *)application continueUserActivity:(NSUserActivity *)userActivity restorationHandler:(void (^)(NSArray * _Nullable))restorationHandler {
if ([userActivity.activityType isEqualToString:NSUserActivityTypeBrowsingWeb]) {
NSURL *url = userActivity.webpageURL;
if (!url) return NO;
NSString *host = url.host.lowercaseString ?: @"";
if ([host hasSuffix:@"app.tknb.net"]) {
NSString *path = url.path.lowercaseString ?: @"";
if ([path hasPrefix:@"/ul/settings"]) { [self kb_openAppSettings]; return YES; }
if ([path hasPrefix:@"/ul/login"]) { [self kb_presentLoginSheetIfNeeded]; return YES; }
}
}
return NO;
}
// iOS 9+
- (BOOL)application:(UIApplication *)app openURL:(NSURL *)url options:(NSDictionary<UIApplicationOpenURLOptionsKey,id> *)options {
if (!url) return NO;
if ([[url.scheme lowercaseString] isEqualToString:@"kbkeyboard"]) {
// scheme
if ([[url.scheme lowercaseString] isEqualToString:@"kbkeyboardappextension"]) {
NSString *urlHost = url.host ?: @"";
NSString *host = [urlHost lowercaseString];
if ([host isEqualToString:@"login"]) { // kbkeyboard://login

View File

@@ -38,7 +38,7 @@
self.tableView.tableHeaderView = header;
[self.view addSubview:self.tableView];
self.items = @[ KBLocalized(@"home_item_lang_test"), KBLocalized(@"home_item_keyboard_permission") ];
self.items = @[ KBLocalized(@"home_item_lang_test"), KBLocalized(@"home_item_keyboard_permission"), @"皮肤中心" ];
dispatch_async(dispatch_get_main_queue(), ^{ [self.textView becomeFirstResponder]; });
@@ -50,7 +50,7 @@
- (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.items = @[ KBLocalized(@"home_item_lang_test"), KBLocalized(@"home_item_keyboard_permission"), @"皮肤中心" ];
[self.tableView reloadData];
}
@@ -85,6 +85,10 @@
//
KBGuideVC *vc = [KBGuideVC new];
[self.navigationController pushViewController:vc animated:YES];
} else if (indexPath.row == 2) {
//
UIViewController *vc = [NSClassFromString(@"KBSkinCenterVC") new];
if (vc) { [self.navigationController pushViewController:vc animated:YES]; }
}
}

View File

@@ -0,0 +1,11 @@
//
// KBSkinCenterVC.h
// 简单的皮肤中心:展示两款示例皮肤,点击“下载并应用”后立即同步到键盘扩展。
//
#import <UIKit/UIKit.h>
@interface KBSkinCenterVC : UIViewController
@end

View File

@@ -0,0 +1,132 @@
//
// KBSkinCenterVC.m
//
#import "KBSkinCenterVC.h"
#import "Masonry.h"
#import "KBNetworkManager.h"
#import "KBSkinManager.h"
#import "KBHUD.h"
@interface KBSkinCell : UITableViewCell
@property (nonatomic, strong) UIButton *applyBtn;
@end
@implementation KBSkinCell
- (instancetype)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier {
if (self = [super initWithStyle:style reuseIdentifier:reuseIdentifier]) {
self.selectionStyle = UITableViewCellSelectionStyleNone;
_applyBtn = [UIButton buttonWithType:UIButtonTypeSystem];
[_applyBtn setTitle:@"下载并应用" forState:UIControlStateNormal];
_applyBtn.layer.cornerRadius = 6; _applyBtn.layer.borderWidth = 1;
_applyBtn.layer.borderColor = [UIColor colorWithWhite:0.85 alpha:1].CGColor;
[self.contentView addSubview:_applyBtn];
[_applyBtn mas_makeConstraints:^(MASConstraintMaker *make) {
make.right.equalTo(self.contentView).offset(-16);
make.centerY.equalTo(self.contentView);
make.width.mas_equalTo(110);
make.height.mas_equalTo(34);
}];
}
return self;
}
@end
@interface KBSkinCenterVC () <UITableViewDelegate, UITableViewDataSource>
@property (nonatomic, strong) UITableView *tableView;
@property (nonatomic, copy) NSArray<NSDictionary *> *skins; // id, name, img(url relative to KB_BASE_URL)
@end
@implementation KBSkinCenterVC
- (void)viewDidLoad {
[super viewDidLoad];
self.title = @"皮肤中心";
self.view.backgroundColor = [UIColor whiteColor];
// URL KB_BASE_URL
// 使 picsum.photos id访
self.skins = @[
@{ @"id": @"aurora", @"name": @"极光", @"img": @"https://picsum.photos/id/1018/1600/900.jpg" },
@{ @"id": @"alps", @"name": @"雪山", @"img": @"https://picsum.photos/id/1016/1600/900.jpg" },
@{ @"id": @"lake", @"name": @"湖面", @"img": @"https://picsum.photos/id/1039/1600/900.jpg" },
];
self.tableView = [[UITableView alloc] initWithFrame:self.view.bounds style:UITableViewStyleInsetGrouped];
self.tableView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
self.tableView.delegate = self; self.tableView.dataSource = self;
[self.view addSubview:self.tableView];
}
#pragma mark - UITableView
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView { return 1; }
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { return self.skins.count; }
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
static NSString *cid = @"skin.cell";
KBSkinCell *cell = [tableView dequeueReusableCellWithIdentifier:cid];
if (!cell) { cell = [[KBSkinCell alloc] initWithStyle:UITableViewCellStyleSubtitle reuseIdentifier:cid]; }
NSDictionary *skin = self.skins[indexPath.row];
cell.textLabel.text = skin[@"name"]; cell.detailTextLabel.text = skin[@"id"];
[cell.applyBtn removeTarget:nil action:NULL forControlEvents:UIControlEventTouchUpInside];
[cell.applyBtn addTarget:self action:@selector(onApplyBtn:) forControlEvents:UIControlEventTouchUpInside];
cell.applyBtn.tag = indexPath.row;
return cell;
}
- (void)onApplyBtn:(UIButton *)sender {
NSInteger idx = sender.tag;
if (idx < 0 || idx >= self.skins.count) return;
NSDictionary *skin = self.skins[idx];
NSString *path = skin[@"img"] ?: @""; // KB_BASE_URL
// JSON NSData
[[KBNetworkManager shared] GET:path parameters:nil headers:nil completion:^(id jsonOrData, NSURLResponse *response, NSError *error) {
NSData *data = ([jsonOrData isKindOfClass:NSData.class] ? (NSData *)jsonOrData : nil);
// Keychain 1500px
if (data && data.length > 0) {
UIImage *img = [UIImage imageWithData:data];
if (img) {
CGFloat maxW = 1500.0;
if (img.size.width > maxW) {
CGFloat scale = maxW / img.size.width;
CGSize newSize = CGSizeMake(maxW, floor(img.size.height * scale));
UIGraphicsBeginImageContextWithOptions(newSize, YES, 1.0);
[img drawInRect:CGRectMake(0, 0, newSize.width, newSize.height)];
UIImage *resized = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
img = resized ?: img;
}
data = UIImageJPEGRepresentation(img, 0.85) ?: data; // JPEG
}
}
dispatch_async(dispatch_get_main_queue(), ^{
NSData *payload = data;
if (payload.length == 0) {
//
CGSize size = CGSizeMake(1200, 600);
UIGraphicsBeginImageContextWithOptions(size, YES, 1.0);
CGContextRef ctx = UIGraphicsGetCurrentContext();
UIColor *c1 = [UIColor colorWithRed:0.76 green:0.91 blue:0.86 alpha:1];
UIColor *c2 = [UIColor colorWithRed:0.93 green:0.97 blue:0.91 alpha:1];
if ([skin[@"id"] hasPrefix:@"dark"]) {
c1 = [UIColor colorWithRed:0.1 green:0.12 blue:0.16 alpha:1];
c2 = [UIColor colorWithRed:0.22 green:0.24 blue:0.28 alpha:1];
}
CGColorSpaceRef space = CGColorSpaceCreateDeviceRGB();
NSArray *colors = @[(__bridge id)c1.CGColor, (__bridge id)c2.CGColor];
CGFloat locs[] = {0,1};
CGGradientRef grad = CGGradientCreateWithColors(space, (__bridge CFArrayRef)colors, locs);
CGContextDrawLinearGradient(ctx, grad, CGPointZero, CGPointMake(size.width, size.height), 0);
CGGradientRelease(grad); CGColorSpaceRelease(space);
UIImage *img = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
payload = UIImageJPEGRepresentation(img, 0.9);
}
BOOL ok = (payload.length > 0) ? [[KBSkinManager shared] applyImageSkinWithData:payload skinId:skin[@"id"] name:skin[@"name"]] : NO;
[KBHUD showInfo:(ok ? @"已应用,切到键盘查看" : @"应用失败")];
});
}];
}
@end

View File

@@ -5,11 +5,13 @@
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleTypeRole</key>
<string>Editor</string>
<key>CFBundleURLName</key>
<string>com.loveKey.nyx.keyboard</string>
<key>CFBundleURLSchemes</key>
<array>
<string>kbkeyboard</string>
<string>kbkeyboardAppExtension</string>
</array>
</dict>
</array>

View File

@@ -35,7 +35,7 @@
//-----------------------------------------------宏定义全局----------------------------------------------------------/
// 通用链接Universal Links统一配置
// 仅需修改这里的域名/前缀,工程内所有使用 UL 的地方都会同步。
#define KB_UL_BASE @"https://your.domain/ul" // 替换为你的真实域名与前缀路径
#define KB_UL_BASE @"https://app.tknb.net/ul" // 与 Associated Domains 一致
#define KB_UL_LOGIN KB_UL_BASE @"/login"
#define KB_UL_SETTINGS KB_UL_BASE @"/settings"

View File

@@ -6,6 +6,10 @@
<array>
<string>Default</string>
</array>
<key>com.apple.developer.associated-domains</key>
<array>
<string>applinks:app.tknb.net</string>
</array>
<key>keychain-access-groups</key>
<array>
<string>$(AppIdentifierPrefix)com.loveKey.nyx.shared</string>