在代碼重用和可配置性之間找到一個很好的平衡點一般是頗有挑戰性的。雖然理想狀況下,咱們但願避免重複代碼並意外地建立多個真實源,但咱們須要配置的各類對象和值的許多方式每每取決於它們所使用的上下文。面試
本週,讓咱們看看幾種不一樣的技術,這些技術可讓咱們實現這種平衡-經過構建輕量級抽象,使咱們可以封裝配置代碼,以及如何在代碼庫之間共享這些抽象,以提升其一致性。編程
在進行任何類型的軟件開發時,一般會將程序分割成不一樣的部分,以便可以將它們做爲單獨的單元處理。對於用戶界面密集的應用程序,如iOS和Mac應用程序,一般很容易根據構成應用程序的各類屏幕進行這種分割。例如,一個購物應用程序可能有一個產品屏幕,一個列表屏幕,一個搜索屏幕,等等。swift
雖然這種屏幕級切片從高層次的角度看頗有意義(尤爲是由於它與咱們傾向於與其餘協做者(好比測試人員和設計人員)討論咱們的應用程序的方式相匹配),但它每每會致使須要對每一個屏幕進行大量配置的UI代碼。bash
拿着這個ProductViewController例如,它包含一個Buy按鈕,以及用於顯示每一個產品的詳細信息和相關項目的視圖-全部這些都是在視圖控制器的viewDidLoad方法:閉包
class ProductViewController: UIViewController {
let product: Product
...
override func viewDidLoad() {
super.viewDidLoad()
// Buy button
let buyButton = UIButton(type: .custom)
buyButton.setImage(.buy, for: .normal)
buyButton.backgroundColor = .systemGreen
buyButton.addTarget(self,
action: #selector(buyButtonTapped),
for: .touchUpInside
)
view.addSubview(buyButton)
// Product detail view
let productDetailView = UIView()
...
// Related products view
let relatedProductsView = UIView()
...
}
}
複製代碼
儘管咱們試圖經過在每一個配置塊以前添加一個註釋來使上面的代碼更容易閱讀,但咱們當前的viewDidLoad實現確實受到缺少結構的影響。由於咱們全部的配置都發生在一個地方,因此變量很容易在錯誤的上下文中被意外地使用,而且隨着時間的推移,咱們的代碼變得愈來愈複雜。app
就像咱們看了看「編寫自記錄的SWIFT代碼」,緩解上述問題的一種方法是簡單地將配置代碼的不一樣部分劃分爲不一樣的方法,其中viewDidLoad而後能夠呼叫:框架
private extension ProductViewController {
func setupBuyButton() {
let buyButton = UIButton(type: .custom)
...
}
func setupProductDetailView() {
let productDetailView = UIView()
...
}
func setupRelatedProductsView() {
let relatedProductsView = UIView()
...
}
}
複製代碼
而上面的方法確實解決了咱們的結構問題,而且確定地使咱們的代碼更多。自證閱讀起來更容易,它仍然將咱們各自的視圖組件與它們的呈現容器緊密地結合在一塊兒-ProductViewController在這種狀況下。ide
對於目前只在單個視圖控制器中使用的一次性視圖來講,這可能不是一個問題,可是對於更通用的UI代碼來講,若是咱們可以輕鬆地在代碼庫中重用咱們的各類配置,那就太好了。函數
一種不須要定義任何新類型的方法是使用靜態工廠法-使咱們可以以既易於定義又易於使用的方式封裝配置每一個視圖的方式:佈局
extension UIView {
static func buyButton(withTarget target: Any, action: Selector) -> UIButton {
let button = UIButton(type: .custom)
button.setImage(.buy, for: .normal)
button.backgroundColor = .systemGreen
button.addTarget(target, action: action, for: .touchUpInside)
return button
}
}
複製代碼
靜態工廠方法的優勢在於,它們使咱們可以以相似枚舉的方式調用API-使用SWIFT很是輕量級的點語法..若是咱們也定義相似的方法來建立一個購買按鈕,而後咱們就能夠viewDidLoad簡單地看上去以下所示的實現:
class ProductViewController: UIViewController {
let product: Product
...
override func viewDidLoad() {
super.viewDidLoad()
view.addSubview(.buyButton(
withTarget: self,
action: #selector(buyButtonTapped)
))
view.addSubview(.productDetailView(
for: product
))
view.addSubview(.relatedProductsView(
for: product.relatedProducts,
delegate: self
))
}
}
複製代碼
真乾淨!局部變量已經消失,咱們仍然能夠將全部視圖設置代碼都放在一個方法中,同時也給了咱們更大程度的封裝和徹底的可重用性,由於咱們如今能夠在須要的地方輕鬆地構造上述類型的視圖。
雖然上面的方法對於UI配置代碼很是有用,在理想狀況下應該在整個代碼庫中保持不變,好比設置公共組件,可是咱們還常常須要以一種更加特定於上下文的方式擴展這些配置。
例如,咱們可能須要對視圖應用某種形式的佈局,更新或綁定某些狀態到視圖,或者根據它們所使用的特性定製它們的行爲或外觀。
爲了更容易地作到這一點,讓咱們擴展UIView使用方便的API-在將給定視圖添加爲子視圖後執行閉包,以下所示:
extension UIView {
@discardableResult
func add<T: UIView>(_ subview: T, then closure: (T) -> Void) -> T {
addSubview(subview)
closure(subview)
return subview
}
}
複製代碼
有了上述方法,咱們如今能夠繼續使用漂亮的點語法來建立視圖,同時仍然容許咱們應用特定於上下文的配置,例如,爲了添加一組自動佈局約束:
class ProductViewController: UIViewController {
...
override func viewDidLoad() {
super.viewDidLoad()
view.add(.buyButton(
withTarget: self,
action: #selector(buyButtonTapped)
), then: {
NSLayoutConstraint.activate([
$0.topAnchor.constraint(equalTo: view.topAnchor),
$0.trailingAnchor.constraint(equalTo: view.trailingAnchor)
...
])
})
...
}
}
複製代碼
雖然上面的語法可能須要一段時間才能適應,但它確實給了咱們兩個世界中最好的東西-咱們如今可以徹底封裝咱們的全局和本地配置,同時也強制執行必定程度的結構。它還容許咱們在不一樣的屏幕之間輕鬆地共享視圖組件,而無需定義任何新的視圖組件。UIView子類。
上述方法的另外一個有趣之處在於它如何開始使基於UIKit的命令式代碼更具聲明性,由於咱們再也不繼續在視圖控制器中設置咱們的各類視圖,而是聲明咱們但願使用什麼樣的配置。這讓咱們更接近於斯威夫特,這將有助於咱們在將來更好地過渡到那個新的世界。
只是比較一下咱們ProductViewController若是將其表示爲SwiftUI視圖,那麼在結構上,它可能與咱們上面基於UIKit的方法很是類似:
struct ProductView: View {
var product: Product
var body: some View {
VStack {
BuyButton {
// Handling code
...
}
ProductDetailView(product: product)
RelatedProductsView(products: product.relatedProducts) {
// Handling code
...
}
}
}
}
複製代碼
固然,這並不意味着咱們已經自動地使基於UIKit的代碼SwiftUI兼容,只須要修改它的結構-可是經過使用相似的思惟方式來組織咱們的各類視圖配置,咱們至少能夠開始對愈來愈多的聲明式編碼風格更加熟悉。
雖然咱們在開發基於UI的應用程序時編寫的大部分配置代碼傾向於以視圖層爲中心,但咱們的代碼庫的其餘部分也須要大量配置,特別是直接在系統API之上編寫的邏輯。
例如,假設咱們正在構建一個類型,用於解析字符串中的某種形式的元數據,而且咱們但願使用一個共享DateFormatter在這種類型的全部實例中。爲此,咱們可能定義一個私有靜態屬性,該屬性使用自動關閉:
struct MetadataParser {
private static let dateFormatter: DateFormatter = {
let formatter = DateFormatter()
formatter.dateFormat = "yyyy-MM-dd HH:mm"
formatter.timeZone = TimeZone(secondsFromGMT: 0)
return formatter
}()
func metadata(from string: String) throws -> Metadata {
...
}
}
複製代碼
雖然自動執行閉包很是方便,但使用它們來配置屬性經常會「推」類型的核心功能-這反過來又會使您更難快速瞭解類型實際上在作什麼。爲了緩解這個問題,讓咱們看看咱們是否可以在不犧牲可讀性的狀況下,使這種配置閉包儘量緊湊。
讓咱們首先定義一個名爲configure,它只接受任何對象或值,並容許咱們在閉包中應用任何類型的突變,使用inout關鍵字-以下所示:
func configure<T>(_ object: T, using closure: (inout T) -> Void) -> T {
var object = object
closure(&object)
return object
}
配置共享DateFormatter對於咱們的元數據解析器,咱們如今能夠簡單地將它傳遞給上面的函數,並使用$0閉包參數簡寫-留給咱們更緊湊的代碼,同時仍然保持可讀性:
struct MetadataParser {
private static let dateFormatter = configure(DateFormatter()) {
$0.dateFormat = "yyyy-MM-dd HH:mm"
$0.timeZone = TimeZone(secondsFromGMT: 0)
}
func metadata(from string: String) throws -> Metadata {
...
}
}
複製代碼
以上配置屬性的方法能夠說比自動執行閉包更容易理解,由於經過將調用添加到configure,咱們很是清楚地代表,伴隨閉包的目的其實是配置傳遞給它的實例。
就像任何與代碼樣式和結構相關的主題同樣,如何最好地配置對象和值也極可能始終是一個趣味問題。然而,無論咱們其實是如何配置代碼的-若是咱們可以以一種徹底封裝的方式來配置代碼,那麼這些配置就更容易重用和管理。
開始採用愈來愈多的聲明式編碼樣式和模式還能夠進一步幫助咱們輕鬆地過渡到SwiftUI並結合起來,即便咱們可能預計須要一兩年時間才能真正開始採用這些框架。能夠說,聲明式編程與API和語法同樣,都是關於思惟方式的。
你認爲如何?當前如何配置視圖和其餘值和對象?讓我知道-連同你的問題,請經過加咱們的交流羣 點擊此處進交流羣 ,來一塊兒交流或者發佈您的問題,意見或反饋