更多关于 DataFlow

Set

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

Button 尾随闭包写法

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 语法

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)")
    // 这里可以继续写核心业务逻辑...
}

Color 的透明度

可以用 .opacity

Color.gray.opacity(0.2)

在 Color 中扩展一个静态函数

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 就是苹果专门用来处理“界面坐标和尺寸”的浮点数(小数)类型,它名字的由来

界面上的几乎所有几何属性(位置、大小、角度、透明度)都是用 CGFloat 来表示的。例如:

@Binding 最小传递

在 View 之间使用 @Binding 共享数据时,不要为了省事直接将整个大State直接绑定传递,用什么字段传什么就好了

不然一个大结构体直接梭哈乱传,肯定不行,早晚是个灾难。

Optional function

如果不是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
}

.onChange

在 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 协议,或者像上面这样直接用数组包装同类型的值

泛型传递自定义View类型

struct CodeView<AncillaryView>: View where AncillaryView: View {
    let ancillaryView: AncillaryView

    var body: some View {
        HStack {
            ancillaryView
        }
    }
}

fileprivate

在 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)") 
    }
}