心得感悟
起初看到 WWDC 上的演示 SwiftUI 时,我就觉得 SwiftUI 有种陌生的熟悉感(声明式语法),所以体验下,看看有没有什么启发。
先说下整体项目完成下来的感受:
- 用 Swift + SwiftUI 开发 iOS 项目效率很高,本人之前没有接触过 Swift 语言,这次是从 0 开始学 swift 语言以及 swiftUI 框架的,每天花 2 个小时断断续续大致花了 3 天时间掌握了基本的 Swift 语法,而 SwiftUI 框架的掌握是按照官方的视频学习的(赞下 Apple 的文档、教学视频的完备性);
- 从我一个前端开发工程师的视角看来,SwiftUI 使用的 “DSL”、状态管理和前端的 React 很相似,不少概念是相通的,比如 State,Props。
- SwiftUI + Xcode11 + macOS Catalina 让研发效率大大提升,预览模式做到了所见即所得,注意这里是预览模式而不是模拟器模式
- 借助于预览模式,UI 也可以做到很好的单页面的单元测试;这一点比较有意思,独立的 View 可以独立预览,然后可以注入数据,实时预览调试。
执行环境
- macOS Mojave: 10.14.5
- xcode: Version 11.0 beta 6 (11M392q)
项目信息
github: https://github.com/young-cowboy/swiftui-app-habits
App 效果预览

2 个关键点
这里提 2 个关键点“尾部闭包语法”以及“状态管理”,因为理解他们就基本能掌握 SwiftUI 来开发一个简单的项目。尾部闭包语法能让我们理解 SwiftUI 是如何通过声明式语法来创建 View 的,“数据传递”能让我们了解怎么实现数据流功能。
尾部闭包语法
Swift 有一个很重要的概念叫做“尾部闭包语法”,这种语法让 Swift DSL 的书写变得更“声明式”,我们先了解下 Swift 闭包定义:
{(parameters) -> return type in // 代码 }
这样来看是不是和 JavaScript 的闭包有一定的相似,但是闭包有很多种语法形式,这里我们举个例子说明下:
import Foundation func wrapClosure (closure: (String, String) -> Void) { let name = "KK" let age = "18" closure(name, age) }
定义一个 function,参数只有一个,并且这个参数为一个函数,然后在方法体内部调用这个函数,传入参数;
func foo (name: String, age: String) { print("My name is \(name) and \(age) years old") } wrapClosure(closure: foo) // My name is KK and 18 years old
通常我们会这样执行,定义个函数来处理传参。但是利用闭包的语法,有更简洁的方式,如下就一个闭包的语法了
wrapClosure(closure: { (name: String, age: String) in print("My name is \(name) and \(age) years old") })
还可以利用 Swift 的类型推断能力,不写闭包参数的类型,如下
wrapClosure(closure: { name, age in print("My name is \(name) and \(age) years old") })
还可以利用快捷参数名来获取参数,如下
wrapClosure(closure: { print("My name is \($0) and \($1) years old") })
最后还有更简洁的方式,这样是 SwiftUI 利用到的一个特性,所以SwiftUI 的 DSL 看起来很有声明式的感觉,这个特性叫 “尾部闭包语法”,这次方法执行连括号都可以不写了。
wrapClosure { print("My name is \($0) and \($1) years old") }
到此应该能看出为什么 SwiftUI 能用声明式的语法来创建各种 View 协议的视图了。
数据传递
在做面向用户的功能时,一个很主要的点是数据传递,不同的设计理念解决特点场景的问题。这里简单介绍下 SwiftUI 的数据怎样传递,这里有一个概念property wrapper,我理解是装饰器 decorate,介绍三个装饰器 @State, @Binding, @ObservedObject,@Published。
- @State 装饰过的属性发生了变化,SwiftUI 会根据新的属性值重新创建视图
- @Binding 修饰器修饰后,属性变成了一个引用类型,传递变成了引用传递,这样父子视图的状态就能关联起
- @ObservedObject 修饰一个复杂类型数据,可以被多个 View 所使用,用 @Published 修饰对象里属性,表示需要被监听;
更多的细节可以参考:https://mecid.github.io/2019/06/12/understanding-property-wrappers-in-swiftui/
页面结构设计

一共有 4 个页面 HabitListView, AddButtonView, HabitDetailView, AddItemView
数据结构设计
项目涉及到三个数据结构: HabitItem(习惯项), HabitIconArray(习惯图标), HabitColor(习惯主题)
HabitColor 主题色,保存了习惯的主题
public let UserColorArray = [ Color(red:75 / 255, green:166 / 255, blue: 239 / 255), Color(red:161 / 255, green:206 / 255, blue: 97 / 255), Color(red:248 / 255, green:214 / 255, blue: 80 / 255), Color(red:243 / 255, green:176 / 255, blue: 74 / 255), Color(red:238 / 255, green:140 / 255, blue: 111 / 255), Color(red:237 / 255, green:113 / 255, blue: 165 / 255), Color(red:207 / 255, green:102 / 255, blue: 247 / 255), Color(red:77 / 255, green:110 / 255, blue: 247 / 255), Color(red:236 / 255, green:107 / 255, blue: 102 / 255) ]
HabitIconArray 习惯的图标库
public let IconNameArray: [String] = [ "alarm", "book", "pencil", "desktopcomputer", "gamecontroller", "sportscourt", "lightbulb" ]
HabitItem 用来保存习惯的详细信息,这里实现 ObservableObject 协议,用来告诉 SwiftUI 这个对象需要监听,用 Published property wrapper 包装了 checkList 属性,表示这个属性是要监听的,因为它可能需要传递给子 View
class HabitItem: Identifiable, ObservableObject { var name: String = "" var iconName: String = "clock" var theme: Color = UserColor.color1.value var uuid: Int = 0 @Published var checkList: [Bool] = [false, false, false, false, false, false, false] init () { } init(name: String, iconName: String, theme: Color) { self.name = name self.iconName = iconName self.theme = theme self.uuid = generatteID() } }
UI 设置
具体代码参考仓库代码,这里讲解一个流程,新建一个“习惯”项,在 MainView 里
MainView
... @State var sheetVisible: Bool = false @State var sheetType: String = "add" ... ... AddButtonView() { self.sheetType = "add" self.sheetVisible = true } ...
AddButtonView 利用尾部闭包语法内联了一个闭包用来相应事件
AddButtonView 里定义了 onPressed 属性
struct AddButtonView: View { var onPressed: () -> Void var body: some View { Button(action: { self.onPressed() }) { HStack { Image(systemName: "plus.circle.fill") .resizable() .frame(width: 60, height: 60) .foregroundColor(Color.blue) } } } }
AddItemView 里的新增按钮点击后相应事件,把选中的数据传递 onSumit 属性回调里
struct AddItemView: View { @State var newItemTitle = "" @State var selectIconIndex: Int = 0 @State var selectColorIndex: Int = 0 var onSumit: (HabitItem) -> Void var onDissmis: () -> Void var body: some View { VStack { ... ... VStack { Button(action: { if self.newItemTitle != "" { let iconName = IconNameArray[self.selectIconIndex]; let theme = UserColorArray[self.selectColorIndex]; self.onSumit(HabitItem(name: self.newItemTitle, iconName: iconName, theme: theme)) } }) { Text("新增").frame(minWidth: 0, maxWidth: .infinity) } ... } ... } ... } } }
在 MainView 里,利用闭包处理回调事件新增选项
AddItemView(onSumit: { item in self.habitItemList.insert(item, at: 0) self.sheetVisible = false }, onDissmis: { self.sheetVisible = false })
剩下的功能大同小异,可以把代码拉下来本地运行看看效果,github: https://github.com/young-cowboy/swiftui-app-habits
谢谢