iOS下Hybird的实现(一)---UIWebView与WKWebView

最近公司要使用Hybird混合开发,所以就要学习一下JS与Swift的交互,以便之后的工作;据我所知,iOS下JS与原生的交互有很多种具体有:

  • 使用UIWebView与WKWebView的代理方法,在JS 中做一次URL跳转,然后在Swift中拦截跳转
  • 使用WKWebView 的MessageHandler
  • 使用系统库JavaScriptCore,来做相互调用(iOS 7推出的)
  • 使用第三方库WebViewJavascriptBridge
  • 使用第三方cordova库,以前叫PhoneGap(这是一个库平台的库)
  • 使用React Native

本文主要是 写使用UIWebView与WKWebView的代理方法,在JS 中做一次URL跳转,然后在Swift中拦截跳转 的情况

1. 使用的情景

当我们在与JS交互的接口比较少时,就适用这种情况

2. UIWebView的情景

picture.gif

首先,创建UIWebView,并加载本地HTML

    lazy var webView: UIWebView = {[unowned self] in
        let view     = UIWebView(frame: self.view.bounds)
        let htmlURL  = Bundle.main.url(forResource: "anran.html", withExtension: nil)
        let request  = URLRequest(url: htmlURL!)
        let request1 =  URLRequest(url: URL(string: "https://www.baidu.com")!)
        view.scrollView.decelerationRate = UIScrollViewDecelerationRateNormal
        view.loadRequest(request)
        view.scrollView.bounces = false
        view.delegate = self
        return view
    }()

然后,在HTML里,定义按钮,来触发调用原生的方法,然后再将执行结果回调到js 里。

        <input type="button" value="点我点我" onclick="scanClick()" />
        <input type="button" value="获取定位" onclick="locationClick()" />
        <input type="button" value="修改导航栏颜色" onclick="colorClick()" />
        <input type="button" value="分享" onclick="shareClick()" />
        <input type="button" value="支付" onclick="payClick()" />
        <input type="button" value="动次打次" onclick="shake()" />
        <input type="button" value="返回" onclick="goBack()" />
     function loadURL(url) {
         var iFrame;
         iFrame = document.createElement("iframe");
         iFrame.setAttribute("src", url);
         iFrame.setAttribute("style", "display:none;");
         iFrame.setAttribute("height", "0px");
         iFrame.setAttribute("width", "0px");
         iFrame.setAttribute("frameborder", "0");
         document.body.appendChild(iFrame);
          // 发起请求后这个iFrame就没用了,所以把它从dom上移除掉
         iFrame.parentNode.removeChild(iFrame);
         iFrame = null;
     }

1.为什么自定义一个loadURL 方法,不直接使用window.location.href?
答:因为如果当前网页正使用window.location.href加载网页的同时,调用window.location.href去调用OC原生方法,会导致加载网页的操作被取消掉。
同样的,如果连续使用window.location.href执行两次OC原生调用,也有可能导致第一次的操作被取消掉。所以我们使用自定义的loadURL,来避免这个问题。
loadURL的实现来自关于UIWebView和PhoneGap的总结一文。
2.为什么loadURL 中的链接,使用统一的scheme?
答:便于在OC 中做拦截处理,减少在JS中调用一些OC 没有实现的方法时,webView 做跳转。因为我在OC 中拦截URL 时,根据scheme (即haleyAction)来区分是调用原生的方法还是正常的网页跳转。然后根据host(即//后的部分getLocation)来区分执行什么操作。
3.为什么自定义一个asyncAlert方法?
答:因为有的JS调用是需要OC 返回结果到JS的。stringByEvaluatingJavaScriptFromString是一个同步方法,会等待js 方法执行完成,而弹出的alert 也会阻塞界面等待用户响应,所以他们可能会造成死锁。导致alert 卡死界面。如果回调的JS 是一个耗时的操作,那么建议将耗时的操作也放入setTimeout的function 中。

最后,拦截URL也就是自定义的协议,UIWebView 有一个代理方法,可以拦截到每一个链接的Request。return true,webView 就会加载这个链接;return false,webView 就不会加载这个连接,我们就在这个拦截的代理方法中处理自己的URL

    func webView(_ webView: UIWebView, shouldStartLoadWith request: URLRequest, navigationType: UIWebViewNavigationType) -> Bool {
        
        let url = request.url
        let scheme = url?.scheme
        
        if let URL = url, scheme == "anranaction" {
            self.handleCustomAction(url: URL)
            return false
        }
        
        return true
    }

这里通过scheme,来拦截掉自定义的URL 就非常容易了,如果不同的方法使用不同的scheme,那么判断起来就非常的麻烦,看看我们是怎么处理的

    // MARK: - 处理URL然后调用方法
    func handleCustomAction(url: URL) {
        
        let host = url.host
        
        if host == "scanClick" {
        
        } else if host == "shareClick" {
            share(url: url)
        } else if host == "getLocation" {
            getLocation()
        } else if host == "setColor" {
            ChangeColor(url: url)
        } else if host == "payAction" {
            payAction(url: url)
        } else if host == "shake" {
            sharedAction()
        } else if host == "back" {
            goBack()
        }
        
    }

我们在JS中调用Swift 方法的时候,也需要传参数到Swift 中,怎么传呢?
就像一个get 请求一样,把参数放在后面:

    function shareClick() {
        loadURL("haleyAction://shareClick?title=标题&content=内容        &url=http://www.baidu.com");
    }

我们如何获取参数,并且将参数传回JS中,所有的参数都在URL的query中,先通过&将字符串拆分,在通过=把参数拆分成key 和实际的值

    func share(url: URL) {
       
        guard let params = url.query?.components(separatedBy: "&") else { return }
       
        var tempDic = [String:Any]()
        for paramStr in params {
            let dicArray = paramStr.components(separatedBy: "=")
            if dicArray.count > 1 {
                guard let str = dicArray[1].removingPercentEncoding else { return }
                tempDic[dicArray[0]] = str
            }
        }
    
        let title = tempDic["title"]
        let content = tempDic["content"]
        let url = tempDic["url"]

        let jsStr = "shareResult('\(title ?? "")','\(content ?? "")','\(url ?? "")')"
        
        webView.stringByEvaluatingJavaScript(from: jsStr)
    }

Swift调用JS

    let jsStr = "setLocation('\("杭州市拱墅区下沙中国计量学院")')"
    webView.stringByEvaluatingJavaScript(from: jsStr)

Swift中可以往HMTL的JS环境中插入全局变量、JS方法

    func webViewDidFinishLoad(_ webView: UIWebView) {
        print("webView加载完成然后调用")
        webView.stringByEvaluatingJavaScript(from: "var arr = [3, 4, 'abc']")
    }

3. WKWebView的情景

picture1.gif

由于UIWebView比较耗内存,性能上不太好,而苹果在iOS 8中推出了WKWebView。
同样的用WKWebView也可以拦截URL,做JS 与Native交互

安然 打开百度网页前 打开百度网页后
UIWebView 内存47M 内存75.6M,最高峰83M
WKWebView 内存47M 内存51M

尽管WKWebView有很多的优点但是也有很多的缺点,比如他的储存模式,是封闭的,我们要访问也是不容易的,这个问题在以后我专门的学习一下,这篇就不在解释了

WKWebView 与 UIWebView 拦截URL 的处理方式基本一样。除了代理方法和WKWebView的使用不太一样,关于WKWebView更详尽的讲解和用法,还是自行搜索学习,本文重点还是讲解如何实现JS 与Native互相调用

首先, 创建WKWebView,WKWebView的创建有几点不同:

  • 初始化configuration参数,当然这个参数我们也可以不传,直接使用默认的设置就好
  • WKWebView的代理有两个navigationDelegate和UIDelegate。我们要拦截URL,就要通过navigationDelegate的一个代理方法来实现。如果在HTML中要使用alert等弹窗,就必须得实现UIDelegate的相应代理方法
  • 在iOS 9之前,WKWebView加载本地HTML会有一些问题(不能加载本地HTML,或者部分CSS/本地图片加载不了等)
    lazy var webView: WKWebView = {[unowned self] in
        
        let configuration = WKWebViewConfiguration()
        configuration.userContentController = WKUserContentController()
        let preferences = WKPreferences()
        preferences.javaScriptCanOpenWindowsAutomatically = true
        preferences.minimumFontSize = 30.0
        configuration.preferences = preferences
        let view = WKWebView(frame: self.view.frame, configuration: configuration)
        let urlStr = Bundle.main.path(forResource: "anran.html", ofType: nil)
        let fileURL = URL(fileURLWithPath: urlStr!)
        view.loadFileURL(fileURL, allowingReadAccessTo: fileURL)
        view.navigationDelegate = self
        view.uiDelegate = self
        return view
    }()

然后,使用WKNavigationDelegate中的代理方法,拦截自定义的URL来实现JS调用OC方法

  • 如果实现了这个代理方法,就必须得调用decisionHandler这个block,否则会导致app 崩溃。block参数是个枚举类型,WKNavigationActionPolicyCancel代表取消加载,相当于UIWebView的代理方法return false的情况;WKNavigationActionPolicyAllow代表允许加载,相当于UIWebView的代理方法中 return true的情况
func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) {
        let url = navigationAction.request.url
        let scheme = url?.scheme
        
        if let URL = url, scheme == "anranaction" {
            self.handleCustomAction(url: URL)
            // 一定要实现否则会崩溃
            decisionHandler(.cancel)
            return
        }
        decisionHandler(.allow)
    }

最后,JS 调用Native方法后,有的操作可能需要将结果返回给JS。这时候就是Native 调用JS 方法的场景,WKWebView 提供了一个新的方法evaluateJavaScript:completionHandler:实现Native调用JS 等场景

    func getLocation() {
        let jsStr = "setLocation('\("杭州市拱墅区下沙中国计量学院")')"
        webView.evaluateJavaScript(jsStr) { (result, error) in
            print("\(result)")
        }
    }

这个方法evaluateJavaScript(<#T##javaScriptString: String##String#>, completionHandler: <#T##((Any?, Error?) -> Void)?##((Any?, Error?) -> Void)?##(Any?, Error?) -> Void#>)没有返回值,JS 执行成功还是失败会在completionHandler 中返回。所以使用这个API 就可以避免执行耗时的JS,或者alert 导致界面卡住的问题

WKWebView中使用弹窗
在上面提到,如果在WKWebView中使用alert、confirm 等弹窗,就得实现WKWebView的WKUIDelegate中相应的代理方法。
如果,我在JS中要显示alert 弹窗,就必须实现如下代理方法,否则alert 并不会弹出

    func webView(_ webView: WKWebView, runJavaScriptAlertPanelWithMessage message: String, initiatedByFrame frame: WKFrameInfo, completionHandler: @escaping () -> Void) {
        
        let alert = UIAlertController(title: "提醒", message: message, preferredStyle: .alert)
        alert.addAction(UIAlertAction(title: "知道", style: .cancel, handler: { (action) in
            completionHandler()
        }))
        
        self.present(alert, animated: true, completion: nil)
    }

其中completionHandler这个block 必须调用

4. 总结这只是简单的实用,本文已用了大神的HTML与方法,在Swift中实现请看源码,要是觉得不错请给个星星

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,547评论 6 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,399评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,428评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,599评论 1 274
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,612评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,577评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,941评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,603评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,852评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,605评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,693评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,375评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,955评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,936评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,172评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,970评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,414评论 2 342

推荐阅读更多精彩内容