讓 view controller & UIKit view 也能享用 SwiftUI 方便的 preview

SwiftUI 提供方便的 preview 功能,讓我們不用啟動模擬器就能預覽和操作 App 畫面,而且還能在修改程式時立即更新 preview。preview 如此先進的功能,看來我們用 storyboard,xib & view controller 設計的 App 畫面應該就無福享有吧 ?

其實是可以的,方法有兩種。

  • 新版做法: 使用 #Preview,Xcode 15 以上。
  • 舊版做法: 利用 UIViewControllerRepresentable & UIViewRepresentable。

利用 UIViewControllerRepresentable 將 view controller 的畫面變成 SwiftUI 的 view,利用 UIViewRepresentable 將 UIKit view 變成 SwiftUI 的 view,一樣可享用 preview 功能。詳細的說明可參考以下連結。

接下來我們就以阿丁同學的七彩彼得潘 App 為例示範。

預覽 storyboard 設計的 controller 的畫面

如下圖所示,阿丁使用傳統的 storyboard & view controller 開發 App。

當我們從程式修改畫面或想測試 App 功能時,原本只能執行 App 才能操作和看到程式修改的畫面。加上 preview 功能後,我們可以直接從 preview 預覽和操作 App 畫面。

  • 設定 controller 的 Storyboard ID。

待會我們將從程式生成 controller,因此要先設定它的 Storyboard ID。

  • 定義型別 ViewControllerView,遵從 protocol UIViewControllerRepresentable。
import SwiftUIstruct ViewControllerPreviews: 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
}

說明:

func makeUIViewController(context: Context) -> ViewController {

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

利用 storyboard 的 instantiateViewController(withIdentifier:) 生成 controller,傳入 controller 的 ID 指定想生成的 controller。(記得要在 storyboard 裡 controller 的 Storyboard ID 欄位設定 controller 的 ID)

  • 定義預覽 ViewControllerView 的 preview 型別。
struct ViewControllerView_Previews: PreviewProvider {
static var previews: some View {
ViewControllerView()
}
}

新版的 Xcode (11.4) 可以點選 Create Preview,自動產生 preview 的相關程式,使用舊版的朋友請稍微辛苦點,自己輸入程式碼。

為了將 struct ViewControllerView 生成的 SwiftUI view 顯示在 preview,我們必須遵從 protocol PreviewProvider,然後宣告它的 static 變數 previews,回傳預覽要顯示的 SwiftUI view,在此我們回傳 ViewControllerPreviews()

大功告成 ! 如下圖所示,preview 畫面順利地現身,顯示著透過 storyboard & 程式設計的 view controller 畫面。

修改 controller 程式,感受 preview 即時更新的快感

為了在修改 controller 程式時即時看到 ViewControllerView 的 preview 畫面,我們必須先點選 preview 左下角的 Pin Preview 按鈕,讓它固定在右邊。

接著當我們切換到 ViewController 時,彼得潘的 preview 畫面仍然會在右邊陪伴我們,不會離我們而去。

修改 viewDidLoad,在畫面上加入顯示 Peter 名字的 label,並將左邊紅色 slider 的 thumb 改成顯示可愛的 Peter icon。

override func viewDidLoad() {
super.viewDidLoad()
greenASlider.setThumbImage(UIImage(named: "peter"), for: .normal)
let label = UILabel()
label.text = "Peter"
label.font = UIFont.systemFont(ofSize: 50)
label.textColor = .white
label.sizeToFit()
view.addSubview(label)
}

Cool,在我們輸入程式的同時,右邊的 preview 畫面也即時更新。

值得注意的,當 preview 預覽 controller 畫面時,它的 viewDidLoad & viewWillAppear 都會執行,因此它們對畫面做的修改都可在 preview 看到。但是 viewDidAppear 不會執行,它要等到點選三角形執行 Live Preview 才會執行。

從 Live Preview 操作 App

我們也可以點選 preview 畫面右下角的三角形啟動 Live Preview,將預覽畫面變成可操作互動的 App。

不用啟動 App,從 live preview 一樣可操作測試 controller 的功能。

修改程式後,Live Preview 也會聰明地自動更新。比方我們將 label 的文字從 Peter 改成 Peter Pan,Live Preview 也會立即更新。

支援 iOS 13 之前的程式

preview 功能很方便,可惜它不支援 iOS 13 之前的程式。比方當 App 的 Development Target 設為 12.0 時,剛剛的 ViewControllerPreviews 將產生一堆紅色錯誤。

解決的方法很簡單,我們只要在 struct ViewControllerView & ViewControllerView_Previews 之前加上 @available(iOS 13.0.0, *),即可讓 preview 的程式只在 iOS 13 以上的環境執行。

預覽 controller 時指定假資料

當 controller 的畫面內容來自前一頁時,我們必須在產生 preview 要預覽的 controller 畫面時給它假資料,否則將產生問題,例如以下 Shiny 同學的咖啡廳 App 例子。

如下圖所示,CafeViewController 依據前一頁選擇的咖啡廳決定顯示的內容。我們先將它的 Storyboard ID 取名為 CafeViewController。

接著我們定義它的 SwiftUI view 型別 CafeViewControllerView & preview 型別 CafeViewControllerView_Previews。

import SwiftUI@available(iOS 13.0.0, *)
struct CafeViewControllerView: UIViewControllerRepresentable {


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

}

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

}

typealias UIViewControllerType = CafeViewController


}
@available(iOS 13.0.0, *)
struct CafeViewControllerView_Previews: PreviewProvider {
static var previews: some View {
CafeViewControllerView()
}
}

令人難過的,此時預覽將無法顯示,點選右上角的 Diagnostics 還告訴我們 App 可能閃退了。

沒錯,preview 也可能閃退。讓我們看一下 CafeViewController 的程式,原來它的內容來自 property cafe。由於它的型別是 cafeDetail!,當我們讀不到 cafe.location 時將造成 App 閃退。

class CafeViewController: UIViewController {@IBOutlet weak var locationLabel: UILabel!
@IBOutlet weak var rankingLabel: UILabel!
@IBOutlet weak var outsideImageView: UIImageView!
@IBOutlet weak var insideImageView: UIImageView!
@IBOutlet weak var introTaxtView: UITextView!

var cafe: cafeDetail!

override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.

locationLabel.text = cafe.location
rankingLabel.text = cafe.ranking
outsideImageView.image = UIImage(named: cafe.outsideImage)
insideImageView.image = UIImage(named: cafe.insideImage)
introTaxtView.text = cafe.intro


}

因此我們在 preview 生成 CafeViewController 時要給它咖啡廳的資料。

func makeUIViewController(context: Context) -> CafeViewController {
let controller = UIStoryboard(name: "Main", bundle: nil).instantiateViewController(identifier: "CafeViewController") as! CafeViewController
controller.cafe = cafeDetail(intro: "咖啡館開張於1894年,因位於當時的紐約保險公司大樓內而得名。據說在一次大戰時期此處即為布達佩斯的文藝中心,許多出版社雜誌社在此設立辦公室,雖然內部裝潢華麗輝煌卻絲毫不以勢壓人,非常樂於提供窮作家們在此寫作及聚會。", location: "布達佩斯", ranking: "第一名", outsideImage: "CaféNewYorkOutside", insideImage: "CaféNewYorkInside")
return controller

}

指定咖啡廳資料後,preview 順利地出現美麗的布達佩斯紐約咖啡館。

預覽從網路抓資料的 controller 畫面

當 controller 的內容來自網路時,有時 preview 只能看到部分內容,或是完全看不到內容,要從 live preview 執行程式後才能看到。

比方以下的 preview 畫面顯示 SongTableViewController,而 SongTableViewController 從 iTunes API 抓取周杰倫的音樂。在 preview 時它已經抓到 JSON,所以可以顯示歌名,但圖片卻要在 live preview 時才能顯示。

通常預覽從網路抓資料的 controller 畫面時,我們會直接搭配假資料,比方讀取 App 裡的 JSON 檔,讓它更方便測試,不須連網路也能看到畫面。

預覽 xib 或程式設計的 controller 畫面

我們不只能預覽 storyboard 設計的畫面,也可以預覽以 xib 或從程式設計的 controller 畫面,只要我們在 function makeUIViewController(context:) 回傳想變成 SwiftUI view 的 controller。

  • 從 xib 設計的畫面
func makeUIViewController(context: Context) -> ViewController {

ViewController(nibName: "ViewController", bundle: nil)
}
  • 從程式設計的 controller 畫面

Sean 大大的課程範例完全從程式手刻畫面,寫程式的過程中只能辛苦地想像畫面,必須將 App 裝到模擬器才能看到畫面的模樣,

有了 SwiftUI preview,我們不用啟動模擬器就能預覽畫面,而且修改程式時還能即時看到畫面更新呢。

import SwiftUIstruct SearchVCView: UIViewControllerRepresentable {
func makeUIViewController(context: Context) -> SearchVC {
SearchVC()
}

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

}

typealias UIViewControllerType = SearchVC

}
struct SearchVCView_Previews: PreviewProvider {
static var previews: some View {
SearchVCView()
}
}

比方將 button 顏色從綠色變成黃色。

預覽 UIKit view

利用 UIViewRepresentable 將 UIKit view 變成 SwiftUI 的 view,我們也可以預覽 xib 或程式設計的 UIKit view。

以下我們以 Sean 大大課程的 FavoriteCell 為例說明。

定義型別 FavoriteCellView,遵從 protocol UIViewRepresentable,然後定義預覽 FavoriteCellView 的 preview 型別。

import SwiftUIstruct FavoriteCellView: UIViewRepresentable {
func makeUIView(context: Context) -> FavoriteCell {
let cell = FavoriteCell(frame: .zero)
cell.set(favorite: Follower(login: "Peter", avatarUrl: "https://avatars2.githubusercontent.com/u/5902131?s=460&v=4"))
return cell
}

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

typealias UIViewType = FavoriteCell


}
struct FavoriteCellView_Previews: PreviewProvider {
static var previews: some View {
FavoriteCellView()
.previewLayout(.fixed(width: 375, height: 80))
}
}

建立 Xcode 的 file template

將 view controller 變成 SwiftUI preview 實在是個很方便的功能,有興趣的朋友也可以參考以下 Xcode file template 的說明,透過 template 自動生成 controller 的 SwiftUI preview。

--

--

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

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