假設你們已對 Swift 語法有基本瞭解,而且已經上手體驗過。雖在工做中可能並不會當即介入 SwiftUI 和 Combine,但經過對這兩個框架的學習和使用能夠從側面給咱們提供一個優化的思路,從以往「流程化」和「命令式」的編程思惟中轉變出來,提高開發效率。git
這次分享在於快速對 SwiftUI 和 Combine 框架有一個基本認識,經過一個常規業務 demo 來驗證 SwiftUI 和 Combine 提高效率的可能性,分享我在學習 SwiftUI 和 Combine 遇到問題和值得開心的地方。github
UIKit
、Core Graphics
、Core Text
等系統框架封裝了完整而優美的 DSL。ViewController
概念。「更多菜單」是一個幾乎全部 App 裏都會去實現的一個組件,其承擔了非主業務,但又十分重要的二級工具類業務入口。若是經過常規的 UIKit
的思路去作,大體的實現思路是這樣的:編程
UIWindow
或 UIViewController
,做爲菜單視圖的容器;UITableView
或循環組件的方式建立出具體的菜單視圖;若是隻想用 SwiftUI 去實現的化,在 SwiftUI 萬物皆 View
,沒有 ViewController
的概念,因此這裏的容器就回落到了 View
身上。包裝一個視圖容器,可能會是這樣的:·swift
struct MASSquareMenuView: View {
var body: some View {
GeometryReader { _ in
// ......
}
.frame(minWidth: UIScreen.main.bounds.width,
minHeight: UIScreen.main.bounds.height)
}
}
複製代碼
MASSquareMenuView
充當了底層的 ViewController
角色。View
其實是個結構體。若是 body
裏返回不肯定的類型,DSL 解析會失敗,例如同時返回兩個 View
,經過 if-else
判斷來返回不一樣的 View
,這種狀況會被拒絕執行。若是咱們就是想經過一個標識位去判斷當前要返回的究竟是什麼視圖,須要使用 @State
關鍵詞修飾的一個變量去操做。網絡
struct MASSquareHostView: View {
var body: some View {
NavigationView {
// ...
ZStack {
MASSquareMenuView {
// ......
}
}
// ...
}
}
}
複製代碼
「鏈式調用的過程」被稱爲是 SwiftUI
中 View
的 modifier
,每一個 modifier
的調用結束後,返回給下一個 modifier
有兩種狀況:第一種狀況只是對 View
(如 Text
)的 font
等與佈局無關的方法,返回給下一個 modifier
相同類型的 View
;第二種狀況對 View
的佈局產生了修改,如調用了 padding
等方法,返回給下一個鏈式調用的 modifier
是一個從新包裝過的全新 View
。閉包
其實我以爲這跟以前用的鏈式調用庫從概念上是同樣的道理,有些鏈式方法的調用必須是依賴於某些方法的先執行,好比自定義 Image
這個標籤的大小,必須先設置 resizeable
才能設置 frame
,不然失效。app
SwiftUI 的 API 設計哲學,強迫我去思考對外公開的組件所提供的定製化功能,以前跟 mentor 討論過,相似這種 ContextMenu
是封裝成一個 UI 組件仍是一個業務組件,最後決定仍是把這個菜單組件作成一個 UI 組件。框架
「更多菜單」的數據源通過調整,最終寫出了一個基本符合 SwiftUI 風格的 API,基本符合是由於多了一個煩人的 Group
,以前已經說過,SwiftUI 不接受多個視圖返回,若是確實要返回多個視圖的「組合視圖」,須要手動對這些視圖使用 Group
包裝成一個 View
進行返回。函數式編程
引起一個新的問題,怎麼接收一組 View
,經過對一個組件傳遞一串 View
來徹底自定義菜單組件裏的內容,使用 UIKit
的話我可能會這麼作:函數
PJPickerView.showPickerView(viewModel: {
$0.titleString = "感情狀態"
$0.pickerType = .custom
$0.dataArray = [["單身", "約會中", "已婚"]]
}) { [weak self] finalString in
if let `self` = self {
self.loveTextField.text = finalString
}
}
複製代碼
但在 SwiftUI 中,因目前版本(beta 7)受限於不支持返回不肯定的內容,所以,個人設計爲:
MASSquareMenuView(isShowMenu: self.$showingMenuView) {
Group {
MASSquareMenuCell(itemName: "筆記",
itemImageName: "square.and.pencil") {
FirstView()
}
MASSquareMenuCell(itemName: "廣場",
itemImageName: "burst") {
SecondView()
}
// ...
}
}
複製代碼
其中 itemName
和 itemImageName
都可經過 ForEach
來完成,目前還沒找到一個能夠完成動態跳轉的比較好的方式。
如何把多個子 View
經過以上相似這種相對優雅的方式進行視圖組合?個人這種封裝方法思想來源於 List
系統組件的使用方式:
List {
// PJPostView(post: post)
ForEach(posts) { post in
PJPostView(post: post)
}
}
複製代碼
先來看 List
這個系統組件的定義:
@available(iOS 13.0, OSX 10.15, tvOS 13.0, watchOS 6.0, *)
public struct List<SelectionValue, Content> : View where SelectionValue : Hashable, Content : View {
@available(watchOS, unavailable)
public init(selection: Binding<Set<SelectionValue>>?, @ViewBuilder content: () -> Content)
@available(watchOS, unavailable)
public init(selection: Binding<SelectionValue?>?, @ViewBuilder content: () -> Content)
public var body: some View { get }
public typealias Body = some View
}
複製代碼
發現有一個全新的關鍵詞 @ViewBuilder
,要求被 @ViewBuilder
修飾的 content
閉包返回的是個 Content
。Content
的定義以下:
@available(iOS 13.0, OSX 10.15, tvOS 13.0, watchOS 6.0, *)
public protocol ViewModifier {
associatedtype Body : View
func body(content: Self.Content) -> Self.Body
typealias Content
}
複製代碼
也就是說,content
裏的能夠被「包含」的對象,只要是 View
類型便可,這一點很完美,但 @ViewBuilder
是什麼?文檔中的定義爲:
@available(iOS 13.0, OSX 10.15, tvOS 13.0, watchOS 6.0, *)
@_functionBuilder public struct ViewBuilder {
/// Builds an empty view from an block containing no statements, `{ }`.
public static func buildBlock() -> EmptyView
/// Passes a single view written as a child view (e..g, `{ Text("Hello") }`) through
/// unmodified.
public static func buildBlock<Content>(_ content: Content) -> Content where Content : View
}
複製代碼
終於看出了點端倪,經過 @ViewBubilder
修飾的 View
能夠接收多個組合視圖,從官方文檔中,咱們能夠得知最多同時單個組件可承載的最大子組件數爲 10 個。若是超過 10 個子組件,官方推薦的作法是再抽象進行封裝成一個新的組件。
大體的菜單 Cell 實現細節爲:
struct MASSquareMenuView<Content: View>: View {
@Binding var isShowMenu: Bool
var content: () -> Content
var body: some View {
GeometryReader { _ in
VStack(alignment: .leading) {
self.content()
}
// ......
}
}
}
複製代碼
對這個 MunuView
初始化的時候,不給 init
方法,補齊 content
,而且由於在 Swift 5.x 中最後一個閉包可省略,這就出現了以前的 API 格式。
這裏引入 CoreData
的意義只是可以給了一個相對穩定的數據來源,目前暫時還未結合網絡請求進行驗證。
這個例子想要完成的事情有:
CloudKit
備份。實話實說,完成這整套無縫的邏輯下來,花了很多時間。主要的時間耗費在理解和適應 SwiftUI 與 Combine 之間的聯合關係,常常在思考如何合理有效的組織各個數據源去控制組件的交互。其中必定要死死握住的就是「單一數據源」,把可以引起某個組件產生某種行爲的源頭限制在同一個數據對象自己。
其中,最爲經常使用的三個狀態修飾符爲:
@State
;@Binding
;@ObservedObject
。在這個例子中的使用方式爲:
@State private var showingSheet = false
@Binding var text: String
@ObservedObject var aritcleManager = AritcleManager()
複製代碼
使用 @State
來修飾 showingSheet
變量做爲控制「輸入框」是否彈出的標識位,使用 @Binding
來修飾 text
從「彈出框」中引用出用戶輸入的內容,使用 @ObservedObject
修飾 aritcleManager
對象,其做爲鏈接首頁數據交互的中樞。
AritcleManager
做爲首頁數據處理的中樞,其承擔了「輸入」和「搜索」兩個任務,而爲了保證單一數據源的理念,引入了 @Published
修飾其內部持有的真正數據源 articles
,每當 articles
發生改變時,都向外部訂閱者發佈通知。
class AritcleManager: NSObject, ObservableObject {
// 寫法 1
var objectWillChange: ObservableObjectPublisher = ObservableObjectPublisher()
// 寫法 2
@Published var articles: [Article] = []
}
複製代碼
與 CoreData 的交互使用了 NSFetchedResultsController
來進行,這部分能夠替換成網絡交互部分的方法:
// MARK: NSFetchedResultsControllerDelegate
extension AritcleManager: NSFetchedResultsControllerDelegate {
func controllerDidChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
articles = controller.fetchedObjects as! [Article]
// 寫法 2 可省略,不須要主動觸發發佈
objectWillChange.send()
}
}
複製代碼
在「首頁」中的初始化和交互操做爲:
struct MASSquareHostView: View {
@ObservedObject var aritcleManager = AritcleManager()
var body: some View {
NavigationView {
MASSquareListView(articles: self.$aritcleManager.articles,
showingSheet: self.$showingSheet) {
self.aritcleManager.articles[$0].delete()
}
}
}
}
複製代碼
從寫法 1 發現了一個奇怪的地方(寫法 2 可暫時理解爲是寫法 1 的語法糖), ObservableObjectPublisher
是怎麼作到「自動監聽」的呢?來看看其定義:
@available(iOS 13.0, OSX 10.15, tvOS 13.0, watchOS 6.0, *)
final public class ObservableObjectPublisher : Publisher {
public typealias Output = Void
public typealias Failure = Never
public init()
final public func receive<S>(subscriber: S) where S : Subscriber, S.Failure == ObservableObjectPublisher.Failure, S.Input == ObservableObjectPublisher.Output
final public func send()
}
複製代碼
其中 ObservableObjectPublisher
是繼承自 Publisher
類,而 Publisher
是 Combine 中三大支柱之一,具體定義爲:
@available(OSX 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *)
public protocol Publisher {
associatedtype Output
associatedtype Failure : Error
func receive<S>(subscriber: S) where S : Subscriber, Self.Failure == S.Failure, Self.Output == S.Input
}
複製代碼
Publisher
,負責發佈事件;Operator
,負責轉換事件和數據;Subscribe
,負責訂閱事件。這三者都是協議,且都是 @propertyWrapper
的具體應用。
Publisher
最主要的工做其實有兩個:發佈新的事件及其數據,以及準備好被 Subscriber
訂閱。Output
及 Failure
定義了某個 Publisher
所發佈的值的類型,以及可能產生的錯誤 的類型。
Publisher
能夠發佈三種事件:
Output
的新值:這表明事件流中出現了新的值;Failure
的錯誤:這表明事件流中發生了問題,事件流到此終止;Publisher
的這三種事件不是必須的,也就是說,Publisher
可能只發一個或者一個都不發,也有可能一直在發,永遠不會中止,這就是無限事件流,還有可能經過發出 failure
或者 finished
的事件代表不會再發出新的事件,這是有限事件流。
每一個 Operator
的行爲模式都同樣:它們使用上游 Publisher
所發佈的數據做爲輸入,以此產生的新的數據,而後自身成爲新的 Publisher
,並將這些新的數據做爲輸出,發佈給下游,這樣至關於獲得了一個響應式的 Publisher
鏈條。
當鏈條最上端的 Publisher
發佈某個事件後,鏈條中的各個 Operator
對事件和數據進行處理。在鏈條的末端咱們但願最終能獲得能夠直接驅動 UI 狀態的事件和數據。這樣,終端的消費者能夠直接使用這些準備好的數據。
問題一:其不適合直接使用在當前「樹形操做流」的工程裏,用戶對 App 的操做以目前的狀況來看是一種「樹形結構」,但 SwiftUI 與 Combine 的強依賴,致使了必須寫大量的兼容代碼去兼容 Combine 的開發哲學,但 Combine 自身的「線性開發模型」與如今的模型是衝突且難以兼容的。因此,問題不只僅只是在對系統版本的依賴上這麼簡單而已。
問題二:目前 SwiftUI 並不具有多行文本組件,只能經過 UITextView
包一層,包完了之後在模擬器上一跑就卡死,只能走真機。換句話說,若是是從零開始想要搞一個大事情,所有基於 SwiftUI 去 UI 表現層上的內容,幾乎不可能,很是很是痛苦。
這兩個問題在我看來都是可解的,尤爲是問題二,正是由於其可以完美的無縫兼容 UIKit
,在接入成本上能夠忽略不計,反而是問題一帶來的影響會更大,雖然 Combine 與如今 Rx 等一套有殊途同歸之處,但對已有業務的改形成本不小,好比埋點,可能會須要從以往的跟隨視圖的變化變爲跟隨數據流。
SwiftUI 與 SB 和 xib 同樣,我認爲其只是個 UI 表現層,且能夠認爲是用於佈局等最上層的操做,對待其應該使用 SB 和 xib 的思路去使用。
歷時五天用 SwiftUI 作了一款 APP,阿里工程師如何作的?