歷時五天用 SwiftUI 作了一款 APP,阿里工程師如何作的?

導讀:自 2014 年蘋果發佈會發佈 Swift 以後, Swift 通過多年迭代,終於達到了 ABI 穩定版本,也意味着 Swift 作爲穩定的得語言,值得用在大型 APP, 用來生產環境中。

2019 年 WWDC , 又發佈了引發無數 Apple 平臺開發者歡呼的框架 SwiftUI, 據非官方消息,SwiftUI 框架孵化於 4 年前,做爲蘋果全平臺的 UI 系統的將來,數十名核心開發者,不許向其餘同事和外部披露任何關於此項目的任何信息,於今年釋出 Beta 版本後,從方方面面都透出出這是目前最強的移動端聲明式 編程框架,沒有之一(我的以爲)。在此實戰以前做者已經編寫了兩篇相關的文章。前端

一、SwiftUI初體驗 (點擊閱讀)程序員

二、系列文章深度解讀|SwiftUI 背後那些事兒 (點擊閱讀)算法

注: 項目代號爲企業內部私有,這裏使用 SOT 代指,意爲 「Swift on Taobao」。編程

背景

爲了研究 SwiftUI 在業務落地的可能性,咱們一直持續關注着 SwiftUI 的發展,但編程這種工做,向來是閱讀千編,不如實戰一次來的深入,恰好咱們有一個業務場景很是適合,那就是觀察穩定性大盤。api

整個淘系也有一個用來觀察穩定性數據的應用,一般來講數據大盤是比較適合在 PC 瀏覽器中展現的,咱們也在 PC 中使用了多年,可是淘寶 APP 是一個重運營類的 APP, 常常會有一些活動在節假日投放。瀏覽器

但此時值班人員或者相關人員可能在外,有時候可能並未攜帶電腦,這時候觀察穩定性狀況就很是窘迫,咱們迫切須要一款能夠隨身攜帶的APP,用於在緊急時刻觀察穩定性問題。網絡

項目耗時

這裏先給出時間結論,閉包

整個 SOT APP 耗時 1.3 人力,共 10 個工做日,整個 Swift 代碼 約 2800 行。架構

因爲這是一款必須工做在內網下的 APP, 接入內網鑑權沒有太多經驗,花費很多時間。app

總體下來大約有 5天左右的工做量花在調試接口,內網鑑權,原型設計部分,真正花在 SwiftUI 的部分約有 5 天,不得不說效率驚人。

項目設計

原型設計

作一款 APP 的最核心的部分是設計 APP 的功能,熟悉 SOT 的同窗,應該知道通常觀察穩定性主要是觀察數據大盤,聚合列表,分析聚合詳情,崩潰分析等比較重要的模塊。

落地 SwiftUI 的計劃預計 兩週,因此 SOT 一期只作作核心經常使用的部分。功能有了,那麼設計怎麼辦呢?

不要慫,做爲 9102 年的程序員,不會作 UI 怎麼能夠?因爲 Mac 平臺的 設計軟件 如 Keynote 和 Sketch 操做方式,基本和 StoryBoard (只會用代碼寫UI的同窗要回去從新學習下 StoryBoard 了 -)操做很是接近,花了一天時間簡單設計了下界面。

這裏刻意模仿 App Store的圓角和陰影設計,至於爲何?緣由就是負責的設計會讓 UI 代碼編寫變的更有挑戰性,若是隻是用系統原生的樣式,那麼遇見的難題就會大大減小,這樣的實戰到了實際的項目中,遇見的問題還會不少。

事實證實負責的 UI 設計對理解 SwiftUI 很是有價值,單單一個圓角,就花去了 6 個小時開發時間。

數據流管理

SwiftUI 是一個典型的單向數據流得聲明式 UI 編程框架, 在 SwiftUI 中 View 只是一個頁面的描述部分,SwiftUI 提供了多個數據流管理對象。

@State @Binding @Obserabled ,經過改變這些數據流的值,SwiftUI 系統能夠理解從新構建 View Tree, 並根據內部變化的範圍,有一層相似 Virtual Dom 的 ViewTree, 因爲 View 都是結構體,SwiftUI 每次構建這個 View Tree 都極快,這使得性能有很強的保障。

在實踐中也發現了一些Bug,但因爲目前 SwiftUI 還在高速變化,這些 Bug 都會在未來的版本中修復,這裏就不過多解釋了。

State

State 是 SwiftUI 中最經常使用的 代理屬性,經過對代理屬性的修改,SwiftUI 內部會自動的從新計算 View的 Body部分,構建 出View Tree。

注意 State 只能在當前 View 的 body 體裏面修改,因此 State 的適用場景就是隻影響當前 View 內部的變化的操做。

舉個實際的例子就是相似下載網絡圖片的部分,調用方一般提供一個 URL 和 Placeholder Image,在 SwiftUI 中使用 State 便可,由於此時的網絡圖變化隻影響當前 View。

如 APP 選擇界面中,圖片資源都來源自網絡。

示例代碼以下 :

struct NetworkImage: SwiftUI.View {

var urlPath: String?
var placeHodlerImage: UIImage
init(url path: String?, placeHolder: String) {
self.urlPath = path
self.placeHodlerImage = UIImage(named: placeHolder)!.withRenderingMode(.alwaysOriginal)
    }

    @State var downLoadedImage: UIImage? = nil
var body: some SwiftUI.View {
Image(uiImage: downLoadedImage ?? placeHodlerImage)
                  .resizable()
                  .aspectRatio(contentMode: .fill)
                  .onAppear(perform: download)
    }
func download() {
if let _ = downLoadedImage {
return
        }
_ = urlPath.flatMap(URL.init(string:)).map {
ImageDownloader.default.downloadImage(with: $0) { result in
switch result {
case .success(let value):
self.downLoadedImage = value.image.withRenderingMode(.alwaysOriginal)
case .failure(let error):
                    log.debug(error)
                }
            }
        }

    }
}

Binding

在傳統的命令式編程中,GUI 程序中最複雜的部分莫過於狀態管理,尤爲是多數據同步,一個數據存在於不一樣的 UI 組成部分,UI 各個部分的變化理論上都有同步,狀態量的變多加上異步的操做,會使程序的可讀性直線降低,而且伴隨着而來的就是 Bug ,而且不敢重構。

SwiftUI 給咱們的理念就是 Single source of truth, 簡單來講就是單一數據源,單一數據源是個很早就有的名詞/方法,可是不少系統並無給出很好的解決辦法,好比習慣 FRP 的同窗可能用 RX/RAC 裏面的 Singnal 去描述,可是 FRP 晦澀的概念,又使其在項目中的接入成本大大提升。

SwiftUI 給咱們的解決辦法就是 @Binding 。做者以前嘗試本身實現一個 Binding,實現起來就是一個簡單的閉包,經過閉包捕獲 Source of truth 的數據,同時 SwiftUI 會幫咱們自動刷新須要同步的界面。使咱們的數據同步變的的很是簡單。

實際例子如,系統提供的 Control(可操做的View) 的構造器基本都須要 @Binding 屬性,能夠自動的同步來自 API 調用方的數據源。

這裏舉個例子如 項目中的版本選擇和日期選擇功能,咱們須要講控件選擇的值同步給數據源。

struct DateVersionPanel : View {
@Binding var version: String
@State var input = ""
@Binding var date: Date
var title: String

@State private var showVersionPicker = false
@State private var showDatePicker = false

var dateFormatter: DateFormatter {
let formatter = DateFormatter()
        formatter.dateFormat = "yyyy-MM-dd"
return formatter
    }
private func showDate() {
        showDatePicker = true
    }
var body: some View {
        HStack(alignment: .center) {
            Text(title)
                .font(.system(size: 14))

            HStack(alignment: .center) {

                TextField(version.isEmpty ? "不區分版本" : version, text: $input, onEditingChanged: { (changed) in
                    log.debug("TextFieldonEditing: \(changed)")
                }) {
                    log.debug("TextFielduserName: \(self.version)")
                    self.version = self.input
                }
                .font(.system(size: 9))
                .padding(.leading, 20)
                .frame(width: 100, height: 20)

                NavigationLink(destination: VersionSelectView(version: $version)) {
                    Image("down_arrow")
                        .frame(width: 24, height: 14)
                        .aspectRatio(contentMode: .fill)
                }
                .offset(x: -20)

            }
            .frame(width: 100, height: 25)
            .border(Color.grayText, width: 0.5)
            .padding(.leading, 40)

            NavigationLink(destination: CalendarView(date: self.$date)) {

                HStack {
                    Text(dateFormatter.string(from: date) )
                        .font(.system(size: 9))
                        .padding(.leading, 10)
                    Image("down_arrow").padding(.trailing, 10)
                }
                .frame(width: 100, height: 25)
                .border(Color.grayText, width: 0.5)
                .padding(.leading, 40)
            }

        }
        .padding(.bottom, 10)

    }
}

ObservableObject

ObservableObject 在 Xcode11 Beta 4 以前叫 ObjectBinding , 這個類型是一個協議,要求咱們實現一個來自 Combine 框架的 Subject Subject 是一個和命令式編程世界交互的橋樑,是一個特殊的 Publisher,SwiftUI 內部會自動的訂閱這個 Subject,在 Subject 發送變化時 SwiftUI 會自動刷新數據。

ObservableObject 適用於多個 UI 組成部分同步數據,ObservableObject 取代了,Cocoa 框架基本編程風格 MVC 中控制器的角色,暫時項目中就叫他 ViewModel 吧。

@Published 是 Xcode11 beta5 以後新增的代理屬性,此屬性若是用在 ObservableObject 內,若是屬性發送了變化,會自動觸發 ObservableObject 的 objectWillChanged 的Subject變化,自動刷新頁面。

同時因爲 Combine 框架的支持,多個條件聯動變成了一個簡單的事情,在 SOT APP 項目中,就很是適合,好比數據大盤,有將近10幾個數據狀態,任何一個觸發,都會致使數據刷新。

class HomeViewModel: ObservableObject {

    @Published var isCorrectionOn = true

    @Published var isForce = false

    @Published var crashType = CrashType.crash

    @Published var pecision = Pecision.fifith

    @Published var quota = Quota.count

    @Published var currentDate = Date()

    @Published var currentVersion = ""

    @Published var comDate = Date().lastDay

    @Published var comVersion = ""

    @Published var refresh = true

    @Published var metric: Metric? = nil

    @Published var trends: [TrendItem] = []

    @Published var summary: Summary? = nil

    var api = SOTAPI()

    // MARK: - Life Cycle

    var cancels = [AnyCancellable]()

    init() {
        var cancel = $refresh.combineLatest($isForce, $isCorrectionOn)
            .combineLatest($crashType, $pecision, $quota)
            .combineLatest($currentDate, $currentVersion)
            .combineLatest($comVersion, $comDate)
            .debounce(for: 0.5, scheduler: RunLoop.main)
            .sink {[weak self](_) in

                self?.requestMetric()
                self?.requestTrends()
        }
        cancels.append(cancel)
        cancel = $refresh.sink{[weak self](_) in
             self?.requestSummary()
        }
        cancels.append(cancel)
    }
    func requestMetric() {}
    func requestTrends() {}
    func requestSummary() {}
}

Work with UIKit

因爲 SwiftUI 是一個封閉的系統,有時候一些控件還不夠豐富,爲了知足開發所用,還須要和一些已有的 UIKit的 UIView 混合編程,一方面能夠減小遷移的負擔,一方面能夠增長 SwiftUI 的能力。

在 SOT 項目中,因爲日期選擇是一個專業的庫,這裏採用了第三方庫,就涉及到於 UIKit 交互, SwiftUI 提供了一套很是簡單清晰的標準,能夠用在多個平臺上交互,並提供一致的表現力。

須要注意的是 UIViewRepresentable 的遵照者,是一個 View 容器,此容器會被建立屢次,若是內部有數據源須要通知,須要建立相應的 Coordinator 將當前的容器當作 View 傳遞進去,因爲 View 是結構體。

此時建立的是一個拷貝副本,因此 Coordinator 修改的部分,最好只是 ObservableObject Binding

struct CalendarView : UIViewRepresentable {

    @Environment(\.presentationMode) var presentationMode

    @Binding var date: Date

init(date: Binding<Date>) {
self._date = date

    }

func makeUIView(context: UIViewRepresentableContext<CalendarView>) -> UIView {
let view = UIView(frame: UIScreen.main.bounds)
        view.backgroundColor = .backgroundTheme

let height: CGFloat = 300.0
let width = view.frame.size.width
let frame = CGRect(x: 0.0, y: 0.0, width: width, height: height)
let calendar = FSCalendar(frame: frame)
        calendar.locale = Locale.init(identifier: "ZH-CN")
        calendar.delegate = context.coordinator
        context.coordinator.fsCalendar = calendar
        calendar.backgroundColor = UIColor.white
        view.addSubview(calendar)

return view
    }

func makeCoordinator() -> CalendarView.Coordinator {
Coordinator(self)
    }

func updateUIView(_ uiView: UIView, context: UIViewRepresentableContext<CalendarView>) {
        log.debug("Date")

        context.coordinator.fsCalendar?.select(date)

    }

func dismiss() {
        presentationMode.wrappedValue.dismiss()
    }

class Coordinator: NSObject, FSCalendarDelegate {
var control: CalendarView
var date: Date
var fsCalendar: FSCalendar?
init(_ control: CalendarView) {
self.control = control
self.date = control.date
        }
func calendar(_ calendar: FSCalendar, didSelect date: Date, at monthPosition: FSCalendarMonthPosition) {
self.control.date = date
        }

    }
}

架構

Combine

在此項目中使用了最基本的 Combine 操做,因爲項目一期主要是爲了探索 SwiftUI ,因此並未對架構模式作精細的設計,能夠觀察到,ViewModel,內部仍是有訂閱,發送網絡請求,最後同步數據的操做,這種編碼方式,仍是典型的命令式編程風格,此部分會在項目二期逐漸探索中修改成響應式風格。

Redux/Flux

SwiftUI 是一個單向數據流框架,在此以前,大前端已經有 React, Flutter , Reactive Native,等比較流行的框架。在這些單向數據流得框架下,Redux 做爲一種比較流行的狀態管理的架構風格,已經通過多方面的驗證,SwiftUI 對於Redux也是比較適用的。

Redux 的基本思想核步驟是:

一、整個頁面甚至 APP 是一個巨大的狀態機,有一個狀態存儲 Store ,在某個時刻處於某種狀態。

二、狀態在頁面表達中是一個簡單的樹型結構,在 SwiftUI,對應的 就是 View Tree。

三、View 操做不能直接修改狀態,只能經過發送 Action, 間接改變 Store。

四、Reducer 經過 Action 加上 oldState 獲取 newSatete。簡單來講就是 State = f(action+oldState)。

附上一份 阮一峯的Redux入門教程的示例圖:

這套風格在前端大型項目中已經了驗證,能夠比較清晰的表達用戶事件交互和狀態管理。
目前因爲 SwiftUI 中 ViewCtonroller的消失,加上方便的 ObserableObject 和 EmviromentObject 。

SOT 項目一期暫未採用,在二期項目中會探索合適的架構設計。

項目總結

此項目在短短的 10 個工做日內就能完成,不得不說 SwiftUI 的開發效率真的驚人,雖然目前還有一些 Bug ,可是相信在將來,SwiftUI 會是 Apple 平臺 UI 佈局的解決辦法,關於 SwiftUI 如何在淘系落地業務,還在持續探索中。

目前此項目已在集團內部開源。

One More Thing

淘寶基礎平臺團隊正在舉行2019實習生(2020年畢業)和社招招聘,崗位有iOS Android客戶端開發工程師、Java研發工程師、C/C++研發工程師、前端開發工程師、算法工程師。

歡迎投遞簡歷至(君展): junzhan.yzw@taobao.com



本文做者:姜沂(傾寒)

閱讀原文

本文爲雲棲社區原創內容,未經容許不得轉載。

相關文章
相關標籤/搜索