25

It's possible with UIWebView with following:

[webView setKeyboardDisplayRequiresUserAction:NO]
Call some JS function

How can you do the same when the webview is WKWebView instead?

related: How can I get a UIWebView to focus on a form input and bring up the keyboard?

Community
  • 1
  • 1
Maximus S
  • 10,759
  • 19
  • 75
  • 154

7 Answers7

32

The accepted answer no longer works in iOS 11.3, since WebKit method signature has changed. Here is a workaround (in Obj-C):

(UPDATE: Method signature has changed a few more times in iOS 12.2 and iOS 13, code below has been updated to reflect these changes)

#import <objc/runtime.h>

@implementation WebViewInjection

+ (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
Alex Staravoitau
  • 2,222
  • 21
  • 27
  • Thanks! I've been trying to figure this out. If you wouldn't mind explaining, how did you figure this out? – Chet Mar 30 '18 at 01:58
  • @Chet WebKit is open-source, so once accepted answer stopped working for me (after iOS 11.3 update) I looked up the changes and realised the method signature has changed. – Alex Staravoitau Mar 30 '18 at 12:54
  • You sir are my hero. Saved my day. Thank you! – Hirbod Mar 31 '18 at 19:05
  • @AlexStaravoitau any chance you could implement your fix for cordova? https://github.com/onderceylan/cordova-plugin-wkwebview-inputfocusfix/blob/master/src/ios/CDVWKWebViewEngine%2BInputFocusFix.m Edit: fixed myself thanks to you. https://github.com/onderceylan/cordova-plugin-wkwebview-inputfocusfix/pull/4 – Hirbod Mar 31 '18 at 19:08
  • 4
    How many apps with this workaround got accepted in the AppStore? Since this is breaking without any deprecation warning I'd guess this isn't supposed to be used that way. Am I the only one worried using that piece of code? – wrtsprt Jul 01 '18 at 08:31
  • @wrtsprt my app got approved with this work around, Apple does not complain about it (yet) every time I submit for updates. – thomasdao Apr 08 '19 at 08:10
  • @AlexStaravoitau okay, but how do you figure out the new method signature though? (would really like to learn) – gx14 Aug 15 '19 at 16:38
  • If there's more than one form element in a webview, how is this expected to work? – Kunal Shah Sep 03 '19 at 22:57
  • 2
    Since this was fixed, @Hirbod's `inputfocusfix` plugin got deprecated because the changes were merged into the main `wkwebview` repos. However, the iOS 13 fix is not there yet, so we had to reinstate the fix until that happens. I did it here: https://github.com/adaptabi/cordova-plugin-wkwebview-inputfocusfix – Teodor Sandu Nov 26 '19 at 10:07
  • I am now facing changes in IOS 13.3 where the view is now being scrolled vertically when the keyboard appears, however it is not consistent and sometimes it stays fixed in place. Does the swizzle need updating? – Steve Brooker Jan 31 '20 at 12:05
  • Hi @AlexStaravoitau, This is working fine on iOS 13.0 and 13.1, whereas I am also testing it on iOS 13.6 & iOS 14 beta and it is not working on there. Any updated solution for that? – Harsh Sharma Aug 26 '20 at 11:55
29

Update: This solution works for iOS 13.0, 12.2, 11.* and 10.* Also, works on iPadOS 13.1

I wrote an extension (in Swift 4 for WKWebView class that adds keyboardDisplayRequiresUserAction as a computed property, just like in UIWebView.

After referring to the Apple’s official open source documents for WebKit, I came up with the following runtime swizzling:

import Foundation
import WebKit

typealias OldClosureType =  @convention(c) (Any, Selector, UnsafeRawPointer, Bool, Bool, Any?) -> Void
typealias NewClosureType =  @convention(c) (Any, Selector, UnsafeRawPointer, Bool, Bool, Bool, Any?) -> Void

extension WKWebView{
    var keyboardDisplayRequiresUserAction: Bool? {
        get {
            return self.keyboardDisplayRequiresUserAction
        }
        set {
            self.setKeyboardRequiresUserInteraction(newValue ?? true)
        }
    }

    func setKeyboardRequiresUserInteraction( _ value: Bool) {
        guard let WKContentView: AnyClass = NSClassFromString("WKContentView") else {
            print("keyboardDisplayRequiresUserAction extension: Cannot find the WKContentView class")
            return
        }
        // For iOS 10, *
        let sel_10: Selector = sel_getUid("_startAssistingNode:userIsInteracting:blurPreviousNode:userObject:")
        // For iOS 11.3, *
        let sel_11_3: Selector = sel_getUid("_startAssistingNode:userIsInteracting:blurPreviousNode:changingActivityState:userObject:")
        // For iOS 12.2, *
        let sel_12_2: Selector = sel_getUid("_elementDidFocus:userIsInteracting:blurPreviousNode:changingActivityState:userObject:")
        // For iOS 13.0, *
        let sel_13_0: Selector = sel_getUid("_elementDidFocus:userIsInteracting:blurPreviousNode:activityStateChanges:userObject:")

        if let method = class_getInstanceMethod(WKContentView, sel_10) {
            let originalImp: IMP = method_getImplementation(method)
            let original: OldClosureType = unsafeBitCast(originalImp, to: OldClosureType.self)
            let block : @convention(block) (Any, UnsafeRawPointer, Bool, Bool, Any?) -> Void = { (me, arg0, arg1, arg2, arg3) in
                original(me, sel_10, arg0, !value, arg2, arg3)
            }
            let imp: IMP = imp_implementationWithBlock(block)
            method_setImplementation(method, imp)
        }

        if let method = class_getInstanceMethod(WKContentView, sel_11_3) {
            let originalImp: IMP = method_getImplementation(method)
            let original: NewClosureType = unsafeBitCast(originalImp, to: NewClosureType.self)
            let block : @convention(block) (Any, UnsafeRawPointer, Bool, Bool, Bool, Any?) -> Void = { (me, arg0, arg1, arg2, arg3, arg4) in
                original(me, sel_11_3, arg0, !value, arg2, arg3, arg4)
            }
            let imp: IMP = imp_implementationWithBlock(block)
            method_setImplementation(method, imp)
        }

        if let method = class_getInstanceMethod(WKContentView, sel_12_2) {
            let originalImp: IMP = method_getImplementation(method)
            let original: NewClosureType = unsafeBitCast(originalImp, to: NewClosureType.self)
            let block : @convention(block) (Any, UnsafeRawPointer, Bool, Bool, Bool, Any?) -> Void = { (me, arg0, arg1, arg2, arg3, arg4) in
                original(me, sel_12_2, arg0, !value, arg2, arg3, arg4)
            }
            let imp: IMP = imp_implementationWithBlock(block)
            method_setImplementation(method, imp)
        }

        if let method = class_getInstanceMethod(WKContentView, sel_13_0) {
            let originalImp: IMP = method_getImplementation(method)
            let original: NewClosureType = unsafeBitCast(originalImp, to: NewClosureType.self)
            let block : @convention(block) (Any, UnsafeRawPointer, Bool, Bool, Bool, Any?) -> Void = { (me, arg0, arg1, arg2, arg3, arg4) in
                original(me, sel_13_0, arg0, !value, arg2, arg3, arg4)
            }
            let imp: IMP = imp_implementationWithBlock(block)
            method_setImplementation(method, imp)
        }
    }
}

Make sure you call property on your WKWebView like this,

let webView = WKWebView()
webView.keyboardDisplayRequiresUserAction = false

Also, make sure your HTML TextArea element has AutoFocus set to true otherwise this won't work.

Pranit Harekar
  • 431
  • 4
  • 9
9

This Swift extension does the job and is compatible with 11.3 as well as earlier point releases.

import Foundation
import WebKit

typealias OlderClosureType =  @convention(c) (Any, Selector, UnsafeRawPointer, Bool, Bool, Any?) -> Void
typealias NewerClosureType =  @convention(c) (Any, Selector, UnsafeRawPointer, Bool, Bool, Bool, Any?) -> Void

extension WKWebView{

    var keyboardDisplayRequiresUserAction: Bool? {
        get {
            return self.keyboardDisplayRequiresUserAction
        }
        set {
            self.setKeyboardRequiresUserInteraction(newValue ?? true)
        }
    }

    func setKeyboardRequiresUserInteraction( _ value: Bool) {

        guard
            let WKContentViewClass: AnyClass = NSClassFromString("WKContentView") else {
                print("Cannot find the WKContentView class")
                return
        }

        let olderSelector: Selector = sel_getUid("_startAssistingNode:userIsInteracting:blurPreviousNode:userObject:")
        let newerSelector: Selector = sel_getUid("_startAssistingNode:userIsInteracting:blurPreviousNode:changingActivityState:userObject:")

        if let method = class_getInstanceMethod(WKContentViewClass, olderSelector) {

            let originalImp: IMP = method_getImplementation(method)
            let original: OlderClosureType = unsafeBitCast(originalImp, to: OlderClosureType.self)
            let block : @convention(block) (Any, UnsafeRawPointer, Bool, Bool, Any?) -> Void = { (me, arg0, arg1, arg2, arg3) in
                original(me, olderSelector, arg0, !value, arg2, arg3)
            }
            let imp: IMP = imp_implementationWithBlock(block)
            method_setImplementation(method, imp)
        }

        if let method = class_getInstanceMethod(WKContentViewClass, newerSelector) {

            let originalImp: IMP = method_getImplementation(method)
            let original: NewerClosureType = unsafeBitCast(originalImp, to: NewerClosureType.self)
            let block : @convention(block) (Any, UnsafeRawPointer, Bool, Bool, Bool, Any?) -> Void = { (me, arg0, arg1, arg2, arg3, arg4) in
                original(me, newerSelector, arg0, !value, arg2, arg3, arg4)
            }
            let imp: IMP = imp_implementationWithBlock(block)
            method_setImplementation(method, imp)
        }

    }

}
Mark Bridges
  • 8,228
  • 4
  • 50
  • 65
  • This check against the runtime to fetch the selector, but the base SDK used to compile is what breaks the old way. I have an app compiled with 11.3 base SDK, running it on a pre-11.3 device fails to get the new selector but crashes on the old one. – kevin Apr 06 '18 at 11:03
6

After digging into the Webkit sources for a couple of weeks, I've managed to get this working on iOS 9 by swizzling _startAssistingNode:userIsInteracting:blurPreviousNode:userObject on WKContentView and overriding the userIsInteracting value:

Pseudo code:

swizzle_intercept("WKContentView", "_startAssistingNode:userIsInteracting:blurPreviousNode:userObject:", &hackAssist);

void hackAssist (id self, SEL _cmd, void* arg0, BOOL arg1, BOOL arg2, id arg3) {
    ((void (*)(id,SEL,void*,BOOL,BOOL,id))swizzle_interceptee(hackAssist))(self, _cmd, arg0, TRUE, arg2, arg3);
}

Cheers!

Emiel Mols
  • 436
  • 4
  • 12
5

I had to change @Mark's answer from an extension to a subclass as Swift 4.2 gave me an "All paths through this function will call itself" warning on the keyboardDisplayRequiresUserAction getter.

import Foundation
import WebKit

typealias OldClosureType = @convention(c) (Any, Selector, UnsafeRawPointer, Bool, Bool, Any?) -> Void
typealias NewClosureType = @convention(c) (Any, Selector, UnsafeRawPointer, Bool, Bool, Bool, Any?) -> Void

class WebView: WKWebView {

    private var _keyboardDisplayRequiresUseraction = true

    var keyboardDisplayRequiresUserAction: Bool? {
        get {
            return _keyboardDisplayRequiresUseraction
        }
        set {
            _keyboardDisplayRequiresUseraction = newValue ?? true
            setKeyboardRequiresUserInteraction(_keyboardDisplayRequiresUseraction)
        }
    }

    private func setKeyboardRequiresUserInteraciton(_ value: Bool) {
        guard let WKContentViewClass: AnyClass = NSClassFromString("WKContentView") else {
            return print("Cannot find WKContentView class")
        }

        let oldSelector: Selector = sel_getUid("_startAssistingNode:userIsInteracting:blurPreviousNode:userObject:")
        let newSelector: Selector = sel_getUid("_startAssistingNode:userIsInteracting:blurPreviousNode:changingActivityState:userObject:")

        if let method = class_getInstanceMethod(WKContentViewClass, oldSelector) {
            let originalImp: IMP = method_getImplementation(method)
            let original: OldClosureType = unsafeBitCast(originalImp, to: OldClosureType.self)
            let block: @convention(block) (Any, UnsafeRawPointer, Bool, Bool, Any?) -> Void = { (me, arg0, arg1, arg2, arg3) in
                original(me, oldSelector, arg0, !value, arg2, arg3)
            }
            let imp: IMP = imp_implementationWithBlock(block)
            method_setImplementation(method, imp)
        }
        if let method = class_getInstanceMethod(WKContentViewClass, newSelector) {
            let originalImp: IMP = method_getImplementation(method)
            let original: NewClosureType = unsafeBitCast(originalImp, to: NewClosureType.self)
            let block: @convention(block) (Any, UnsafeRawPointer, Bool, Bool, Bool, Any?) -> Void = { (me, arg0, arg1, arg2, arg3, arg4) in
                original(me, newSelector, arg0, !value, arg2, arg3, arg4)
            }
            let imp: IMP = imp_implementationWithBlock(block)
            method_setImplementation(method, imp)
        }
    }
}

Tested on iOS 11.2 and 12.0

David
  • 3,285
  • 1
  • 37
  • 54
Kramer
  • 519
  • 1
  • 5
  • 13
  • 1
    Works like a charm, thanks! Note that you have to set `myWebView.keyboardDisplayRequiresUserAction = true` when you use this because it is initialized only in the setter. – Roben Dec 21 '18 at 09:39
5

Update for iOS 13 as the method changed again:

Objective-C

#import <objc/runtime.h>

@implementation WebViewInjection

+ (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};
    char * methodSignature = "_startAssistingNode:userIsInteracting:blurPreviousNode:changingActivityState:userObject:";

    if ([[NSProcessInfo processInfo] isOperatingSystemAtLeastVersion: iOS_13_0_0]) {
        methodSignature = "_elementDidFocus:userIsInteracting:blurPreviousNode:activityStateChanges:userObject:";
    } else if ([[NSProcessInfo processInfo] isOperatingSystemAtLeastVersion: iOS_12_2_0]) {
        methodSignature = "_elementDidFocus:userIsInteracting:blurPreviousNode:changingActivityState:userObject:";
    }

    if ([[NSProcessInfo processInfo] isOperatingSystemAtLeastVersion: iOS_11_3_0]) {
        SEL selector = sel_getUid(methodSignature);
        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

Swift:

    func setKeyboardRequiresUserInteraction( _ value: Bool) {

        guard
            let WKContentViewClass: AnyClass = NSClassFromString("WKContentView") else {
                print("Cannot find the WKContentView class")
                return
        }

        let olderSelector: Selector = sel_getUid("_startAssistingNode:userIsInteracting:blurPreviousNode:userObject:")
        let newSelector: Selector = sel_getUid("_startAssistingNode:userIsInteracting:blurPreviousNode:changingActivityState:userObject:")
        let newerSelector: Selector = sel_getUid("_elementDidFocus:userIsInteracting:blurPreviousNode:changingActivityState:userObject:")
        let ios13Selector: Selector = sel_getUid("_elementDidFocus:userIsInteracting:blurPreviousNode:activityStateChanges:userObject:")

        if let method = class_getInstanceMethod(WKContentViewClass, olderSelector) {

            let originalImp: IMP = method_getImplementation(method)
            let original: OlderClosureType = unsafeBitCast(originalImp, to: OlderClosureType.self)
            let block : @convention(block) (Any, UnsafeRawPointer, Bool, Bool, Any?) -> Void = { (me, arg0, arg1, arg2, arg3) in
                original(me, olderSelector, arg0, !value, arg2, arg3)
            }
            let imp: IMP = imp_implementationWithBlock(block)
            method_setImplementation(method, imp)
        }

        if let method = class_getInstanceMethod(wkc, newSelector) {
            self.swizzleAutofocusMethod(method, newSelector, value)
        }

        if let method = class_getInstanceMethod(wkc, newerSelector) {
            self.swizzleAutofocusMethod(method, newerSelector, value)
        }

        if let method = class_getInstanceMethod(wkc, ios13Selector) {
            self.swizzleAutofocusMethod(method, ios13Selector, value)
        }
    }

    func swizzleAutofocusMethod(_ method: Method, _ selector: Selector, _ value: Bool) {
        let originalImp: IMP = method_getImplementation(method)
        let original: NewClosureType = unsafeBitCast(originalImp, to: NewClosureType.self)
        let block : @convention(block) (Any, UnsafeRawPointer, Bool, Bool, Bool, Any?) -> Void = { (me, arg0, arg1, arg2, arg3, arg4) in
            original(me, selector, arg0, !value, arg2, arg3, arg4)
        }
        let imp: IMP = imp_implementationWithBlock(block)
        method_setImplementation(method, imp)
   }
jcesarmobile
  • 51,328
  • 11
  • 132
  • 176
  • This method is working in 13.0, but not working 13.6. Any update needed in this method to support 13.6? – Harsh Sharma Aug 27 '20 at 11:51
  • which one are you using, the swift or the objective-c? seems to work fine on both 13.6.1 and 14 beta 6 – jcesarmobile Aug 27 '20 at 14:16
  • I am using objective c. and it is not working for me. I am using the iPad with 13.6 version and iPad with 14 beta. – Harsh Sharma Aug 27 '20 at 15:17
  • Hello jcesarmobile, can you tell the inexperienced Xcode developer where we're supposed to place this code? I am using a basic Cordova project and getting the warning "Cannot find interface declaration for 'WebViewInjection'". Thank you :) – Louis LC Mar 30 '21 at 01:33
  • WebViewInjection is the class name, should match the .m file name where you put that code – jcesarmobile Mar 30 '21 at 10:27
0

P.S for @jcesarmobile answer, the answer is effective at platforms: iPhone SE 1 iOS 14.1,iPhone 6s plus iOS 14.4.2;

And for those who suffering from jumping or size mismatch ,when load the content in the swizzled WKWebview for second time; you can avoid the mess described above with the example bellow:

setTimeout(function(){
    document.getElementById("myTextElementId").focus();
},250);

/* in the mount or onload method;*/

Think: why the SECOND time the mess comes out? might be this way: the webview cached the css styles and files at first time,so the SECOND time the web view load content much faster;and the key board jumps out directly ,meanwhile the window is size fitting,rendering etc; so the mess comes out; and that's why we need a delay;

nice2m
  • 1
  • Did you test this on real device? For me, on simulators, it works well. But for devices, it doesn't work. – WildCat Aug 24 '21 at 07:01
  • the example tested in iPhone SE 1 iOS 14.1,iPhone 6s plus iOS 14.4.2 – nice2m Aug 25 '21 at 08:05
  • please make sure your delay works in h5;tested in: iPhone12mini 14.5,iPhone SE 1 iOS 14.1,iPhone 6s plus iOS 14.4.2 – nice2m Aug 25 '21 at 08:10