Xcode11發(fā)布后,我們一直在驚嘆SwiftUI的強(qiáng)大,卻忽略了storyboard的一些改進(jìn)。據(jù)我所知,Apple在WWDC期間也沒(méi)有提到過(guò)segue或自定義初始化(initializers)的修改。 我們可以從Xcode和iOS 13發(fā)行說(shuō)明中看到一些修改提示。
SegueAction
假設(shè)我們使用storyboards時(shí)通過(guò)Segue進(jìn)行頁(yè)面跳轉(zhuǎn)
左側(cè)的BookController有一個(gè)Button,點(diǎn)擊該按鈕時(shí),它會(huì)跳轉(zhuǎn)到右側(cè)的PreviewController,以顯示該Book的詳情。 使用storyboard,您可以通過(guò)從按鈕拖拽到目標(biāo)控制器創(chuàng)建segue實(shí)現(xiàn)頁(yè)面跳轉(zhuǎn)。
要完成segue的配置,必須在屬性檢查器( attributes inspector)中添加一個(gè)唯一標(biāo)識(shí)符(Identifier):
頁(yè)面跳轉(zhuǎn)時(shí),我們需要將Model數(shù)據(jù)(本例中的Book)從源傳遞到目標(biāo)ViewController。 在Xcode 10和iOS 12中,我們使用源視圖控制器中的 prepare(for:sender) 來(lái)實(shí)現(xiàn):
// BookController
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
guard let previewController = segue.destination as? PreviewController else {
fatalError("Missing PreviewController")
}
previewController.book = book
}
注意,我們沒(méi)有創(chuàng)建目標(biāo)視圖控制器。 UIKit通過(guò)調(diào)用init(coder:) 方法來(lái)初始化新的ViewController。 我們可以配置數(shù)據(jù)并將其傳遞給controller,但只能在UIKit創(chuàng)建controller之后。
我的PreviewController有一個(gè)Book屬性,但它是可選的:
// PreviewController
import UIKit
final class PreviewController: UIViewController {
@IBOutlet private var textView: UITextView!
var book: Book?
override func viewDidLoad() {
super.viewDidLoad()
title = book?.title
textView.text = book?.preview
}
}
book屬性是optional的,因?yàn)樵诔跏蓟?code>PreviewController期間無(wú)法設(shè)置它。我想在創(chuàng)建PreviewController時(shí)初始化book,并把book成員變量改成let不可修改的,怎么辦呢?
segue是UIKit在調(diào)用segue方法期間,創(chuàng)建目標(biāo)視圖控制器的一種方法。
從Xcode 11開(kāi)始,我們還有另一種將數(shù)據(jù)傳遞到目標(biāo)視圖控制器的方法。我們可以通過(guò)使用@IBSequeAction在源視圖控制器中標(biāo)記一個(gè)方法來(lái)創(chuàng)建segue action:
@IBSegueAction
private func showPreview(coder: NSCoder, sender: Any?, segueIdentifier: String?)
-> PreviewController? {
return PreviewController(coder: coder, book: book)
}
SegueAction方法具有三個(gè)參數(shù)。 必需的NSCoder參數(shù)以及可選的sender和segueIdentifier。 如果不需要,我們可以省略可選參數(shù):
@IBSegueAction
private func showPreview(coder: NSCoder)
-> PreviewController? {
return PreviewController(coder: coder, book: book)
}
如果該方法返回nil,則UIKit將調(diào)用 init(coder:) 方法來(lái)創(chuàng)建視圖控制器。 SegueAction不會(huì)阻止segue的發(fā)生。 無(wú)論哪種方式,都會(huì)在segue對(duì)象中傳遞新創(chuàng)建的視圖控制器給prepare(for:sender)方法。 由于這里我們不再需要它,因此從視圖控制器中刪除了prepare(for:sender)方法。
注意,Swift 5.1允許我們省略具有單個(gè)表達(dá)式的方法的return語(yǔ)句,我們進(jìn)一步簡(jiǎn)化SegueAction方法:
@IBSegueAction
private func showPreview(coder: NSCoder)
-> PreviewController? {
PreviewController(coder: coder, book: book)
}
要將storyboard中的segue連接到ViewController中的SegueAction方法,請(qǐng)從segue對(duì)象向view controller拖動(dòng),然后選擇SegueAction方法:
如果正確連接了SegueAction,在segue的屬性檢查器(attributes inspector)中會(huì)看到該方法的Selector變成了你定義的方法:
現(xiàn)在,PreviewController可以創(chuàng)建一個(gè)自定義initializer方法,該初始化方法在創(chuàng)建視圖控制器時(shí) 用我們SegueAction傳遞的Book做參數(shù):
// PreviewController
import UIKit
final class PreviewController: UIViewController {
@IBOutlet private var textView: UITextView!
let book: Book
init?(coder: NSCoder, book: Book) {
self.book = book
super.init(coder: coder)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func viewDidLoad() {
super.viewDidLoad()
title = book.title
textView.text = book.preview
}
}
自定義初始化方法必須調(diào)用super.init(coder:) 并傳遞從SegueAction中接收到的coder參數(shù)。 另外,Book屬性不再是可選的,因此我們可以將其從var更改為let。
自定時(shí)實(shí)例化方法
頁(yè)面跳轉(zhuǎn)時(shí)我們可以使用storyboards而不必使用segue。 例如,我可以將按鈕連接到視圖控制器中的IBAction方法,而不是創(chuàng)建segue:
@IBAction private func showPreview(_ sender: UIButton) {
guard let previewController = storyboard?.instantiateViewController(
withIdentifier: "PreviewController") as? PreviewController else {
fatalError("Unable to create PreviewController")
}
previewController.book = book
show(previewController, sender: self)
}
showPreview方法從storyboard中實(shí)例化視圖控制器,進(jìn)行配置然后顯示。 這種方式存在與segue相同的一些問(wèn)題。 我們調(diào)用storyboard的instantiateViewController(withIdentifier:)方法,然后返回一個(gè)ViewController。 任何Model數(shù)據(jù)(例如我們的Book)都必須是目標(biāo)視圖控制器的可選屬性。
蘋(píng)果在iOS 13發(fā)行說(shuō)明中對(duì)這個(gè)問(wèn)題進(jìn)行了完善:
You can now invoke a custom initializer from a creation block that’s passed through instantiateInitialViewController(creator:) or instantiateViewController(identifier:creator:).
例如,使用我們的自定義初始化方法傳遞Model數(shù)據(jù):
@IBAction private func showPreview(_ sender: UIButton) {
guard let previewController = storyboard?.instantiateViewController(
identifier: "PreviewController",
creator: { coder in
PreviewController(coder: coder, book: self.book)
}) else {
fatalError("Unable to create PreviewController")
}
show(previewController, sender: self)
}
storyboard.instantiateViewController多了一個(gè)creator參數(shù),creator block提供了我們需要傳遞給自定義view controller初始化方法的coder,現(xiàn)在就把book可選參數(shù)改成let了。
遲到總比沒(méi)有好
未來(lái)可能是SwiftUI一統(tǒng)天下,但我仍然很高興storyboard有所完善。 第一個(gè)改動(dòng)使用Xcode11開(kāi)發(fā)低版本iOS也可以使用,第二個(gè)改動(dòng)就需要iOS13及以上版本支持了。 所以也許這就是遲到總比沒(méi)有好。
作者:kharrison
翻譯整理:樂(lè)Coding
原文地址:https://useyourloaf.com/blog/better-storyboards-with-xcode-11/