// // KeyboardViewController.m // CustomKeyboard // // Created by Mac on 2025/10/27. // #import "KeyboardViewController+Private.h" #import "KBBackspaceUndoManager.h" #import "KBChatLimitPopView.h" #import "KBChatPanelView.h" #import "KBFullAccessManager.h" #import "KBFunctionView.h" #import "KBInputBufferManager.h" #import "KBKeyBoardMainView.h" #import "KBKeyboardSubscriptionView.h" #import "KBLocalizationManager.h" #import "KBSettingView.h" #import "KBSkinManager.h" #import "KBSkinInstallBridge.h" #import "KBSuggestionEngine.h" #import "KBKeyboardLayoutResolver.h" #import #if DEBUG #import #endif #if DEBUG static NSInteger sKBKeyboardVCAliveCount = 0; static uint64_t KBPhysFootprintBytes(void) { task_vm_info_data_t vmInfo; mach_msg_type_number_t count = TASK_VM_INFO_COUNT; kern_return_t kr = task_info(mach_task_self(), TASK_VM_INFO, (task_info_t)&vmInfo, &count); if (kr != KERN_SUCCESS) { return 0; } return (uint64_t)vmInfo.phys_footprint; } static NSString *KBFormatMB(uint64_t bytes) { double mb = (double)bytes / 1024.0 / 1024.0; return [NSString stringWithFormat:@"%.1fMB", mb]; } #endif @implementation KeyboardViewController { BOOL _kb_didTriggerLoginDeepLinkOnce; NSString *_kb_lastLoadedProfileId; // 记录上次加载的 profileId #if DEBUG BOOL _kb_debugDidCountAlive; #endif } - (void)viewDidLoad { [super viewDidLoad]; #if DEBUG if (!_kb_debugDidCountAlive) { _kb_debugDidCountAlive = YES; sKBKeyboardVCAliveCount += 1; } NSLog(@"[Keyboard] KeyboardViewController viewDidLoad alive=%ld self=%p mem=%@", (long)sKBKeyboardVCAliveCount, self, KBFormatMB(KBPhysFootprintBytes())); #endif // 撤销删除是“上一段删除操作”的临时状态;键盘被系统回收/重建或跨页面回来时应当清空,避免误显示。 [[KBBackspaceUndoManager shared] registerNonClearAction]; [self setupUI]; self.suggestionEngine = [KBSuggestionEngine shared]; self.currentWord = @""; // 指定 HUD 的承载视图(扩展里无法取到 App 的 KeyWindow) [KBHUD setContainerView:self.view]; // 绑定完全访问管理器,便于统一感知和联动网络开关 [[KBFullAccessManager shared] bindInputController:self]; self.kb_fullAccessObserverToken = [[NSNotificationCenter defaultCenter] addObserverForName:KBFullAccessChangedNotification object:nil queue:[NSOperationQueue mainQueue] usingBlock:^(__unused NSNotification *_Nonnull note){ // 如需,可在此刷新与完全访问相关的 UI }]; // 皮肤变化时,立即应用 __weak typeof(self) weakSelf = self; self.kb_skinObserverToken = [[NSNotificationCenter defaultCenter] addObserverForName:KBSkinDidChangeNotification object:nil queue:[NSOperationQueue mainQueue] usingBlock:^(__unused NSNotification *_Nonnull note) { __strong typeof(weakSelf) self = weakSelf; if (!self) { return; } [self kb_applyTheme]; }]; // 语言变化时,重建键盘 UI(保证“App 语言=键盘语言”,并支持 App 内切换语言后键盘即时刷新) self.kb_localizationObserverToken = [[NSNotificationCenter defaultCenter] addObserverForName:KBLocalizationDidChangeNotification object:nil queue:[NSOperationQueue mainQueue] usingBlock:^(__unused NSNotification *_Nonnull note) { __strong typeof(weakSelf) self = weakSelf; if (!self) { return; } [self kb_reloadUIForLocalizationChange]; }]; [self kb_applyTheme]; [self kb_registerDarwinSkinInstallObserver]; [self kb_consumePendingShopSkin]; [self kb_applyDefaultSkinIfNeeded]; [self kb_startObservingAppGroupChanges]; // 监听 App Group 配置变化,动态切换键盘布局 [self kb_checkAndApplyLayoutIfNeeded]; } - (void)didReceiveMemoryWarning { [super didReceiveMemoryWarning]; // 扩展进程内存上限较小:在系统发出内存警告时主动清理可重建的缓存,降低被系统杀死概率。 self.kb_cachedGradientImage = nil; [self.kb_defaultGradientLayer removeFromSuperlayer]; self.kb_defaultGradientLayer = nil; [[KBSkinManager shared] clearRuntimeImageCaches]; [[SDImageCache sharedImageCache] clearMemory]; } - (void)viewWillAppear:(BOOL)animated { [super viewWillAppear:animated]; // FIX: iOS 26 键盘闪烁问题 —— 恢复键盘正确高度 // setupUI 中高度初始为 0(防止系统预渲染快照闪烁),此处恢复为实际键盘高度。 // 此时系统已准备好键盘滑入动画,恢复高度后键盘将正常从底部滑入。 CGFloat portraitWidth = [self kb_portraitWidth]; CGFloat keyboardHeight = [self kb_keyboardHeightForWidth:portraitWidth]; if (self.kb_heightConstraint) { self.kb_heightConstraint.constant = keyboardHeight; } // 进入/重新进入输入界面时,清理上一次会话残留的撤销状态与缓存,避免显示“撤销删除”但实际上已不可撤销。 [[KBBackspaceUndoManager shared] registerNonClearAction]; [[KBInputBufferManager shared] resetWithText:@""]; [[KBLocalizationManager shared] reloadFromSharedStorageIfNeeded]; // 键盘再次出现时,恢复 HUD 容器与主题(viewDidDisappear 里可能已清理图片/缓存)。 [KBHUD setContainerView:self.view]; [self kb_ensureKeyBoardMainViewIfNeeded]; [self kb_applyTheme]; #if DEBUG NSLog(@"[Keyboard] viewWillAppear self=%p mem=%@", self, KBFormatMB(KBPhysFootprintBytes())); #endif // 注意:微信/QQ 等宿主的 documentContext 可能是“截断窗口”,这里只更新 // liveText,不要把它当作全文 manualSnapshot。 [[KBInputBufferManager shared] updateFromExternalContextBefore:self.textDocumentProxy .documentContextBeforeInput after:self.textDocumentProxy .documentContextAfterInput]; } - (void)viewWillDisappear:(BOOL)animated { [super viewWillDisappear:animated]; [[KBBackspaceUndoManager shared] registerNonClearAction]; [self kb_releaseMemoryWhenKeyboardHidden]; #if DEBUG NSLog(@"[Keyboard] viewWillDisappear self=%p mem=%@", self, KBFormatMB(KBPhysFootprintBytes())); #endif } - (void)viewDidDisappear:(BOOL)animated { [super viewDidDisappear:animated]; // 再兜底一次,防止某些宿主只触发 willDisappear 而未触发 didDisappear。 [self kb_releaseMemoryWhenKeyboardHidden]; } - (void)traitCollectionDidChange:(UITraitCollection *)previousTraitCollection { [super traitCollectionDidChange:previousTraitCollection]; if (@available(iOS 13.0, *)) { if (previousTraitCollection.userInterfaceStyle != self.traitCollection.userInterfaceStyle) { self.kb_cachedGradientImage = nil; [self kb_applyDefaultSkinIfNeeded]; } } } - (void)textDidChange:(id)textInput { [super textDidChange:textInput]; [[KBInputBufferManager shared] updateFromExternalContextBefore:self.textDocumentProxy .documentContextBeforeInput after:self.textDocumentProxy .documentContextAfterInput]; } - (void)viewDidAppear:(BOOL)animated { [super viewDidAppear:animated]; // if (!_kb_didTriggerLoginDeepLinkOnce) { // _kb_didTriggerLoginDeepLinkOnce = YES; // // 仅在未登录时尝试拉起主App登录 // if (!KBAuthManager.shared.isLoggedIn) { // [self kb_tryOpenContainerForLoginIfNeeded]; // } // } } - (void)viewDidLayoutSubviews { [super viewDidLayoutSubviews]; // [self kb_updateKeyboardLayoutIfNeeded]; // 首次布局完成后显示,避免闪烁 if (self.contentView.hidden) { self.contentView.hidden = NO; } if (self.kb_defaultGradientLayer) { self.kb_defaultGradientLayer.frame = self.bgImageView.bounds; } // 每次布局时检查是否需要切换键盘布局 [self kb_checkAndApplyLayoutIfNeeded]; } - (void)viewWillTransitionToSize:(CGSize)size withTransitionCoordinator: (id)coordinator { [super viewWillTransitionToSize:size withTransitionCoordinator:coordinator]; __weak typeof(self) weakSelf = self; [coordinator animateAlongsideTransition:^( id _Nonnull context) { [weakSelf kb_updateKeyboardLayoutIfNeeded]; } completion:^( __unused id< UIViewControllerTransitionCoordinatorContext> _Nonnull context) { [weakSelf kb_updateKeyboardLayoutIfNeeded]; }]; } - (void)dealloc { if (self.kb_fullAccessObserverToken) { [[NSNotificationCenter defaultCenter] removeObserver:self.kb_fullAccessObserverToken]; self.kb_fullAccessObserverToken = nil; } if (self.kb_skinObserverToken) { [[NSNotificationCenter defaultCenter] removeObserver:self.kb_skinObserverToken]; self.kb_skinObserverToken = nil; } if (self.kb_localizationObserverToken) { [[NSNotificationCenter defaultCenter] removeObserver:self.kb_localizationObserverToken]; self.kb_localizationObserverToken = nil; } [self kb_stopObservingAppGroupChanges]; [self kb_unregisterDarwinSkinInstallObserver]; #if DEBUG if (_kb_debugDidCountAlive) { sKBKeyboardVCAliveCount -= 1; } NSLog(@"[Keyboard] KeyboardViewController dealloc alive=%ld self=%p mem=%@", (long)sKBKeyboardVCAliveCount, self, KBFormatMB(KBPhysFootprintBytes())); #endif } #pragma mark - Localization - (void)kb_reloadUIForLocalizationChange { if (![NSThread isMainThread]) { __weak typeof(self) weakSelf = self; dispatch_async(dispatch_get_main_queue(), ^{ [weakSelf kb_reloadUIForLocalizationChange]; }); return; } // 记录当前面板状态,重建后尽量恢复。 KBKeyboardPanelMode targetMode = self.kb_panelMode; // 强制下次布局刷新:即使 profileId 未变,也需要让新建的主视图应用一次当前 profile。 _kb_lastLoadedProfileId = nil; // 主键盘/面板里有大量静态文案(init 时设置),语言变化后需要重建才能刷新。 if (_keyBoardMainView) { [_keyBoardMainView removeFromSuperview]; _keyBoardMainView = nil; } self.keyBoardMainHeightConstraint = nil; if (_functionView) { [_functionView removeFromSuperview]; _functionView = nil; } if (_settingView) { [_settingView removeFromSuperview]; _settingView = nil; } if (_subscriptionView) { [_subscriptionView removeFromSuperview]; _subscriptionView = nil; } if (_chatPanelView) { [_chatPanelView removeFromSuperview]; _chatPanelView = nil; } self.chatPanelVisible = NO; self.chatPanelHeightConstraint = nil; // 强制触发面板刷新:先回到 Main,再切回目标面板(避免 kb_setPanelMode 直接 return)。 self.kb_panelMode = KBKeyboardPanelModeMain; [self kb_setPanelMode:targetMode animated:NO]; // 语言变化后,键盘布局/profile 也可能需要同步更新(未手动选择键盘配置时会随 App 语言变化) [self kb_checkAndApplyLayoutIfNeeded]; [KBHUD setContainerView:self.view]; [self kb_applyTheme]; } #pragma mark - Layout Switching - (void)kb_checkAndApplyLayoutIfNeeded { NSString *currentProfileId = [[KBKeyboardLayoutResolver sharedResolver] currentProfileId]; if (currentProfileId.length == 0) { currentProfileId = @"en_US_qwerty"; } if ([currentProfileId isEqualToString:_kb_lastLoadedProfileId]) { return; } NSLog(@"[KeyboardViewController] Detected profileId change: %@ -> %@", _kb_lastLoadedProfileId, currentProfileId); _kb_lastLoadedProfileId = currentProfileId; if (self.keyBoardMainView && [self.keyBoardMainView respondsToSelector:@selector(reloadLayoutWithProfileId:)]) { [self.keyBoardMainView performSelector:@selector(reloadLayoutWithProfileId:) withObject:currentProfileId]; } NSString *suggestionEngine = [[KBKeyboardLayoutResolver sharedResolver] suggestionEngineForProfileId:currentProfileId]; if (suggestionEngine.length > 0) { [self kb_updateSuggestionEngineType:suggestionEngine]; } NSString *languageCode = [[KBKeyboardLayoutResolver sharedResolver] currentLanguageCode]; if (languageCode.length > 0) { NSLog(@"[KeyboardViewController] Reloading skin icon map for language: %@", languageCode); [KBSkinInstallBridge reloadCurrentSkinIconMapForLanguageCode:languageCode]; } } - (void)kb_updateSuggestionEngineType:(NSString *)engineType { NSLog(@"[KeyboardViewController] Switching suggestion engine to: %@", engineType); [[KBSuggestionEngine shared] setEngineTypeFromString:engineType]; } #pragma mark - App Group KVO - (void)kb_startObservingAppGroupChanges { NSUserDefaults *appGroup = [[NSUserDefaults alloc] initWithSuiteName:AppGroup]; __weak typeof(self) weakSelf = self; self.kb_appGroupObserverToken = [[NSNotificationCenter defaultCenter] addObserverForName:NSUserDefaultsDidChangeNotification object:appGroup queue:[NSOperationQueue mainQueue] usingBlock:^(__unused NSNotification *_Nonnull note) { __strong typeof(weakSelf) strongSelf = weakSelf; if (!strongSelf) { return; } [strongSelf kb_checkAndApplyLayoutIfNeeded]; }]; NSLog(@"[KeyboardViewController] Started observing App Group changes"); } - (void)kb_stopObservingAppGroupChanges { if (self.kb_appGroupObserverToken) { [[NSNotificationCenter defaultCenter] removeObserver:self.kb_appGroupObserverToken]; self.kb_appGroupObserverToken = nil; } } @end