容納 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)
}