容納 SwiftUI view 的 UIHostingController ~ 從 storyboard 畫面切換到 SwiftUI 畫面

Apple 發明了更方便製作 App 畫面的 SwiftUI 後,SwiftUI 將是未來設計 iOS App 畫面的主流,但目前仍然有許多 App 採用傳統的 UIKit view controller 開發,所以我們該如何選擇呢 ?

別擔心,小孩子才做選擇,我們全都要。我們可以將 view controller 的畫面跟 SwiftUI 的畫面結合在同一個 App,接下來就讓我們一步步示範第一種常見的情境,利用容納 SwiftUI view 的 UIHostingController 從 view controller 切換到 SwiftUI 的畫面。

假設我們有兩個畫面,如下圖所示,第一個畫面是 storyboard 裡的 view controller,我們希望點選 controller 上的 button 後切換到 SwiftUI 的 view 顯示 Penny 佛跳牆超好聽的新歌,可有可無的歌詞。

SwiftUI 的畫面定義在 LyricsView 裡。

import SwiftUIstruct LyricsView: View {
var body: some View {
Text("""
嘿 有些話我沒說清楚 是故意讓它變模糊
也為了避免 過於理智的衝突
嘿 有些事我沒有宣布 是無疑讓它更懸殊
也為了減少 重覆彼此不舒服
因為太有感觸 所以才會如此反撲
任一時糊塗 違規的信徒 恍恍惚惚
誰當眾地退出 主動地刪除 都視若無睹
你才真的可惡 莫名傷得體無完膚
為認真演出 結束得倉促 吞吞吐吐
誰抱頭地痛哭 熱情地歡呼 都可有可無
""")
.padding()
.background(Color.yellow)
}
}

當我們想從 storyboard 拉 segue 切換頁面時,每個畫面都要有個控制它的 controller ,因此我們只要能將 SwiftUI 的 view 包在一個 controller 裡,即可順利地切換到 SwiftUI 的畫面。

有哪個 controller 這麼厲害可以裝 SwiftUI view 呢 ? 答案就是 SwiftUI 全新發明的新類別,UIHostingController !

class UIHostingController<Content> : UIViewController where Content : View

從 Storyboard 加入 UIHostingController

如下圖所示,我們可從 storyboard 的 Objects library 視窗裡找到 Hosting View Controller,直接將它加到 storyboard 設計畫面的畫布上。

UIHostingController 顯示的畫面將是 SwiftUI 的 view,不過它並不知道該顯示哪個 SwiftUI view,因此待會我們必須從程式設定。

利用 segue 切換頁面

在 storyboard 裡我們可以使用 segue 切換頁面,直接從 button 連 segue 到 UIHostingController。

利用 IBSegueAction 設定 UIHostingController 顯示的 SwiftUI view

現在我們剩下最後一個難題,如何設定 UIHostingController 顯示的SwiftUI view 呢? 答案就藏在 iOS 13 新發明的 IBSegueAction

segue 頁面切換時將觸發 IBSegueAction function,此 function 將回傳 segue 終點連接的 controller,在我們的例子裡將是 UIHostingController,因此我們可定義此 function,在產生 UIHostingController 時設定它顯示的 SwiftUI view。

產生 IBSegueAction function 的方法如下:

  • 將 segue 起點的 controller 類別設成自訂的類別。

IBSegueAction function 要定義在 segue 起點的 controller 裡,因此我們要先將 controller 類別設成自訂的類別,在此我們將類別設為 ViewController。

此步驟不清楚的同學可參考以下連結。

  • 顯示 Assistant 頁面。

點選下圖右上角紅色圈圈標示的 Adjust Editor Options 按鈕,然後點選 Assistant。

  • 在 Assistant 頁面顯示 segue 起點 controller 的程式。

點選 segue,點選 Assistant 頁面的 <> 切換檔案,在 Assistant 頁面顯示 segue 起點 controller 的類別 ViewController。

  • 從 segue 連線到 controller 的程式。
  • 輸入 function 的名字。

就像我們熟悉的 IBSegue & IBAction,此時 Xcode 將貼心地幫我們輸入 IBSegueAction 的相關程式。

接著我們要在 function 裡產生要回傳的 UIHostingController,UIHostingController 的 init 如下,關鍵在它的第二個參數 rootView,它代表的即是我們想要 UIHostingController 顯示的 SwiftUI view。

init?(coder aDecoder: NSCoder, rootView: Content)

請先點選 IBSegueAction function 裡等待我們輸入的灰色框框,讓它變藍色,然後按下 enter。

此時 Xcode 將幫我們輸入部分內容,我們只要自己輸入參數 rootView 的內容即可。因此我們輸入想要顯示的 LyricsView()

@IBSegueAction func showLyrics(_ coder: NSCoder) -> UIViewController? {   return UIHostingController(coder: coder, rootView: LyricsView())}

另外在程式的前面記得 import SwiftUI,不然會有紅色錯誤,因為 UIHostingController 是 SwiftUI 定義的類別。

import SwiftUI

如果只有一行程式,我們還可以更精簡,省略 return。

@IBSegueAction func showLyrics(_ coder: NSCoder) -> UIViewController? {    UIHostingController(coder: coder, rootView: LyricsView())}

App 執行

Cool,點選 button 後我們成功地從 storyboard 設計的畫面切換到 SwiftUI 畫面。

利用 Container View 容納 SwiftUI view

SwiftUI view 不一定要霸道地佔滿整個螢幕,我們也可以利用 Container View 將 SwiftUI view 變成 view controller 裡某個區塊的畫面。

程式和剛剛一樣,只是現在變成從 Container View 的 segue 拉線定義 IBSegueAction function。

@IBSegueAction func showLyrics(_ coder: NSCoder) -> UIViewController? {   UIHostingController(coder: coder, rootView: LyricsView())}

切換到 SwiftUI view 時傳資料

我們也可以在切換到 SwiftUI view 時傳資料,比方以下例子:

  • 在 SwiftUI view 宣告儲存資料的變數。

在 LyricsView 裡顯示變數 name 儲存的歌名,name 的內容將由前一頁的 view controller 傳入。

struct LyricsView: View {

var name: String

var body: some View {
VStack {
Text(name)
.font(.largeTitle)
Text("""
嘿 有些話我沒說清楚 是故意讓它變模糊
也為了避免 過於理智的衝突
嘿 有些事我沒有宣布 是無疑讓它更懸殊
也為了減少 重覆彼此不舒服
因為太有感觸 所以才會如此反撲
任一時糊塗 違規的信徒 恍恍惚惚
誰當眾地退出 主動地刪除 都視若無睹
你才真的可惡 莫名傷得體無完膚
為認真演出 結束得倉促 吞吞吐吐
誰抱頭地痛哭 熱情地歡呼 都可有可無
""")
.padding()
.background(Color.yellow)

}
}
}
  • IBSegueAction 生成 SwiftUI view 時傳入資料。

在生成 LyricsView 時我們傳入歌名可有可無

@IBSegueAction func showLyrics(_ coder: NSCoder) -> UIViewController? {
UIHostingController(coder: coder, rootView: LyricsView(name: "可有可無"))
}

App 執行

從 view controller 的程式切換到 SwiftUI view

學會用 UIHostingController 容納 SwiftUI view 後,我們也可以不依靠 segue,直接從程式生成 UIHostingController,然後呼叫 present(_:animated:completion:),pushViewController(_:animated:),show(_:sender:) & addChild(_:) 顯示。

  • 例子 1: 呼叫 present(_:animated:completion:)
@IBAction func showLyrics(_ sender: Any) {   let controller = UIHostingController(rootView: LyricsView(name: "可有可無"))   present(controller, animated: true, completion: nil)}
  • 例子 2: 呼叫 pushViewController(_:animated:)
@IBAction func showLyrics(_ sender: Any) {
let controller = UIHostingController(rootView: LyricsView(name: "可有可無"))

self.navigationController?.pushViewController(controller, animated: true)
}

--

--

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

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