百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 技术文章 > 正文

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

ccwgpt 2024-11-03 12:42 145 浏览 0 评论

作者|姜沂(倾寒)

出品|阿里巴巴新零售淘系技术部

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

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

1、SwiftUI初体验 (点击阅读)

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

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

背景

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

整个淘系也有一个用来观察稳定性数据的应用,通常来说数据大盘是比较适合在 PC 浏览器中展示的,我们也在 PC 中使用了多年,但是淘宝 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 选择界面中,图片资源都来源自网络。

示例代码如下 :

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 的基本思想核步骤是:

1、整个页面甚至 APP 是一个巨大的状态机,有一个状态存储 Store ,在某个时刻处于某种状态。

2、状态在页面表达中是一个简单的树型结构,在 SwiftUI,对应的 就是 View Tree。

3、View 操作不能直接修改状态,只能通过发送 Action, 间接改变 Store。

4、Reducer 通过 Action 加上 oldState 获取 newSatete。简单来说就是 State = f(action+oldState)。

附上一份 阮一峰的Redux入门教程的示例图:

这套风格在前端大型项目中已经了验证,可以比较清晰的表达用户事件交互和状态管理。

目前由于 SwiftUI 中 ViewCtonroller的消失,加上方便的 ObserableObject 和 EmviromentObject 。

SOT 项目一期暂未采用,在二期项目中会探索合适的架构设计。

项目总结

此项目在短短的 10 个工作日内就能完成,不得不说 SwiftUI 的开发效率真的惊人,虽然目前还有一些 Bug ,但是相信在未来,SwiftUI 会是 Apple 平台 UI 布局的解决办法,关于 SwiftUI 如何在淘系落地业务,还在持续探索中。

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

作者:姜沂(倾寒) | 阿里巴巴新零售淘系技术部

相关推荐

如何让老师看完文章后还啧啧称奇?满分作文有框架,这3点是关键

历年来语文考试中,作文的分数都占着相当大的一个比例,同时作文也是最容易拉开差距的一个模块。别人拿满分,而你却只有20分左右,分数的差距就是这样拉开的。作文想拿很高的分数却是不容易,但不是完全不可能的事...

小学作文写作技巧和方法,万能公式框架法。家长收藏

小学语文老师用心整理,将写作框架编成万能公式。作文的写作是语文学习中的重要一环,从小学到初中再到高中,作文一直是语文考试中占分比重最高的部分。小学阶段的语文写作相对来说比较简单,主要是打基础,但是很多...

如何用爆款改写技巧提升文章吸引力结构重塑:打破原文框架悬念前

如何用爆款改写技巧提升文章吸引力?结构重塑:打破原文框架悬念前置法-在开头设置悬念或提出反常识的结论,吸引读者注意力。例如,将“接纳不完美是治愈的开始”改写为“天天逼自己当完美超人?别杠了!生活本就...

守护袁昆:是否有必要按框架去写文章,拍摄剪辑视频?

(文/守护袁昆)如今不管是写文章还是剪辑视频,越来越多的朋友喜欢用框架、用脚本,作为互联网创作者,我们是否有必要按框架去写文章,拍摄剪辑短视频呢?其实在内容创作过程中,是否使用框架始终是一个充满争议的...

揭秘!爆款文章的秘密:让读者无法抗拒的文章框架

说说我自己一开始写文章都会犯一个毛病,文章,通常是想到哪里,写到哪里,“管不住字儿”。这样往往会出现以下问题绊住我们继续写下去1、很容易,写着就跑偏了,最终出来的成品和最初的设想偏离很大2、会写得很慢...

想要写出逻辑清晰的文章,你需要掌握哪些写作结构

想要写出好文章,就必须要了解文章的结构和框架。一篇文章结构清晰,读者就很容易跟上作者的思路,看出文章的重点内容。如果你对新媒体的文章有进行过研究,你就会发现很多公众号的文章结构都是类似的。所以你需要掌...

写作结构拆解:从选题到框架,如何让文章说服力翻倍?

你有没有想过,为什么有些文章能轻松获得很高的阅读量,而你的文章却始终无人问津?其实,写出爆款文章并没有想象中那么难。关键在于选择一个吸引人的主题,并用一个清晰的写作框架,通过2-3个有力的子观点支撑...

申论怎么写?结构怎么弄?这篇文章是最基本的好文章框架

写作总被吐槽逻辑混乱?三步搭建框架法,新手也能写出漂亮文章

一、结构决定论:信息传递的桥梁写作者和读者之间始终存在一道隐形的鸿沟。作者脑海中的想法如同一棵枝繁叶茂的大树,但直接倾倒给读者时,往往只剩下零散的枝叶,信息在传达过程中的丢失,作者输出的和读者读到的不...

如何搭建文章框架:新手写作者很有必要看

#头条深一度-深度阅读计划#见面好呀,我是潼臻~37岁,边上班边带娃的二胎妈妈藏起生活里的琐碎,期望你我都能遇到更好的自己~~~~~~~~~~~~~~~~~~果然多读书是可以真切学习到有用的东西最近把...

模型上下文协议(MCP)的可视化向导

最近,模型上下文协议(MCP)引起了广泛关注。你一定听说过它。今天,让我们来了解一下它是什么。直观地说,MCP就像是AI应用的USB-C接口。正如USB-C提供了一种标准化的方式,用于将...

97个人放一页PPT!用对Smartart架构图直接开挂!

从讯飞出差回来的路上,在高铁上看到一条微博,关于红楼梦人物的思维导图:下面很多家长说,帮助很大,能帮助上学的孩子梳理清人物关系,我看了下,清楚是清楚,但真的不太好看!作为一名PPT博主,我就顺带在高...

技术架构规范与实践(二)架构设计示例

1.逻辑架构1.1领域概念1.2宏观应用架构1.3宏观流程1.4微服务拆分与分层2.技术架构3.开发架构3.1后端技术栈分类名称版本描述后端框架/组件JavaJdk8(openjdk:8u342)后...

倾斜柱模板安装加固

1、适用范围:呈梯形逐层向内侧倾斜的框架柱。2、工艺流程:定位放线-配模-校正梁位置-安装加固。3、工艺方法:(1)定位放线首先现场技术管理人员对每颗不同标高的梁底、梁中边线及200mm控制线进行平面...

地基与基础工程、主体工程节点构造

#去班味吧#桩头凿除环切法工艺说明:1、根据桩头预留长度(深入承台10cm)放样桩顶标高,施工人员根据测量结果在基桩上用红油漆标注环切线;2、在切割线以上部分桩底,人工用钢钎打入桩底约15cm,打入时...

取消回复欢迎 发表评论: