WKWebView定制

设置Cookie

WKWebView load cookie 与UIWebView的方式不同,UIWebView 使用的是Share Cookie Storage( URLSession 中的也是这个,App唯一), WKWebView在HTTP中没有Cookie时加载share cookie. WKWebView自定义Cookie办法

guard let u = URL(string: url) else { return }
var request = URLRequest(url: url)
request.setValue("token = 'xxxx'", forHTTPHeaderField: "Cookie")
wkwebView.load(request)

类似的还可以自定义HTTP的UserAgent

长按和双击菜单

显示事件

长按菜单属于 UIMenuController 的功能, 对于普通的控件, 通过片设置 share.menuItems控制内容, 使用 setTargetRectsetMenuVisible 控制位置和显示

如果是WKWebView, 需要做以下几点

  • 屏蔽Web默认的弹窗, 如图片, link等

    public func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
      self.webView.evaluateJavaScript("document.documentElement.style.webkitTouchCallout='none';", completionHandler: nil)
    }
    
  • 添加长按事件, 双击事件, 在事件中修改内容

    func menusgestur(gestureRecognizer: UIGestureRecognizer) {
      if gestureRecognizer.state == UIGestureRecognizerState.ended {
          UIMenuController.shared.menuItems = menuItems
          // 如果选中有内容会自动显示, 无需手动设置rect和visiable
      }
    }
    
  • 此外需要对WKWeb的手势做处理, 长按和双击的手势delegate = UIGestureRecognizerDelegate

    public func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
      return true
    }
    

控制 menuItems

  • 关于系统默认按钮选项的屏蔽, 通过override canPerformAction 来控制允许出现的选项

    open override func canPerformAction(_ action: Selector, withSender sender: Any?) -> Bool {
        return self.whiteList.contains(action)
    }
    
  • 需要注意的是, WKWebView真正显示内容的是不可直接访问的WKContentView, 在就版本系统中, 需要对它的canPerformAction也做相同的“override” 首先获取这个contentView 如下

    var targetView: UIView? = nil
    for view in self.scrollView.subviews {
      if String(describing: type(of: view)).hasPrefix("WKContentView") {
          targetView = view
      }
    }
    return targetView
    
  • 然后通过运行时, 把WKWebView的canPerformAction替换它的
    let targetClass: AnyClass = type(of: target)
      let selectors = [
      #selector(WKWebView.canPerformAction(_:withSender:))
    ]
    selectors.forEach { (selector) in
      guard let method = class_getInstanceMethod(WKWebView.self, selector) else { return }
      class_replaceMethod(targetClass.self,
                          selector,
                          method_getImplementation(method),
                          method_getTypeEncoding(method))
    }
    object_setClass(target, targetClass.self)
    
    注意, 由于是运行时的, 所以canPerformAction中如果使用了变量, 需要注意这个变量在WKContentView中是否能访问, 否则会crash,
    建议的解决办法是给他们的共同父类“扩展存储属性”, 分别各自访问自己的变量.

动态UIMenuItem事件

因为UIMenuItem的target的参数是UIMenucontroller而非menuItem本身, 所以无法区分具体点击的是哪一个item, 解决办法是使用NSStringFromSelector创建selector并把参数通过固定的协议放到selector中,

let selector = NSSelectorFromString(prefix + info.script)
let item = UIMenuItem(title: "xxx", action: selector)

由于没有实现改selector, 点击会crash, 隐私需要在WKWebView事件转发中捕捉到改事件, 并从selector中解析出参数, 然后动态创建一个类,实现改selector, 并把解析出来的参数和需要做的事情给它

extension WKWebView {
    // forward context menu sector to one sector
    override open func forwardingTarget(for aSelector: Selector!) -> Any? {
        if NSStringFromSelector(aSelector).hasPrefix(xxx) {
            var cls: AnyClass? = NSClassFromString("Protector")
            if cls == nil, let newCls = objc_allocateClassPair(NSObject.self, "Protector", 0) {
                objc_registerClassPair(newCls)
                cls = newCls
            }
            if let cls = cls, existSelector(selector: aSelector, cls: cls) == false {
                class_addMethod(cls,
                                aSelector,
                                WKWebView.safeImplementation(aSelector: aSelector),
                                UnsafeMutablePointer(mutating: "v"))
            }
            if let cls = cls as? NSObject.Type  {
                return cls.init()
            }
        }
        return super.forwardingTarget(for: aSelector)
    }
    //...
    private class func safeImplementation(aSelector: Selector) -> IMP {
        let block = { () -> Swift.Void in
            // do some thing with selector
        }
        let castedBlock: AnyObject = unsafeBitCast(block as @convention(block) () -> Swift.Void, to: AnyObject.self)
        return imp_implementationWithBlock(castedBlock)
    }
}

同样的, 改事件转达需要用前面相同的办法“设置给” WKContentView

方法二, 在创建UIMenuItem的时候,给所有将要接收它的事件的对象动态添加action, 这里是WKWebView和WKContentView

let targetClasses: [AnyClass] = [
    type(of: (self.webView as WKWebView)),
    type(of: wkContentView)
]
let aSelector = NSSelectorFromString("xxxx_prifix" + info.id)
let block = { () -> Swift.Void in
    // item action
}
let castedBlock: AnyObject = unsafeBitCast(block as @convention(block) () -> Swift.Void, to: AnyObject.self)
let imp = imp_implementationWithBlock(castedBlock)
targetClasses.forEach({ (targetClass) in
if class_addMethod(targetClass,
                   aSelector,
                   imp,
                   UnsafeMutablePointer(mutating: "v")) {
} else {
    class_replaceMethod(targetClass,
                        aSelector,
                        imp,
                        UnsafeMutablePointer(mutating: "v"))
}
let item = UIMenuItem(title: "xxxx", action: aSelector)

允许手动弹起键盘

参考

键盘工具条

WKWebView的键盘工具条核心在于 inputAccessoryView , 同样要使用前面的方法, 将WKContextView的inputAccessoryView“设置“成同一个

open override var inputAccessoryView: UIView? {
    return customView
}

customView 可以是任意View, 可以使用ScrollView和button实现一个工具条

更新customView后分别调用WKWebView和WKContentView的reloadInputViews更新

支持WebP

拦截请求

wkwebView 在iOS 11以后才有接口自定义URLProtocol 但较低版本还没有, 如果要支持,就需要做一些特殊“操作”

extension URLProtocol {
    fileprivate class func wk_browsing_contextController() -> (NSObject.Type)? {
        guard let obj =  WKWebView().value(forKey: "browsingCo"+"ntextController") else { return nil }
        return type(of: obj) as? NSObject.Type
    }
    fileprivate static let register = NSSelectorFromString("registerSchemeFo"+"rCustomProtocol:")
    fileprivate static let unregister = NSSelectorFromString("unregisterSchemeFo"+"rCustomProtocol:")
    class func wk_register(schemes: [String]) {
        if let obj = URLProtocol.wk_browsing_contextController(),
            obj.responds(to: URLProtocol.register) {
            schemes.forEach({ (scheme) in
                obj.perform(URLProtocol.register, with: scheme)
            })
        }
    }
    class func wk_unregister(schemes: [String]) {
        if let obj = URLProtocol.wk_browsing_contextController(),
            obj.responds(to: URLProtocol.unregister) {
            schemes.forEach({ (scheme) in
                obj.perform(URLProtocol.unregister, with: scheme)
            })
        }
    }
}

参考

其中为绕过苹果审核问题, 字符串做了简单的处理

注意⚠️, 不能注册http 或https协议, 否则post请求的body会丢失, 这是个bug, 可以使用自定义scheme, 拦截后再处理

URLProtocol.wk_register(schemes: ["webp"])

然后注册自定义协议

URLProtocol.registerClass(WebPURLProtocol.self)

关于URLProtocol, 要点入下

  • 过滤请求, 所有的请求都会走canInit, 需要自己处理的return true, 注意过滤自定义的请求
  • 处理请求
  • 请求结果处理
class WebPURLProtocol: URLProtocol {
    private static let handledKey = "WebPURLProtocol_handleKey"
    override open class func canInit(with request: URLRequest) -> Bool {
        if let handled = URLProtocol.property(forKey: handledKey, in: request) as? Bool, handled { return false }
        if let scheme = request.url?.scheme, scheme.lowercased() == "webp" {
            return true
        }
        return false
    }
    override open class func canonicalRequest(for request: URLRequest) -> URLRequest {
        return request
    }
    var customTask: URLSessionDataTask?
    override func startLoading() {
        guard var url = self.request.url?.absoluteString else { return }
        url.removeFirst(4)
        url = "http" + url
        guard let u = URL(string: url) else { return }
        let request = NSMutableURLRequest(url: u)
        URLProtocol.setProperty(true, forKey: WebPURLProtocol.handledKey, in: request)
        self.customTask = URLSession.shared.dataTask(with: request as URLRequest, completionHandler: { [weak self] (data, response, error) in
            guard let strongSelf = self else { return }
            if let error = error {
                self?.client?.urlProtocol(strongSelf, didFailWithError: error)
                return
            }
            guard let response = response, let data = data else {
                self?.client?.urlProtocol(strongSelf,
                                          didFailWithError: NSError(domain: "unkonw response and data",
                                                                    code: -1,
                                                                    userInfo: nil) as Error)
                return
            }
            self?.client?.urlProtocol(strongSelf, didReceive: response, cacheStoragePolicy: .allowed)
            self?.client?.urlProtocol(strongSelf, didLoad: data)
            self?.client?.urlProtocolDidFinishLoading(strongSelf)
        })
        self.customTask?.resume()
    }
    override func stopLoading() {
        self.customTask?.cancel()
    }
}

处理WebP

首先是从Google获取libwebp
接下来需要用到LLVM的module.modulemap 方便在Swift工程中使用C

module libwebp[system] {
    header "libwebp/src/webp/decode.h"
    header "libwebp/src/webp/encode.h"
    header "libwebp/src/webp/types.h"
    export *
}

对CocoaPod, 需要添加C源码和swift include path头文件并配置

spec.resource     = [
   'Project/*.lproj/*.strings',
   'Project/SupportFiles/*', 
   'path_to/module.modulemap'
]
spec.source_files = [
   'Classes/**/{*.swift}', 
   'Project/Project.h',
   'libwebp/src/**/{*.h,*.c}',
]
spec.xcconfig     = {
   'SWIFT_INCLUDE_PATHS'      => '$(inherited) $(SRCROOT)/path_to_module.modulemap',
}

对于Xcode工程和支持Carthage, 配置Xcode 的project -> build setting ->
'SWIFT_INCLUDE_PATHS' => '$(inherited) $(SRCROOT)/path_to/module.modulemap'

更多modulemap的使用参考这里

然后就可以import libwebp模块写自己的功能, 如将WebP转成PNG, UIImage等

import libwebp
import UIKit
extension UIImage {
    class func image(webp: Data) -> UIImage? {
        let dataP = webp.bytes.assumingMemoryBound(to: UInt8.self)
        var config = WebPDecoderConfig.init()
        guard WebPInitDecoderConfig(&config) > 0  else { return nil }
        guard WebPGetFeatures(dataP, webp.length, &config.input) == VP8_STATUS_OK else { return nil }
        config.output.colorspace = config.input.has_alpha != 0 ? MODE_rgbA : MODE_RGB
        config.options.use_threads = 1
        var width: Int32 = config.input.width
        var height: Int32 = config.input.height
        if config.options.use_scaling != 0 {
            width = config.options.scaled_width
            height = config.options.scaled_height
        }
        guard WebPDecode(webp.bytes.assumingMemoryBound(to: UInt8.self), webp.length, &config) == VP8_STATUS_OK else { return nil }
        guard let provider = CGDataProvider(dataInfo: nil,
                                            data: config.output.u.RGBA.rgba,
                                            size: config.output.u.RGBA.size,
                                            releaseData: { (_, data, _) in
                                                free(UnsafeMutableRawPointer(mutating: data))
        }) else { return nil }
        let components: size_t = config.input.has_alpha != 0 ? 4 : 3
        guard let imageRef = CGImage.init(width: Int(width),
                                          height: Int(height),
                                          bitsPerComponent: 8,
                                          bitsPerPixel: components * 8,
                                          bytesPerRow: components * Int(width),
                                          space: CGColorSpaceCreateDeviceRGB(),
                                          bitmapInfo: config.input.has_alpha != 0 ? [.byteOrder32Big, CGBitmapInfo(rawValue: CGImageAlphaInfo.premultipliedLast.rawValue)] : CGBitmapInfo(rawValue: 0),
                                          provider: provider,
                                          decode: nil,
                                          shouldInterpolate: true,
                                          intent: CGColorRenderingIntent.defaultIntent) else { return nil }
        let image = UIImage.init(cgImage: imageRef)
        return image
    }
}

参考

startLoading 的请求回调中将WebP data转成PNG data, WKWebView既可以显示了

如果成功了, 打开本页面, 会在下方看见一张scheme 为webp的图

webp scheme
原图链接

results matching ""

    No results matching ""