iOS 加载本地 HTML 的一些实践

在应用的一些不重要的界面(比如功能的使用说明页面)上使用 HTML 进行开发可以在不降低用户体验的基础上,加快开发效率。

这篇文章尝试介绍一些这方面的实践。

# 加载本地HTML

我们在本地创建好一个名为 index.html 的文件后,将文件拖入到 iOS 项目的 Xcode 工程中。

iOS 加载本地 HTML 的方式如下:

let wkconfig = WKWebViewConfiguration.init()
webView = WKWebView.init(frame: view.frame, configuration: wkconfig)
var url = Bundle.main.url(forResource: "index", withExtension: "html", subdirectory: "")!
webView.loadFileURL(url, allowingReadAccessTo: url)
view.addSubview(webView)

# 本地HTML加载资源

如果我们需要在 index.html 里面引入一个本地的 JS 文件或者 CSS 文件怎么做?

  1. 本地创建一个文件夹,比如叫 www。将创建好的 index.html 和 index.js 文件放入 www 文件夹中。html 就按照正常的语法引入 JS 文件就好。

    //index.html
    <head>
      <script src="./index.js">
      </script>
    </head>
    <body>
      <div>Hello world!</div>
      <div onclick="showAlert(this)">点击这里</div>
    </body>
    </html>
    //index.js 
    function showAlert() {
        alert("Helloworld!");
    }
    
  2. 将 www 文件夹拖入到 iOS 工程中,拖入的时候选择「Create folder references」

    Untitled

    拖入后的项目目录结构如图

    Untitled

  3. 本地加载对应H5的方式需要更新,代码如下,最关键的是要给定对应文件夹的的读写权限

    let wkconfig = WKWebViewConfiguration.init()
    webView = WKWebView.init(frame: view.frame, configuration: wkconfig)
    //这里要指定文件夹为 www,因为 www 是文件夹形式引入的
    var url = Bundle.main.url(forResource: "index", withExtension: "html", subdirectory: "www")!
    let dirUrl = url.deletingLastPathComponent()
    webView.loadFileURL(url, allowingReadAccessTo: dirUrl)
    view.addSubview(webView)
    

    读写权限主要是这个方法控制

    func loadFileURL(_ URL: URL, allowingReadAccessTo readAccessURL: URL) -> WKNavigation?

    If readAccessURL references a single file, only that file may be loaded by WebKit. If readAccessURL references a directory, files inside that file may be loaded by WebKit.

  4. 点击对应 H5 页上对应的元素节点,发现还是没有弹窗。原因是iOS 系统接管了 JS 的 alert 事件。当 alert 事件执行的时候,对应的视图控制器会执行 WKUIDelegate 协议的代理方法 webView:runJavaScriptAlertPanelWithMessage:initiatedByFrame: ,代码如下

    //设置代理
    webView.uiDelegate = self  //current instance confirm WKUIDelegate protocol
    //实现代理方法
    func webView(_ webView: WKWebView, runJavaScriptAlertPanelWithMessage message: String, initiatedByFrame frame: WKFrameInfo) async {
        let alertCtrol = UIAlertController.init(title: message, message: nil, preferredStyle: .alert)
        alertCtrol.addAction(UIAlertAction.init(title: "OK", style: .cancel))
        self.present(alertCtrol, animated: true)
    }
    

基本上按照如上步骤就搞定了 iOS 本地 html 页面加载 H5 的方式。

同理,如果想要在 html 中加载对应的本地一些资源文件,比如图片的话,直接放在和 html 相同的文件夹下,然后给文件夹读写权限就好了,和上面的步骤基本上一样。

# 使用Vue开发本地HTML

如果原生的 html 开发的有点烦的话,也可以在 html 里使用 Vue 框架开发。

iOS 本地加载 html 和外部浏览器加载 HTML 大部分行为都一致,我们可以使用 Vue 的 CDN 安装方式,直接在 html 内部使用 Vue 框架来让开发过程更顺畅。

html 代码如下,我们本地就正常加载 html 就可以。

<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
<div id="app" >
  {{ message }}
		<div> email:
      <div @click="handleClick">fanthus@gmail.com</div>
    </div>
</div>
<script>
  const { createApp } = Vue
  createApp({
    data() {
      return { message: 'Hello Vue!' }
    },
    methods: {
      handleClick() { console.log('handleClick'); }
    },
  }).mount('#app')
</script>

# Vue 调用 iOS 方法

需要在 Vue 和 iOS 两端分别进行配置,基本思路还是 JSBridge

  1. 在 Vue 中使用 window.webkit.messageHandlers 对象发送消息给iOS端。

    比如你想要调用一个名为 sendMail 的原生方法,可以使用如下代码:

    handleClick() {
      window.webkit.messageHandlers.sendMail.postMessage({ "mail": "f@mail.com"})
    }
    
  2. 在 iOS 端需要做如下事情

    1. 注册 sendMail 方法并实现

      //注册sendMail方法
      let wkconfig = WKWebViewConfiguration.init()
      wkconfig.userContentController.add(self, name: "sendMail")
      //实现方法
      @objc func sendMail(_ mail:String) {
      	print("send mail \(mail)")
      }
      
    2. 当前视图控制器遵守 WKScriptMessageHandler 协议,并实现 userContentController(_:didReceive:) 方法,Vue 点击回调会触发这个方法的执行。

      extension ViewController: WKScriptMessageHandler {
          func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
              if message.name == "sendMail" {
                  guard let body = message.body as? [String: Any], let mail = body["mail"] as? String else { return }
                  self.sendMail(mail)
              }
          }
      }
      

这样整个流程就配置完毕,点击 Vue 会触发对应 Native 端代码发邮件动作执行。

# iOS 调用 Vue 方法

iOS 和 Vue 两端分别配置如下

  1. HTML Vue 端的配置如下。

    1. 创建需要被 Native 调用的方法

      methods: {
      	//1. 接收从 iOS 端出来参数
      	//2. 返回 H5 中定义的数据信息 email
        getData(info) {
          console.log('iOS call Vue code',info);
          return "f@gmail.com"
        }
      }
      
    2. 将方法放在全局可访问的函数中,挂载到 window 对象上。

      created() {
        window.getData = this.getData
      },
      
      
  2. iOS 端需主动调用对应 JS 代码

    webView.evaluateJavaScript("getData('Hello from iOS')") { value, error in
        print("value \(value); error \(error)")
    }
    

最后的执行结果是对应 Vue 中方法输出

value Optional(f@gmail.com); error nil

# 开发调试方式

在 iOS 加载本地 HTML 的这种开发方式里,我使用 Visual Studio Code 作为我的 html 开发工具。VSCode 里有一个 Open with live Server 的插件非常好用,基本上在改动 html 的同时,界面也会同步刷新。

同一份前端代码在 VSCode 和 Xcode 里可以同时打开,VSCode 负责前端部分的开发,Xcode 负责 Native 部分的开发。可以在期间来回切换。就是有的时候脑子有点不够用。

有的时候需要查看前端的 console.log 日志,这部分日志没有办法在 Xcode 的终端输出。但是我们可以通过一些配置,看到前端的 console.log 日志。

# 查看前端console.log日志方式

查看应用前端文件输出的日志方式是

  1. 在手机端打开网页检查。路径是「设置」→ 「Safari浏览器」→ 「高级」→「网页检查器」,打开这个配置的开关。

  2. 初始化 wkwebview 实例的时候需要设置 webview 的属性 isInspectable 为 true,允许调试。如果不想上线后 release 版本被调试的话,这里可以设置一个DEBUG判断。

  3. 在电脑端 Safari 浏览器里打开开发功能,同时手机端展示对应的网页,就能在目标手机上看到我们要调试的网页了。

    Untitled

  4. 点击我们要调试的 html 文件名字(这里是 index.html ),然后就会出现弹窗,点击能输出 console.log 的日志,观察到窗口如下输出。

    Untitled

这些都是一些基本的流程使用,如果大家有更好的实践方法的话,欢迎给我留言👏🏻👏🏻👏🏻。

参考地址:

  1. code linking folder for local HTML, CSS, javascript app (opens new window)
  2. Javascript console.log() in an iOS UIWebView (opens new window)
  3. 记一个ios WKWebView无法调试的坑 (opens new window)

关注我的微信公众号,我在上面会分享我的日常所思所想。