Files
keyboard/CustomKeyboard/KeyboardViewController.m
2026-03-09 17:34:08 +08:00

381 lines
14 KiB
Objective-C
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//
// 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 "KBSkinManager.h"
#import "KBSkinInstallBridge.h"
#import "KBSuggestionEngine.h"
#import "KBKeyboardLayoutResolver.h"
#import <SDWebImage/SDWebImage.h>
#if DEBUG
#import <mach/mach.h>
#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<UITextInput>)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<UIViewControllerTransitionCoordinator>)coordinator {
[super viewWillTransitionToSize:size withTransitionCoordinator:coordinator];
__weak typeof(self) weakSelf = self;
[coordinator
animateAlongsideTransition:^(
id<UIViewControllerTransitionCoordinatorContext> _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 (_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