316 lines
11 KiB
Objective-C
316 lines
11 KiB
Objective-C
//
|
||
// 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];
|
||
}];
|
||
[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;
|
||
}
|
||
[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 - 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
|