SwiftUI学习整理(一)
最前面想说的话
转眼已经是2020年10月了,2020已经过去大半了,上半年的计划已经完成,所以下半年的学习应该提上日程了,利用两周时间每周一三学习周六整理归纳学习的东西发表一篇文章,二四锻炼,周五周日休息
SwiftUI 学习的想法
其实本来不是不打算先学习SwiftUI的,但是因为现在iOS14推出了小组件功能必须要用SwiftUI实现,所以提前学习一下是很有必要的。预计发表三篇关于SwiftUI的日志,分别整理SwiftUI的基础布局和SwiftUI里的响应式布局,最后结合一个实际项目输出自己的心得
这篇文章参考的书籍是《SwiftUI和Combine编程》这本书第一二章,仅为自己学习归纳使用。
你好,SwiftUI
创建第一个项目
“选择 iOS tab 下的 Single View App 模板,Xcode 将为我们创建一个单页的 iOS app。”“接下来,将项目命名为 Calculator,并且选择 “Use SwiftUI” 作为用户界面的构建方式,让 Xcode 使用 SwiftUI 来创建第一个画面。“点击 Next 并选择合适的位置后,我们可以得到一个使用 SwiftUI 作为界面开发方式的新项目。在项目导航中找到 ContentView.swift,它的内容如下:
import SwiftUI
struct ContentView : View {
var body: some View {
Text("Hello World")
}
}
struct ContentView_Previews : PreviewProvider {
static var previews: some View {
ContentView()
}
}
上面代码中 ContentView 所定义的是实际的 UI,ContentView_Previews 则是一个满足了 PreviewProvider 的 dummy 界面。如果你的操作系统是 macOS 10.15 或以上的话,你可以使用编辑器右上角的切换编辑器视图按钮,选中 “Editor and Canvas” (快捷键 Option + Command + 回车),并单击右上角的 Resume 按钮。Xcode 预览界面将会读取 ContentView_Previews 的内容,并渲染在编辑器右侧的实时预览栏中。它就是 ContentView_Previews 的 previews 属性所返回的 View,也就是 ContentView 的内容。默认情况下,Xcode 会使用你当前选取的 iOS 模拟器作为预览尺寸,为了能让你得到的内容和书中一致,建议你在这个例子中选择 iPhone XR 模拟器作为目标设备。
使用 Modifier 描述 Text 及 Button
除了改变 Text 的文本内容外,我们会通过在 Text 声明之后使用方法调用的方式,来定义文本样式。让我们从单个的计算器按钮入手,比如加号键。将 ContentView 的 body 部分替换为以下内容:
var body: some View {
Text("+")
.font(.title) /// 1
.foregroundColor(.white) /// 2
.padding() /// 3
.background(Color.orange) /// 4
}
font,foregroundColor,padding 和 background 各自定义了 Text 的一项属性:

font, foregroundColor, background 不用多说,padding将把当前的 View 包裹在一个新的 View 里,并在四周填充空白部分。在上面例子中,我们没有给定参数,这会在文本的四周都填上系统默认尺寸的空白。我们可以为 padding 指定需要填充的方向以及大小,比如:.padding(.top, 16) 将在上方填充 16 point 的空白,.padding(.horizontal, 8) 在水平方向 (也即 [.leading, .trailing]) 填充 8 point
上面的四种方法被成为View的Modifier,modifier 一般来说对顺序不敏感,对布局也不关心,它们更像是针对对象 View 本身的属性的修改。而与之相反,封装类的 modifier 的顺序十分重要。大致来说,view modifier 分为两种类别:
像是 font,foregroundColor 这样定义在具体类型 (比如例中的 Text) 上,然后返回同样类型 (Text) 的原地 modifier。
像是 padding,background 这样定义在 View extension 中,将原来的 View 进行包装并返回新的 View 的封装类 modifier。
基本布局
Stack 容器
横向排列的布局空间叫HStack,竖向排列的布局控件叫VStack, 叠加排列的布局控件叫ZStack,除了ZStack,H/V Stack 类似UIStackView的横向和纵向
struct CalculatorButton : View {
let fontSize: CGFloat = 38
let title: String
let size: CGSize
let backgroundColorName: String
let action: () -> Void
var body: some View {
Button(action: action) {
Text(title)
.font(.system(size: fontSize))
.foregroundColor(.white)
.frame(width: size.width, height: size.height)
.background(Color(backgroundColorName))
.cornerRadius(size.width / 2)
}
}
}
这样一来,ContentView 的 body 里可以简化为:
var body: some View {
HStack {
CalculatorButton(
title: "1",
size: CGSize(width: 88, height: 88),
backgroundColorName: "digitBackground")
{
print("Button: 1")
}
CalculatorButton(
title: "2",
size: CGSize(width: 88, height: 88),
backgroundColorName: "digitBackground")
{
print("Button: 2")
}
CalculatorButton(
title: "3",
size: CGSize(width: 88, height: 88),
backgroundColorName: "digitBackground")
{
print("Button: 3")
}
CalculatorButton(
title: "+",
size: CGSize(width: 88, height: 88),
backgroundColorName: "operatorBackground")
{
print("Button: +")
}
}
}
ForEach
在项目中新建一个 Swift 文件,将它命名为 CalculatorButtonItem.swift,添加如下代码:
enum CalculatorButtonItem {
enum Op: String {
case plus = "+"
case minus = "-"
case divide = "÷"
case multiply = "×"
case equal = "="
}
enum Command: String {
case clear = "AC"
case flip = "+/-"
case percent = "%"
}
case digit(Int)
case dot
case op(Op)
case command(Command)
}
我们可以粗略地把计算器上的按钮分为四大类:代表从 0 至 9 的数字 digit,小数点 dot,加减乘除等号这样的操作 (Op),以及清空、符号翻转等这类命令 (Command)。接下来,可以在 extension 里追加定义必要的外观:
extension CalculatorButtonItem {
var title: String {
switch self {
case .digit(let value): return String(value)
case .dot: return "."
case .op(let op): return op.rawValue
case .command(let command): return command.rawValue
}
}
var size: CGSize {
CGSize(width: 88, height: 88)
}
var backgroundColorName: String {
switch self {
case .digit, .dot: return "digitBackground"
case .op: return "operatorBackground"
case .command: return "commandBackground"
}
}
}
///@propertyWrapper
struct RowView: View {
let row: [CalculatorButtonItem]
@Binding var brain: JiSuanQiState
var body: some View {
HStack {
ForEach(row, id: \.self) { item in
BtnView(
content: item.title,
backgroundcolorName: item.backgroundColorName,
size: item.size,
action: {
debugPrint("输出按钮:\(item.title)")
self.brain = self.brain.apply(item: item)
})
}
}
}
}
struct JiSuanqiButtonPad: View {
let pad: [[CalculatorButtonItem]] = [
[.command(.clear), .command(.flip),
.command(.percent), .op(.divide)],
[.digit(7), .digit(8), .digit(9), .op(.multiply)],
[.digit(4), .digit(5), .digit(6), .op(.minus)],
[.digit(1), .digit(2), .digit(3), .op(.plus)],
[.digit(0), .dot, .op(.equal)]
]
@Binding var brain: JiSuanQiState
var body: some View {
VStack(spacing: 8) {
ForEach(pad①, id: \.self②) { row in
RowView(row: row, brain: self.$brain)③
}
}
}
}
我们需要对此进行一些解释。ForEach 是 SwiftUI 中一个用来列举元素,并生成对应 View collection 的类型。它接受一个数组,且数组中的元素需要满足 Identifiable 协议。如果数组元素不满足 Identifiable,我们可以使用 ForEach(_:id:) 来通过某个支持 Hashable 的 key path 获取一个等效的元素是 Identifiable 的数组。在我们的例子中,数组 row 中的元素类型 CalculatorButtonItem 是不遵守 Identifiable 的。为了解决这个问题,我们可以为 CalculatorButtonItem 加上 Hashable,这样就可以直接用 ForEach(row, id: .self) 的方式转换为可以接受的类型了。在 CalculatorButtonItem.swift 文件最后,加上一行:
extension CalculatorButtonItem: Hashable {}
使用 frame 或 Spacer 占满屏幕
frame: 中的 minWidth 和 maxWidth 为 Text 的宽度定义了范围,这会“提示” Text 不必遵守内容尺寸,而是去适应容器的尺寸。将 .infinity 传递给 maxWidth,表示不对最大宽度进行限制,这种情况下 Text 会尽可能占据它的容器的宽度,变为全屏宽。frame 还提供了一个 alignment 参数,让我们可以设定其对齐
Spacer: SwiftUI 允许我们定义可伸缩的空白:Spacer,它会尝试将可占据的空间全部填满。在我们的 body 中,可以加入一个 Spacer 来把 VStack 的上半部分全部填满
VStack(spacing: 12) {
Spacer()
Text("0")
.font(.system(size: 76))
.minimumScaleFactor(0.5)
.padding(.trailing, 24)
.lineLimit(1)
.frame(minWidth: 0, maxWidth: .infinity, alignment: .trailing)
CalculatorButtonPad()
.padding(.bottom)
}
至此一个完整的计算器页面就算完成了
