在 SwiftUI 中,我们所有的视图以及所有动画都必须是状态的函数
Form 表单
- 头部
Section("小标题"){
}
Section(){
}header: {
// 添加 自定义视图
}
- 输入检查
表单一般用于信息输入场景,更重要的一步是在继续之前检查该输入以确保其有效。
有一个专门用于此目的的修饰符:disabled()。这需要一个条件来检查,如果条件为真,那么它所附加的任何内容都不会响应用户输入 - 按钮无法点击,滑块无法拖动,等等。您可以在这里使用简单的属性,但任何条件都可以:读取计算属性、调用方法等等。
Form {
Section {
TextField("姓名", text: $username)
TextField("邮箱", text: $email)
}
Section {
Button("注册") {
// ..
}
}
.disabled(username.isEmpty || email.isEmpty)
}
这意味着“如果用户名为空或电子邮件为空,则禁用此部分Section”,
按钮 & 属性包装器 @State
Button("删除", action: executeDelete)
Button("删除", role: .destructive) {
...
}
.buttonStyle(.borderedProminent) // 样式风格
.tint(.blue) // 颜色
Button("编辑", systemImage: "pencil") {
//...
}
- 添加触觉效果
Button("点击") {
...
}
.sensoryFeedback(.increase, trigger: counter)
每当您按下按钮时,您都应该感受到轻柔的触觉点击 。
触觉反馈 还有很多其他选项可供选择,包括
.success、.warning、.error、.start、.stop等
对触觉效果进行更多控制,可以使用 .impact()
// 中等碰撞
.sensoryFeedback(.impact(flexibility: .soft, intensity: 0.5), trigger: counter)
// 剧烈碰撞
.sensoryFeedback(.impact(weight: .heavy, intensity: 1), trigger: counter)
对于更高级的触觉,Apple 为我们提供了一个名为 Core Haptics 的完整框架 ,让我们能够通过组合敲击、连续振动、参数曲线等来创建高度可定制的触觉。 有兴趣可进一步研究
- 自定义按钮
Button {
//...
} label: {
// 自定义控件 样式 布局
Label("Edit", systemImage: "pencil")
.padding()
.foregroundStyle(.white)
.background(.red)
}
更改属性
由于Swift的结构体是值类型(固定的),不允许修改其属性。若要更改属性,Swift提供了@State。@State允许 SwiftUI 将该值单独存储在可以修改的位置。
@State private var number = 20
@State 会自动监视更改,并且当发生某些情况时它会自动重新调用该body属性,它将重新加载您的 UI 以反映更改后的状态,这是 SwiftUI 工作方式的基本功能。
@State专为存储在一个视图中的简单属性而设计。Apple 建议我们为这些属性添加访问控制private。
当然,我们可以使用类来代替———它们可以自由修改属性。但是,出于性能的考虑而使用结构体。因为SwiftUI 会频繁地销毁和重新创建您的结构体,因此保持它们小而简单对于性能非常重要。
- 结构体的情况
对于结构体数据,也一样
struct User {
var firstName = "Bilbo"
var lastName = "Baggins"
}
struct ContentView: View {
@State private var user = User()
var body: some View {
Text("姓名是:\(user.firstName) \(user.lastName).")
TextField("姓:", text: $user.firstName)
}
}
这一切都有效:SwiftUI 足够智能,可以理解一个对象包含的所有数据,并且当任一值发生变化时都会更新 UI。在幕后,实际发生的情况是,每次结构体中的值更改时,整个结构体都会发生更改。这听起来可能很浪费,但实际上速度非常快。
- 类的情况 @Observable
如果我们想要在多个视图之间共享数据(可通过传递参数的方式)——如果我们希望两个或多个视图指向相同的数据,以便当一个视图发生更改时,它们都会得到这些更改。这个时候,就是使用类的时候了。我们需要使用类,而不是结构。
对于一个类,当修改其对象的某属性时,UI并不会产生变化。因为@State不会监视这些值,实际上发生的情况是类内的值正在更改,但视图没有重新加载以反映该更改。 ——类不需要关键字mutating,因为即使类实例被标记为常量,Swift 仍然可以修改变量属性。
通过一个小改动来解决这个问题 :
@Observable
class User {
var firstName = "Bilbo"
var lastName = "Baggins"
}
小结:如果@State与结构体一起使用,则当值更改时,您的 SwiftUI 视图将自动更新;但如果@State与类一起使用,则还必须将该类标记为@Observable。
有了@Observable,可以在多个 SwiftUI 视图中使用,并且当类的属性发生更改时,所有这些视图都会更新。
探索 @Observable
@Observable 告诉 SwiftUI 监视类中每个单独的属性的更改,并在属性发生更改时重新加载依赖于该属性的任何视图。 这,到底发生了什么?
@Observable是一个宏,这是 Swift 悄悄重写代码 以添加额外功能的方式。
如果您@Observable在代码中右键单击,您可以选择展开宏来准确查看正在发生的重写 – Xcode 将向您显示所有隐藏的代码正在生成。
我不打算在这里写出整个宏扩展,因为它太多了,但我确实想指出三件事:
- 两个属性被标记为
@ObservationTracked,这意味着 Swift 和 SwiftUI 正在监视它们的更改。 - 如果右键单击
@ObservationTracked您也可以展开该宏 - 是的,它是宏中的宏。该宏的作用是跟踪任何属性的读取或写入,以便 SwiftUI 只能更新绝对需要刷新的视图。 - 我们的类是为了遵守
Observable协议而设计的。这很重要,因为 SwiftUI 的某些部分认为这意味着“可以监视此类的更改”。
所有这三个都很重要,但中间的一个承担了繁重的任务:iOS 跟踪每个从对象读取属性的 SwiftUI 视图@Observed,这样当属性更改时,它可以智能地更新依赖于它的所有视图同时保持其他不变。
使用结构体时,@State属性包装器会保持值处于活动状态并监视它的更改。另一方面,当使用类时,@State只是为了保持对象存活——所有对变化的观察和更新视图都由 @Observable来处理。
文本框 & 双向绑定
- 视图是其状态的函数
禁用大写.textInputAutocapitalization(.never)
边框样式.textFieldStyle(.roundedBorder)
自动扩展 TextField("请输入", text: $notes, axis: .vertical)
- 双向绑定
使用符号$和 @State 组合
输入框绑定文本属性 => 输入框显示文本属性,同时,输入框编辑也会更新属性。
我们在属性名前写一个美元符号。这告诉 Swift 它应该读取属性的值,但也应该在发生任何更改时将其写回。
- 格式
- 键盘的隐藏
需要用到第二个属性包装器:@FocusState。这与常规的@State完全相同,只不过它是专门为处理 UI 中的输入焦点而设计的。
- State属性的初始化
类的双向绑定 与 @Bindable
我们知道类如果使用了@Observable宏,这意味着 SwiftUI 能够观察这些数据的变化。因此,@Bindable属性包装器所做的就是为我们创建缺少的绑定 - 它生成能够与@Observable宏一起使用的双向绑定,而无需使用它@State来创建本地数据。它在这里非常完美,您将在未来的项目中大量使用它。
@Observable
class Order{
var name = ""
var streetAddress = ""
var city = ""
}
@Bindable var order = Order()
var body: some View {
Form{
Section{
TextField("名称", text: $order.name)
TextField("城市", text: $order.city)
TextField("街道", text: $order.streetAddress)
}
}
多行文本输入 TextEditor
使用TextEditor实际上更容易,你不能调整它的样式或添加占位符文本,你只需将它绑定到一个字符串。
不过,您确实需要小心,确保它不会超出 安全区域
在循环中创建视图 ForEach
想要在循环内创建多个 SwiftUI 视图是很常见的。例如,想要循环遍历一组名称,并让每个名称都是一个文本视图。SwiftUI 为此目的提供了一个专用的视图类型,称为ForEach。 这可以循环数组和范围,根据需要创建尽可能多的视图。ForEach将为它循环的每个项目运行一次闭包,并传入当前循环项目。
ForEach是一个视图
- ForEach不能使用封闭范围,如(0...5) - 现在这是不可能的
唯一标识
很多时候,数组的因素可能重复。事实上有一个更简单的解决方案,它被称为UUID“通用唯一标识符”。Swift 将确保它们始终是唯一的
struct ExpenseItem: Identifiable{
let id = UUID() // 需手动生成
let name: String
let type: String
let amount: Int
}
另外结合Identifiable协议。这是 Swift 内置的协议之一,意味着“这种类型可以被唯一地识别”。它只有一个要求,那就是必须有一个名为id的属性,包含唯一标识符。
好处就是,有了Identifiable,ForEach就不需要指明唯一id了
ForEach(expenses.items){ item in
Text(item.name)
}
选择器 Picker
其中,id: \.self部分很重要。因为 SwiftUI 需要能唯一地识别屏幕上的每个视图,以便它可以检测到事物何时发生变化。这里,id: \.self意味着“字符串本身是唯一的”,如果您向options数组添加重复的字符串,可能会遇到问题
- 设置风格
堆栈视图
VStack 垂直排列布局
- 间距
VStack(spacing: 30){
}
- 对齐方式
VStack(alignment: .leading) {
}
- Spacer() 占用/排挤
将堆栈的内容推到一侧。可添加多个
HStack 水平排列布局
同理
ZStack 重叠布局
按深度排列控件——它使视图重叠。
没有间距的概念,因为视图重叠,但它确实有对齐。
颜色
SwiftUI 为我们提供了一系列渲染颜色的功能,并且做到了既简单又强大。
- 安全区域
白色空间为故意留空,因为 Apple 不希望重要内容被其他 UI 功能或设备上的任何圆角遮挡。所以,剩下的部分——整个中间的空间——被称为 “安全区域”,你可以自由地绘制它,而不用担心它可能被 iPhone 上的凹口剪掉。 白色空间的大小取决于您的设备,但在具有 Face ID 的 iPhone(例如 iPhone 15)上,您会发现动态岛区域(顶部的胶囊形区域)和主页指示器(水平方向)底部的条纹)保持未着色。
如果需要,你可以设置忽略安全区
重要的内容不要放置在安全区域之外,这一点至关重要。如果您的内容只是装饰性的(就像我们这里的背景颜色一样),那么将其扩展到安全区域之外是可以的。
- frame
颜色会自动占据所有可用空间,但您也可以使用修饰符frame()来请求特定尺寸;还可以根据所需的布局指定最小和最大宽度和高度。
Color.red
.frame(width: 200, height: 200)
Color.red
.frame(minWidth: 200, maxWidth: .infinity, maxHeight: 200)
事实上,Color.red 本身就是一个视图,这就是为什么它可以像形状和文本一样使用。
- 内置颜色
如Color.blue、Color.green、Color.indigo
- 语义颜色
颜色不说明它们包含什么色调,而是描述它们的用途。
如,Color.primary是 SwiftUI 中文本的默认颜色,并且将是黑色或白色,具体取决于用户的设备是在浅色模式还是深色模式下运行。还有Color.secondary,根据设备的不同,它也是黑色或白色,但现在具有轻微的透明度,因此其背后的一些颜色会透过。
- 自定义颜色
Color(red: 1, green: 0.8, blue: 0)
- 渐变
SwiftUI 为我们提供了 四种 可供使用的渐变,就像颜色一样,它们中的大多数也是可以在我们的 UI 中绘制的视图。
渐变由几个部分组成:
- 显示一系列颜色
- 尺寸和方向信息
- 要使用的渐变类型
一、线性渐变:朝一个方向移动
LinearGradient(colors: [.white, .black], startPoint: .top, endPoint: .bottom)
可以提供渐变停止点,让您指定颜色以及颜色应该沿渐变多远使用
LinearGradient(stops: [
.init(color: .white, location: 0.45),
.init(color: .black, location: 0.55),
], startPoint: .top, endPoint: .bottom)
二、径向渐变:以圆形形状向外移动。因此我们不指定方向,而是指定开始和结束半径——颜色应开始和停止变化距离圆心多远。
RadialGradient(colors: [.blue, .white], center: .center, startRadius: 20, endRadius: 200)
三、角度渐变,(圆锥形或圆锥形渐变)
这样颜色会围绕一个圆圈循环,而不是向外辐射,并且可以产生一些美丽的效果。
AngularGradient(colors: [.red, .yellow, .green, .blue, .purple, .red], center: .center)
四、渐变背景
您无法对其进行任何控制,并且您也只能将它们用作背景和前景样式,而不是单独的视图。 只需在任何颜色后添加即可创建此渐变.gradient
弹窗
状态属性控制弹窗。提示:双向数据绑定,当警报解除时 SwiftUI 会自动设置回 false,不需要手动处理。
小技巧
- 数组
var arr = [2,6,4,8,9,3]
arr.shuffled() // 进行 数组随机排序
// 随机 元素
arr.randomElement()
- 文本
// 粗体
Text("提示").font(.subheadline.weight(.heavy))
// 大标题
Text("提示").font(.largeTitle.weight(.semibold))
// 小标题
Text("提示").font(.title3.weight(.bold))
.font(.title.bold())
// 复数,处理数量的单位。当大于1,cup变成cups
"^[\(coffeeAmount) cup](inflect: true)"
// 分割字符串
let input = "a b c"
let letters = input.components(separatedBy: " ")
// 修剪字符串开头和结尾的所有空格
let trimmed = letter?.trimmingCharacters(in: .whitespacesAndNewlines)
- 圆角处理
.clipShape(.capsule) // 胶囊
.clipShape(.rect(cornerRadius: 10)) // 圆角
- 描边
.overlay(
RoundedRectangle(cornerRadius: 10)
.stroke(.lightBackground)
)
-
间距
.padding([.horizontal, .bottom])//.padding(.bottom, 5) -
模糊处理
.blur(radius: 2) -
阴影处理
.shadow(radius: 12) -
去除颜色
.saturation(0) -
颜色加透明度
.white.opacity(0.5) -
深浅色主题
.preferredColorScheme(.dark) -
最大宽高
.frame(maxWidth: .infinity, maxHeight: .infinity)
.frame(maxWidth: .infinity)
小技巧:长按显示 隐藏菜单
长按视图,可以显示隐藏菜单。使用修饰符:contextMenu