SwiftUI学习笔记day2—Stanford lecture2
- 课程链接: Lecture 2 | Stanford CS193p 2023
- 代码仓库: manredeshanxiren/iOS
- 课程大纲:
文章目录
1.let和var
在 SwiftUI 中,let 和 var 的基本用法与纯 Swift 语言中完全一致,但在视图构建和状态管理上有一些特别的考虑:
let(常量):
用于声明不可变的值。对于在视图生命周期内不需要改变的属性(例如静态文本、固定配置等),使用 let 可以提高代码的安全性和可读性,同时也有助于 SwiftUI 做出更好的优化,因为它知道这些值不会变化。var(变量):
用于声明可变的值。如果一个属性需要在运行过程中修改,比如用户交互后的数值变化,那么就应使用 var。特别是在 SwiftUI 中,很多动态数据会配合状态属性包装器(如 @State、@Binding、@ObservedObject 等)一起使用,确保数据更新能够触发视图刷新。
示例:
struct ContentView: View {
// 使用 let 声明静态、不会变化的数据
let title: String = "欢迎使用 SwiftUI"
// 使用 var 结合 @State 声明会改变的状态数据
@State var count: Int = 0
var body: some View {
VStack {
Text(title)
Text("计数:\(count)")
Button("增加计数") {
count += 1
}
}
}
}
小结:
- 选择 let: 当确定属性在视图创建后不会发生改变时使用,可以提高代码的安全性。
- 选择 var: 当属性的值需要根据用户交互或其他逻辑进行修改时使用,并结合状态管理属性包
2.Type Inference
简单来说类似于C++中的auto关键字;
在 SwiftUI 中,类型推断(Type Inference)是 Swift 编译器的一项强大特性,它能够根据上下文自动推断出变量、常量和函数返回值的类型,从而减少了显式声明的冗余代码。这一特性在 SwiftUI 的视图构建中尤为重要,具体体现在以下几个方面:
简化代码书写:
Swift 编译器可以根据初始化表达式推断出变量或常量的类型,例如:let title = "欢迎使用 SwiftUI" // 编译器自动推断为 String 类型 var count = 0 // 自动推断为 Int 类型
这使得代码更加简洁易读。
视图组合与返回值类型:
在 SwiftUI 中,每个视图的 body 属性要求返回一个符合 View 协议的类型。使用类型推断,可以让你专注于视图的布局而不必担心复杂的类型声明。例如:struct ContentView: View { var body: some View { VStack { Text("Hello, SwiftUI!") Button("Click me") { print("Button clicked") } } } }
这里,编译器会自动推断出 VStack 内部各个子视图的组合类型,而开发者只需要关心布局逻辑。
使用 opaque types:
SwiftUI 中常用的some View
就是一种不暴露具体类型的 opaque 类型,它依赖于类型推断隐藏具体的视图实现细节,从而简化了 API 设计和使用。减少类型标注冗余:
在复杂的视图层次结构中,类型推断能够帮助避免显式声明每个视图组件的具体类型,使代码更易于维护。例如,在视图构建器(ViewBuilder)闭包中,编译器可以自动推断出返回的视图组合,而不需要开发者手动组合复杂的泛型类型。
3. .OnTapGesture
在 SwiftUI 中,.onTapGesture 是一个非常便捷的视图修饰符,用于为视图添加点击(tap)手势处理。它允许你在用户点击某个视图时执行特定的操作,而无需显式使用手势识别器。以下是一些关键点和示例:
主要特点
简化手势处理:
使用 .onTapGesture 可以很方便地为任何视图添加点击事件,而无需手动设置 UIGestureRecognizer。可定制点击次数:
默认情况下,.onTapGesture 响应单次点击,但你可以通过传递count
参数来设置响应双击或多次点击。例如,.onTapGesture(count: 2) { ... }
用于双击事件。与状态结合:
你可以利用该手势来修改视图状态,从而触发 SwiftUI 的视图更新。例如,通过点击按钮改变状态值,从而动态更新界面。
示例代码
下面是一个简单示例,展示如何在 Text 视图上添加单次点击手势,点击后修改状态:
struct ContentView: View {
@State private var message = "点击我试试!"
var body: some View {
Text(message)
.padding()
.background(Color.yellow)
.cornerRadius(8)
.onTapGesture {
message = "你点击了 Text 视图!"
}
}
}
在这个示例中,Text 视图通过 .onTapGesture 添加了点击事件。当用户点击视图时,闭包内的代码会执行,将 message 的值修改,从而触发视图刷新,显示新的文本内容。
使用场景
按钮或交互视图:
可以将 .onTapGesture 用于自定义按钮、图片、甚至整个视图的点击响应,而不需要使用 UIButton 或其他 UIKit 组件。简单的用户交互:
对于简单的点击操作,比如展示提示信息、切换状态或触发动画,.onTapGesture 是一个理想的选择。
注意事项
手势冲突:
如果一个视图同时添加了多个手势(例如 .onTapGesture 与 .gesture),需要注意手势之间的优先级和冲突问题,可以通过手势组合来解决。多点击数限制:
当需要处理双击或三击时,要使用count
参数进行区分,确保用户体验符合预期。
4.@State关键字
SwiftUI 中的 @State 关键字是一种属性包装器,用于管理视图内部的状态数据,并确保当状态发生变化时能够自动触发视图刷新。下面详细说明 @State 的作用、使用场景以及其底层原理。
1. @State 的作用与使用场景
局部状态管理:
@State 用于存储那些只在单个视图内部有效的状态数据,例如开关状态、计数器、文本输入内容等。因为这些状态不需要被外部访问或共享,所以将它们声明为私有状态非常合适。自动视图更新:
当 @State 修饰的变量发生变化时,SwiftUI 会自动重新计算视图的 body。这意味着你不必手动调用刷新或更新函数,而是通过修改状态,系统自动帮你实现视图的响应式更新。示例代码:
struct ContentView: View { @State private var count: Int = 0 var body: some View { VStack { Text("计数:\(count)") .font(.largeTitle) Button("增加计数") { count += 1 } } .padding() } }
在上面的例子中,@State 属性 count 的改变会导致 ContentView 的 body 被重新计算,从而更新界面显示的计数值。
2. 底层原理解析
在 SwiftUI 中,视图本质上是值类型(struct),而状态(state)需要在视图的生命周期内持续存在,即使视图的结构体可能会频繁被销毁和重建。为了解决这一矛盾,@State 采用了一种类似“指针”的机制,将状态存储在堆上,从而实现持久化和引用语义。下面从指针的角度解析 @State 的底层原理:
状态存储的间接性
视图与状态的分离:
由于 SwiftUI 的视图是值类型,每次刷新时都会重新创建,但状态需要持久化保存。为此,@State 会在幕后创建一个存储“盒子”(Box),这个盒子在堆上分配内存,用于存放实际的数据。指针引用:
当你在视图中声明一个 @State 属性时,编译器实际上会生成一个包装类型,其中包含了一个指向这个堆上状态存储盒子的指针。这个指针保持不变,保证了即使视图结构体重新构造,也能通过该指针访问到原始的状态数据。
更新与视图刷新机制
通过指针更新状态:
当 @State 修饰的变量发生改变时,修改的实际上是堆内存中存储的值。由于视图内部持有对该状态存储盒子的指针,所有的状态更新都是在同一个内存地址上完成的。触发响应式更新:
SwiftUI 内部有一套依赖关系追踪机制,当状态盒子中的数据通过指针发生改变后,系统检测到这一变化,并将依赖该状态的视图标记为“脏”,进而只重新渲染必要的部分。这里的“指针”作用就像一个全局的标识符,确保更新能够准确地映射到相应的状态存储上。
指针机制的优势
持久性:
通过指针间接引用堆内存,即使视图的值类型实例被重新创建,指向状态的指针依然指向相同的内存区域,确保了状态不会丢失。性能优化:
指针让 SwiftUI 能够高效地比较状态前后变化,仅更新真正需要刷新的一部分视图,而不是整个视图结构,这对于性能优化至关重要。
总结
从指针的角度看,@State 的底层实现核心在于使用一个存储盒子(堆上的内存块)来持有状态数据,并在视图中通过一个不变的指针来引用这个盒子。这样,即使视图(值类型)的实例不断重建,指向状态的指针依然稳定,保证了状态的持久性和一致性。同时,状态的更新通过修改该内存区域的数据来完成,进而触发 SwiftUI 内部的依赖追踪和视图刷新机制,实现了高效的响应式界面更新。
5.Array
在 SwiftUI 中,Array 本身并没有特殊的实现,而是依然使用 Swift 的内置数组。不过,数组在 SwiftUI 的数据驱动视图构建中扮演着极其重要的角色,特别是在生成动态内容和列表时。下面从几个角度详细说明 SwiftUI 中 Array 的使用和其在数据绑定、视图刷新中的作用:
1. 动态视图生成
SwiftUI 提倡声明式、数据驱动的 UI 构建模式。利用数组,可以根据数据源动态生成多个视图。通常会使用 ForEach 与数组配合,自动遍历数组中的每个元素,生成对应的视图。例如:
let emojis = ["👻", "😼", "🤡", "🐸", "🕷️", "👹", "🍭", "🙀","👻", "😼", "🤡", "🐸", "🕷️", "👹", "🍭", "🙀"]
@State var cardCount: Int = 4
var body: some View {
VStack {
ScrollView {
cards
}
Spacer()
cardCountAdjusters
}
.padding()
}
var cards: some View {
LazyVGrid(columns: [GridItem(.adaptive(minimum: 120))], content: {
ForEach(0..<cardCount, id: \.self) { index in
CardView(content: emojis[index])
.aspectRatio(2/3, contentMode: .fit)
}
})
.foregroundColor(.blue)
}
这段代码展示了如何利用 SwiftUI 的声明式语法动态生成视图,并结合状态管理和响应式更新机制来控制显示的卡片数量。下面详细解析各个部分及其底层实现原理:
2. 与状态绑定
在需要动态更新数据时,数组通常会被声明为 @State 或者放在 ObservableObject 中。这样,数组中的数据发生变化时,SwiftUI 会自动重新计算视图的 body,从而实现 UI 更新。例如:
struct DynamicListView: View {
@State private var items = ["Swift", "Kotlin", "JavaScript"]
var body: some View {
List {
ForEach(items, id: \.self) { item in
Text(item)
}
.onDelete(perform: deleteItems)
}
}
func deleteItems(at offsets: IndexSet) {
items.remove(atOffsets: offsets)
}
}
这里,数组 items
被 @State 修饰,当用户通过删除操作修改数组内容时,SwiftUI 自动刷新视图,确保界面与数据状态保持一致。
3. 数组的值类型特性
Swift 的 Array 是值类型,遵循拷贝-写入(copy-on-write)机制。这意味着当你对数组进行修改操作时,实际上可能会触发拷贝,但 Swift 通过优化保证了性能不会受到较大影响。在 SwiftUI 中,当数组被用作状态时(比如 @State 数组),每次状态变更都会触发视图刷新。开发者需要注意在进行大量数据操作时,尽可能利用 Swift 的高效集合操作方法(如 map、filter 等)来简化代码。
总结
- 声明式动态视图: SwiftUI 利用 ForEach 与 Array 的结合,实现了基于数据驱动的动态视图生成。
- 状态绑定: 通过 @State 或 ObservableObject 管理数组数据,确保数据变化能自动刷新 UI。
- 底层实现: Swift 的 Array 作为值类型在内存中以连续块存储,通过指针高效管理;而 SwiftUI 的状态更新机制则利用 diffing 和依赖追踪,实现局部视图刷新,优化性能。
这种设计充分体现了 SwiftUI 数据驱动与高效状态管理的理念,使得开发者可以以简洁的代码快速构建响应式和动态的用户界面。
6.ForEach
下面我结合你给的代码,系统、全面地介绍一下SwiftUI中ForEach
的用法:
- 你使用了
LazyVGrid
进行网格布局; - 通过
ForEach
构造了一系列的CardView
; - 每张卡片通过索引
index
从数组emojis
中取出具体的内容。
1. ForEach基础概念
(1)ForEach是什么?
在SwiftUI中,ForEach
是一个用于生成一系列视图的结构,它允许你从数据集合中创建动态的视图列表。
- 常见用途:
- 动态生成列表视图、网格布局;
- 根据数据源自动更新界面。
(2)基本语法结构
ForEach(数据集合, id: 唯一标识符) { 单个数据项 in
// 根据单个数据项创建视图
}
- 数据集合可以是一个数组、范围或任意可遍历的集合;
id
是标识数据项唯一性的键值路径,用来告诉SwiftUI如何识别每个视图。
2.代码实例详解
(1)使用范围作为数据源
ForEach(0..<cardCount, id: \.self) { index in
- 数据源是一个整数范围(
0..<cardCount
); id: \.self
表示用数据自身(这里即整数本身)作为唯一标识符。- 这种方式适合当数据本身是唯一的(如整数索引)时使用。
注意:
如果数据项不是简单的整数,而是复杂的结构体或类,应提供一个独特的标识符,比如id: \.id
。
(2)闭包参数
{ index in
CardView(content: emojis[index])
.aspectRatio(2/3, contentMode: .fit)
}
- 这里的
index
是SwiftUI自动传递进闭包的当前遍历索引。 - 通过
emojis[index]
,你从数组中取到对应的内容。 - 每个循环都会创建一个
CardView
,形成网格布局。
3.配合LazyVGrid
实现网格布局
你使用的LazyVGrid
结合ForEach
实现了网格:
LazyVGrid(columns: [GridItem(.adaptive(minimum: 120))]) { ... }
LazyVGrid
允许你创建一个垂直网格布局;- 参数
GridItem(.adaptive(minimum: 120))
表示自动根据屏幕宽度自适应,最小宽度为120; - 在网格内部使用
ForEach
动态生成多个视图,使视图更加灵活、响应式。
4.常见的其他ForEach
用法
为了帮助你更全面理解,下面补充几个常见的场景:
(1)遍历数组中的对象(推荐用法)
假如你有这样一个模型:
struct Person: Identifiable {
let id = UUID()
let name: String
}
遍历:
let people = [Person(name: "Alice"), Person(name: "Bob")]
ForEach(people) { person in
Text(person.name)
}
- 因为
Person
符合了Identifiable
协议,所以不需要显式提供id
。
(2)绑定数据的遍历(可编辑数据)
@State var names = ["Tom", "Jerry", "Bob"]
ForEach($names, id: \.self) { $name in
TextField("名字", text: $name)
}
- 这里使用了绑定(Binding)数据,使每个视图可编辑并实时更新。
5.ForEach
的注意事项
- SwiftUI 中的
ForEach
本身不是循环语句(不像传统编程语言中的for
循环),而是一个结构体; - 它专注于数据到视图的转换,不要用
ForEach
来做非视图构造相关的逻辑; - 注意唯一标识符(
id
)的重要性,确保它们能准确代表数据唯一性,避免视图刷新异常。
6.总结:
ForEach
用于数据驱动动态生成视图;- 数据集合可以是范围、数组等;
- 提供唯一标识符(
id
)帮助SwiftUI区分视图; - 经常配合
LazyVGrid
、LazyHGrid
、List
等容器使用; - 支持数据绑定,让视图与数据实时联动。
你提供的代码很好地体现了ForEach
在动态布局场景下的应用,希望以上内容能帮助你更全面地理解SwiftUI的ForEach
。
7. Button
下面详细、全面地介绍一下 SwiftUI 中的 Button
(按钮):
7.1 SwiftUI 中的 Button
基础概念
在 SwiftUI 中,Button
是用户交互的最基本控件之一。当用户点击按钮时,会触发指定的操作(Action)。
基本语法:
Button("按钮文字") {
// 点击按钮后执行的代码
}
或:
Button(action: {
// 点击按钮后执行的代码
}, label: {
// 自定义按钮的显示样式
})
7.2 简单实例
最常见的按钮形式:
Button("点我一下") {
print("按钮被点击了!")
}
7.3 Button
的常见样式与修饰符
(1)基础按钮样式
Button("确定") {
// 动作代码
}
.buttonStyle(.bordered) // SwiftUI内置风格
内置样式包括:
.bordered
.borderedProminent
.plain
(2)自定义按钮的内容
按钮不仅限于文字,可以自定义视图:
Button(action: {
print("按钮点击!")
}) {
HStack {
Image(systemName: "heart.fill")
Text("喜欢")
}
.padding()
.background(.red.opacity(0.2))
.cornerRadius(10)
}
7.4 按钮状态与交互动画
SwiftUI 中可通过按钮的状态提供视觉反馈:
@State private var isPressed = false
Button {
isPressed.toggle()
} label: {
Text(isPressed ? "取消" : "确定")
.padding()
.foregroundColor(.white)
.background(isPressed ? Color.red : Color.blue)
.cornerRadius(8)
.animation(.default, value: isPressed)
}
- 点击后按钮颜色和文字实时切换,使用了动画效果。
7.5 按钮与状态管理(@State
和 @Binding
)
Button
经常用于修改状态,驱动界面更新:
(1)使用@State
@State private var count = 0
Button("点击次数: \(count)") {
count += 1
}
(2)配合@Binding
(适用于子视图)
struct CounterButton: View {
@Binding var count: Int
var body: some View {
Button("增加数量") {
count += 1
}
}
}
在父视图中使用:
@State private var number = 0
CounterButton(count: $number)
7.6 禁用与启用按钮
按钮可以动态启用或禁用:
@State private var agreeTerms = false
VStack {
Toggle("我同意使用条款", isOn: $agreeTerms)
Button("下一步") {
print("进入下一步")
}
.disabled(!agreeTerms) // 如果未勾选同意条款,则按钮被禁用
}
7.7 按钮角色(ButtonRole)
SwiftUI 可以明确指定按钮角色,用于明确按钮意图和外观:
Button("删除", role: .destructive) {
print("删除")
}
Button("取消", role: .cancel) {
print("取消")
}
.destructive
:用于强调删除等破坏性操作,通常以红色呈现。.cancel
:用于表示取消操作。
7.8 确认对话框(ConfirmationDialog)
SwiftUI 提供了按钮点击后弹出确认对话框的方式:
@State private var showDialog = false
Button("删除文件") {
showDialog = true
}
.confirmationDialog("确认删除吗?", isPresented: $showDialog) {
Button("删除", role: .destructive) { print("删除成功") }
Button("取消", role: .cancel) { }
}
7.9 菜单按钮(Menu)
SwiftUI 中提供 Menu
按钮:
Menu("更多选项") {
Button("分享") { }
Button("保存") { }
Button("删除", role: .destructive) { }
}
7.10 SwiftUI 中按钮的注意点:
- 按钮的默认点击范围自动适配内容大小,但你可以通过
.padding()
扩展其点击区域; Button
中的操作代码应尽量简洁,不要进行耗时的操作;- 复杂交互场景推荐配合
@State
或@Binding
来管理状态; Button
标签可以是任意视图,自定义能力非常强。
8.系统自带的Symbol
这里分别是两个使用了系统自带的Symbol的按钮的代码:
var Adder: some View {
cardCountAjdusters(by: +1, symbol: "rectangle.stack.badge.plus.fill")
}
var Remover: some View {
cardCountAjdusters(by: -1, symbol: "rectangle.stack.badge.minus.fill")
}
UI效果:
如何查看有哪些系统Symbol:
9.优化代码
使用var或者struct来优化代码结构提高可读性
- 增加了垂直滚动视图ScrollView。
- 改用LazyVGrid实现了灵活的网格布局,能自适应更多卡片。
- 引入了调整卡片数量的功能,新增了底部按钮栏(cardCountAdjusters)。
- 通过@State变量cardCount实现动态显示。
import SwiftUI
struct ContentView: View {
let emojis = ["👻", "😼", "🤡", "🐸", "🕷️", "👹", "🍭", "🙀","👻", "😼", "🤡", "🐸", "🕷️", "👹", "🍭", "🙀"]
@State var cardCount: Int = 4
var body: some View {
VStack {
ScrollView {
cards
}
Spacer()
cardCountAdjusters
}
.padding()
}
var cards: some View {
LazyVGrid(columns: [GridItem(.adaptive(minimum: 120))], content: {
ForEach(0..<cardCount, id: \.self) { index in
CardView(content: emojis[index])
.aspectRatio(2/3, contentMode: .fit)
}
})
.foregroundColor(.blue)
}
var cardCountAdjusters: some View {
HStack{
Adder
Spacer()
Remover
}
.imageScale(.large)
.font(.largeTitle)
.padding()
}
func cardCountAjdusters(by offset: Int, symbol: String) -> some View {
Button(action: {
cardCount += offset
}, label: {
Image(systemName: symbol)
})
.disabled(cardCount + offset < 1 || cardCount + offset >= emojis.count)
}
var Adder: some View {
cardCountAjdusters(by: +1, symbol: "rectangle.stack.badge.plus.fill")
}
var Remover: some View {
cardCountAjdusters(by: -1, symbol: "rectangle.stack.badge.minus.fill")
}
}
struct CardView: View {
let content: String
@State var isFaceUp: Bool = true
var body: some View {
ZStack(alignment: .center) {
let base = RoundedRectangle(cornerRadius: 12)
Group {
base.foregroundColor(.white)
base.strokeBorder(lineWidth: 2)
Text(content)
.font(.largeTitle)
}
.opacity(isFaceUp ? 1 : 0)
base.opacity(isFaceUp ? 0 : 1)
}
.onTapGesture {
isFaceUp.toggle()
}
}
}
#Preview {
ContentView()
}
总结
以下是你的SwiftUI学习笔记(Day2)的简短总结:
SwiftUI学习笔记 Day2 小结(Stanford CS193p Lecture2)
本次学习了SwiftUI中的几个核心概念:
let
与var
let
声明常量,适用于不变的数据。var
声明变量,配合@State
用于视图的动态更新。
类型推断(Type Inference)
Swift自动推导变量类型,无需显式声明,简化代码。.onTapGesture
手势
快捷为视图添加点击交互,能与状态绑定,实现交互式UI。@State
属性包装器
用于视图内部状态管理,数据变化自动触发视图刷新,底层通过堆存储和指针实现高效更新。Array 与动态视图
利用数组和ForEach
动态生成视图,配合状态管理可实现数据驱动的UI更新。ForEach
的用法
用于动态生成视图,常见于网格布局(如LazyVGrid
),需提供唯一标识符。Button
控件
基本交互元素,提供丰富样式、状态管理和动画支持。系统自带Symbol
使用系统图标美化按钮,提升界面美观度和一致性。代码优化实践
通过结构化代码、引入滚动视图ScrollView
、网格布局LazyVGrid
,并抽取功能为函数,提高代码的可读性和可维护性。