欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  移动技术

iOS9中的WebKit 与 Safari带来的惊喜

程序员文章站 2022-05-27 09:09:50
每个用过 uiwebview 的ios开发者对其诸多的限制和有限的功能也深有感触。悻然,自ios8推出 webkit 框架后将改变这一窘境。在本文我将会深入webkit来体...

每个用过 uiwebview 的ios开发者对其诸多的限制和有限的功能也深有感触。悻然,自ios8推出 webkit 框架后将改变这一窘境。在本文我将会深入webkit来体验一下它给我们带来的好处,同时也看看在ios9中新加入的 sfsafariviewcontroller 有些什么新的惊喜。

通用的浏览行为

所谓的通用浏览行为主要可以归纳为以下的几种:

网页载入进度
前进
后退
刷新

如果每个用到 webview 的 app都要做一个专用的controller也挺麻烦的,我以前就直接采用其它第三方写好的包来完成。

但现在,如果用 wkwebview 将变得很方便,以代码说话吧:

class viewcontroller: uiviewcontroller {

  var webview: wkwebview!
  @iboutlet weak var progressview: uiprogressview!
    
  required init(coder adecoder: nscoder) {
    super.init(coder: adecoder)!
    
    // 实例化 wkwebview
    self.webview = wkwebview(frame: cgrectzero)
  }
  
 
  override func viewdidload() {
    super.viewdidload()

    // 编程式加入 wkwebview
    view.addsubview(webview)
    view.insertsubview(webview, abovesubview: progressview)
    webview.translatesautoresizingmaskintoconstraints = false
    
    let widthconstraint = nslayoutconstraint(item:webview, attribute: .width, relatedby: .equal, toitem: view, attribute: .width, multiplier: 1 , constant: 0)
    view.addconstraint(widthconstraint)
    
    let heightconstraint = nslayoutconstraint(item:webview,attribute: .height, relatedby: .equal,toitem: view, attribute: .height, multiplier:1, constant: -46)
    
    view.addconstraint(heightconstraint)
    
    // 检测webview对象属性的变化
    webview.addobserver(self, forkeypath: "loading", options: .new, context: nil)
    webview.addobserver(self, forkeypath: "title", options: .new, context: nil)
    
    //加载网页

    let request = nsurlrequest(url: nsurl(string: "http://ray.dotnetage.com")!)
    
    webview.loadrequest(request)
    
  }


  override func observevalueforkeypath(keypath: string?, ofobject object: anyobject?, change: [string : anyobject]?, context: unsafemutablepointer<void>) {
  
    if (keypath == "loading") {

      // 检测按钮的可用性
      forwardbutton.enabled = webview.cangoback
      backbutton.enabled = webview.cangoback
      stopbutton.image = webview.loading ? uiimage(name: "cross") : uiimage(named: "syncing")

    } else if keypath == "title" {

      title = webview.title

    } else if keypath == "estimatedprogress" {

      progressview.hidden = webview.estimatedprogress == 1
      progressview.setprogress(float(webview.estimatedprogress), animated: true)

    }
  }
}

这些代码我觉得没什么好说的,除了wkwebview不能通过 ib 来可视化构建外,以上的代码最多是将 autolayout 部分的代码优化一下就是了。写一写,做个 example 就懂了。

与 javascript 通信

通过webkit就不需要通过 javascript 桥的方式来与dom通信了。其实这也不是什么新技术,早再 windows98 在vb或者在delphi中也可以通过com接口用完全相类似的手法与dom通信了。

废话不多说,讲讲 webkit 的基本原理吧。以下是 webkit host 的web进程 与 app 主进程的通信关系示意图:

这里包含两个过程

执行 javascript 脚本

我们可以将 javascript 脚本包含于 app 的 bundle 内,作为应用程序资源。在运行期将其通过 webkit 注入至目标网页内执行。

首先我们要准备一个目标网页,这里就以我自己的博客来做一个示例( app 中用webkit打开是这样的

iOS9中的WebKit 与 Safari带来的惊喜

现在,我就将我博客上首页的大标题的文字改掉,具体的代码很简单:

$(".page-header h1").text("ios注入测试");

然后,在 ios项目内增加一个叫 inject.js 的脚本文件,将上述代码复制其内。

在 app 内包含的 javascript 脚本最好先在浏览器的控制台内执行一次,以确保脚本自身是可以被正确执行的。如果脚本中含有潜在错误,在app内是无法检测得到的。

然后,在控制器的构造函数内创建一个 wkwebviewconfiguration 实例,并作为参数传入 wkwebview的构造函数,具体代码如下:

// viewcontroller.swift

import webkit

class viewcontroller : uiviewcontroller {

	var webview: wkwebview!

	required init(coder adecoder: nscoder) {
	  super.init(coder: adecoder)!
	  

	  let configuation = wkwebviewconfiguration()

	  configuation.usercontentcontroller.adduserscript(getuserscript("inject"))
	  
	  self.webview = wkwebview(frame: cgrectzero,configuration: configuation)
	}

  // 从资源中读取 javascript 脚本
  func getuserscript(fromname: string)-> wkuserscript {
    let filepath = nsbundle.mainbundle().pathforresource(fromname, oftype: "js")
    let js = try! string(contentsoffile: filepath!, encoding: nsutf8stringencoding)
    return wkuserscript(source: js, injectiontime: .atdocumentend, formainframeonly: true)
  }
	...
}

此代码段中需要注意的另一点是在自定义方法 getuserscript() 所返回的 wkuserscript 对象。我们可以通过 injectiontime 决定将脚本注入至html的开始部分还是在文档的尾部。

再次执行代码,效果如下:

iOS9中的WebKit 与 Safari带来的惊喜

也就是说我们可以在 app 内通过 webkit 注入javascript后就可以任意地操控页面内的所有 dom 对象!

javascript 的回调

除了从 app 一端将代码注入到浏览器,执行一个动作。某些情况下我们还需要从网页上做某一些处理后,例如将网页内的某些元素读出并转为一个 json 对象集合,回传给 app 处理。又或者我们的 app 在加载一个网页之后想一次性地读出页面内的所有图像,当用户点击这些图像的时候我们用 app 的本地方式来全屏预览,诸如此类。在这些语境下:

我们都得从网页内返回对象

也就是说,在网页的进程内要向 app 进程通信,那么我们就需要在脚本中使用:

webkit.messagehandlers.{messagename}.postmessage([params]);

这个方法在标准的html5浏览器是不能直接执行的,例如 chrome和 safair。只有通过 webkit host 的页面才会出现这个 webkit 对象。 这并不难理解,只是 webkit 在加载页面后向 windows 注入了 webkit 这个实例,使得 javascript 可以通过它来向 app 发送信息。

如果我们要向 app 发送一个信息,例如:在页面上的一个按钮被点击后,执行 app 内打开相册的代码,那么就得先在 javascript 上写好这样的代码:

$("#mybutton").click(function(){
  webkit.messagehandlers.openphotolibrary.postmessage();
});

请留意 openphotolibrary 这个对象在swift是没有,当这个方法被回传到 swift 的时候这只是一个消息的名字,而在swift中要接收这种来至于浏览器发送的信息我们的控制器就需要实现 wkscriptmessagehandler 这个接口,它只有一个方法,我们多花些篇幅直接将这个接口的代码打开:

/*! a class conforming to the wkscriptmessagehandler protocol provides a
 method for receiving messages from javascript running in a webpage.
 */
public protocol wkscriptmessagehandler : nsobjectprotocol {
  
  /*! @abstract invoked when a script message is received from a webpage.
   @param usercontentcontroller the user content controller invoking the
   delegate method.
   @param message the script message received.
   */
  @available(ios 8.0, *)
  public func usercontentcontroller(usercontentcontroller: wkusercontentcontroller, didreceivescriptmessage message: wkscriptmessage)
}

那么,我们就直接实现这个接口:

class viewcontroller: uiviewcontroller, wkscriptmessagehandler {


  required init(coder adecoder: nscoder) {

    // ... 之前的代码同上

    configuation.usercontentcontroller.addscriptmessagehandler(self, name: "openphotolibrary")

    self.webview = wkwebview(frame: cgrectzero,configuration: configuation)
  }

  ...


  func usercontentcontroller(usercontentcontroller: wkusercontentcontroller, didreceivescriptmessage message: wkscriptmessage) {
    if message.name == "openphotolibrary" {
      // 这里就可以加入打开相册的代码了
    }
  }
}

从代码就可以看出原理的一二:

在构造 wkwebview 之前要用 addscriptmessagehandler 方法向配置对象注册一个消息名,这里的例程是 "openphotolibrary"。
实现 wkscriptmessagehandler 接口,从 usercontentcontroller() 方法的 message.name 参数中判断消息的源头,执行对应的代码。

另外,如果我们需要从javascript脚本中向 app 传入对象,可以直接在 postmessage() 方法内将对象作为参数输入,但通常这个参数的类型应该是一个数组或者是普通的json对象,这样在 app 才能用字典对象将其从新读出。

例如,我从当前网页中将所有的菜单的地址和名称读出,并生成了一个 menus 的 javascript 数组对象:

var menus = $(".navbar a").map(function(n,i){
  return {
    title: $(n).text,
    link: $(n).attr("href")
  };
});

webkit.messagehandlers.didfetchmenus.postmessage(menus);

这里就略过接口实现,直接看 usercontentcontroller 方法实现:

var menus: [menus]?

func usercontentcontroller(usercontentcontroller: wkusercontentcontroller, didreceivescriptmessage message: wkscriptmessage) {
  if message.name == "didfetchmenus" {
    if let resultarray = message.body as? [dictionary<string,string>] {
      menus = resultarray.map{ menu(dict: $0) }

      // 这里就取出并将json转换为 swift 的menu对象了
      print(menus)
    }
  }
}

ios9 中的 safair 浏览器

在 ios9 中加入了 safariservices 这个新的模块,其作用就是提供了一个全功能的内嵌式 safair,通过
sfsafariviewcontroller 就能像普通的 控制器那样使用。

以下是一个简单的例子

import uikit
import safariservices

class viewcontroller: uiviewcontroller {
  @ibaction func openbrowser(sender: anyobject) {
    let safari = sfsafariviewcontroller(url:nsurl(string:"http://www.apple.com")!)
      
    self.showviewcontroller(safari, sender: self)
  }
}

sfsafariviewcontroller 和 webkit 的最大区别是 sfsafariviewcontroller 没有什么可控制方法,只是一个可以完全嵌入到 app 中的一个控制器,避免了像以前那样如果打开一个外部链接要跳出当前的app,而且 sfsafariviewcontroller 嵌入的 safari 和 safari 内的所有功能是一样的,同样支持 3d touch 和切页的等特色功能。且当我们的 app 采用外部网络帐号进行集成登录时,safari 能更直接获取到当前 app的应用上下文,而无须再跳出重新在外部登入后再返回至app。这无疑是大大地增强了 app 在与 safari 集成的时的使用体验。

在 apple 的开发者网站上对 webkit 与 safariservices 的选择上给出了这样的意见:

如果需要与网页交互则选择 webkit
如果需要与safari具有同样的使用体验且不需要与网页交互推荐使用 safariservices

这确实是一项很不错的更新。