WKWebView支持代码focus唤起键盘

8,086 阅读3分钟

0x00 背景

iOS H5 Hybrid应用中,让对应的input或TextArea获得focus状态,自动唤起系统键盘,方便用户直接输入,是一种比较常见的场景。

而在iOS的移动端Web页中,这一操作是有限制的。

苹果为了安全,在iOS的WebView里,需要用户实际点击到input输入区域后,才会触发聚焦并自动唤起键盘。使用js代码来设置聚焦(input.focus)默认是不生效的。

0x01 处理方法

在UIWebView里,系统提供了一个keyboardDisplayRequiresUserAction属性,这个属性默认为YES,也就是说键盘的出现必须要用户交互。

在UIWebView里,我们可以把keyboardDisplayRequiresUserAction设置为NO,这样就可以通过js代码来自动聚焦并唤起键盘了。

注:UIWebView已经被苹果废弃了,APP也不再建议使用UIWebView了。

到了WKWebView里,苹果为了强安全,取消了这个属性(在Public的 API里已经找不到了),默认键盘的出现必须要用户交互。

我们知道,WKWebViewd的内核是WebKit的,而WebKit是开源的,WebKit源码:

trac.webkit.org/browser

github.com/WebKit/webk…

有了源码,就有可能定位源码里的控制逻辑,通过runtime的方式,动态hook WebKit,实现类似 UIWebView中keyboardDisplayRequiresUserAction = NO 的行为。

通过Google和查看WebKit的内核源码,最新的代码里控制这一逻辑的方法签名是: _elementDidFocus:userIsInteracting:blurPreviousNode:activityStateChanges:userObject:

注意:不同的iOS系统版本,这一方法签名可能是不一样的,所以需要对iOS系统版本做兼容。

兼容到最新的iOS13.5.1的hack参考代码:

@interface CVTWebViewHack : NSObject

+ (void)allowDisplayingKeyboardWithoutUserAction;

@end

------------------------------------------------------

#import <objc/runtime.h>

@implementation CVTWebViewHack

+ (void)allowDisplayingKeyboardWithoutUserAction {
    Class class = NSClassFromString(@"WKContentView");
    NSOperatingSystemVersion iOS_11_3_0 = (NSOperatingSystemVersion){11, 3, 0};
    NSOperatingSystemVersion iOS_12_2_0 = (NSOperatingSystemVersion){12, 2, 0};
    NSOperatingSystemVersion iOS_13_0_0 = (NSOperatingSystemVersion){13, 0, 0};
    if ([[NSProcessInfo processInfo] isOperatingSystemAtLeastVersion: iOS_13_0_0]) {
        SEL selector = sel_getUid("_elementDidFocus:userIsInteracting:blurPreviousNode:activityStateChanges:userObject:");
        Method method = class_getInstanceMethod(class, selector);
        IMP original = method_getImplementation(method);
        IMP override = imp_implementationWithBlock(^void(id me, void* arg0, BOOL arg1, BOOL arg2, BOOL arg3, id arg4) {
        ((void (*)(id, SEL, void*, BOOL, BOOL, BOOL, id))original)(me, selector, arg0, TRUE, arg2, arg3, arg4);
        });
        method_setImplementation(method, override);
    } else if ([[NSProcessInfo processInfo] isOperatingSystemAtLeastVersion: iOS_12_2_0]) {
        SEL selector = sel_getUid("_elementDidFocus:userIsInteracting:blurPreviousNode:changingActivityState:userObject:");
        Method method = class_getInstanceMethod(class, selector);
        IMP original = method_getImplementation(method);
        IMP override = imp_implementationWithBlock(^void(id me, void* arg0, BOOL arg1, BOOL arg2, BOOL arg3, id arg4) {
        ((void (*)(id, SEL, void*, BOOL, BOOL, BOOL, id))original)(me, selector, arg0, TRUE, arg2, arg3, arg4);
        });
        method_setImplementation(method, override);
    } else if ([[NSProcessInfo processInfo] isOperatingSystemAtLeastVersion: iOS_11_3_0]) {
        SEL selector = sel_getUid("_startAssistingNode:userIsInteracting:blurPreviousNode:changingActivityState:userObject:");
        Method method = class_getInstanceMethod(class, selector);
        IMP original = method_getImplementation(method);
        IMP override = imp_implementationWithBlock(^void(id me, void* arg0, BOOL arg1, BOOL arg2, BOOL arg3, id arg4) {
            ((void (*)(id, SEL, void*, BOOL, BOOL, BOOL, id))original)(me, selector, arg0, TRUE, arg2, arg3, arg4);
        });
        method_setImplementation(method, override);
    } else {
        SEL selector = sel_getUid("_startAssistingNode:userIsInteracting:blurPreviousNode:userObject:");
        Method method = class_getInstanceMethod(class, selector);
        IMP original = method_getImplementation(method);
        IMP override = imp_implementationWithBlock(^void(id me, void* arg0, BOOL arg1, BOOL arg2, id arg3) {
            ((void (*)(id, SEL, void*, BOOL, BOOL, id))original)(me, selector, arg0, TRUE, arg2, arg3);
        });
        method_setImplementation(method, override);
    }
}

@end

0x02 风险分析

由于是hook了系统的私有方法,当iOS系统升级时,有失效的风险,代码实现上需保证即使找不到方法签名也不能引起程序崩溃。 由于hook的私有方法签名字符串,有可能在AppStore审核期间被拒(2020年5~6月实测上架不会被拒绝)。

0x03 授人以渔

授人以鱼不如授人以渔,假设未来的iOS14的某个版本,苹果又调整了下面这个方法签名: _elementDidFocus:userIsInteracting:blurPreviousNode:activityStateChanges:userObject: 最简单的办法就是到这里 trac.webkit.org/browser, 根据旧的方法签名关键字去查询变更记录,找到新的方法签名和iOS系统版本后,调整为新的方法签名。

webkit source

欢迎大家交流