Removing WKWebView Accesory bar in Swift
Asked Answered
H

4

11

I am trying for a few days now to get this converted into Swift without really having much background with it.

This is what I've got so far ... and I have been looking on google not really knowing what to search for in order to be more specific. Can you please shed some light on what I'm doing wrong ? Thanks

Update:

I have aded the objective-c tag just so more people that are related to this thread may be able to see it and hopefully get an answer.

enter image description here

Homozygote answered 22/11, 2015 at 10:47 Comment(1)
How did you solve this in detail please?Lanell
S
21

For those who are still looking, the WebKit team updated WKWebView (iOS 13+) so that you can subclass it to remove/update the input accessory view:

https://trac.webkit.org/changeset/246229/webkit#file1

In Swift, I subclassed it, and returned nil. Worked as expected. I hope it helps.

FYI: I checked the docs, and it doesn't mention not to subclass WKWebView, so subclassing is allowed.

import WebKit

class RichEditorWebView: WKWebView {

    var accessoryView: UIView?

    override var inputAccessoryView: UIView? {
        // remove/replace the default accessory view
        return accessoryView
    }

}

You can find a working version of it here: https://github.com/cbess/RichEditorView/commits/master

Scleroprotein answered 18/9, 2019 at 22:16 Comment(2)
Thank you so much for doing the Lord's working porting RichEditorView to WKWebView... saved me a ton of time!!!Possum
Brilliant. My app was crashing any time I tapped on a text input in my webview. It turned out that the problem had to do with trying to display the accessory bar. In this case, the accessory bar would look like an up arrow, a down arrow, and the word 'Done', trying to help a user navigate a form. This solution stopped my app from crashing.Agave
S
9

Michael Dautermann answer has got everything right, but in order to hide the accessory bar you need to swizzle the method inputAccessoryView() of UIView Class with the inputAccessoryView() of the _NoInputAccessoryView class. I have just added the couple of extra lines to the code which does this job of method swizzling.

First you'll need a fake class to swap with

final class FauxBarHelper: NSObject {
    var inputAccessoryView: AnyObject? { return nil }
}

Then create this method in your controller class

/// Removes the keyboard accessory view from the web view
/// Source: https://mcmap.net/q/1013236/-hiding-keyboard-accessorybar-in-wkwebview / https://mcmap.net/q/956529/-removing-wkwebview-accesory-bar-in-swift
func _removeInputAccessoryView(webView: UIWebView) {
    var targetView: UIView? = nil

    for view in webView.scrollView.subviews {
        if String(describing: type(of: view)).hasPrefix("WKContent") {
            targetView = view
        }
    }

    guard let target = targetView else { return }

    let noInputAccessoryViewClassName = "\(target.superclass!)_NoInputAccessoryView"
    var newClass: AnyClass? = NSClassFromString(noInputAccessoryViewClassName)
    if newClass == nil {
        let targetClass: AnyClass = object_getClass(target)
        newClass = objc_allocateClassPair(targetClass, noInputAccessoryViewClassName.cString(using: String.Encoding.ascii)!, 0)
    }

    let originalMethod = class_getInstanceMethod(FauxBarHelper.self, #selector(getter: FauxBarHelper.inputAccessoryView))
    class_addMethod(newClass!.self, #selector(getter: FauxBarHelper.inputAccessoryView), method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod))
    object_setClass(target, newClass)
}

HTH ;)

Saltus answered 26/11, 2015 at 13:9 Comment(6)
@Lanell Thanks for suggestion, I'd try to write one on Medium, I will keep you posted :)Saltus
@iWasRobbed thanks for updating the answer to Swift 3.Saltus
Any update on this? Can't seem to get it to work running the latest version of Swift and iOS.Weddle
Small note: I hit an issue with this approach when using dictation. Got a weird crash from UIDictationController. The newClass isn't visible to Obj-C, adding objc_registerClassPair(newClass) right after objc_allocateClassPair(…) call seems to fix the issue.Dialectology
Like @Dialectology I observed a crash when using the above code with voice dictation. I recommend making his suggested changes.Carbamate
@Dialectology Thanks so much for that comment, I ran into the same issue and it took me a day to find out that it was related to the accessory view, then I was stuck. Your fix works perfectly!Webfooted
M
9

Here's a slightly safer (no unsafe unwraps) version that works with Swift 4 and (at least) iOS 9 trough 12.

fileprivate final class InputAccessoryHackHelper: NSObject {
    @objc var inputAccessoryView: AnyObject? { return nil }
}

extension WKWebView {
    func hack_removeInputAccessory() {
        guard let target = scrollView.subviews.first(where: {
            String(describing: type(of: $0)).hasPrefix("WKContent")
        }), let superclass = target.superclass else {
            return
        }

        let noInputAccessoryViewClassName = "\(superclass)_NoInputAccessoryView"
        var newClass: AnyClass? = NSClassFromString(noInputAccessoryViewClassName)

        if newClass == nil, let targetClass = object_getClass(target), let classNameCString = noInputAccessoryViewClassName.cString(using: .ascii) {
            newClass = objc_allocateClassPair(targetClass, classNameCString, 0)

            if let newClass = newClass {
                objc_registerClassPair(newClass)
            }
        }

        guard let noInputAccessoryClass = newClass, let originalMethod = class_getInstanceMethod(InputAccessoryHackHelper.self, #selector(getter: InputAccessoryHackHelper.inputAccessoryView)) else {
            return
        }
        class_addMethod(noInputAccessoryClass.self, #selector(getter: InputAccessoryHackHelper.inputAccessoryView), method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod))
        object_setClass(target, noInputAccessoryClass)
    }
}
Mclean answered 29/11, 2017 at 12:35 Comment(3)
Works like a charm. Thanks.Payment
When and where to call this ?Hornbill
@JamshedAlam pretty much anywhere and when you want, I do it when setting up other properties (tintColor etc)Mclean
N
3

This code snippet should get you over your issue:

class _NoInputAccessoryView: NSObject {

    func removeInputAccessoryViewFromWKWebView(webView: WKWebView) {
        // make sure to make UIView an optional here...
        var targetView: UIView? = nil
        for view in webView.scrollView.subviews {
            if String(view.dynamicType).hasPrefix("WKContent") {
                targetView = view
            }
        }

        // only optionals can be nil
        if targetView == nil {
            return
        }

        let noInputAccessoryViewClassName = "\(targetView!.superclass)_NoInputAccessoryView"
        var newClass : AnyObject? = NSClassFromString(noInputAccessoryViewClassName)
        if newClass == nil {
            let uiViewClass : AnyClass = object_getClass(targetView!)
            newClass = objc_allocateClassPair(uiViewClass, noInputAccessoryViewClassName.cStringUsingEncoding(NSASCIIStringEncoding)!, 0)
        }
    } 

You can also use "String(view.dynamicType)" to get the class name of the object you're looking at, as I noticed via this answer as I was researching the way to solve your problem.

Using hasPrefix like that in both Objective-C and Swift is really hacky and perhaps a better way of hiding the keyboard could be found for production code?

Numskull answered 22/11, 2015 at 11:7 Comment(12)
I have done the changes and a small fix which I edited your post with but now i get this. I've tried adding newClass: AnyClass? = NSClassFromString which solves the warning and the newClass == nil comparison but then it leaves me with the third error, Cannot convert value of type UIView to expected type AnyClass!Homozygote
I've made an adjustment to my code... give it a try now.Numskull
one more stupid question, how do I apply this class on top of the webview object ? I'm just new and stupid.. sorryHomozygote
it's not a stupid question and neither are you. If you look at the original Objective-C source code you're porting, the author made his "removeInputAccessoryViewFromWKWebView" function a part of the view controller. You're doing yours as a standalone class deriving from NSObject. Assuming you're calling this from a pure Swift view controller, you'd need to do something like "let navHelper = _NoInputAccessoryView.init()" and "navHelper. removeInputAccessoryViewFromWKWebView(passInWebViewHere)". Makes sense?Numskull
Also, you really should change the name of that class from "_NoInputAccessoryView" to something that doesn't start with an underscore (which is a style thing usually reserved for instance variables).Numskull
Yea, it makes sense and I've done it. Check this picture as it runs with no error, but... unfortunately it still shows me accesorry bar. printscreen here . It just makes me sad after spending so many days figuring out a solution, and hitting a dead end. damnHomozygote
When I hit problems like this, I usually just use the Xcode debugger to step through the code and see if anything is unexpectedly nil (e.g. see if targetView or newClass are actually populated and used). You'll get the hang of things, eventually.Numskull
I will mark this as solved as it helped me finish the conversion to swift. I'll try some debugging.. but , do tell me, is it ok as I applied it ? Thanks for the time you spent helping meHomozygote
It looks good to me so far. We are likely just missing something very small.Numskull
Won't Apple reject this on App Store?Asternal
@Homozygote did you ever figure this out? Nothing seems to work.Weddle
@Matt Unfortunately no. I've quit trying. The solutions from above work in some extenr but it feels like a crappy experience, nothing smooth or anything. Went on, learned ReactJS, then moved to React Native :)Homozygote

© 2022 - 2024 — McMap. All rights reserved.