This commit is contained in:
2025-11-12 21:23:31 +08:00
parent 0aead49816
commit bc261661ae
7 changed files with 317 additions and 24 deletions

View File

@@ -15,6 +15,8 @@
#import "KBFullAccessGuideView.h"
#import "KBFullAccessManager.h"
#import "KBSkinManager.h"
#import "KBAuthManager.h" //
#import "KBULBridge.h" // Darwin UL
#import "KBURLOpenBridge.h" // openURL:
#import "KBStreamTextView.h" //
#import "KBStreamOverlayView.h" //
@@ -47,6 +49,10 @@
//
@property (nonatomic, strong) NSTimer *pasteboardTimer; // 线
@property (nonatomic, assign) NSInteger lastHandledPBCount; // changeCount
// UL
@property (nonatomic, assign) NSUInteger kb_ulSeq; // UL
@property (nonatomic, assign) BOOL kb_ulHandledFlag; // App UL
@end
@implementation KBFunctionView
@@ -64,6 +70,14 @@
// 访访 TCC/XPC
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(kb_fullAccessChanged) name:KBFullAccessChangedNotification object:nil];
// App Darwin UL
CFNotificationCenterAddObserver(CFNotificationCenterGetDarwinNotifyCenter(),
(__bridge const void *)(self),
KBULDarwinCallback,
(__bridge CFStringRef)KBDarwinULHandled,
NULL,
CFNotificationSuspensionBehaviorDeliverImmediately);
}
return self;
}
@@ -81,6 +95,7 @@
[self stopPasteboardMonitor];
[self kb_stopNetworkStreaming];
[[NSNotificationCenter defaultCenter] removeObserver:self];
CFNotificationCenterRemoveObserver(CFNotificationCenterGetDarwinNotifyCenter(), (__bridge const void *)(self), (__bridge CFStringRef)KBDarwinULHandled, NULL);
}
#pragma mark - UI
@@ -278,27 +293,34 @@ static NSString * const kKBStreamDemoURL = @"http://192.168.1.144:7529/api/demo/
#pragma mark - KBFunctionTagListViewDelegate
- (void)tagListView:(KBFunctionTagListView *)view didSelectIndex:(NSInteger)index title:(NSString *)title {
// + 访访
if ([[KBFullAccessManager shared] hasFullAccess]) {
// cell overlay
[self.tagListView setLoading:YES atIndex:index];
self.loadingTagIndex = @(index);
self.loadingTagTitle = title ?: @"";
[self kb_startNetworkStreamingWithSeed:self.loadingTagTitle];
// 1) 访
if (![[KBFullAccessManager shared] hasFullAccess]) {
// 访
[KBHUD showInfo:@"处理中…"];
[[KBFullAccessManager shared] ensureFullAccessOrGuideInView:self];
return;
}
// 访
[KBHUD showInfo:@"处理中…"];
UIInputViewController *ivc = KBFindInputViewController(self);
if (!ivc) return;
NSString *encodedTitle = [title stringByAddingPercentEncodingWithAllowedCharacters:[NSCharacterSet URLQueryAllowedCharacterSet]] ?: @"";
NSURL *ul = [NSURL URLWithString:[NSString stringWithFormat:@"%@?src=functionView&index=%ld&title=%@", KB_UL_LOGIN, (long)index, encodedTitle]];
if (!ul) return;
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.05 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
[ivc.extensionContext openURL:ul completionHandler:^(BOOL ok) {
if (ok) return; // Universal Link
NSURL *scheme = [NSURL URLWithString:[NSString stringWithFormat:@"%@@//login?src=functionView&index=%ld&title=%@", KB_APP_SCHEME, (long)index, encodedTitle]];
[ivc.extensionContext openURL:scheme completionHandler:^(BOOL ok2) {
// 2) -> App App
if (!KBAuthManager.shared.isLoggedIn) {
UIInputViewController *ivc = KBFindInputViewController(self);
if (!ivc) return;
NSString *encodedTitle = [title stringByAddingPercentEncodingWithAllowedCharacters:[NSCharacterSet URLQueryAllowedCharacterSet]] ?: @"";
NSURL *ul = [NSURL URLWithString:[NSString stringWithFormat:@"%@?src=functionView&index=%ld&title=%@", KB_UL_LOGIN, (long)index, encodedTitle]];
if (!ul) return;
// UL ok
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.05 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
[ivc.extensionContext openURL:ul completionHandler:^(__unused BOOL ok) {}];
});
// 500ms App 退 Scheme
self.kb_ulHandledFlag = NO;
NSUInteger token = ++self.kb_ulSeq;
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.5 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
if (token != self.kb_ulSeq) return; //
if (self.kb_ulHandledFlag) return; // App
NSURL *scheme = [NSURL URLWithString:[NSString stringWithFormat:@"%@://login?src=functionView&index=%ld&title=%@", KB_APP_SCHEME, (long)index, encodedTitle]];
if (!scheme) return;
[ivc.extensionContext openURL:scheme completionHandler:^(__unused BOOL ok2) {
if (ok2) return;
BOOL bridged = NO;
@try {
@@ -308,11 +330,26 @@ static NSString * const kKBStreamDemoURL = @"http://192.168.1.144:7529/api/demo/
#pragma clang diagnostic pop
} @catch (__unused NSException *e) { bridged = NO; }
if (!bridged) {
dispatch_async(dispatch_get_main_queue(), ^{ [[KBFullAccessManager shared] ensureFullAccessOrGuideInView:self]; });
[KBHUD showInfo:@"请切换到主App完成登录"];
}
}];
}];
});
});
return;
}
// 3)
[self.tagListView setLoading:YES atIndex:index];
self.loadingTagIndex = @(index);
self.loadingTagTitle = title ?: @"";
[self kb_startNetworkStreamingWithSeed:self.loadingTagTitle];
return;
}
// Darwin App UL
static void KBULDarwinCallback(CFNotificationCenterRef center, void *observer, CFStringRef name, const void *object, CFDictionaryRef userInfo) {
KBFunctionView *self_ = (__bridge KBFunctionView *)observer;
if (!self_) return;
dispatch_async(dispatch_get_main_queue(), ^{ self_.kb_ulHandledFlag = YES; });
}
// UL App Scheme访
@@ -578,6 +615,7 @@ static NSString * const kKBStreamDemoURL = @"http://192.168.1.144:7529/api/demo/
return _sendButtonInternal;
}
#pragma mark - Expose
- (UICollectionView *)collectionView { return self.tagListView.collectionView; }

17
Shared/KBULBridge.h Normal file
View File

@@ -0,0 +1,17 @@
//
// KBULBridge.h
// 通用的 UL/Scheme 拉起桥接常量App 与键盘扩展共享)
//
// 用途:主 App 在成功处理 Universal Link如 /ul/login
// 通过 Darwin 通知告知扩展“已拉起”,扩展据此取消回退到 Scheme。
//
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
/// 主 App 成功处理 Universal Link 后发送的 Darwin 通知名
extern NSString * const KBDarwinULHandled; // "com.loveKey.nyx.ul.opened"
NS_ASSUME_NONNULL_END

9
Shared/KBULBridge.m Normal file
View File

@@ -0,0 +1,9 @@
//
// KBULBridge.m
//
#import "KBULBridge.h"
/// Darwin UL App
NSString * const KBDarwinULHandled = @"com.loveKey.nyx.ul.opened";

View File

@@ -91,6 +91,9 @@
049FB2352EC45C6A00FAB05D /* NetworkStreamHandler.m in Sources */ = {isa = PBXBuildFile; fileRef = 049FB2342EC45C6A00FAB05D /* NetworkStreamHandler.m */; };
049FB23B2EC4766700FAB05D /* KBFunctionTagListView.m in Sources */ = {isa = PBXBuildFile; fileRef = 049FB2372EC4766700FAB05D /* KBFunctionTagListView.m */; };
049FB23C2EC4766700FAB05D /* KBStreamOverlayView.m in Sources */ = {isa = PBXBuildFile; fileRef = 049FB2392EC4766700FAB05D /* KBStreamOverlayView.m */; };
049FB23F2EC4B6EF00FAB05D /* KBULBridge.m in Sources */ = {isa = PBXBuildFile; fileRef = 049FB23E2EC4B6EF00FAB05D /* KBULBridge.m */; };
049FB2402EC4B6EF00FAB05D /* KBULBridge.m in Sources */ = {isa = PBXBuildFile; fileRef = 049FB23E2EC4B6EF00FAB05D /* KBULBridge.m */; };
049FB2432EC4BBB700FAB05D /* KBLoginPopView.m in Sources */ = {isa = PBXBuildFile; fileRef = 049FB2422EC4BBB700FAB05D /* KBLoginPopView.m */; };
049FB31D2EC21BCD00FAB05D /* KBMyKeyboardCell.m in Sources */ = {isa = PBXBuildFile; fileRef = 049FB31C2EC21BCD00FAB05D /* KBMyKeyboardCell.m */; };
04A9FE0F2EB481100020DB6D /* KBHUD.m in Sources */ = {isa = PBXBuildFile; fileRef = 04FC97082EB31B14007BD342 /* KBHUD.m */; };
04A9FE132EB4D0D20020DB6D /* KBFullAccessManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 04A9FE112EB4D0D20020DB6D /* KBFullAccessManager.m */; };
@@ -314,6 +317,10 @@
049FB2372EC4766700FAB05D /* KBFunctionTagListView.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KBFunctionTagListView.m; sourceTree = "<group>"; };
049FB2382EC4766700FAB05D /* KBStreamOverlayView.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KBStreamOverlayView.h; sourceTree = "<group>"; };
049FB2392EC4766700FAB05D /* KBStreamOverlayView.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KBStreamOverlayView.m; sourceTree = "<group>"; };
049FB23D2EC4B6EF00FAB05D /* KBULBridge.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KBULBridge.h; sourceTree = "<group>"; };
049FB23E2EC4B6EF00FAB05D /* KBULBridge.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KBULBridge.m; sourceTree = "<group>"; };
049FB2412EC4BBB700FAB05D /* KBLoginPopView.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KBLoginPopView.h; sourceTree = "<group>"; };
049FB2422EC4BBB700FAB05D /* KBLoginPopView.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KBLoginPopView.m; sourceTree = "<group>"; };
049FB31B2EC21BCD00FAB05D /* KBMyKeyboardCell.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KBMyKeyboardCell.h; sourceTree = "<group>"; };
049FB31C2EC21BCD00FAB05D /* KBMyKeyboardCell.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KBMyKeyboardCell.m; sourceTree = "<group>"; };
04A9A67D2EB9E1690023B8F4 /* KBResponderUtils.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KBResponderUtils.h; sourceTree = "<group>"; };
@@ -1098,6 +1105,8 @@
04FC95EB2EB33611007BD342 /* V */ = {
isa = PBXGroup;
children = (
049FB2412EC4BBB700FAB05D /* KBLoginPopView.h */,
049FB2422EC4BBB700FAB05D /* KBLoginPopView.m */,
);
path = V;
sourceTree = "<group>";
@@ -1145,6 +1154,8 @@
04A9FE192EB892460020DB6D /* KBLocalizationManager.m */,
0459D1B52EBA287900F2D189 /* KBSkinManager.h */,
0459D1B62EBA287900F2D189 /* KBSkinManager.m */,
049FB23D2EC4B6EF00FAB05D /* KBULBridge.h */,
049FB23E2EC4B6EF00FAB05D /* KBULBridge.m */,
);
path = Shared;
sourceTree = "<group>";
@@ -1431,6 +1442,7 @@
049FB23C2EC4766700FAB05D /* KBStreamOverlayView.m in Sources */,
049FB22F2EC34EB900FAB05D /* KBStreamTextView.m in Sources */,
04FC95702EB09516007BD342 /* KBFunctionView.m in Sources */,
049FB23F2EC4B6EF00FAB05D /* KBULBridge.m in Sources */,
04FC956D2EB054B7007BD342 /* KBKeyboardView.m in Sources */,
04FC95672EB0546C007BD342 /* KBKey.m in Sources */,
A1B2C3F42EB35A9900000001 /* KBFullAccessGuideView.m in Sources */,
@@ -1473,6 +1485,7 @@
048908D22EBF611D00FABA60 /* KBHistoryMoreCell.m in Sources */,
04FC95D82EB1EA16007BD342 /* BaseCell.m in Sources */,
0477BDF72EBC63A80055D639 /* KBTestVC.m in Sources */,
049FB2402EC4B6EF00FAB05D /* KBULBridge.m in Sources */,
04FC95C92EB1E4C9007BD342 /* BaseNavigationController.m in Sources */,
048908DD2EBF67EB00FABA60 /* KBSearchResultVC.m in Sources */,
047C65102EBCA8DD0035E841 /* HomeRankContentVC.m in Sources */,
@@ -1508,6 +1521,7 @@
047C65502EBCBA9E0035E841 /* KBShopVC.m in Sources */,
0477BE042EBC83130055D639 /* HomeMainVC.m in Sources */,
0477BDFD2EBC6A170055D639 /* HomeHotVC.m in Sources */,
049FB2432EC4BBB700FAB05D /* KBLoginPopView.m in Sources */,
048908CC2EBE373500FABA60 /* KBSearchBarView.m in Sources */,
048908CD2EBE373500FABA60 /* KBSearchSectionHeader.m in Sources */,
049FB2202EC30D2700FAB05D /* HomeRankDetailPopView.m in Sources */,

View File

@@ -16,6 +16,9 @@
#import "KBLoginSheetViewController.h"
#import "AppleSignInManager.h"
#import <objc/message.h>
#import "KBULBridge.h" // Darwin UL
#import "LSTPopView.h"
#import "KBLoginPopView.h"
// bundle id target
// PRODUCT_BUNDLE_IDENTIFIER
@@ -69,7 +72,14 @@ static NSString * const kKBKeyboardExtensionBundleId = @"com.loveKey.nyx.CustomK
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; }
if ([path hasPrefix:@"/ul/login"]) {
// UL App 退 Scheme
CFNotificationCenterPostNotification(CFNotificationCenterGetDarwinNotifyCenter(),
(__bridge CFStringRef)KBDarwinULHandled,
NULL, NULL, true);
[self kb_presentLoginSheetIfNeeded];
return YES;
}
}
}
return NO;
@@ -83,6 +93,10 @@ static NSString * const kKBKeyboardExtensionBundleId = @"com.loveKey.nyx.CustomK
NSString *urlHost = url.host ?: @"";
NSString *host = [urlHost lowercaseString];
if ([host isEqualToString:@"login"]) { // kbkeyboard://login
// Scheme
CFNotificationCenterPostNotification(CFNotificationCenterGetDarwinNotifyCenter(),
(__bridge CFStringRef)KBDarwinULHandled,
NULL, NULL, true);
[self kb_presentLoginSheetIfNeeded];
return YES;
} else if ([host isEqualToString:@"settings"]) { // kbkeyboard://settings
@@ -100,7 +114,37 @@ static NSString * const kKBKeyboardExtensionBundleId = @"com.loveKey.nyx.CustomK
if (loggedIn) return;
UIViewController *top = [UIViewController kb_topMostViewController];
if (!top) return;
[KBLoginSheetViewController presentIfNeededFrom:top];
// [KBLoginSheetViewController presentIfNeededFrom:top];
[self goLogin];
}
- (void)goLogin{
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.25 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
KBLoginPopView *view = [[KBLoginPopView alloc] initWithFrame:CGRectMake(0, 0, KB_SCREEN_WIDTH, KB_SCREEN_WIDTH)];
//
LSTPopView *pop = [LSTPopView initWithCustomView:view
parentView:nil
popStyle:LSTPopStyleSmoothFromBottom
dismissStyle:LSTDismissStyleSmoothToBottom];
pop.hemStyle = LSTHemStyleBottom;
pop.bgColor = [[UIColor blackColor] colorWithAlphaComponent:0.4];
pop.isClickBgDismiss = YES; //
pop.cornerRadius = 0; // view
__weak typeof(pop) weakPop = pop;
view.appleLoginHandler = ^{
[weakPop dismiss];
[[AppleSignInManager shared] signInFromViewController:KB_CURRENT_NAV completion:^(ASAuthorizationAppleIDCredential * _Nullable credential, NSError * _Nullable error) {
NSLog(@"=====");
}];
};
view.closeHandler = ^{ [weakPop dismiss]; };
[pop pop];
});
}
- (void)kb_openAppSettings {

View File

@@ -0,0 +1,28 @@
//
// KBLoginPopView.h
// 主 App 登录弹窗自定义视图(给 LSTPopView 使用)
//
// 说明:
// - 仅负责展示 UI 与回调,不直接触发 Apple 登录;
// - 外层通过 LSTPopView 承载,点击按钮后先关闭弹窗,再由外层触发 Apple 登录。
//
#import <UIKit/UIKit.h>
NS_ASSUME_NONNULL_BEGIN
@interface KBLoginPopView : UIView
/// 关闭按钮回调(外层收到后可调用 [pop dismiss]
@property (nonatomic, copy, nullable) void (^closeHandler)(void);
/// 点击“用 Apple 登录”回调(外层收到后:先 dismiss 弹窗,再触发 Apple 登录)
@property (nonatomic, copy, nullable) void (^appleLoginHandler)(void);
/// 指定宽度初始化(高度内部自适应)
- (instancetype)initWithWidth:(CGFloat)width;
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,143 @@
//
// KBLoginPopView.m
// LSTPopView 使
//
//
// - 使 Masonry iOS 13+ ASAuthorizationAppleIDButton
// -
//
#import "KBLoginPopView.h"
#import <Masonry/Masonry.h>
#import <AuthenticationServices/AuthenticationServices.h>
@interface KBLoginPopView ()
@property (nonatomic, strong) UILabel *titleLabel; //
@property (nonatomic, strong) UILabel *descLabel; // /
@property (nonatomic, strong) UIButton *closeButton; //
@property (nonatomic, strong) UIView *appleContainer; // Apple iOS13-
@end
@implementation KBLoginPopView
- (instancetype)initWithFrame:(CGRect)frame{
if (self = [super initWithFrame:frame]) {
self.backgroundColor = [UIColor whiteColor];
self.layer.cornerRadius = 16.0;
self.layer.masksToBounds = YES;
[self setupUI];
}
return self;
}
#pragma mark - UI
//
- (void)setupUI {
[self addSubview:self.titleLabel];
[self addSubview:self.descLabel];
[self addSubview:self.appleContainer];
[self addSubview:self.closeButton];
[self.titleLabel mas_makeConstraints:^(MASConstraintMaker *make) {
make.top.equalTo(self).offset(18);
make.left.equalTo(self).offset(16);
make.right.equalTo(self).offset(-16);
}];
[self.descLabel mas_makeConstraints:^(MASConstraintMaker *make) {
make.top.equalTo(self.titleLabel.mas_bottom).offset(6);
make.left.right.equalTo(self.titleLabel);
}];
[self.appleContainer mas_makeConstraints:^(MASConstraintMaker *make) {
make.left.equalTo(self).offset(16);
make.right.equalTo(self).offset(-16);
make.top.equalTo(self.descLabel.mas_bottom).offset(20);
make.height.mas_equalTo(52);
make.bottom.equalTo(self).offset(-24); //
}];
[self.closeButton mas_makeConstraints:^(MASConstraintMaker *make) {
make.top.equalTo(self).offset(6);
make.right.equalTo(self).offset(-6);
make.width.height.mas_equalTo(36);
}];
// Apple
if (@available(iOS 13.0, *)) {
ASAuthorizationAppleIDButton *btn = [ASAuthorizationAppleIDButton new];
[btn addTarget:self action:@selector(onTapApple) forControlEvents:UIControlEventTouchUpInside];
[self.appleContainer addSubview:btn];
[btn mas_makeConstraints:^(MASConstraintMaker *make) {
make.edges.equalTo(self.appleContainer);
}];
} else {
UIButton *btn = [UIButton buttonWithType:UIButtonTypeSystem];
[btn setTitle:@"需要 iOS13+ 才能使用 Apple 登录" forState:UIControlStateNormal];
btn.enabled = NO;
btn.layer.cornerRadius = 8.0;
btn.layer.borderWidth = 1.0;
btn.layer.borderColor = [UIColor lightGrayColor].CGColor;
[self.appleContainer addSubview:btn];
[btn mas_makeConstraints:^(MASConstraintMaker *make) {
make.edges.equalTo(self.appleContainer);
}];
}
}
#pragma mark - Actions
// Apple
- (void)onTapApple {
if (self.appleLoginHandler) self.appleLoginHandler();
}
//
- (void)onTapClose {
if (self.closeHandler) self.closeHandler();
}
#pragma mark - Lazy
- (UILabel *)titleLabel {
if (!_titleLabel) {
_titleLabel = [UILabel new];
_titleLabel.text = @"登录后可使用全部功能";
_titleLabel.font = [UIFont boldSystemFontOfSize:18];
_titleLabel.textColor = [UIColor blackColor];
_titleLabel.textAlignment = NSTextAlignmentCenter;
_titleLabel.numberOfLines = 1;
}
return _titleLabel;
}
- (UILabel *)descLabel {
if (!_descLabel) {
_descLabel = [UILabel new];
_descLabel.text = @"我们将使用 Apple 进行快速安全登录";
_descLabel.font = [UIFont systemFontOfSize:14];
_descLabel.textColor = [UIColor colorWithWhite:0.2 alpha:0.8];
_descLabel.textAlignment = NSTextAlignmentCenter;
_descLabel.numberOfLines = 0;
}
return _descLabel;
}
- (UIButton *)closeButton {
if (!_closeButton) {
_closeButton = [UIButton buttonWithType:UIButtonTypeSystem];
[_closeButton setTitle:@"✕" forState:UIControlStateNormal];
[_closeButton setTitleColor:[UIColor darkTextColor] forState:UIControlStateNormal];
_closeButton.titleLabel.font = [UIFont systemFontOfSize:18 weight:UIFontWeightSemibold];
[_closeButton addTarget:self action:@selector(onTapClose) forControlEvents:UIControlEventTouchUpInside];
}
return _closeButton;
}
- (UIView *)appleContainer {
if (!_appleContainer) {
_appleContainer = [UIView new];
}
return _appleContainer;
}
@end