热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

iOS9中的WebKit与Safari带来的惊喜

iOS9中的WebKit与Safari带来的惊喜-每个用过UIWebView的iOS开发者对其诸多的限制和有限的功能也深有感触。悻然,自iOS8推出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) {
  
    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 注入至目标网页内执行。

首先我们要准备一个目标网页,这里就以我自己的博客来做一个示例(http://ray.dotnetage.com)。在 App 中用WebKit打开是这样的

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

$(".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的开始部分还是在文档的尾部。

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

也就是说我们可以在 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] {
      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

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


推荐阅读
  • 本文详细介绍了如何在Linux系统上安装和配置Smokeping,以实现对网络链路质量的实时监控。通过详细的步骤和必要的依赖包安装,确保用户能够顺利完成部署并优化其网络性能监控。 ... [详细]
  • 在前两篇文章中,我们探讨了 ControllerDescriptor 和 ActionDescriptor 这两个描述对象,分别对应控制器和操作方法。本文将基于 MVC3 源码进一步分析 ParameterDescriptor,即用于描述 Action 方法参数的对象,并详细介绍其工作原理。 ... [详细]
  • 自己用过的一些比较有用的css3新属性【HTML】
    web前端|html教程自己用过的一些比较用的css3新属性web前端-html教程css3刚推出不久,虽然大多数的css3属性在很多流行的浏览器中不支持,但我个人觉得还是要尽量开 ... [详细]
  • ElasticSearch 集群监控与优化
    本文详细介绍了如何有效地监控 ElasticSearch 集群,涵盖了关键性能指标、集群健康状况、统计信息以及内存和垃圾回收的监控方法。 ... [详细]
  • 本文详细介绍了如何使用 HTML 和 CSS 对文件上传按钮进行样式美化,使用户界面更加友好和美观。 ... [详细]
  • 优化ListView性能
    本文深入探讨了如何通过多种技术手段优化ListView的性能,包括视图复用、ViewHolder模式、分批加载数据、图片优化及内存管理等。这些方法能够显著提升应用的响应速度和用户体验。 ... [详细]
  • C++实现经典排序算法
    本文详细介绍了七种经典的排序算法及其性能分析。每种算法的平均、最坏和最好情况的时间复杂度、辅助空间需求以及稳定性都被列出,帮助读者全面了解这些排序方法的特点。 ... [详细]
  • 本文基于刘洪波老师的《英文词根词缀精讲》,深入探讨了多个重要词根词缀的起源及其相关词汇,帮助读者更好地理解和记忆英语单词。 ... [详细]
  • 本文介绍如何通过Windows批处理脚本定期检查并重启Java应用程序,确保其持续稳定运行。脚本每30分钟检查一次,并在需要时重启Java程序。同时,它会将任务结果发送到Redis。 ... [详细]
  • 本章将深入探讨移动 UI 设计的核心原则,帮助开发者构建简洁、高效且用户友好的界面。通过学习设计规则和用户体验优化技巧,您将能够创建出既美观又实用的移动应用。 ... [详细]
  • 扫描线三巨头 hdu1928hdu 1255  hdu 1542 [POJ 1151]
    学习链接:http:blog.csdn.netlwt36articledetails48908031学习扫描线主要学习的是一种扫描的思想,后期可以求解很 ... [详细]
  • SDN网络拓扑发现机制解析
    本文深入探讨了SDN(软件定义网络)中拓扑发现的原理与实现方法,重点介绍了LLDP协议在OpenFlow环境中的应用,并讨论了非OpenFlow设备存在时的链路发现策略。 ... [详细]
  • 本文探讨了一个场景:用户成功登录后,如何确保Master-Detail视图控制器以模态形式展示。 ... [详细]
  • iOS 开发技巧:TabBarController 自定义与本地通知设置
    本文介绍了如何在 iOS 中自定义 TabBarController 的背景颜色和选中项的颜色,以及如何使用本地通知设置应用程序图标上的提醒个数。通过这些技巧,可以提升应用的用户体验。 ... [详细]
  • Java 架构:深入理解 JDK 动态代理机制
    代理模式是 Java 中常用的设计模式之一,其核心在于代理类与委托类共享相同的接口。代理类主要用于为委托类提供预处理、过滤、转发及后处理等功能,以增强或改变原有功能的行为。 ... [详细]
author-avatar
dingzhi521
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有