Day13 SwiftUI 06 - WebView

接下来我们来看看在SwiftUI 怎麽使用WebView 网页的元件,SwiftUI 框架 有一个缺点,就是它未能提供所有 UIKit 有的 UI 控件,例如说由於 SwiftUI 还没有定义 WebView,所以Apple 有一个 UIViewRepresentable 协定,让你可以轻松打包 (wrap) 一个 UIView,并让 SwiftUI 专案使用,以下我们就将传统的 UI 元件包装成 SwiftUI view,并使用 WKWebView 来加载 Web 应用程序,让我们照着以下步骤来完成

  1. 在 SwiftUI 中使用 UIKit 中的任何组件都需要用 UIViewRepresentable 包装

    首先我们先新增一个 Swift File:并命名为 WebView.swift,里面内容为struct WebView,并实作UIViewRepresentable协定

    import SwiftUI
    import WebKit
    
    struct SwiftUIWebView: UIViewRepresentable {
    
    }
    

    实作 UIViewRepresentable 表示此 struct 定义的WebView,是有着 UIView 元件特性的 SwiftUI view,所以它可以包装 UICollectionView,WKWebView,MKMapView 等 UI 元件,待会我们将用它包装 WKWebView

  2. 要符合 UIViewRepresentable 协定,需实作两个方法,makeUIView(context:)以及updateUIView(_:context:)

    在程序区块上建立makeUIView(context:)方法,可经由Xcode 能自动完成

    https://ithelp.ithome.com.tw/upload/images/20210928/20118479rMsO2VqEVj.png

    再把makeUIView方法里回传的型别改成我们要用的WKWebView

     func makeUIView(context: Context) -> WKWebView {
     }
    

    同样的方式,建立updateUIView(_:context:)方法,记得把参数uiView改成我们要用的WKWebView

    func updateUIView(_ uiView: WKWebView, context: Context) {
    }
    

    现在我们来完成以上这两个方法的实作,首先makeUIView里我们要回传 WKWebView 的物件,这边可以透过WKWebpagePreferences以及WKWebViewConfiguration来对我们的WebView 设置一些偏好设定

    范例:

      func makeUIView(context: Context) -> WKWebView {
            let prefs = WKWebpagePreferences()
            prefs.allowsContentJavaScript = false
            let config = WKWebViewConfiguration()
            config.defaultWebpagePreferences = prefs
            return WKWebView(
                frame: .zero,
                configuration: config
            )
        }
    

    接下来是updateUIView,它将在 SwiftUI view 画面更新时被呼叫,我们可以在 WebView 里宣告一个属性来由外部传入网址,然後在生成 WebView 时传入内容,在 updateUIView 才载入网页

    如下:

    struct WebView: UIViewRepresentable {
    
        let url: URL?
    
        func makeUIView(context: Context) -> WKWebView {
            let prefs = WKWebpagePreferences()
            prefs.allowsContentJavaScript = false
            let config = WKWebViewConfiguration()
            config.defaultWebpagePreferences = prefs
            return WKWebView(
                frame: .zero,
                configuration: config
            )
        }
    
        func updateUIView(_ uiView: WKWebView, context: Context) {
            guard let myURL = url else {
                return
            }
            let  request = URLRequest(url: myURL)
            uiView.load(request)
        }
    }
    

    接下来我们就可以使用我们的WebView 来载入我们要的网页了

    struct ContentView: View {
        var body: some View {
            WebView(url: URL(string: "https://www.youtube.com/"))
        }
    }
    

目前我们只讨论了 UIViewRepresentable 协定中的几种方法。如果你需要在 UIKit 中使用委托 (delegate) 并与 SwiftUI 沟通,就必须实现 makeCoordinator 方法,并提供一个 Coordinator 实例。Coordinator 是 UIView 的委托 和 SwiftUI 之间的桥梁

我们在WebView struct 中,创建一个 Coordinator 类别并采用WKNavigationDelegate协定来处理webView 的导航事件,并在 makeCoordinator 方法回传其实例,可搭配数据流等来做资料的传递

范例:

struct WebView: UIViewRepresentable {
    @Binding var title: String
    var url: URL
    var loadStatusChanged: ((Bool, Error?) -> Void)? = nil

    func makeCoordinator() -> WebView.Coordinator {
        Coordinator(self)
    }

    func makeUIView(context: Context) -> WKWebView {
        let view = WKWebView()
        view.navigationDelegate = context.coordinator
        view.load(URLRequest(url: url))
        return view
    }

    func updateUIView(_ uiView: WKWebView, context: Context) {
        // you can access environment via context.environment here
        // Note that this method will be called A LOT
    }

    func onLoadStatusChanged(perform: ((Bool, Error?) -> Void)?) -> some View {
        var copy = self
        copy.loadStatusChanged = perform
        return copy
    }

    class Coordinator: NSObject, WKNavigationDelegate {
        let parent: WebView

        init(_ parent: WebView) {
            self.parent = parent
        }

        func webView(_ webView: WKWebView, didCommit navigation: WKNavigation!) {
            parent.loadStatusChanged?(true, nil)
        }

        func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
            parent.title = webView.title ?? ""
            parent.loadStatusChanged?(false, nil)
        }

        func webView(_ webView: WKWebView, didFail navigation: WKNavigation!, withError error: Error) {
            parent.loadStatusChanged?(false, error)
        }
    }
}
struct ContentView: View {
    @State var title: String = ""
    @State var error: Error? = nil
    
    var body: some View {
        WebView(title: $title, url: URL(string: "https://www.apple.com/")!)
                       .onLoadStatusChanged { loading, error in
                           if loading {
                               print("Loading started")
                               self.title = "Loading…"
                           }
                           else {
                               print("Done loading.")
                               if let error = error {
                                   self.error = error
                                   if self.title.isEmpty {
                                       self.title = "Error"
                                   }
                               }
                               else if self.title.isEmpty {
                                   self.title = "Some Place"
                               }
                           }
                       }
    }
}

<<:  Day 23 资料宝石:【Lab】RDS架构 建立自己的第一台云端资料库 (下)

>>:  鬼故事 - 真的有监控吗

day22: declarative vs. imperative

今天我们谈到 declarative 声明式和 imperative 命令式, 他的概念比较像是以下...

参赛前言 & 系列文规划

大家好,我是 Ian,因为一些原因,我在年初的时候接触到嵌入式系统开发与系统软件实作。在这一年来也...

JavaScript Day06 - 流程判断

if else 与 else if 参考 Day06 - 变数(03) - BMI、Day11 - ...

[披萨吃到饱-1] 义米兰手作披萨 #如何不花钱找到观众

花一份单点披萨的钱,就可以吃到12种口味的披萨,是很划算的。 这一天在爱买水湳店附近处理一些事情,忙...

C国某大学生在校看色色的东西被简讯通知「...文明上网...写心得报告 」

未经实锤的资讯 中国电子科大表示:学校学工部从未发布过相关的简讯内容,表示「已经提醒各院系辅导员通知...