使用 #Preview macro 定義預覽畫面
透過 Xcode 的 preview 功能,我們可以方便地從 preview 預覽和操作畫面,不用啟動模擬器,而且修改程式後 preview 也會立即更新,馬上就能測試畫面和功能是否正確。
從 Xcode 15 開始,幫畫面加上 preview 變得更方便了。它有以下三大改進。
- preview 的程式變得更精簡了。
由於 Swift 5.9 macro 語法的發明,現在只要用 #Preview { } 即可定義 preview,比方以下是 ContentView 的 preview。
#Preview {
ContentView()
}
舊版的 preview 寫法如下,寫法冗長許多。
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
- SwiftUI 的 view 和 UIKit 的 view 和 view controller 都能快速加上 preview。
從前 UIKit 的 view 和 view controller 也能加上 preview,但是需要撰寫麻煩的 UIViewRepresentable 和 UIViewControllerRepresentable。現在有了 #Preview macro,寫法變得簡單許多。
- 方便預覽 widget 不同時間點的樣子。
以下我們將重點放在第二點,示範如何利用 #Preview macro 幫以下幾種畫面加上 preview。
- SwiftUI 的 view。
- 從 storyboard 設計的 view controller 畫面。
- 從程式製作的 view controller 畫面。
- 從 xib 製作的 view controller 畫面。
- 從程式製作的 UIKit view。
- 從 xib 製作的 UIKit view。
SwiftUI 的 view
struct ContentView: View {
var body: some View {
VStack {
Image(systemName: "globe")
.imageScale(.large)
.foregroundStyle(.tint)
Text("Hello, world!")
}
.padding()
}
}
在 #Preview { } 裡回傳 SwiftUI view。
#Preview {
ContentView()
}
從 storyboard 設計的 view controller 畫面
以 Apple Develop in Swift Explorations 電子書的 RPS 為例。
在 #Preview { } 裡回傳從 storyboard 生成的 view controller。
- 寫法 1: 生成的 controller 是 initial view controller。
#Preview {
let storyboard = UIStoryboard(name: "Main", bundle: nil)
return storyboard.instantiateInitialViewController()!
}
- 寫法 2: 生成指定 id 的 view controller。
#Preview {
let storyboard = UIStoryboard(name: "Main", bundle: nil)
return storyboard.instantiateViewController(withIdentifier: "ViewController")
}
在 Storyboard ID 欄位設定 controller 的名字,下圖設定 ViewController。
透過以上寫法,現在我們從 preview 就能操作畫面,開心地跟電腦玩剪刀石頭布。
我們也可以一邊修改程式,一邊從 preview 測試功能。以下我們將機器人換成吸血鬼,背景色換成橘色。
從程式製作的 view controller 畫面
以 freeCodeCamp.org 的 Netflix App 為例。
HomeViewController 的畫面完全從程式製作。
加上 preview。
#Preview {
UINavigationController(rootViewController: HomeViewController())
}
preview 順利地呈現 HomeViewController 從網路抓取的資料。
當我們修改字型大小和顏色時,preview 也會立即更新。
func tableView(_ tableView: UITableView, willDisplayHeaderView view: UIView, forSection section: Int) {
guard let header = view as? UITableViewHeaderFooterView else {return}
header.textLabel?.font = .systemFont(ofSize: 25, weight: .semibold)
header.textLabel?.frame = CGRect(x: header.bounds.origin.x + 20, y: header.bounds.origin.y, width: 100, height: header.bounds.height)
header.textLabel?.textColor = .red
header.textLabel?.text = header.textLabel?.text?.capitalizeFirstLetter()
}
preview 的 Trending movies 變大了,字也變成了紅色。
preview 不只能顯示 #Preview { } 裡回傳的畫面,點選電影還會跳到下一個畫面。
從 xib 製作的 view controller 畫面
class DemoViewController: UIViewController {
@IBOutlet var diceImageViews: [UIImageView]!
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
}
@IBAction func play(_ sender: Any) {
for diceImageViwe in diceImageViews {
let number = Int.random(in: 1...6)
diceImageViwe.image = UIImage(systemName: "die.face.\(number).fill")
}
}
}
加上 preview。
#Preview {
DemoViewController()
}
我們可以直接在 preview 玩骰子改變點數。
從程式製作的 UIKit view
以 freeCodeCamp.org 的 Netflix App 為例,以下為 table view cell 的設計畫面。
加上 preview,參數 traits 傳入 fixedLayout 設定 cell 的大小。
#Preview(traits: .fixedLayout(width: 393, height: 140), body: {
let model = TitleViewModel(titleName: "Spider-Man: Across the Spider", posterURL: "/8Vt6mWEReuy4Of61Lnj5Xj704m8.jpg")
let cell = TitleTableViewCell()
cell.configure(with: model)
return cell
})
從 xib 製作的 UIKit view
class DemoView: UIView {
@IBOutlet weak var imageView: UIImageView!
}
加上 preview,參數 traits 傳入 fixedLayout 設定 cell 的大小。
#Preview(traits: .fixedLayout(width: 100, height: 100), body: {
if let demoView = Bundle.main.loadNibNamed("DemoView", owner: nil)?.first as? DemoView {
demoView.imageView.image = UIImage(systemName: "apple")
return demoView
} else {
return UIView()
}
})
相容舊版的 iOS
舊版的 iOS 也可以使用 #Preview。不過當專案的 Minimum Deployments 小於 iOS 17 時,在某些情況會遇到版本問題,相關說明可參考以下連結。