Set<T> 是 Swift
中一种存储唯一、无序元素的集合类型,所有元素必须是相同类型且可哈希(Hashable)。
struct ContentView: View {
@State private var colors: Set<Color> = [.red, .blue, .green]
var body: some View {
VStack {
ForEach(colors, id: \.self) { color in // ← 是否合法? 不合法
Circle()
.fill(color)
.frame(width: 50, height: 50)
}
}
}
}问题出在 ForEach(colors, id: \.self) 这行代码上。在
SwiftUI 中,ForEach 的第一个参数不能直接传入一个无序的
Set(集合),即使集合中的元素实现了 Hashable。
方案一:在ForEach中实时转为数组,直接调用
.sorted()(如果元素可排序)或者通过 Array(colors)
强制转换。因为 Color 没有默认的排序规则,直接转成 Array 即可:
ForEach(Array(colors), id: \.self) { color in
Circle()
.fill(color)
.frame(width: 50, height: 50)
}方案二:直接将数据源修改为
Array,如果你的业务逻辑不需要严格去重,最直截了当的方法是把
@State 的类型直接改成数组:
// 将 Set 改为 Array
@State private var colors: [Color] = [.red, .blue, .green]
var body: some View {
VStack {
// 自己要保证 colors中内容都唯一
ForEach(colors, id: \.self) { color in // 这样写就完全合法了
Circle()
.fill(color)
.frame(width: 50, height: 50)
}
}
}SwiftUI 的 Button 标准结构中,第一个代码块通常是点击要执行的动作(action),而第二个代码块(或者放在最后的尾随闭包)才是标签(label)。
Button {
// 这里的第一个代码块实际上是 action!
print("按钮被点击了")
} label: {
// 这里的第二个代码块才是 label
Text("点击我")
}标准的双闭包写法, 最清晰的写法是明确写出 action 和 label 的参数名
Button(action: {
// 1. 这里是点击按钮后要运行的代码
print("保存成功")
}) {
// 2. 这里是 label 代码块,决定按钮长什么样
HStack {
Image(systemName: "checkmark")
Text("保存")
}
}如果你使用的是最基础的纯文本按钮,SwiftUI 提供了一个便捷构造器,这时候不需要写 label 闭包,文本直接写在最前面:
Button("点击我") {
// 这里的唯一一个代码块是 action
print("Hello World")
}guard 是一个非常强大的控制流语句。它的核心作用是“提前退出”(Early Exit)——也就是说,它会检查一个条件,如果条件不满足,就立刻离开当前的方法或闭包。
核心规则:
guard 条件 else {
// 如果条件为 false,执行这里的代码
// 这里必须包含 return, break, continue 或 throw
return
}
// 如果条件为 true,继续执行后面的代码func TestingGuard(num: Int) -> Text {
guard num > 10 else {
return Text("num <= 10").bold()
}
return Text("num > 10").bold()
}常用场景,可选值解包 (Optional Unwrapping),用 guard 解包最爽的一点是:解包后的变量,在 guard 语句之外的整个作用域都可以使用。
// if let
func processUser(name: String?) {
if let unwrappedName = name {
// 所有逻辑都要写在这个大括号里
print("Hello, \(unwrappedName)")
} // 如果还要解包别的东西,大括号会越套越多
}
// guard let
func processUser(name: String?) {
// 如果 name 是 nil,直接拦截并退出
guard let unwrappedName = name else {
print("名字为空,无法处理")
return
}
// 出了 guard,unwrappedName 依然可用!
print("Hello, \(unwrappedName)")
// 这里可以继续写核心业务逻辑...
}可以用 .opacity
Color.gray.opacity(0.2)static(静态方法),意味着这个方法直接绑定在 Color
类型本身上,而不是它的某个具体实例上。调用时你可以直接写
Color.gray(0.5)。
// hue 色相 saturation 饱和度 brightness 亮度
extension Color {
static func gray(_ brightness: CGFloat) -> Color {
return Color(hue: 148/360, saturation: 0, brightness: brightness)
}
}CGFloat 就是苹果专门用来处理“界面坐标和尺寸”的浮点数(小数)类型,它名字的由来
界面上的几乎所有几何属性(位置、大小、角度、透明度)都是用 CGFloat 来表示的。例如:
width: 120.5)cornerRadius: 8.0)opacity: 0.5,也就是你上一题中代码里的
brightness)在 View 之间使用 @Binding
共享数据时,不要为了省事直接将整个大State直接绑定传递,用什么字段传什么就好了
不然一个大结构体直接梭哈乱传,肯定不行,早晚是个灾难。
如果不是nil则调用,是nil则什么都不做。
import SwiftUI
struct PegChooser: View {
let choices: [Peg]
let onChoose: ((Peg) -> void)?
var body: some View {
HStack {
ForEach(choices, id: \.self) { peg in
Button {
onChoose?(peg)
} label: {
PegView(peg: peg)
}
}
}
}
}PegChooser 调用样例
PegChooser(choices: game.pegChoices, onChoose: { peg in
game.setGuessPeg(peg, at: selection)
selection = (selection + 1) % game.masterCode.pegs.count
})
// 简化
PegChooser(choices: game.pegChoices) { peg in
game.setGuessPeg(peg, at: selection)
selection = (selection + 1) % game.masterCode.pegs.count
}在 SwiftUI 中,.onChange 是一个非常高频且实用的修饰符(Modifier),它用于监听某个状态(State)或绑定值(Binding)的变化,并在变化时触发特定的操作。
可以把 .onChange 附加在任何视图(包括 VStack)上。它的作用范围是全局的,只要它所在的视图层级处于活跃状态,它就能捕捉到指定值的变化。
VStack {
Text("当前计数: \(count)")
Button("增加") {
count += 1
}
}
.onChange(of: count) { oldValue, newValue in
print("值改变了!旧值是: \(oldValue),新值是: \(newValue)")
}默认情况下,.onChange 只有在值真正发生改变时才会触发。如果你希望在视图刚显示(App 启动或页面加载)时也执行一次这个逻辑,可以加上 .initial 参数:
VStack {
// 视图内容
}
.onChange(of: count, initial: true) { oldValue, newValue in
// 页面一打开就会执行一次,此时 oldValue 是 nil(如果它是 Optional)或其初始值
print("初始化或值改变:\(newValue)")
}监听多个值,想同时监听两个或多个变量,可以把它们打包成一个元组(Tuple):
.onChange(of: [firstName, lastName]) { oldValue, newValue in
print("姓名发生了变化")
}注意:元组需要遵循 Equatable 协议,或者像上面这样直接用数组包装同类型的值
struct CodeView<AncillaryView>: View where AncillaryView: View {
let ancillaryView: AncillaryView
var body: some View {
HStack {
ancillaryView
}
}
}在 Swift 中,fileprivate 是一个访问控制(Access Control)关键字。它的核心作用非常明确:将实体(变量、常量、函数、类、结构体等)的访问权限限制在它所在的同一个源文件(.swift 文件)内部。
// 文件:User.swift
// 这是一个 fileprivate 的类,只能在 User.swift 这个文件内被实例化或继承
fileprivate class HelperManager {
func doSomethingSecret() {
print("执行一些只有本文件知道的秘密操作")
}
}
public class User {
// 这是一个 fileprivate 的属性,同一文件内的其他类可以访问它
fileprivate var internalID: String = "12345"
public init() {}
public func testHelper() {
let helper = HelperManager() // 允许!因为同在一个文件
helper.doSomethingSecret()
}
}
// 即使是在 User 类外部,只要还在 User.swift 这个文件里,就可以访问 internalID
class UserValidator {
func validate(user: User) {
// 允许访问!因为 Validator 和 User 在同一个源文件中
print("校验用户的内部 ID: \(user.internalID)")
}
}