SwiftUI開發總結(一) 這大概是最容易理解的combine

語言: CN / TW / HK

最近在自研一個新的項目,在考慮使用的技術棧時,調研了許多,比如react-native,flutter,以及端原生的oc跟swift,但是最終選擇了swiftUI + combine,之所以有如此決定,一方面是希望可以完善自己對於iOS系統開發的技術完整性,另一方面希望瞭解iOS開發未來的一個技術方向,那麼閒言少敍切入正題。什麼是swiftUI?

SwiftUI是什麼?

更準確地解釋可以移步到蘋果開發者中心,概念性的東西,這裏不做過多介紹。通過對其的一段時間開發,個人總結,swiftUI絕不是swift+UI這麼簡單的概念,從設計上,swiftUI十分趨近於web前端,蘋果似乎有意將swift做得更加簡化,swiftUI也是將開發者得注意力從之前無窮盡地修改UI轉到更加關注其app內部的邏輯處理。

簡而言之,如果你的項目需求崇尚極簡主義,注重邏輯而不採用複雜且臃腫的交互設計,那麼swiftUI絕對是值得一試的技術手段。

對於swiftUI的各個組件,官方都給出的事例,這裏先不做研究,之後我會在自研項目上線之後,對於其中所用到的組件,遇到的問題,進行逐步彙總,其中會有一些在國內論壇並不容易找到的問題答案。但是現在我們先從基礎數據入手,我們先了解一下什麼是combine

如何理解combine

談到combine不得不提的就是swift中的屬性修飾器-- @propertyWrapper

@propertyWrapper

實話實説,如果你還沒有用過propertyWrapper,那一定要嘗試的使用一下,因為這個功能確實太好用了,這裏引用官方解釋的一段話:

For example, if you have properties that provide thread-safety checks or store their underlying data in a database, you have to write that code on every property. When you use a property wrapper, you write the management code once when you define the wrapper, and then reuse that management code by applying it to multiple properties.

塑料翻譯:

例如,如果你要為數據存儲的一些基礎屬性提供線程安全或者存儲它們,你不得不在每一個屬性中都寫同樣的方法,這會讓代碼變得十分噁心。但是當你使用propertyWrapper時,當你為操作代碼定義了一個修飾器,那麼這些操作代碼會應用在它修飾的多個屬性中。

上面的解釋,是我在學習propertyWrapper所能看到的最為通俗的解釋。下面也是提供了一段官方代碼,幫助理解。

@propertyWrapper struct TwelveOrLess {    private var number = 0    var wrappedValue: Int {        get { return number }        set { number = min(newValue, 12) }    } } struct SmallRectangle {    @TwelveOrLess var height: Int    @TwelveOrLess var width: Int } ​ var rectangle = SmallRectangle() print(rectangle.height) // Prints "0" ​ rectangle.height = 10 print(rectangle.height) // Prints "10" ​ rectangle.height = 24 print(rectangle.height) // Prints "12"

簡單解釋一下上面的代碼,聲明一個屬性修飾器TwelveOrLess,內部的邏輯是輸出的屬性都比12小,如果大於12則輸出12。

下面的SmallRectangle包裝了兩個屬性heightwidth,當我們為這兩個屬性賦值,再調用get方法時,可以看到,我們的邏輯代碼生效了,輸出數字被控制在小於或等於12的值。

無需多餘代碼,屬性修飾器給了swift開發者更多的想象空間。

簡單的介紹了一下propertyWrapper,接下來我們迴歸正題,繼續説回combine

Publishers 與 subscribers

如果想使用combine就不得不瞭解兩個概念,Publishers 與 subscribers。如果你之前有做過Rxswift,或者對於RAC有一定了解的話,對於這兩個概念一定不陌生。即便是對於上述框架並不瞭解,想要理解Publisherssubscribers也不難,因為可以把它理解為觀察者模式中的發送者與監聽者。

由於官方的事例採用的是通知中心的demo,這在我初學combine時給我帶來了極大的困擾,因此,本文的事例並不打算採用官方事例,避免給讀者帶來同樣的困擾。而是通過一段自己的部分開源代碼對其進行講解。

struct XXAssetModel{    var id = UUID()    var currency: Int } class XXResourceViewModel: ObservableObject {      @Published var myAsset: XXAssetModel = UserData.userCurrency fileprivate func editCurrency() {        myAsset.currency = myAsset.currency + 10      } } struct ConverterView : View {    @ObservedObject var viewModel = XXResourceViewModel() var body: some View {         return Text(viewModel.myAsset.currency) } }

這個例子相對簡單,便於入門,我們來看一下,首先,在XXResourceViewModel中聲明一個被 @Published修飾的屬性myAsset,因為我們剛剛已經介紹過屬性修飾器了,所以應該不難理解這個修飾的作用。下面引用官方的一段話。

Add the @Published annotation to a property of one of your own types. In doing so, the property gains a publisher that emits an event whenever the property’s value changes.

將 @Published 註釋添加到類中的屬性。這樣做使該屬性成為了一個publisher,只要該屬性的值發生變化,publisher就會發出一個事件。

回到上面一段代碼,publisher就像是電影《風聲》中的老鬼,他的責任就是將自己獲取的情報傳遞給他的上級老槍,那麼,誰是subscribers老槍。上例中,Text控件就是老槍。他與viewModel.myAsset.currency形成了一種綁定關係,一旦viewModel.myAsset.currency發生改變,Text接收到信號之後,就會做出對應行動。

看到這有沒有人在想到了一種設計模式?沒錯,就是MVVM。

Subject的使用

combine作為蘋果官方推出的響應式編程框架,很大程度的融合了其他響應式編程框架的優點。除了這種自動發送信號的publisher,還有一種可以主動發送信號的Subject,看一下下面的例子。

``` final class UserData: ObservableObject {    let objectWillChange = PassthroughSubject()      var allCurrencies: [Currency] {        didSet {            objectWillChange.send(self)        }    } }

struct ConverterView : View {    @EnvironmentObject var userData: UserData var body: some View {         return list(userData.allCurrencies) {         Item()         } } } ```

UserData作為信號發送方,沒有采用publisher的方式,而是利用重寫set方法對其進行了主動發送。

當然如何選擇要具體問題,具體分析,蘋果提供了相對豐富的方法,應對不同的使用場景。

Operators的使用

當然不只是監聽信號這麼簡單,蘋果還為開發者提供了多種Operators,意在更加輕鬆的讓開發者完成函數式編程。代碼如下:

static func request(_ kind: XXKind, _ queryItems: [URLQueryItem]?) -> AnyPublisher<XXResource, Error> {        guard var components = URLComponents(url: baseUrl.appendingPathComponent(kind.rawValue), resolvingAgainstBaseURL: true)            else { fatalError("Couldn't create URLComponents") }        components.queryItems = queryItems ​        let request = URLRequest(url: components.url!) ​        return apiClient.run(request)            .map(.value) // 為XXResource中定義的實際值            .eraseToAnyPublisher()    }

上述例子中,將返回的數據,通過map()函數進行了過濾操作,提取出返回值中value的數據,並將其發送給subscribers。如圖所示:

op_map.svg

總結

本文作為SwiftUI學習的第一章,着重的介紹了combine及其使用方法。文章主要以實戰為主,少了許多花裏胡哨的介紹跟修飾,希望可以讓同學們可以更加快速容易的理解。如開頭所説,後續還會總結一下swiftUI中控件在使用時,與正常UIKit不太一樣的坑。畢竟國內對於swiftUI的學習並不多,所以希望可以跟同學們一同進步。

respect!!!