【AICon】 如何构建高效的 RAG 系统?RAG 技术在实际应用中遇到的挑战及应对策略?>>> 了解详情
写点什么

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

  • 2019-12-18
  • 本文字数:6266 字

    阅读完需:约 21 分钟

历时五天用 SwiftUI 做了一款 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: UIImageinit(url path: String?, placeHolder: String) {self.urlPath = pathself.placeHodlerImage = UIImage(named: placeHolder)!.withRenderingMode(.alwaysOriginal)    }        @State var downLoadedImage: UIImage? = nilvar 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 inswitch 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: Datevar 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 = <a href="">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</a href="">
复制代码


由于 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.0let width = view.frame.size.widthlet 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: CalendarViewvar date: Datevar fsCalendar: FSCalendar?init(_ control: CalendarView) {self.control = controlself.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


如果你想更详细了解淘宝基础平台团队,欢迎观看团队介绍视频更多淘宝基础平台团队的技术分享,可关注淘宝技术微信公众号 AlibabaMTT


参考


关注「淘宝技术」微信公众号,回复「链接」,即可获得全部参考网址~


1、Session 402 What’s New in Swift


2、Session 204 Introducing SwiftUI: Building Your First App


3、Session 216 Introducing SwiftUI Essentials


4、Session 226 SwiftUI Essentials


5、Session 231 Integrating SwiftUI


6、Session 237 Building Custom Views with SwiftUI


7、Session 238 accessibility in swiftui


8、Session 240 SwiftUI on all Device


9、Session 219 SwiftUI on watchOS


10、Session 415 Modern Swift API Design


11、FSCalendar


12、Kingfisher


13、Redux 入门教程(一):基本用法


14、Redux 中文文档


本文转载自淘系技术公众号。


原文链接:https://mp.weixin.qq.com/s/QgDSuTFjwFlXzhksfgmkIQ


2019-12-18 16:07991

评论

发布
暂无评论
发现更多内容

智能汽车颠覆世界!(28天写作 Day28/28)

mtfelix

28天写作 智能汽车 未来世界

程序员成长第四篇:程序员的职业天花板

石云升

28天写作 2月春节不断更 职场天花板

28天写作

lidaobing

28天写作

下不了的开人的手

Ian哥

28天写作

有用的信息安全资质查询网站大全

石君

信息安全 28天写作

CDN加速原理的那些事

IDEA永久注册码来了!!!(支持2020.3.1版本)

程序员生活志

IDEA

CSS开发过程中的20个快速提升技巧

华为云开发者联盟

CSS

MyBatis入门

小马哥

Java mybatis 七日更 2月春节不断更

机器学习笔记之:Matrix Vector Multiplication

Nydia

养成好习惯,需要的不只是意志力

Justin

习惯养成 碎碎念 28天写作

一款基于 Web 的通用数据管理工具(转载)

BinTools图尔兹

数据库 运维 开发工具 dba 数据管理工具

终于搞懂了Python模块之间的相互引用问题

华为云开发者联盟

Python 路径 代码 模块

【无偿分享】史上最全Python学习大礼包

sum56

Python 学习 学习方法 python 爬虫 资料整理

天天向上跑分模式介绍开发

luluhulian

关于京东技术,你想了解的都在这里丨征文活动获奖及优秀专栏推荐

京东科技开发者

京东 征文大赛

使用V8和node轻松profile分析nodejs应用程序

程序那些事

性能优化 Profile nodejs 性能分析 程序那些事

浪潮云洲工业互联网平台创新实践,受权威机构关注!

浪潮云

工业互联网

redis分布式锁的这些坑,我怀疑你是假的开发

华为云开发者联盟

redis 分布式 分布式锁 服务器 value

深度集成 Flink: Apache Iceberg 0.11.0 最新功能解读

Apache Flink

flink

Elasticsearch 搜索结果解析

escray

elastic 七日更 死磕Elasticsearch 60天通过Elastic认证考试 2月春节不断更

目前的区块链IPFS矿机模式交易系统开发的简单解析

v16629866266

“嗖”一下28 天过去啦,我们都在交作业!

李忠良

28天写作

28天写作复盘

一笑

28天写作

谈谈我所理解的科幻「-28/28」

道伟

28天写作

工业绿色哪家强?3D可视化盾构机构建隧道,推动设备预测性运维

一只数据鲸鱼

物联网 数据可视化 绿色交通 绿色工业 盾构机

并发队列:PriorityBlockingQueue和DelayQueue案例使用

叫练

并发编程 队列 优先级队列 条件队列 并发队列

面试时遇到一致性哈希算法这样回答会让面试官眼前一亮

中间件兴趣圈

负载均衡 面试 一致性哈希

2021年的十五个DevOps趋势预测

禅道项目管理

DevOps 微服务 自动化 趋势

长篇总结之JavaScript,巩固前端基础

我是哪吒

JavaScript 程序员 面试 大前端 2月春节不断更

看KubeEdge携手K8S,如何管理中国高速公路上的10万边缘节点

华为云开发者联盟

Kubernetes 微服务 kubeedge 边缘

历时五天用 SwiftUI 做了一款 APP,阿里工程师如何做的?(二)_文化 & 方法_淘系技术_InfoQ精选文章