SwiftUI基础控件

1,870 阅读8分钟

在 SwiftUI 中,我们所有的视图以及所有动画都必须是状态的函数

Form 表单

image.png

  • 头部
Section("小标题"){

}


Section(){

}header: {
    // 添加 自定义视图
}
  • 输入检查

表单一般用于信息输入场景,更重要的一步是在继续之前检查该输入以确保其有效。

有一个专门用于此目的的修饰符:disabled()。这需要一个条件来检查,如果条件为真,那么它所附加的任何内容都不会响应用户输入 - 按钮无法点击,滑块无法拖动,等等。您可以在这里使用简单的属性,但任何条件都可以:读取计算属性、调用方法等等。

Form {

    Section {
        TextField("姓名", text: $username)
        TextField("邮箱", text: $email)
    }

    Section {

        Button("注册") {
            // ..
        }
    }
    .disabled(username.isEmpty || email.isEmpty)  
}

这意味着“如果用户名为空或电子邮件为空,则禁用此部分Section”,

按钮 & 属性包装器 @State

image.png

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 将向您显示所有隐藏的代码正在生成。

我不打算在这里写出整个宏扩展,因为它太多了,但我确实想指出三件事:

  1. 两个属性被标记为@ObservationTracked,这意味着 Swift 和 SwiftUI 正在监视它们的更改。
  2. 如果右键单击@ObservationTracked您也可以展开该宏 - 是的,它是宏中的宏。该宏的作用是跟踪任何属性的读取或写入,以便 SwiftUI 只能更新绝对需要刷新的视图。
  3. 我们的类是为了遵守Observable协议而设计的。这很重要,因为 SwiftUI 的某些部分认为这意味着“可以监视此类的更改”。

所有这三个都很重要,但中间的一个承担了繁重的任务:iOS 跟踪每个从对象读取属性的 SwiftUI 视图@Observed,这样当属性更改时,它可以智能地更新依赖于它的所有视图同时保持其他不变。

使用结构体时,@State属性包装器会保持值处于活动状态并监视它的更改。另一方面,当使用类时,@State只是为了保持对象存活——所有对变化的观察和更新视图都由 @Observable来处理。

文本框 & 双向绑定

  • 视图是其状态的函数

image.png

禁用大写.textInputAutocapitalization(.never)

边框样式.textFieldStyle(.roundedBorder)

自动扩展 TextField("请输入", text: $notes, axis: .vertical)

  • 双向绑定

使用符号$@State 组合

输入框绑定文本属性 => 输入框显示文本属性,同时,输入框编辑也会更新属性。

我们在属性名前写一个美元符号。这告诉 Swift 它应该读取属性的值,但也应该在发生任何更改时将其写回。

  • 格式

image.png

  • 键盘的隐藏

需要用到第二个属性包装器:@FocusState。这与常规的@State完全相同,只不过它是专门为处理 UI 中的输入焦点而设计的。

image.png

image.png

  • State属性的初始化

image.png

类的双向绑定 与 @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是一个视图

image.png

  • 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

image.png

其中,id: \.self部分很重要。因为 SwiftUI 需要能唯一地识别屏幕上的每个视图,以便它可以检测到事物何时发生变化。这里,id: \.self意味着“字符串本身是唯一的”,如果您向options数组添加重复的字符串,可能会遇到问题

  • 设置风格

image.png

堆栈视图

VStack 垂直排列布局

image.png

  • 间距
VStack(spacing: 30){

}
  • 对齐方式
VStack(alignment: .leading) { 

}  
  • Spacer() 占用/排挤

将堆栈的内容推到一侧。可添加多个

image.png

HStack 水平排列布局

同理

ZStack 重叠布局

按深度排列控件——它使视图重叠。

image.png

没有间距的概念,因为视图重叠,但它确实有对齐。

image.png

颜色

SwiftUI 为我们提供了一系列渲染颜色的功能,并且做到了既简单又强大。

image.png

  • 安全区域

白色空间为故意留空,因为 Apple 不希望重要内容被其他 UI 功能或设备上的任何圆角遮挡。所以,剩下的部分——整个中间的空间——被称为 “安全区域”,你可以自由地绘制它,而不用担心它可能被 iPhone 上的凹口剪掉。 白色空间的大小取决于您的设备,但在具有 Face ID 的 iPhone(例如 iPhone 15)上,您会发现动态岛区域(顶部的胶囊形区域)和主页指示器(水平方向)底部的条纹)保持未着色。

如果需要,你可以设置忽略安全区

image.png

重要的内容不要放置在安全区域之外,这一点至关重要。如果您的内容只是装饰性的(就像我们这里的背景颜色一样),那么将其扩展到安全区域之外是可以的。

  • 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)

  • 渐变

image.png

SwiftUI 为我们提供了 四种 可供使用的渐变,就像颜色一样,它们中的大多数也是可以在我们的 UI 中绘制的视图。

渐变由几个部分组成:

  1. 显示一系列颜色
  2. 尺寸和方向信息
  3. 要使用的渐变类型

一、线性渐变:朝一个方向移动

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)

image.png

三、角度渐变,(圆锥形或圆锥形渐变)

这样颜色会围绕一个圆圈循环,而不是向外辐射,并且可以产生一些美丽的效果。

AngularGradient(colors: [.red, .yellow, .green, .blue, .purple, .red], center: .center) 

image.png

四、渐变背景

您无法对其进行任何控制,并且您也只能将它们用作背景和前景样式,而不是单独的视图。 只需在任何颜色后添加即可创建此渐变.gradient

image.png

弹窗

状态属性控制弹窗。提示:双向数据绑定,当警报解除时 SwiftUI 会自动设置回 false,不需要手动处理。 image.png

image.png

小技巧

  • 数组
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

image.png