历时五天用 SwiftUI 作了一款 APP,阿里工程师如何作的?

做者|姜沂(倾寒) 
出品|阿里巴巴新零售淘系技术部前端

导读:自 2014 年苹果发布会发布 Swift 以后, Swift 通过多年迭代,终于达到了 ABI 稳定版本,也意味着 Swift 作为稳定的得语言,值得用在大型 APP, 用来生产环境中。程序员

2019 年 WWDC , 又发布了引发无数 Apple 平台开发者欢呼的框架 SwiftUI, 据非官方消息,SwiftUI 框架孵化于 4 年前,做为苹果全平台的 UI 系统的将来,数十名核心开发者,不许向其余同事和外部披露任何关于此项目的任何信息,于今年释出 Beta 版本后,从方方面面都透出出这是目前最强的移动端声明式 编程框架,没有之一(我的以为)。在此实战以前做者已经编写了两篇相关的文章。编程

一、SwiftUI初体验 (点击阅读)api

二、系列文章深度解读|SwiftUI 背后那些事儿 (点击阅读)浏览器

注: 项目代号为企业内部私有,这里使用 SOT 代指,意为 “Swift on Taobao”。网络

背景

为了研究 SwiftUI 在业务落地的可能性,咱们一直持续关注着 SwiftUI 的发展,但编程这种工做,向来是阅读千编,不如实战一次来的深入,恰好咱们有一个业务场景很是适合,那就是观察稳定性大盘。闭包

整个淘系也有一个用来观察稳定性数据的应用,一般来讲数据大盘是比较适合在 PC 浏览器中展现的,咱们也在 PC 中使用了多年,可是淘宝 APP 是一个重运营类的 APP, 常常会有一些活动在节假日投放。架构

但此时值班人员或者相关人员可能在外,有时候可能并未携带电脑,这时候观察稳定性状况就很是窘迫,咱们迫切须要一款能够随身携带的APP,用于在紧急时刻观察稳定性问题。app

项目耗时

这里先给出时间结论,框架

整个 SOT APP 耗时 1.3 人力,共 10 个工做日,整个 Swift 代码 约 2800 行。

因为这是一款必须工做在内网下的 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 如何在淘系落地业务,还在持续探索中。

目前此项目已在集团内部开源。


原文连接 本文为云栖社区原创内容,未经容许不得转载。

相关文章
相关标签/搜索