在 SwiftUI App 加入 UIKit View,比方網頁

SwiftUI 有很多很棒很炫的功能,幫助我們更容易地開發 iOS App,不過因為它才剛出生,所以一些傳統 iOS App 常用的 UI 元件還不支援,比方它缺少像 WebKit WKWebView 一樣顯示網頁的元件。

所以 SwiftUI App 要放棄網頁功能了嗎 ? 別擔心,我們可以將傳統的 UI 元件包裝成 SwiftUI view,讓它假裝成 SwiftUI view,混入 SwiftUI 的世界裡,就像最近彼得潘戴上黑武士的頭盔闖進星際大戰的世界。

為了將傳統的 UI 元件包裝成 SwiftUI view,我們必須以 struct 定義 SwiftUI view 的型別,讓它遵從 protocol UIViewRepresentable,然後定義 protocol UIViewRepresentable 的相關 function,在裡面產生傳統的 UI 元件。

將顯示網頁的 WKWebView 包裝成 SwiftUI view

接下來讓我們以顯示網頁的 WKWebView 為例,將它變成能在 SwiftUI App 顯示的 SwiftUI view。

以 struct 定義包裝 UI 元件的 SwiftUI view 型別,遵從 protocol UIViewRepresentable

按 cmd + n 新增 Swift File。

將包裝 WKWebView 的 SwiftUI View 取名為 WebView。

import SwiftUI & WebKit(因為待會將用到 WKWebView),以 struct 定義遵從 protocol UIViewRepresentable 的 WebView。

import SwiftUI
import WebKit

struct WebView: UIViewRepresentable {

}

UIViewRepresentable 的 representable 是代表的意思,遵從 protocol UIViewRepresentable 暗示此 struct 定義的型別是一種包含 UIView 元件的 SwiftUI view,所以它可以包裝 UICollectionView,WKWebView,MKMapView 等 UI 元件,待會我們將用它包裝 WKWebView。

產生遵從 protocol UIViewRepresentable 需定義的相關 function

由於我們尚未定義 protocol UIViewRepresentable 的相關 function,此時會產生紅色錯誤。

點選 Fix 後,Xcode 會幫我們加入 typealias UIViewType = ,請在 = 後補上 SwiftUI view 想呈現的畫面的型別。

在此我們填入 WKWebView。

typealias UIViewType = WKWebView

此時還是會有紅色錯誤,因為 protocol UIViewRepresentable 的 function 還未定義。請耐心地再按一次 Fix 修正。

Xcode 將幫我們補上遵從 protocol UIViewRepresentable 需定義的兩個 function,makeUIView(context:) & updateUIView(_:context:)。

makeUIView(context:) 回傳的元件即為 WebView 顯示的畫面。由於我們剛剛在 typealias 裡填入 WKWebView,因此 Xcode 知道 makeUIView(context:) 要回傳的型別是 WKWebView。

定義 function makeUIView & updateUIView

我們須在 makeUIView(context:) 裡回傳型別 WKWebView 的物件, 因此我們在裡面產生 WKWebView 物件,設定它載入彼得潘最愛的書小王子的網頁。

func makeUIView(context: Context) -> WKWebView {
let webView = WKWebView()
if let url = URL(string: "https://www.thelittleprince.com") {
let request = URLRequest(url: url)
webView.load(request)
}
return webView
}

那麼另一個 updateUIView(_:context:) function 呢 ? 它將在 SwiftUI view 畫面更新時被呼叫,不過現在我們先目標固定顯示小王子,看它千遍也不厭,所以 updateUIView(_:context:) 不需寫任何程式。

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

}

定義預覽 WebView 的 preview 型別

從 Xcode menu 點選 Editor > Create Preview。

struct Previews_WebView_Previews: PreviewProvider {
static var previews: some View {
Text("Hello, World!")
}
}

將 previews 的內容從 Text 改成 WebView。

struct Previews_WebView_Previews: PreviewProvider {
static var previews: some View {
WebView()
}
}

由於網頁要連網抓資料,因此我們要點選三角形的 live preview 按鈕執行後才能顯示網頁。

顯示 WebView

現在我們已經可在 SwiftUI 的世界顯示網頁了,因為我們有包裝了 WKWebView,假扮成 SwiftUI view 的 WebView。

struct ContentView: View {
var body: some View {
WebView()
}
}

從 WebView 呼叫 modifier,調整網頁的大小形狀

剛剛我們的 ContentView 只有顯示 WebView,因此它霸氣地佔滿整個螢幕。我們也可以控制它的大小,因為 WebView 就是 SwiftUI view,所以它可以呼叫許多 SwiftUI 的 modifier。

比方以下例子我們在 VStack 裡容納 Image & WebView,將 WebView 的高度指定為 300。

struct ContentView: View {
var body: some View {
VStack {
Image(systemName: "star")
.resizable()
.scaledToFit()
WebView()
.frame(height: 300)
}
}
}

顯示圓形的網頁也不是問題,從 WebView 呼叫 clipShape 即可。

var body: some View {
WebView()
.frame(width: 400, height: 400)
.clipShape(Circle())
}

搭配 property 控制 web view 顯示的網頁

雖然小王子很可愛,不過也許有天我們會厭倦,能不能在產生 WebView 時傳入網址,讓它顯示我們想看的網頁呢 ?

當然可以,我們可以在 WebView 裡宣告儲存資料的 property,然後在生成 WebView 時傳入內容,比方以下例子宣告儲存網址的 urlString。

struct WebView: UIViewRepresentable {

let urlString: String

func makeUIView(context: Context) -> WKWebView {
let webView = WKWebView()
if let url = URL(string: urlString) {
let request = URLRequest(url: url)
webView.load(request)
}

return webView
}

生成 WebView 時傳入每天回家老婆都在裝死的預告連結。

WebView(urlString: "https://www.youtube.com/watch?v=JsX2lsLxLBI")

利用 updateUIView 更新網頁

有時我們會希望 SwiftUI view 生成後還能更新改變,此時正是剛剛失落的 function updateUIView 派上用場的時候。

接著就讓我們試試點選 button 隨機顯示網頁的功能吧。假設 array urls 裡分別是樂高彼得潘 & 虎克船長的網址。

在 ContentView 裡利用 VStack 包含 WebView & Button,然後每次點選 button 時從 array urls 裡亂數其中一個網址顯示吧。

struct ContentView: View {

let urls = [
"https://ideascdn.lego.com/media/generate/lego_ci/608d9427-2606-4384-b5e9-77fd3c7b30e7/resize:800:450",
"https://ideascdn.lego.com/media/generate/lego_ci/f88a8415-09c3-4dcb-bba4-e7720afdc3c0/resize:800:450"
]

@State private var urlString = ""

var body: some View {
VStack {
WebView(urlString: urlString)
Button("給我一個好看的網頁") {
urlString = urls.randomElement()!
}
}
}
}

struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}

修改 WebView 的 makeUIView & updateUIView。

struct WebView: UIViewRepresentable {

let urlString: String

func makeUIView(context: Context) -> WKWebView {
let webView = WKWebView()
return webView
}


func updateUIView(_ uiView: WKWebView, context: Context) {
if let url = URL(string: urlString) {
let request = URLRequest(url: url)
uiView.load(request)
}
}

typealias UIViewType = WKWebView
}

此時 makeUIView 只有生成 WKWebView,我們改在 updateUIView 才載入網頁。到時候點選 button,ContentView 的 urlString 改變時將觸發畫面更新,於是讓 WebView 的 function updateUIView 被呼叫,載入新的網頁內容。

對於 makeUIView & updateUIView 何時會執行好奇的朋友,可試試加入 print 觀察。

func makeUIView(context: Context) -> WKWebView {
let webView = WKWebView()
print("makeUIView", webView)
return webView
}

func updateUIView(_ uiView: WKWebView, context: Context) {
print("updateUIView", uiView)
if let url = URL(string: urlString) {
let request = URLRequest(url: url)
uiView.load(request)
}
}

當 ContentView 畫面顯示時,它在顯示裡面的 WebView 時將生成 WebView,然後呼叫 makeUIView & updateUIView。

makeUIView <WKWebView: 0x7fa88d80c400; frame = (0 0; 0 0); layer = <CALayer: 0x6000008fbd60>>
updateUIView <WKWebView: 0x7fa88d80c400; frame = (0 0; 0 0); layer = <CALayer: 0x6000008fbd60>>

接著當我們點選 button 時,若網址不變,網頁不需更新,WebView 將不受任何影響。當網址改變,需要顯示另一個網頁時,則會呼叫 updateUIView。

updateUIView <WKWebView: 0x7fa88d80c400; frame = (0 0; 414 789.5); layer = <CALayer: 0x6000008fbd60>>

從以上 updateUIView 列印訊息顯示的 WKWebView 記憶體位置,我們也可看出 updateUIView 的參數 uiView 正是當初 makeUIView 產生的 WKWebView。

One more thing, Coordinator

開發 SwiftUI App 時,我們較少用到 delegate & data source,不過傳統的 UIKit 元件卻常依靠 delegate & data source 實現一些功能,比方 MKMapView 利用 delegate 設定地圖標記的圖案。

類似 delegate & data source 這類進階的功能必須依靠 coordinator 實現,有興趣的朋友可進一步查詢相關說明,彼得潘未來也會再撰寫文章說明。

將 UI 元件包裝成 SwiftUI view 的步驟整理

  1. 以 struct 定義 SwiftUI view 的型別,遵從 protocol UIViewRepresentable
  2. 在 SwiftUI view 的型別裡定義 function makeUIView,回傳傳統的 UI 元件,此 function 將在 SwiftUI view 生成時呼叫。

3. 在 SwiftUI view 的型別裡定義 function updateUIView,修改 UI 元件的內容,此 function 將在 SwiftUI view 畫面更新時呼叫。

4. 若要在生成 SwiftUI view 時傳入資料,請在 SwiftUI view 的型別裡宣告儲存資料的 property。(optional)

5. 若要實現 delegate & data source 等進階功能,請搭配 coordinator。(optional)

6. 若需要預覽,請定義遵從 protocol PreviewProvider 的型別。

--

--

彼得潘的 iOS App Neverland
彼得潘的 Swift iOS App 開發問題解答集

彼得潘的iOS App程式設計入門,文組生的iOS App程式設計入門講師,彼得潘的 Swift 程式設計入門,App程式設計入門作者,http://apppeterpan.strikingly.com