在 SwiftUI App 加入 view controller & 從 SwiftUI 畫面切換到 Storyboard 畫面

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

  • 像 UIKit UIImagePickerController 一樣拍照選照片的元件。
  • 像 SafariServices SFSafariViewController 一樣顯示網頁的元件。

所以 SwiftUI App 要放棄拍照 & 網頁功能了嗎 ? 別擔心,我們可以將 view controller 包裝成 SwiftUI view,讓它假裝成 SwiftUI view,混入 SwiftUI 的世界裡,甚至從 SwiftUI 畫面切換到 Storyboard 的 controller 畫面,就像最近彼得潘戴上黑武士的頭盔闖進星際大戰的世界。

將 view controller 包裝成 SwiftUI view 的步驟

將 view controller 包裝成 SwiftUI view 的方法跟將 UIKit view 包裝成 SwiftUI view 的方法類似,還不清楚的朋友可先參考以下連結的說明,再閱讀接下來的說明。

將 view controller 包裝成 SwiftUI view 包含以下步驟:

  1. 以 struct 定義 SwiftUI view 的型別,遵從 protocol UIViewControllerRepresentable
  2. 在 SwiftUI view 的型別裡定義 function makeUIViewController,回傳 view controller,此 function 將在 SwiftUI view 生成時呼叫。

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

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

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

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

將 SFSafariViewController 包裝成 SwiftUI view

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

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

按 cmd + n 新增 Swift File,import SwiftUI & SafariServices(因為待會將用到 SFSafariViewController),以 struct 定義遵從 protocol UIViewControllerRepresentable 的 SafariView。

import SwiftUI
import SafariServices

struct SafariView: UIViewControllerRepresentable {

}

宣告儲存網址的 property url

struct SafariView: UIViewControllerRepresentable {

let url: URL
}

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

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

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

在此我們填入 SFSafariViewController,然後再次點選 Xcode 的 Fix,自動產生 function makeUIViewController & updateUIViewController。

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

定義 function makeUIViewController & updateUIViewController

我們須在 makeUIViewController(context:) 裡回傳型別 SFSafariViewController 的物件, 因此我們在裡面利用 url 產生 SFSafariViewController 物件。

func makeUIViewController(context: Context) -> SFSafariViewController {
SFSafariViewController(url: url)
}

那麼另一個 updateUIViewController(_:context:) function 呢 ? 它將在 SwiftUI view 畫面更新時被呼叫,不過現在我們先目標顯示網頁,顯示後不須更新,所以 updateUIViewController(_:context:) 不須寫任何程式。

func updateUIViewController(_ uiViewController: SFSafariViewController, context: Context) {

}

經過以上修改後,SafariView.swift 的程式如下。

import SwiftUI
import SafariServices

struct SafariView: UIViewControllerRepresentable {

func makeUIViewController(context: Context) -> SFSafariViewController {
SFSafariViewController(url: url)
}

func updateUIViewController(_ uiViewController: SFSafariViewController, context: Context) {

}
typealias UIViewControllerType = SFSafariViewController
let url: URL
}

定義預覽 SafariView 的 preview 型別

從 Xcode menu 點選 Editor > Create Preview。

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

將 previews 的內容從 Text 改成 SafariView。

struct Previews_SafariView_Previews: PreviewProvider {
static var previews: some View {
SafariView(url: URL(string: "https://medium.com/@apppeterpan")!)
}
}

SafariView 宣告了 property url,因此我們在 preview 程式裡生成 SafariView 必須傳入 url,指定網頁的網址,在此我們輸入彼得潘 medium blog 的網址。

顯示 SafariView

現在我們已經可在 SwiftUI 的世界顯示網頁了,因為我們有包裝了 SFSafariViewController,假扮成 SwiftUI view 的 SafariView,讓我們試試顯示彼得潘超想買的 AirPods Pro 網頁吧。

struct ContentView: View {
@State private var showWebpage = false
var body: some View {

Button("show webpage") {
showWebpage = true
}
.sheet(isPresented: $showWebpage) {
SafariView(url: URL(string: "https://www.apple.com/tw/airpods-pro/")!)
}
}
}

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

SafariView 就是 SwiftUI view,所以它可以呼叫許多 SwiftUI 的 modifier。比方以下例子我們從 SafariView 呼叫 frame & clipShape,指定它的大小和形狀。

struct ContentView: View {
@State private var showWebpage = false
var body: some View {

Button("show webpage") {
showWebpage = true
}
.sheet(isPresented: $showWebpage) {
SafariView(url: URL(string: "https://www.apple.com/tw/airpods-pro/")!)
.frame(width: 300, height: 300)
.clipShape(Circle())
}
}
}

SafariView 變成圓形的網頁了。

將 AVPlayerViewController 包裝成 SwiftUI view & 利用 updateUIViewController 更新

有時我們希望 SwiftUI view 生成後還能更新改變,此時正是剛剛失落的 function updateUIViewController 派上用場的時候。接著我們試試將 播放影片的 AVPlayerViewController 包裝成 SwiftUI view,測試點選 button 隨機播放電影的例子。

ps: 播放影片建議使用 SwiftUI 的 VideoPlayer 會更容易,在這裡為了說明 updateUIViewController,因此使用 AVPlayerViewController。

將播放影片的 AVPlayerViewController 包裝成 SwiftUI view

import SwiftUI
import AVKit

struct PlayerView: UIViewControllerRepresentable {

var urlString: String

func makeUIViewController(context: Context) -> AVPlayerViewController {
let controller = AVPlayerViewController()
controller.player = AVPlayer()
return controller
}

func updateUIViewController(_ uiViewController: AVPlayerViewController, context: Context) {
if let url = URL(string: urlString) {
let item = AVPlayerItem(url: url)
uiViewController.player?.replaceCurrentItem(with: item)
}

}

typealias UIViewControllerType = AVPlayerViewController


}

說明:

我們在 function makeUIViewController 裡產生 AVPlayerViewController & AVPlayer,但實際播放的電影則在 function updateUIViewController 設定。

我們從 urlString 產生的 AVPlayerItem,然後從 updateUIViewController 的參數 uiViewController 讀取 AVPlayerViewController,透過它呼叫 replaceCurrentItem 設定播放的電影。

利用 PlayerView 播放電影

假設 array urls 裡儲存彼得潘最愛的約會電影網址,真愛每一天 & 生命中的美好缺憾。

struct ContentView: View {

let urls = [
"https://video-ssl.itunes.apple.com/itunes-assets/Video111/v4/a8/17/ad/a817adba-e586-ef27-95ff-5039b67b5709/mzvf_8830173944917680013.640x354.h264lc.U.p.m4v",
"https://video-ssl.itunes.apple.com/itunes-assets/Video118/v4/e5/e4/9a/e5e49a1e-4c65-48b8-bd46-633daa976710/mzvf_2765762855115952106.640x458.h264lc.U.p.m4v"
]

@State private var urlString = ""

var body: some View {
VStack {
PlayerView(urlString: urlString)

Button("給我一部約會電影") {
urlString = urls.randomElement()!
}
}
}
}


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

說明:

點選 button 時將隨機的電影網址存入 urlString,如果 urlString 內容有改變,將觸發畫面更新,呼叫 PlayerView 的 updateUIViewController 改變播放的電影。

對於 makeUIViewController & updateUIViewController 何時會執行好奇的朋友,可試試加入 print 觀察。當 ContentView 畫面顯示時,它在顯示 PlayerView 時將生成 PlayerView,然後呼叫 makeUIViewController & updateUIViewController。接著當我們點選 button 時,若網址不變,影片不須更新,PlayerView 將不受任何影響。當網址改變,需要顯示另一個影片時,則會呼叫 updateUIViewController。

包裝 storyboard,xib 或程式設計的 controller 畫面

我們除了可以將 iOS SDK 內建的 view controller 包裝成 SwiftUI view,也可以包裝自訂的 view controller。換句話說,我們從 storyboard,xib 或程式設計的 controller 畫面都可以變成 SwiftUI view,加到 SwiftUI App 裡。

比方我們以阿丁同學的七彩彼得潘 App 為例,將她在 storyboard 設計的 view controller 畫面變成 SwiftUI view。

import SwiftUI
struct ViewControllerView: UIViewControllerRepresentable {

func makeUIViewController(context: Context) -> ViewController {
UIStoryboard(name: "Main", bundle: nil).instantiateViewController(identifier: "ViewController") as! ViewController
}

func updateUIViewController(_ uiViewController: ViewController, context: Context) {
}

typealias UIViewControllerType = ViewController
}

說明:

UIStoryboard(name: "Main", bundle: nil).instantiateViewController(identifier: "ViewController") as! ViewController

利用 storyboard 的 instantiateViewController(withIdentifier:) 生成 controller,傳入 controller 的 ID 指定想生成的 controller。

controller ID 的設定方法如下:

  • 點選 storyboard 裡的 controller。
  • 從 Storyboard ID 欄位設定 controller 的 ID。

切換到 Identity inspector 頁面,從 Storyboard ID 欄位設定 controller 的 ID。此 ID 可自由取名,只要別跟其它 controller 的 ID 同名即可,通常我們會讓 Storyboard ID 跟 controller 的類別同名。

One more thing, Coordinator

開發 SwiftUI App 時,我們較少用到 delegate & data source,不過傳統的 UIKit 元件卻常依靠 delegate & data source 實現一些功能,比方 UIImagePickerController 利用 delegate 讀取拍攝的照片。

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

--

--

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

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