阅读视图

发现新文章,点击刷新页面。

Swift Continuations 完全指南:一口气弄懂 4 种“桥梁”

一、为什么需要 Continuations?

Swift 5.5 带来 async/await,但:

  • 老 SDK / 三方库仍用回调
  • 自己封装的 DispatchQueueTimerNotificationCenter 也是回调

Continuation 就是“回调 → async”的官方桥梁,

核心思想:“记住挂起点,等回调来了再恢复。”

二、4 种函数一张表

函数 是否检查误用 是否支持 throws 典型用途
withUnsafeContinuation 性能敏感、自己保证只 resume 一次
withCheckedContinuation 开发阶段、调试逻辑
withUnsafeThrowingContinuation 老回调用 Result/Error
withCheckedThrowingContinuation 推荐默认,又能抛又能查

口诀:“开发用 Checked,上线改 Unsafe;要抛错就带 Throwing。”

三、最小例子:把 DispatchQueue 计时器变成 async

老回调版:

func oldTimer(seconds: Int, completion: @Sendable @escaping () -> Void) {
    DispatchQueue.global().asyncAfter(deadline: .now() + .seconds(seconds)) {
        completion()
    }
}
  1. unsafe 版(最简)
func modernTimer(seconds: Int) async {
    await withUnsafeContinuation { continuation in
        oldTimer(seconds: seconds) {
            continuation.resume()          // 必须且只能调一次
        }
    }
}
  1. checked 版(开发推荐)
func safeTimer(seconds: Int) async {
    await withCheckedContinuation { continuation in
        oldTimer(seconds: seconds) {
            continuation.resume()
            // 如果这里手滑再 resume 一次 → 运行时直接 fatal + 清晰提示
        }
    }
}

错误示例:

SWIFT TASK CONTINUATION MISUSE: safeTimer(seconds:) tried to resume its continuation more than once

四、Throwing 版本:Network 回调 → async throws

老接口:

struct User {
    let name: String
    init(name: String) {
        self.name = name
    }
}

enum NetworkError: Error { case invalidID }

func fetchUser(id: String, completion: @escaping (Result<User, Error>) -> Void) {
    DispatchQueue.global().asyncAfter(deadline: .now() + 0.3) {
        id == "invalid" ? completion(.failure(NetworkError.invalidID))
                        : completion(.success(User(name: "A")))
    }
}

  1. throwing + checked(默认选它)
func modernFetch(id: String) async throws -> User {
    try await withCheckedThrowingContinuation { continuation in
        fetchUser(id: id) { result in
            switch result {
            case .success(let user):
                continuation.resume(returning: user)
            case .failure(let error):
                continuation.resume(throwing: error)
            }
        }
    }
}

// 使用
Task {
    do {
        let user = try await modernFetch(id: "123")
        print("Got", user.name)
    } catch {
        print("Error:", error)
    }
}
  1. 如果回调用两个单独参数 (Data?, Error?)
continuation.resume(
    with: Result(success: data, failure: error)   // 方便构造
)

五、Actor 隔离专用参数 isolation

场景:在 @MainActor 类里发起网络,回来必须主线程刷新 UI。

@MainActor
class VM {
    let label = UILabel()

    func updateText() async {
        let text = try? await withCheckedThrowingContinuation(
            isolation: MainActor.shared          // 👈 指定“恢复点”隔离域
        ) { continuation in
            fetchString { continuation.resume(returning: $0) }
        }
        label.text = text                        // 保证在主线程
    }
}
  • isolation: 让 resume 后的代码直接跑到指定 Actor
  • 省去一次 await MainActor.run { } 切换,更轻量
  • 仅 Throwing 系列支持(Swift 6.0+)

六、常见坑 & Tips

  1. 必须且只能 resume 一次

    忘记 → 任务永远挂起;多调 → 运行时 fatal

  2. 不要存储 continuation 到外部属性

    生命周期仅限 {} 块,出来就 UB。

  3. Task 取消感知

    若业务需支持取消,请用 withTaskCancellationHandlerAsyncStream

  4. 闭包捕获 self 小心循环引用

    老回调里 weak self 的习惯继续保持。

七、一句话总结

Continuation = 回调 → async/await 的“官方翻译器”。

记住口诀:

“开发用 Checked,上线 Unsafe;要抛错就 Throwing;主线程回来加 isolation。”

用好这把桥,就能一口气把全项目的老回调全部现代化,同时享受编译期检查和运行时性能。

深入理解 SwiftUI 的 Structural Identity:为什么“换个条件分支”就会丢状态?

什么是 Structural Identity?

SwiftUI 通过结构身份(Structural Identity)判断新旧视图树中的同一个节点:

  • 类型相同
  • 在层级中的位置相同
  • 祖先链的身份相同

只有当三者一致时,SwiftUI 才认为“这是老熟人”,保留其内部 @State / @StateObject 等局部状态;否则旧节点被销毁,新节点重新创建 → 状态归零。

Update vs Redraw:先分清两个概念

术语 含义 是否一定刷像素
Update 重新初始化 View 值(执行 body ❌ 仅 diff
Redraw 向 GPU 提交绘制指令 ✅ 真正刷界面

Structural Identity 决定的是能否复用旧节点,从而影响Update 次数与状态生命周期;Redraw只在属性变化时发生。

经典踩坑:if-else 导致状态丢失

struct ExampleView: View {
    @State private var isOn = true
    
    var body: some View {
        VStack {
            Text("Is on: \(isOn ? "yes" : "no")")
            Button("Switch") { isOn.toggle() }
            
            if isOn {              // ← 条件分支
                BottomViewOn()     // ① 类型与位置在变
            } else {
                BottomViewOff()    // ② SwiftUI 认为是两个**不同**节点
            }
        }
    }
}

结果:

  • isOn 变化 → 分支切换 → BottomViewOn/Off 类型不同 → Structural Identity 失效 → 旧节点被销毁。
  • 两个子视图内部的 @State / @StateObject 全部重置。

保持身份的两种策略

外部化状态(推荐)

把需要持久的属性提升到父视图或注入 Observable 对象:

@StateObject private var bottomVM = BottomViewModel()
...
if isOn {
    BottomViewOn(vm: bottomVM)
} else {
    BottomViewOff(vm: bottomVM)
}

节点虽换,但状态由外部 VM 持有,不再依赖局部 @State

使用透明/位移技巧(ZStack + opacity)

ZStack {
    BottomViewOn()
        .opacity(isOn ? 1 : 0)
        .accessibilityHidden(!isOn)
    
    BottomViewOff()
        .opacity(isOn ? 0 : 1)
        .accessibilityHidden(isOn)
}
  • 两个子视图始终存在 → 类型 & 位置不变 → 身份保留。
  • 仅改变视觉属性(opacity),无节点销毁 → 状态常驻。

代价:同时占用内存/渲染通道,适合轻量级视图。

列表中的身份:为什么 id: 如此重要

List(users, id: \.id) { user in
    RowView(user: user)
}
  • 提供稳定且唯一的 id 后,SwiftUI 才能追踪同一数据项在插入/删除/移动后的位置。
  • 用数组索引或可能重复的属性当 id → 身份错乱 → 出现“行内容错位”或“状态串台”。

性能提示:不要滥用 .id(UUID()) 强制 reload

MyComplexView()
    .id(viewId)          // 每次改 UUID → 节点被判定为新实例
  • 确实能强制刷新,但会丢弃所有内部状态 & 重建整个子树。

  • 只在你真正需要重置(retry、错误恢复)时使用;

    日常状态更新应靠数据驱动,而非改身份。

一句话总结

“类型 + 位置 + 祖先” 三要素只要变了,SwiftUI 就认不出旧视图 → 状态清零。

想让数据常驻:

  • 外部化状态(首选)
  • 保持结构不动(ZStack/opacity 技巧)
  • 给列表稳定 id

记住这三点,再不会被“莫名其妙丢状态”坑到。

参考资料

  1. Understanding structural identity in SwiftUI

Swift 的 `withoutActuallyEscaping`:借一个 `@escaping` 身份,但不真的逃跑

一、为什么会有“假逃跑”需求?

默认情况下,函数参数的闭包是 non-escaping:

  • 只能在函数体内同步调用
  • 编译器可把闭包放在栈上,更快、无堆分配

但某些标准库 API(lazy.filterDispatchQueue.async 等)签名要求 @escaping

于是出现尴尬场景:

“我知道闭包不会真的逃出去,只是传进另一个立即执行的 API,可编译器非要 @escaping!”

withoutActuallyEscaping(_:do:) 就是为此而生的临时逃生通道。

二、签名与语义

public func withoutActuallyEscaping<ClosureType, ResultType, Failure>(_ closure: ClosureType, do body: (_ escapingClosure: ClosureType) throws(Failure) -> ResultType) throws(Failure) -> ResultType where Failure : Error
  • 入参:原 non-escaping 闭包
  • 闭包:拿到一个临时的 @escaping 副本
  • 保证:副本不会真的逃出 do 块,否则编译器报错

三、最小示例:lazy.filter 的编译错误

func allPositive(in nums: [Int], match predicate: (Int) -> Bool) -> Bool {
    // ❌ Escaping closure captures non-escaping parameter 'predicate'
    return nums.lazy.filter { !predicate($0) }.isEmpty
}

修复:借身份

func allPositive(in nums: [Int], match predicate: (Int) -> Bool) -> Bool {
    withoutActuallyEscaping(predicate) { escapablePredicate in
        nums.lazy.filter { !escapablePredicate($0) }.isEmpty
    }
} // ✅ 编译通过,predicate 仍保持 non-escaping
  • escapablePredicate 只能活在 do 块里
  • 块结束后,副本失效,原闭包生命周期不变

四、并发场景:同时派两个闭包

func perform(_ f: @Sendable () -> Void, simultaneouslyWith g: @Sendable () -> Void) {
    withoutActuallyEscaping(f) { escapableF in
        withoutActuallyEscaping(g) { escapableG in
            let queue = DispatchQueue(label: "perf", attributes: .concurrent)
            queue.async(execute: escapableF)
            queue.async(execute: escapableG)
            queue.sync(flags: .barrier) {}   // 等待二者完成
        }
    }
}

优势:

  • API 表面仍声明 non-escaping,调用者无需关心内部并发
  • 无堆分配:闭包仍在调用栈上,性能优于真正的 @escaping

五、性能对比:escaping vs withoutActuallyEscaping

测试环境:M2 | Release | -Ounchecked

闭包体量:捕获 3 个 Int

方案 每次调用耗时 内存
@escaping堆分配 ≈ 85 ns 堆块
withoutActuallyEscaping ≈ 12 ns

→ 7 倍速度差,高频场景(每帧 1000 次)收益明显。

六、使用场景 checklist

✅ 适合

  • lazy.* / Sequence 需要 @escaping 但立即执行
  • 并发小任务(DispatchQueue.concurrentPerform、自定义 barrier)
  • 想把API 保持 non-escaping 同时用内部 escaping 库

❌ 不适合

  • 闭包真的要存属性、逃逸到块外
  • 异步结构化并发(Task {})——已自动 escaping,无需此技巧

七、常见编译错误速查

错误 原因 修复
Escaping closure captures non-escaping parameter 直接把 non-escaping 传进 escaping API 包一层 withoutActuallyEscaping
Call to main actor-isolated property in escapable closure 副本可能跑到别的隔离域 把捕获值先拉到局部 let
Closure consumed in withActuallyEscaping block 试图把副本存属性/逃逸 别存,保证只在块内使用

八、Swift 6 并发模式下的注意点

withoutActuallyEscaping 的副本隔离与原始闭包相同:

@MainActor
func work(_ fn: () -> Void) {
    withoutActuallyEscaping(fn) { esc in
        DispatchQueue.global().async {
            esc()   // ❌ 主线程闭包在后台执行
        }
    }
}

→ 隔离违规,编译器会报错。

解决:要么在同隔离域执行,要么先把数据拉成 Sendable 再传递。

九、一句话总结

withoutActuallyEscaping = “借一个 @escaping 身份证,但不真的逃跑。”

它让你:

  • 保持 API 干净(non-escaping)
  • 临时满足标准库 escaping 需求
  • 兼得性能(栈分配)与安全(编译器确保不逃出作用域)

记住口诀:

“需要 escaping 签名,却不想真的逃逸——就上 withoutActuallyEscaping!”

Opaque Types 完全指南:Swift 的“密封盒子”魔法

一、什么是 Opaque Type?

一句话:“函数返回一个具体类型,但调用者只能看到它遵守的协议。”

语法:

func makeButton() -> some View {
    Text("Hi")
}

some View 就是不透明返回类型(opaque return type)。

编译器知道盒子里是 Text,但调用者只能把它当 View 用,看不见牌子。

二、为什么不用 any View

先踩坑:

func makeButton() -> View {   // ❌ 编译错误
    Text("Hi")
}

错误:

Type 'any View' cannot conform to 'View'

原因:View 含关联类型(Body),Swift 无法把“任意盒子”再当成 View 继续拼接。

→ 必须用 some 保证单箱型 + 协议一致。

三、some 的三大规则

规则 示例 结果
单型 始终返回同一具体类型
协议限制 协议含关联类型也可
不能分支返回不同型 Bool ? Text : Image

四、实战:SwiftUI 日常

func primaryButton(_ title: String) -> some View {
    Text(title)
        .padding()
        .foregroundStyle(.white)
        .background(.blue)
        .clipShape(Capsule())
}

用法:

var body: some View {
    primaryButton("Save")   // 透明盒子,继续链式修饰
        .scaleEffect(0.9)
}

性能:零类型擦除,无运行时开销。

五、分支返回不同类型?用 AnyView 救场

错误示例:

func errorView(hasError: Bool) -> some View {
    hasError ? Text("Error") : Image(systemName: "checkmark")
    // ❌ Branches have mismatching types
}

修复:

func errorView(hasError: Bool) -> some View {
    Group {          // ← 同一容器类型
        if hasError {
            Text("Error")
        } else {
            Image(systemName: "checkmark")
        }
    }
}

或显式擦除:

func errorView(hasError: Bool) -> some View {
    AnyView(hasError ? Text("Error") : Image(systemName: "checkmark"))
}

提醒:AnyView 有微小性能成本,优先用 Group@ViewBuilder 保持单型。

六、自定义协议同样玩转

protocol Weapon {
    associatedtype Element
    func damage() -> Int
}

struct Sword: Weapon {
    typealias Element = String
    
    func damage() -> Int { 20 }
}

struct Bow: Weapon {
    typealias Element = Int
    func damage() -> Int { 12 }
}

// ✅ 单型返回
func equipSword() -> some Weapon {
    Sword()
}

// ❌ 随机返回两种类型
func equipRandom() -> some Weapon {
    Bool.random() ? Sword() : Bow()
}

→ 与 SwiftUI 同理:some 要求 1 个具体箱型。

七、some vs any 速查表

维度 some T any T
内部实现 单型,编译期已知 类型擦除,运行期装箱
性能 零开销 有间接调用 & 内存分配
返回多型
协议含关联类型
使用场景 SwiftUI 链式、泛型算法 存储异构集合、回调多型

口诀:“链式用 some,多型用 any。”

八、泛型 + Opaque 进阶:返回“不透明集合”

func makeButtons() -> some RandomAccessCollection<some View> {
    (0..<5).map { i in
        Text("Btn \(i)")
    }
}

→ 集合元素也是不透明 View,外部只能当 View 用,继续链式拼接。

九、常见编译错误对照

错误原文 原因 修复
Branches have mismatching types 返回不同具体类型 用 Group / AnyView 包成单型
Protocol with associated type can’t be used as return type 写成 -> Weapon 改成 -> some Weapon
Return type of function declared opaque could not be inferred 没返回或返回协议不一致 确保返回单个遵守协议的具体类型

十、总结:一句话背下来

some = “密封盒子,里面只有一个玩具,但我不告诉你牌子。”

它让 Swift 在不暴露具体类型的前提下, 依旧享有泛型性能 + 协议抽象 + 链式调用。

记住口诀:“链式 SwiftUI 用 some,异构集合用 any;分支不同型,Group 先包装。”

下次再看到 some View,你就知道—— 不是魔法,只是编译器帮你守着的密封盒子。

Thread.sleep vs Task.sleep:一句话记住“别再阻塞线程”

一、两句话区分

API 阻塞谁 后果
Thread.sleep(forTimeInterval:) 整条线程 线程池“饿死”,其他任务无法调度
Task.sleep(nanoseconds:) 当前任务 线程立刻转去跑别的任务,资源不浪费

结论:

Swift Concurrency 时代,永远用 Task.sleep,不要用 Thread.sleep

二、为什么 Thread.sleep 这么毒?

  1. 线程池大小固定

    Swift 并发运行时默认只开 CPU 核心数条线程(M1 ≈ 8)。

  2. 你睡一条,就少一条

    Thread.sleep 让线程进入内核阻塞状态,不会被运行时回收。

  3. 四条全睡 → App 卡死

示例:

for _ in 0..<4 {
    Task {
        Thread.sleep(forTimeInterval: 10)
    }   // 4 条线程瞬间用完
}

此时所有 Task(网络、UI、动画)都排不上队,应用假死 10 秒。

三、Task.sleep 是怎么做到“不堵线程”的?

await try Task.sleep(nanoseconds: 1_000_000_000) // 1 秒

内部流程:

  1. 当前任务被挂起 → 让出线程
  2. 运行时把线程分配给其他待执行任务
  3. 1 秒后,原任务重新入队 → 任意空闲线程继续执行

→ 零线程浪费,零阻塞,零内核调用(用户态挂起)。

四、代码对比:同样“等 1 秒”,效果天差地别

Thread.sleep 版(灾难)

Task {
    print("start", Date())
    Thread.sleep(forTimeInterval: 1)          // 阻塞整条线程
    print("end  ", Date())
}

Task.sleep 版(安全)

Task {
    print("start", Date())
    await try Task.sleep(nanoseconds: 1_000_000_000) // 让出线程
    print("end  ", Date())
}

并行 10 个任务:

  • Thread.sleep → 10 秒总耗时(串行)
  • Task.sleep → 约 1 秒全部完成(并发)

五、常见踩坑 QA

❓ “我就想在 Playground 里拖延一下,也不能 Thread.sleep?”

→ 用 Task.sleep 一样简单:

Task {
    await try Task.sleep(nanoseconds: 2_000_000_000)
    print("done")
}

❓ “需要主线程延迟,用哪个?”

→ 依旧 Task.sleep,它会在任意线程醒来,若需主线程再切回来:

await MainActor.run {
    // 主线程工作
}

❓ “老代码里大量 Thread.sleep 怎么批量替换?”

→ 正则 + 脚本一键迁移:

# 示例:sed -i 's/Thread.sleep(\([^)]*\))/await Task.sleep(UInt64(\1 * 1_000_000_000))/g' *.swift

六、一句话总结

“睡线程”是毒药,“睡任务”才是解药。

记住:

Swift Concurrency 世界里,看到 Thread.sleep 就改成 Task.sleep——没有任何例外。

Swift 6.2 新特性

Swift 6.2 内置于 Xcode 26,主要带来了如下的新特性。

标识符

显著扩展了创建标识符的字符范围,当使用``时,可以更随性。

func `this is a function`(param: String) -> String {
    return "Hello, \(param)"
}

enum HTTPStatus: String {
    case `200` = "Success"
    case `404` = "Not Found"
    case `500` = "Internal Server Error"
}

字符串插值支持默认值

字符串插值,可以设置默认值。当插值为可选型并且其值为nil时,可以使用提供的默认值。

var name: String? = nil
// Swift6.2之前
print("Hello, \(name ?? "zhangsan")!")
// Swift6.2之后
print("Hello, \(name, default: "zhangsan")!")

InlineArray

引入了一种新的数组类型,表示固定大小的数组,性能优越。

var array: InlineArray<3, String> = ["zhangsan", "lisi", "wangwu"]
var array2: InlineArray = ["zhangsan", "lisi", "wangwu"]

enumerated()返回的类型遵守Collection

进一步简化了在 SwiftUI 中的使用。

import SwiftUI

struct ContentView: View {
    @State private var names = ["ZhangSan", "LiSi", "WangWu"]

    var body: some View {
        // Swift6.2之前
        List(Array(names.enumerated()), id: \.element) { turple in
            HStack {
                Text("\(turple.offset)")
                
                Text(turple.element)
            }
        }
        // Swift6.2之后
        List(names.enumerated(), id: \.element) { turple in
            HStack {
                Text("\(turple.offset)")
                
                Text(turple.element)
            }
        }
    }
}

weak let

  • 引入了weak let,允许声明不可变的弱引用属性。
  • 解决 Sendable 类型中弱引用的问题。
import UIKit

class ViewController: UIViewController {
    // Swift6.2之前
    @IBOutlet weak var redView: UIView!
    // Swift6.2之后
    @IBOutlet weak let greenView: UIView!

    override func viewDidLoad() {
        super.viewDidLoad()

    }
}

Backtrace

引入了一个新的结构体,提供运行时堆栈追踪,可以捕获从当前代码处到调用处的函数调用序列。

import Runtime

func functionOne() {
    do {
        if let frames = try? Backtrace.capture().symbolicated()?.frames {
            print(frames)
        }
        else {
            print("Failed to capture backtrace.")
        }
    } catch {
        print(error.localizedDescription)
    }
}

func functionTwo() {
    functionOne()
}

func functionThree() {
    functionTwo()
}

functionThree()

并发编程

  • 在 Swift 6.2 之前,nonisolated 异步函数会自动在后台线程执行。但在 Swift 6.2 之后,nonisolated 异步函数将会在调用者的 actor 上执行。此时通过@concurrent进行修饰,可以继续让其按照之前的方式运行。
  • 当使用@concurrent时,函数会发生以下行为。
    • 将在后台线程运行,即使从主线程调用。
    • 创建一个新的隔离,与调用者分离。
    • 所有参数和返回值必须符合 Sendable 协议。
  • 适用场景。
    • 函数执行时间较长。
    • 函数是 CPU 密集型或者可能阻塞线程,比如大量的数据转换,I/O 操作等。
actor SomeActor {
  // 不允许
  @concurrent
  func doSomething() async throws {
  }

  // 允许
  @concurrent
  nonisolated func doAnotherthing() async throws {
  }
}

Combine在swiftUI中的使用

Combine 在 SwiftUI 中的使用是天作之合。SwiftUI 的整个设计理念就是响应式,而 Combine 正是 Apple 为 Swift 生态提供的官方响应式编程框架。它们协同工作,为构建现代、声明式的 UI 提供了强大的支持。

核心用途:驱动数据流和状态更新

在 SwiftUI 中,Combine 主要被用于以下三个核心场景:

  1. 状态管理:使用 @Published 包装属性,使其成为可观察的 Publisher。
  2. 生命周期与UI事件处理:使用 .onReceive 修饰符监听外部事件。
  3. 异步操作处理:封装网络请求、定时器等异步任务。

SwiftUI 内置了对 Combine 的深度集成,你通常不需要手动调用 sinkstore,框架会自动帮你处理订阅和生命周期。


1. 状态管理:@PublishedObservableObject

这是 Combine 在 SwiftUI 中最常见、最重要的用法。它让你能够创建一个可观察的数据模型,当模型发生变化时,自动触发 UI 更新。

示例:创建一个可观察的 ViewModel

import Combine
import SwiftUI

// 1. 让 class 遵循 ObservableObject 协议
class TimerViewModel: ObservableObject {
    
    // 2. 使用 @Published 包装任何需要被观察的属性
    // 当这些属性的值改变时,会发出事件,通知所有订阅的 View 更新。
    @Published var currentTime: String = "00:00:00"
    @Published var isRunning: Bool = false
    
    private var timer: AnyCancellable?
    private var startDate: Date?

    func startTimer() {
        isRunning = true
        startDate = Date()
        
        // 3. 使用 Combine 创建定时器 Publisher
        timer = Timer.publish(every: 1.0, on: .main, in: .common)
            .autoconnect() // 自动连接
            .sink { [weak self] _ in
                guard let self = self, let startDate = self.startDate else { return }
                // 计算时间差
                let elapsed = Date().timeIntervalSince(startDate)
                // 4. 更新 @Published 属性,触发 UI 更新
                self.currentTime = self.formatTimeInterval(elapsed)
            }
    }
    
    func stopTimer() {
        isRunning = false
        timer?.cancel() // 取消订阅,停止定时器
        timer = nil
    }
    
    private func formatTimeInterval(_ interval: TimeInterval) -> String {
        // ... 格式化时间的逻辑
        let hours = Int(interval) / 3600
        let minutes = Int(interval) / 60 % 60
        let seconds = Int(interval) % 60
        return String(format: "%02i:%02i:%02i", hours, minutes, seconds)
    }
}

在 SwiftUI View 中使用

struct TimerView: View {
    // 4. 使用 @StateObject 或 @ObservedObject 来注入 ObservableObject
    // SwiftUI 会自动订阅这个对象的 @Published 属性变化
    @StateObject var viewModel = TimerViewModel()
    
    var body: some View {
        VStack {
            Text(viewModel.currentTime) // 5. 直接使用 @Published 属性
                .font(.largeTitle)
            
            Button(action: {
                if viewModel.isRunning {
                    viewModel.stopTimer()
                } else {
                    viewModel.startTimer()
                }
            }) {
                Text(viewModel.isRunning ? "Stop" : "Start")
                    .padding()
            }
        }
    }
}

发生了什么?

  1. @StateObject 创建并持有 TimerViewModel 实例。
  2. SwiftUI 自动订阅了 viewModel对象发布者(即 objectWillChange Publisher)。
  3. 当任何一个 @Published 属性(currentTimeisRunning)发生变化时,viewModel 会发出事件。
  4. SwiftUI 收到事件后,会重新计算 body 属性,从而更新 UI。
  5. 永远不需要手动调用 viewModel.objectWillChange.sink {...},SwiftUI 帮你完成了所有订阅和管理工作。

2. 监听外部事件:.onReceive 修饰符

当你需要监听一个外部的 Publisher(不是 @Published 属性),并对它的值做出反应时,使用 .onReceive 修饰符。

示例:监听系统时间或通知

struct ContentView: View {
    @State private var systemTime: String = ""
    
    // 创建一个定时器 Publisher
    private let timerPublisher = Timer.publish(every: 1.0, on: .main, in: .common).autoconnect()
    
    var body: some View {
        Text("System Time: \(systemTime)")
            .font(.title)
            .onReceive(timerPublisher) { date in
                // 这个闭包每秒都会在主线程被调用一次
                let formatter = DateFormatter()
                formatter.timeStyle = .medium
                systemTime = formatter.string(from: date)
            }
    }
}

3. 处理异步任务:网络请求、数据库查询

将异步操作封装成 Combine Publisher,然后在 View 的初始化或 .task 修饰符中启动它。

示例:在 ViewModel 中发起网络请求

class UserProfileViewModel: ObservableObject {
    @Published var user: User?
    @Published var isLoading = false
    @Published var error: Error?
    
    private var cancellables = Set<AnyCancellable>()
    
    func fetchUser(userId: String) {
        isLoading = true
        error = nil
        
        // 1. 创建网络请求 Publisher
        let url = URL(string: "https://api.example.com/users/\(userId)")!
        URLSession.shared.dataTaskPublisher(for: url)
            .map(\.data)
            .decode(type: User.self, decoder: JSONDecoder())
            .receive(on: DispatchQueue.main) // 2. 确保回到主线程更新 UI
            .sink { [weak self] completion in
                self?.isLoading = false
                if case .failure(let error) = completion {
                    self?.error = error
                }
            } receiveValue: { [weak self] user in
                self?.user = user // 3. 更新 @Published 属性,触发 UI 更新
            }
            .store(in: &cancellables) // 4. 必须存储订阅!
    }
}
struct UserProfileView: View {
    @StateObject var viewModel = UserProfileViewModel()
    let userId: String
    
    var body: some View {
        VStack {
            if viewModel.isLoading {
                ProgressView()
            } else if let error = viewModel.error {
                Text("Error: \(error.localizedDescription)")
            } else if let user = viewModel.user {
                Text("Hello, \(user.name)!")
            }
        }
        .onAppear {
            // 5. 在视图出现时触发网络请求
            viewModel.fetchUser(userId: userId)
        }
    }
}

最佳实践总结

  1. 使用 ObservableObject + @Published:这是管理状态和驱动 UI 更新的首选方式
  2. 使用 @StateObject 用于创建@ObservedObject 用于从父视图传递。
  3. ViewModel 中处理逻辑:将异步操作、业务逻辑放在 ViewModel 中,保持 View 的简洁。
  4. 妥善管理订阅:在 ViewModel 中使用 Set<AnyCancellable> 来存储所有订阅,确保它们在 ViewModel 销毁时被自动取消。
  5. 线程安全:使用 .receive(on:) 操作符确保在收到数据后切换回主线程再更新 @Published 属性。
  6. 使用 .onReceive 处理特殊事件:用于监听那些不适合或无法用 @Published 属性表示的外部事件流。

Combine 和 SwiftUI 的组合,使得数据流变得清晰、直接且易于维护:数据从 ViewModel 的 @Published 属性流出,自动驱动 SwiftUI View 的更新。你几乎不需要手动处理订阅的生命周期,框架为你搞定了一切。

SwiftUI @ViewBuilder 的魔法

定义

先看一下ViewBuilder的定义,实际上这是一个@resultBuilder 的 struct。

@resultBuilder public struct ViewBuilder {

    public static func buildBlock() -> EmptyView

    public static func buildBlock<Content>(_ content: Content) -> Content where Content : View
}

@resultBuilder 属性封装具体用法查看官方文档

用于函数参数的用法

下面是一个简单的例子,将 @ViewBuilder 用于参数

func contextMenu<MenuItems: View>(@ViewBuilder menuItems: () -> MenuItems) -> some View

在调用的时候可以指定多个 View,而且不需要逗号分割,

myView.contextMenu {
    Text("Cut")
    Text("Copy")
    Text("Paste")
    if isSymbol {
        Text("Jump to Definition")
    }
}

多个Text是因为 buildBlock 的多参数重载实现,最多到 C9:

static func buildBlock<C0, C1>(C0, C1) -> TupleView<(C0, C1)>
static func buildBlock<C0, C1, C2>(C0, C1, C2) -> TupleView<(C0, C1, C2)>
static func buildBlock<C0, C1, C2, C3>(C0, C1, C2, C3) -> TupleView<(C0, C1, C2, C3)>
static func buildBlock<C0, C1, C2, C3, C4>(C0, C1, C2, C3, C4) -> TupleView<(C0, C1, C2, C3, C4)>
static func buildBlock<C0, C1, C2, C3, C4, C5>(C0, C1, C2, C3, C4, C5) -> TupleView<(C0, C1, C2, C3, C4, C5)>
static func buildBlock<C0, C1, C2, C3, C4, C5, C6>(C0, C1, C2, C3, C4, C5, C6) -> TupleView<(C0, C1, C2, C3, C4, C5, C6)>
static func buildBlock<C0, C1, C2, C3, C4, C5, C6, C7>(C0, C1, C2, C3, C4, C5, C6, C7) -> TupleView<(C0, C1, C2, C3, C4, C5, C6, C7)>
static func buildBlock<C0, C1, C2, C3, C4, C5, C6, C7, C8>(C0, C1, C2, C3, C4, C5, C6, C7, C8) -> TupleView<(C0, C1, C2, C3, C4, C5, C6, C7, C8)>
static func buildBlock<C0, C1, C2, C3, C4, C5, C6, C7, C8, C9>(C0, C1, C2, C3, C4, C5, C6, C7, C8, C9) -> TupleView<(C0, C1, C2, C3, C4, C5, C6, C7, C8, C9)>

上面的if支持是因为ViewBuilder实现了buildEither(second:) 静态方法,还有其他更多的写法,比如For循环。

用于返回值的用法

先来梳理一下问题,当你创建一个函数,返回类型是View时,如果编译器不能在编译阶段就确定类型,那就会出现泛型无法推断类型的编译错误。

比如下面的例子,只能在运行期才能确定返回值类型。

func showTextOrImage(isImage: Bool) -> some View {

    if !isImage {
        Text("This is a title")
            .foregroundColor(.red)
    }
    else {
        Image(systemName: "square.and.arrow.up")
            .foregroundColor(.blue)
    }
}

有几种方式解决这个问题,核心就是再包一层,比如容易想到的就是自定义一个 View:

struct ShowTextOrImage: View {
    let isImage: Bool

    var body: some View {
        if !isImage {
            Text("This is a title")
                .foregroundColor(.red)
        }
        else {
            Image(systemName: "square.and.arrow.up")
                .foregroundColor(.blue)
        }
    }
}

这种方式不好的地方就是需要另写一个 struct,更好的方式是在 struct 内部通过函数就可以得到需要的 View,我们可以使用Group来实现:

// 使用 Group 包装以下
func groupDemo(isImage: Bool) -> some View {
    Group {
        if !isImage {
            Text("This is a title")
                .foregroundColor(.red)
        }
        else {
            return AnyView(Image(systemName: "square.and.arrow.up")
                .foregroundColor(.blue))
        }
    }
}

或者 转成AnyView擦除类型具体的类型:

// AnyView 擦除类型
func anyViewDemo(isImage: Bool) -> some View {
    if !isImage {
        return AnyView(Text("This is a title")
            .foregroundColor(.red))
    }
    else {
        return AnyView(Image(systemName: "square.and.arrow.up")
            .foregroundColor(.blue))
    }
}

最后一种方式就是使用 @ViewBuilder 属性封装,也可以达到目的。

@ViewBuilder
func viewBuilderDemo(isImage: Bool) -> some View {
    if !isImage {
        Text("This is a title")
            .foregroundColor(.red)
    }
    else {
        Image(systemName: "square.and.arrow.up")
            .foregroundColor(.blue)
    }
}

这里不会报错的原因,也是@resultBuilder的作用,因为ViewBuilder实现了buildEither(second:),支持 if-else 语法

用于属性

当你想实现一个自定义的VStack时,可以这么做:

struct CustomVStack<Content: View>: View {
    let content: () -> Content

    var body: some View {
        VStack {
            // custom stuff here
            content()
        }
    }
}

但是这种方式只能接收单个View,无法传入多个 View:

CustomVStack {
    Text("Hello")
    Text("Hello")
}

为了达到原生VStack的效果,就必须增加一个构造函数:

init(@ViewBuilder content: @escaping () -> Content) {
    self.content = content
}

每次定义容器 View 时,都得这么写的话就很啰嗦,所以有人向官方提建议,看是否能把@ViewBuilder直接用于属性。

最终这个提案通过了,发布在 Swift 5.4 版本:

struct CustomVStack<Content: View>: View {
    @ViewBuilder let content: Content

    var body: some View {
        VStack {
            content
        }
    }
}

其他

Flutter混合开发:在iOS工程中嵌入Flutter Module


最近项目需求,需要在iOS原生工程中嵌入Flutter应用。启动APP后,进入到原生iOS工程的启动页、登录页,登录后就进入到Flutter侧的页面;在Flutter侧的应用中又需要进入到原生iOS工程的内购页,以及策略模式下的H5页面;在Flutter侧应用退出登录、删除账号后返回到原生iOS的登录页。如下图所示流程。

基于此需求,本文档将详细介绍如何创建Flutter Module并将其集成到iOS宿主工程中。


创建Flutter Module工程

1. 使用Flutter CLI创建项目

flutter create flutter_module --template=module

cd flutter_module

2. Flutter Module项目结构

项目创建成功后,项目结构如下:

flutter_module/
├── .ios/ # iOS相关配置文件
│ └── Flutter/
│ ├── podhelper.rb # CocoaPods集成脚本
│ └── Flutter.podspec # Pod规范文件
├── lib/ # Flutter Dart代码
│ └── main.dart # 入口文件
├── pubspec.yaml # 依赖配置
└── .metadata # Flutter元数据

3. 配置pubspec.yaml文件

因为我的是iOS工程,安卓的配置直接注释掉了,这里的iosBundleIdentifier和version,是这个Flutter Module的,并不会影响iOS宿主工程中BundleIdentifier和version的配置。

name: flutter_module
description: A Flutter module for iOS integration.

publish_to: 'none'
version: 1.0.0+1

environment:
  sdk: ^3.24.3

dependencies:
  flutter:
    sdk: flutter
  cupertino_icons: ^1.0.8

dev_dependencies:
  flutter_test:
    sdk: flutter
  flutter_lints: ^4.0.0

flutter:
  uses-material-design: true
  
  # 配置Module相关设置
  module:
   # androidX: true
   # androidPackage: com.example.flutter_module
    iosBundleIdentifier: com.example.flutterModule

创建iOS宿主工程

1. 创建iOS项目

打开XCode,选择iOS App。

这里的Interface选择Storyboard,这样才会有AppDelegate文件。

2. iOS项目结构

创建完成后,得到下面的项目结构:

HostApp/
├── HostApp/
│ ├── AppDelegate.swift
│ ├── SceneDelegate.swift
│ ├── ViewController.swift
│ ├── Main.storyboard
│ └── Info.plist
├── HostApp.xcodeproj
└── Podfile (将要创建)

3. 创建Podfile文件

使用CocoaPods命令创建。

pod init

添加上项目所需的依赖,这里举例添加一些iOS原生工程需要的依赖。

# Uncomment the next line to define a global platform for your project

platform :ios, '16.0'

target 'Foody' do
  # Comment the next line if you don't want to use dynamic frameworks
  use_frameworks!

  pod 'Alamofire'
  pod 'FBSDKCoreKit', '18.0.0'
  pod 'CodableWrapper'
  pod 'Adjust', '4.38.4'
  pod 'SnapKit', '~> 5.0'

end

使用CocoaPods命令安装依赖。

pod install

完成后使用.xcworkspace文件打开项目,注意是.xcworkspace,不是.xcproject,.xcproject不包含CocoaPods依赖。

open HostApp.xcworkspace

集成Flutter Module工程到iOS工程

1. 调整项目结构

将Flutter Module工程和iOS工程放在同一个根目录下,如下结构:

MyApp/
  |-HostApp
  |-flutter_module

2. 修改Podfile

新增Flutter Module的配置,链接源码。同时在Flutter Module中可能需要使用permission_handler插件请求权限,所以在这里也需要配置需要的权限,注意是在这个iOS原生宿主工程中进行配置,而不是在Flutter Module中的.ios目录下的Podfile配置。

# Uncomment the next line to define a global platform for your project

platform :ios, '16.0'

# 新增配置,链接Flutter Module代码
flutter_application_path = '../foody_flutter'
load File.join(flutter_application_path, '.ios', 'Flutter', 'podhelper.rb')

target 'Foody' do
  # Comment the next line if you don't want to use dynamic frameworks
  use_frameworks!

  # 新增
  install_all_flutter_pods(flutter_application_path)

  pod 'Alamofire'
  pod 'IQKeyboardManagerSwift'
  pod 'FBSDKCoreKit', '18.0.0'
  pod 'CodableWrapper'
  pod 'Adjust', '4.38.4'
  pod 'SnapKit', '~> 5.0'
  pod 'SVProgressHUD'

end

# 新增
post_install do |installer|
  flutter_post_install(installer) if defined?(flutter_post_install)

  # 使用PermissionHandler处理Flutter侧的权限请求
  installer.pods_project.targets.each do |target|
    target.build_configurations.each do |config|
      config.build_settings['GCC_PREPROCESSOR_DEFINITIONS'] ||= [
        '$(inherited)',
        'PERMISSION_CAMERA=1',
        'PERMISSION_MICROPHONE=1',
        'PERMISSION_PHOTOS=1',
        'PERMISSION_SPEECH_RECOGNIZER=1',
        'PERMISSION_APP_TRACKING_TRANSPARENCY=1',
      ]
    end
  end
end

3. 修改Info.plist

将iOS工程下的Info.plist拆分成Info-Debug.plist和Info.Release.plist两个,分别对应Debug和Release下的两种。

在Info-Debug.plist中新增Bonjour services,并新增Item值为_dartVmService._tcp。

对于需要使用到的权限,分别在Debug和Release下的文件进行配置即可。

4. 修改Target配置

首先找到Build Phases下的Copy Bundle Resources,检查是否有Info-Release.plist文件,如果有的话就删除。

在Build Setting中找到Packing中修改配置,修改Info.plist File,将值修改成Foody/Info-$(CONFIGURATION).plist,分别对应刚才创建的不同环境下的Info.plist文件。这里的Foody是自己iOS项目的名称。

5. 修改AppDelegate

创建Flutter引擎实例,调用run()方法启动引擎,并通过GeneratedPluginRegistrant.registe来注册Flutter侧使用的插件。

import UIKit
import Flutter
import FlutterPluginRegistrant

@main
class AppDelegate: UIResponder, UIApplicationDelegate {

    // Flutter引擎实例 - 全局单例,提高性能
    lazy var flutterEngine = FlutterEngine(name: "my flutter engine")

    func application(_ application: UIApplication, 
                     didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {

        // 初始化Flutter引擎
        flutterEngine.run()

        // 注册Flutter插件
        GeneratedPluginRegistrant.register(with: self.flutterEngine)

        return true
    }

    // MARK: UISceneSession Lifecycle
    func application(_ application: UIApplication, 
                     configurationForConnecting connectingSceneSession: UISceneSession, 
                     options: UIScene.ConnectionOptions) -> UISceneConfiguration {
        return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role)
    }
}

通信机制及页面切换

1. 通信机制

对于Flutter Module和iOS工程的双向通信,主要是通过Channel通道来实现的。对于同一个通道,在Flutter侧和iOS侧中的通道名要保持一致。

  • Flutter侧创建通道
// 用于与原生iOS通信的MethodChannel
final channel = MethodChannel('channel_name');
  • iOS侧创建通道,需要使用刚才在AppDelegate中创建的FlutterEngine实例
// MethodChannel用于与Flutter通信
private var methodChannel: FlutterMethodChannel?


// 设置MethodChannel
methodChannel = FlutterMethodChannel(
    name: "channel_name",
    binaryMessenger: flutterEngine.binaryMessenger
)

2. Flutter侧调用

  • 在iOS侧设置通道方法监听
channel.setMethodCallHandler { (call, result) in
    switch call.method {
    case "signOut":
        signOut { isSuccess in
            result(isSuccess)
        }
    case "deleteAccount":
        deleteAccount { isSuccess in
            result(isSuccess)
        }
    case "toCoinView":
        navigateToCoinView()
        result(nil)
    default:
        result(FlutterMethodNotImplemented)
    }
}
  • Flutter侧调用通道方法
// 调用通道方法退出登录
Future<void> logout() async {
  await channel.invokeMethod("signOut");
}

3. iOS侧调用

  • 在Flutter侧设置通道方法监听
channel.setMethodCallHandler((call) async {
  switch (call.method) {
    case "requireATT":

      await Future.delayed(const Duration(seconds: 1));
      await PermissionService.shared
          .checkAppTrackingTransparencyPermission();
  }
});
  • iOS侧调用通道方法
// 进入Flutter侧后请求ATT权限
channel.invokeMethod("requireATT");

4. iOS原生到Flutter页面

  • 创建FlutterViewController
let flutterViewController = FlutterViewController(
    engine: flutterEngine,
    nibName: nil,
    bundle: nil
)

// 设置初始路由(可选)
flutterViewController.setInitialRoute("/")
  • 替换iOS根视图
func switchRootViewController(_ viewController: UIViewController) {
    // 获取当前的 UIApplicationDelegate
    if let appDelegate = UIApplication.shared.delegate, let window = appDelegate.window ?? UIApplication.shared.windows.first(where: { $0.isKeyWindow }) {
        
        // 使用动画过渡切换根视图控制器
        UIView.transition(with: window, duration: 0.3, options: .transitionCrossDissolve, animations: {
            window.rootViewController = viewController
        })
    }
}

func navigateToFlutter() {
    DispatchQueue.main.async {
        let nav = UINavigationController(rootViewController: flutterViewController)
        nav.setNavigationBarHidden(true, animated: false)
        // 替换为flutterViewController
        switchRootViewController(nav)
    }
}

5. Flutter到iOS页面

  • Flutter调用通道方法
// 删除账号返回iOS登录页
Future<void> deleteAccount() async {
  await channel.invokeMethod("deleteAccount")
}

// 进入到iOS内购页面
Future<void> toCoinView () async {
  await channel.invokeMethod("toCoinView")
}
  • iOS侧处理页面跳转
// iOS侧通道方法监听
case "deleteAccount":
    deleteAccount { isSuccess in
        result(isSuccess)
    }
case "toCoinView":
    navigateToCoinView()
    result(nil)


// 删除账号
func deleteAccount(completion: @escaping (Bool) -> Void){
    // 创建确认删除的弹窗
    let alertController = UIAlertController(
        title: "Delete Account",
        message: "Are you sure you want to delete your account? This action cannot be undone, and all data will be permanently deleted.",
        preferredStyle: .alert
    )
    
    // 取消按钮
    let cancelAction = UIAlertAction(title: "Cancel", style: .cancel, handler: nil)
    
    // 确认删除按钮
    let deleteAction = UIAlertAction(title: "Delete", style: .destructive) { _ in
        ProgressHUD.showNetworking(message: "Delecting....")
        Task{
            await UserRepository.deleteAccount { isSuccess in
                if isSuccess {
                    ProgressHUD.showSuccessAndDismiss()
                    backToLogin()
                }
                completion(isSuccess)
            }
        }
    }
    
    alertController.addAction(cancelAction)
    alertController.addAction(deleteAction)
    
    if let topVC = getTopViewController() {
        topVC.present(alertController, animated: true, completion: nil)
    }
}


// 处理页面跳转

// 返回到登录页面
func backToLogin() {
    DispatchQueue.main.async {
        let loginViewController = LoginViewController()
        switchRootViewController(loginViewController)
    }
}

// 进入到内购页,使用pushViewController
func navigateToCoinView() {
    DispatchQueue.main.async {
        let coinsViewController = CoinsViewController()
        
        if let currentNav = self.getCurrentNavigationController() {
            currentNav.pushViewController(coinsViewController, animated: true)
            // 获取动画协调器
            if let coordinator = currentNav.transitionCoordinator {
                coordinator.animate(alongsideTransition: nil) { _ in
                    // 动画完成后显示导航栏
                    currentNav.setNavigationBarHidden(false, animated: true)
                }
            } else {
                // 如果没有动画协调器,直接显示
                currentNav.setNavigationBarHidden(false, animated: true)
            }
        }
    }
}


// 获取当前的 NavigationController
func getCurrentNavigationController() -> UINavigationController? {
    if let flutterVC = currentFlutterViewController,
       let nav = flutterVC.navigationController {
        return nav
    }
    
    if let topVC = getTopViewController() {
        if let nav = topVC as? UINavigationController {
            return nav
        } else if let nav = topVC.navigationController {
            return nav
        }
    }
    
    if let window = UIApplication.shared.windows.first(where: { $0.isKeyWindow }),
       let rootVC = window.rootViewController {
        if let nav = rootVC as? UINavigationController {
            return nav
        } else if let nav = rootVC.navigationController {
            return nav
        }
    }
    
    return nil
}

// 获取顶层视图控制器
func getTopViewController() -> UIViewController? {
    guard let window = UIApplication.shared.windows.first(where: { $0.isKeyWindow }) else {
        return nil
    }
    
    var topViewController = window.rootViewController
    
    while let presentedViewController = topViewController?.presentedViewController {
        topViewController = presentedViewController
    }
    
    return topViewController
}

常见问题

1. 编译错误

问题: Xcode中import Flutter报错

解决方案:

# 清理并重新安装
cd flutter_module
flutter clean
flutter pub get

cd ../HostApp
pod deintegrate
pod install

2. 运行时错误

问题: Flutter引擎初始化失败

解决方案:

  • 确保Flutter引擎在AppDelegate中正确初始化
  • 检查Flutter Module的路径是否正确
  • 验证Podfile配置

3. 调试技巧

Flutter调试:

# 在Flutter Module目录
flutter attach

iOS调试:

  • 使用Xcode的调试工具
  • 查看控制台日志
  • 使用断点调试原生代码

总结

通过以上步骤,成功创建了一个Flutter Module并将其集成到iOS宿主应用中。这种混合开发模式允许:

  1. 渐进式迁移: 逐步将原生功能迁移到Flutter
  2. 团队协作: iOS和Flutter团队可以并行开发
  3. 代码复用: Flutter代码可以在多个平台间共享
  4. 性能优化: 关键功能保持原生实现

Flutter官方文档参考:docs.flutter.dev/add-to-app/…

SwiftUI ObservableObject 观察者模式学习笔记

SwiftUI ObservableObject 观察者模式学习笔记

什么是 ObservableObject

ObservableObject 是 SwiftUI 中实现观察者模式的协议,允许多个视图观察同一个数据源,当数据发生变化时,所有相关视图会自动更新UI。

核心概念

观察者模式的本质

  • 一个数据源,多个观察者
  • 数据驱动UI更新:数据变化 → 自动刷新视图
  • 状态集中管理:统一的数据管理中心

数据流方向

数据源 (ObservableObject)
     数据变化
多个视图 (观察者) 自动更新

核心组件

1. @Published 属性包装器

class DataStore: ObservableObject {
    @Published var count: Int = 0      // 可观察属性
    @Published var name: String = ""   // 当值改变时自动通知观察者
}

作用:标记需要观察的属性,值改变时自动通知所有观察者

2. @StateObject 属性包装器

struct ContentView: View {
    @StateObject private var store = DataStore()  // 创建并管理实例
}

作用:创建并拥有 ObservableObject 实例的生命周期

3. @ObservedObject 属性包装器

struct ChildView: View {
    @ObservedObject var store: DataStore  // 观察传入的实例
}

作用:观察已存在的 ObservableObject 实例

实现步骤

步骤1:创建数据源类

class Counter: ObservableObject {
    @Published var count: Int = 0
    
    func increment() {
        count += 1  // 修改 @Published 属性会自动通知观察者
    }
}

步骤2:主视图创建实例

struct ContentView: View {
    @StateObject private var counter = Counter()
    
    var body: some View {
        // 将实例传递给子视图
        ChildView(counter: counter)
    }
}

步骤3:子视图观察数据

struct ChildView: View {
    @ObservedObject var counter: Counter
    
    var body: some View {
        Text("Count: (counter.count)")  // 自动响应数据变化
    }
}

@StateObject vs @ObservedObject

特性 @StateObject @ObservedObject
职责 创建并管理实例 观察已有实例
生命周期 与视图绑定 由外部管理
使用场景 根视图或数据拥有者 子视图或数据使用者
内存管理 自动处理 依赖传入方
// ✅ 正确用法
struct ParentView: View {
    @StateObject private var store = DataStore()  // 创建者用 @StateObject
    
    var body: some View {
        ChildView(store: store)
    }
}

struct ChildView: View {
    @ObservedObject var store: DataStore  // 使用者用 @ObservedObject
}

工作流程

1. 用户操作 (点击按钮等)
    
2. 调用业务方法 (store.increment())
      
3. 修改 @Published 属性 (count += 1)
    
4. 自动通知所有观察者
    
5. 观察者视图重新渲染
    
6. UI 自动更新显示新数据

应用场景

1. 计数器应用

class Counter: ObservableObject {
    @Published var count = 0
    func increment() { count += 1 }
}

2. 用户信息管理

class UserStore: ObservableObject {
    @Published var currentUser: User?
    @Published var isLoggedIn = false
}

3. 购物车状态

class ShoppingCart: ObservableObject {
    @Published var items: [Item] = []
    @Published var totalPrice: Double = 0
}

4. 网络请求状态

class NetworkManager: ObservableObject {
    @Published var isLoading = false
    @Published var data: [String] = []
    @Published var errorMessage: String?
}

最佳实践

1. 命名规范

// 推荐的命名模式
class UserStore: ObservableObject { }      // Store 后缀
class DataManager: ObservableObject { }    // Manager 后缀
class AppState: ObservableObject { }       // State 后缀

2. 属性分类

class AppStore: ObservableObject {
    // 可观察属性
    @Published var userName: String = ""
    @Published var isLoading: Bool = false
    
    // 私有属性(不需要观察)
    private let apiKey: String = "secret"
    
    // 计算属性
    var displayName: String {
        userName.isEmpty ? "Guest" : userName
    }
}

3. 业务方法封装

class TodoStore: ObservableObject {
    @Published var todos: [Todo] = []
    
    // 封装业务逻辑,不直接暴露数据修改
    func addTodo(_ title: String) {
        let newTodo = Todo(title: title)
        todos.append(newTodo)
    }
    
    func completeTodo(_ id: UUID) {
        if let index = todos.firstIndex(where: { $0.id == id }) {
            todos[index].isCompleted = true
        }
    }
}

4. 避免过度使用

// ❌ 不好:为简单数据创建 ObservableObject
class SimpleCounter: ObservableObject {
    @Published var count = 0
}

// ✅ 更好:简单数据用 @State
struct CounterView: View {
    @State private var count = 0  // 简单状态用 @State
}

常见错误

1. 混淆 @StateObject 和 @ObservedObject

// ❌ 错误:子视图用 @StateObject 会创建新实例
struct ChildView: View {
    @StateObject var store = DataStore()  // 每次重新创建
}

// ✅ 正确:子视图用 @ObservedObject 观察传入的实例
struct ChildView: View {
    @ObservedObject var store: DataStore
}

2. 忘记标记 @Published

class DataStore: ObservableObject {
    var count: Int = 0  // ❌ 忘记 @Published,不会触发更新
    
    @Published var count: Int = 0  // ✅ 正确
}

3. 在非主线程修改 @Published 属性

class DataStore: ObservableObject {
    @Published var data: [String] = []
    
    func loadData() {
        DispatchQueue.global().async {
            // ❌ 错误:后台线程修改 @Published 属性
            self.data = ["new data"]
            
            // ✅ 正确:切换到主线程
            DispatchQueue.main.async {
                self.data = ["new data"]
            }
        }
    }
}

与其他模式对比

vs. @State/@Binding

  • @State/@Binding:简单的视图内部状态
  • ObservableObject:复杂的跨视图状态管理

vs. 回调模式

  • 回调:一对一,事件驱动,临时通信
  • ObservableObject:一对多,状态驱动,持续观察

vs. 环境对象 (@EnvironmentObject)

  • @ObservedObject:显式传递,层级清晰
  • @EnvironmentObject:环境注入,跨层级传递

性能优化

1. 避免不必要的更新

class OptimizedStore: ObservableObject {
    @Published var importantData: String = ""
    
    // 不需要观察的数据不用 @Published
    private var cacheData: [String] = []
    
    // 批量更新避免频繁刷新
    func batchUpdate() {
        // 多个操作...
        objectWillChange.send()  // 手动触发一次更新
    }
}

2. 合理分割数据源

// ❌ 避免:巨大的单一数据源
class MassiveStore: ObservableObject {
    @Published var userData: User?
    @Published var posts: [Post] = []
    @Published var comments: [Comment] = []
    // ... 太多属性
}

// ✅ 推荐:按功能分割
class UserStore: ObservableObject {
    @Published var currentUser: User?
}

class PostStore: ObservableObject {
    @Published var posts: [Post] = []
}

总结

ObservableObject 是 SwiftUI 中强大的状态管理工具:

优势

自动更新:数据变化时UI自动刷新
一对多:一个数据源支持多个观察者
集中管理:统一的状态管理中心
类型安全:编译时类型检查

适用场景

  • 复杂的应用状态管理
  • 多个视图共享数据
  • 需要响应式更新的场景
  • 业务逻辑与视图分离

记忆要点

  • @Published = "我变了就通知大家"
  • @StateObject = "我创建并管理你"
  • @ObservedObject = "我在观察你"

掌握 ObservableObject 是构建复杂 SwiftUI 应用的关键技能,它让状态管理变得简单而强大。

Swift异步详解

Swift 的异步编程模型是现代 Swift 开发的核心特性之一,尤其在 Swift 5.5 引入 async/await 语法后,极大简化了异步操作的处理。以下是对 Swift 异步编程的详细解析:

一、异步编程的核心问题

在传统同步代码中,操作按顺序执行,耗时操作(如网络请求、文件读写)会阻塞当前线程。而异步编程的目的是:

  • 避免耗时操作阻塞 UI 或主线程
  • 提高代码可读性和可维护性
  • 简化复杂异步逻辑(如依赖多个异步任务的场景)

二、Swift 异步的三种主要方式

1. 回调闭包(Closure Callback)

最传统的方式,通过闭包传递异步结果:

func fetchData(completion: @escaping (Result<String, Error>) -> Void) {
    DispatchQueue.global().asyncAfter(deadline: .now() + 1) {
        // 模拟网络请求
        completion(.success("Data from server"))
    }
}

// 调用
fetchData { result in
    switch result {
    case .success(let data):
        print(data)
    case .failure(let error):
        print(error)
    }
}

缺点:多层嵌套易形成“回调地狱”,逻辑复杂时可读性差。

2. Combine 框架(响应式编程)

基于发布者(Publisher)和订阅者(Subscriber)模式,适合处理数据流:

import Combine

func fetchData() -> AnyPublisher<String, Error> {
    return Future<String, Error> { promise in
        DispatchQueue.global().asyncAfter(deadline: .now() + 1) {
            promise(.success("Data from server"))
        }
    }
    .eraseToAnyPublisher()
}

// 调用
let cancellable = fetchData()
    .receive(on: DispatchQueue.main)
    .sink(receiveCompletion: { completion in
        // 处理完成或错误
    }, receiveValue: { data in
        print(data)
    })

优势:强大的操作符(如 mapflatMap)支持复杂数据流处理,适合响应式场景。

3. async/await 语法(Swift 5.5+)

现代异步编程的主流方式,用同步代码的形式编写异步逻辑:

// 定义异步函数
func fetchData() async throws -> String {
    try await Task.sleep(nanoseconds: 1_000_000_000) // 模拟耗时1秒
    return "Data from server"
}

// 调用异步函数(需在异步上下文中)
Task {
    do {
        let data = try await fetchData()
        print(data)
    } catch {
        print(error)
    }
}

核心特点

  • async 标记函数为异步
  • await 等待异步结果(不会阻塞线程)
  • try 处理可能的错误
  • 必须在异步上下文(如 Task)中调用

三、async/await 深度解析

1. 异步函数的定义与调用

  • 函数声明:用 async 关键字标记,可返回值或抛出错误
    func loadUser() async throws -> User
    func cacheData() async // 无返回值
    
  • 调用限制:必须在 async 函数内或 Task 中使用 await 调用
    // 正确:在Task中调用
    Task {
        let user = try await loadUser()
    }
    
    // 正确:在async函数中调用
    func process() async throws {
        let user = try await loadUser()
    }
    

2. 任务(Task)与并发

Task 是异步操作的载体,负责管理异步任务的生命周期:

  • 创建任务
    let task = Task {
        try await fetchData()
    }
    
  • 取消任务
    task.cancel() // 触发任务内部的CancellationError
    
  • 任务优先级
    Task(priority: .high) { ... } // 高优先级任务
    

3. 并发执行多个任务

  • 并行执行:用 async let 同时启动多个任务,最后统一等待结果
    async let data1 = fetchData(url: url1)
    async let data2 = fetchData(url: url2)
    
    let result = try await (data1, data2) // 等待所有任务完成
    
  • 结构化并发:通过 TaskGroup 管理一组动态任务
    await withTaskGroup(of: String.self) { group in
        for url in urls {
            group.addTask {
                try await fetchData(url: url)
            }
        }
        // 收集结果
        for try await data in group {
            print(data)
        }
    }
    

4. 主队列调度

UI 操作必须在主线程执行,可通过 @MainActor 约束:

// 标记函数必须在主线程执行
@MainActor
func updateUI(text: String) {
    label.text = text
}

// 调用时自动切换到主线程
Task {
    let data = try await fetchData()
    await updateUI(text: data) // 自动切换到主线程
}

四、异常处理

异步函数的错误通过 throws 抛出,在调用时用 try 捕获:

func riskyOperation() async throws {
    if failureCondition {
        throw MyError.somethingWrong
    }
}

// 调用时处理错误
Task {
    do {
        try await riskyOperation()
    } catch MyError.somethingWrong {
        print("特定错误处理")
    } catch {
        print("通用错误处理: \(error)")
    }
}

五、与其他异步模型的互操作

1. 回调转 async/await

withCheckedThrowingContinuation 将传统回调转换为异步函数:

func fetchData() async throws -> String {
    try await withCheckedThrowingContinuation { continuation in
        // 传统回调函数
        legacyFetch { result in
            switch result {
            case .success(let data):
                continuation.resume(returning: data)
            case .failure(let error):
                continuation.resume(throwing: error)
            }
        }
    }
}

2. async/await 转 Combine

Future 包装异步函数:

func fetchDataPublisher() -> AnyPublisher<String, Error> {
    Future { promise in
        Task {
            do {
                let data = try await fetchData()
                promise(.success(data))
            } catch {
                promise(.failure(error))
            }
        }
    }
    .eraseToAnyPublisher()
}

六、最佳实践

  1. 避免阻塞await 不会阻塞线程,无需手动切换到后台队列
  2. 任务取消:在长时间任务中检查取消状态
    func longRunningTask() async throws {
        for i in 0..<100 {
            if Task.isCancelled {
                throw CancellationError()
            }
            // 执行部分任务
            try await Task.sleep(nanoseconds: 100_000_000)
        }
    }
    
  3. 限制并发量:用 TaskGroup 配合信号量控制并发数量
  4. 优先使用 async/await:相比回调和 Combine,代码更简洁易读

总结

Swift 的异步编程模型从回调到 Combine,再到 async/await,逐步向更简洁、更安全的方向发展。async/await 结合结构化并发,成为处理异步逻辑的首选方式,尤其适合网络请求、文件操作等场景,同时保持了与传统异步模型的兼容性。

强制 SwiftUI 重新渲染:`.id()` 这把“重启键”你用对了吗?

参考原文:Forcing a View Reload in SwiftUI

为什么需要“强制 reload”?

SwiftUI 的声明式 DSL 依赖 状态 diff 自动更新视图,但以下场景需要“硬重启”:

  • 网络请求失败后的“重试”按钮
  • 图片/视频加载损坏,需重新解码
  • 底层 @StateObject 内部状态错乱,手动复位成本过高

核心思路:

改变视图 身份 (identity) → SwiftUI 认为“旧视图已消失”→ 重建整个子树。

官方逃生舱:.id(_:) 一行代码搞定

struct DemoView: View {
    @State private var viewId = UUID()

    var body: some View {
        VStack {
            // 1️⃣ 用 .id 绑定唯一标识
            Text(viewId.uuidString)
                .id(viewId)

            // 2️⃣ 刷新标识 → 强制重建
            Button("Retry") {
                viewId = UUID()
            }
        }
    }
}
  • 每次 viewId 变化,Text 被视为全新视图,旧实例被销毁。
  • 子树内所有 @State / @StateObject / 内部绑定一并丢弃,状态清零。

优点:快、狠、准

优势 说明
✅ 一键复位 无需手动清空 N 个 @State
✅ 行为可预测 基于 SwiftUI 身份机制,官方支持
✅ 适用 retry 场景 网络/解码失败时瞬间“满血复活”

代价:性能 & 状态损失

风险 场景
⚠️ 局部状态全灭 用户输入/滚动位置/播放器进度 会丢失(除非提前迁出子树)
⚠️ 大视图重建开销 复杂 UI / 大图 / 3D 场景可能出现掉帧
⚠️ 掩盖架构问题 频繁 .id()往往意味着状态建模不合理,应优先重构

实战指南:何时该用、何时避免

场景 建议
临时 retry / reset 按钮 ✅ 首选 .id()
列表 item 偶发错乱 ✅ 给 item 加 .id(item.unique)
用户输入表单 ❌ 别把 .id()绑在输入框外层,会丢键盘/光标
高频刷新(如计时器) ❌ 用专门的状态驱动,而非改 .id()

进阶技巧:把“ reload”封装成 Modifier

struct Reloadable<Content: View>: View {
    @State private var reloadID = UUID()
    let content: (UUID) -> Content
    
    var body: some View {
        content(reloadID)
            .id(reloadID)
    }
    
    func reload() {
        reloadID = UUID()
    }
}

// 使用
struct PlayerView: View {
    @State private var player = Reloadable { id in
        VideoPlayer(url: url)
            .id(id)          // 绑定唯一身份
    }
    
    var body: some View {
        player
            .onReceive(retryNotification) { _ in
                player.reload()   // 硬重启播放器
            }
    }
}
  • 将 reload 动作 暴露给外部,不污染子树状态。
  • 支持 动画过渡(可再包 .transition)。

一句话总结

.id(UUID()) 是 SwiftUI 的“重启键”——

应急可用,滥用伤身。

在 retry / 纠错场景下它是救命稻草;若发现自己在每页都用,请先回头看看状态建模是否出了问题。

Swift 6.2 新语法糖:在字符串插值里直接给 Optional 写默认值

参考原文:Making String Interpolation Smarter in Swift 6.2: Default Values for Optionals

一句话看懂新特性

旧写法(nil-coalescing)

let name: String? = nil
print("Hi \(name ?? "Guest")")          // OK,同类型

新写法(Swift 6.2 插值默认值)

print("Hi \(name, default: "Guest")")   // 等价,但支持**异类型**默认值

关键差异:?? 要求左右类型一致;(default:) 把默认值直接当字符串用,无视原始类型!

异类型痛点:旧语法搞不定

let count: Int? = nil

// ❌ 编译失败:Int 与 String 不匹配
print("Count: \(count ?? "Unknown")")

// ✅ Swift 6.2 直接通过
print("Count: \(count, default: "Unknown")")   // 输出 "Count: Unknown"

省去了手动 .map(String.init) ?? "Unknown" 的繁琐步骤。

真实场景:用户资料拼接

struct User {
    var username: String?
    var email: String?
    var age: Int?
}

let user = User(username: nil, email: "jane@example.com", age: nil)

print("""
User Info:
- Username: \(user.username, default: "Guest")
- Email: \(user.email, default: "Not provided")
- Age: \(user.age, default: "Unknown")
""")

旧写法对比

"- Username: \(user.username ?? "Guest")"
"- Age: \(user.age.map(String.init) ?? "Unknown")"   // 手动 map

新语法一行搞定,可读性大幅提升。

日志 & 调试神器

func logEvent(name: String?, duration: Double?) {
    print("Event '\(name, default: "Unnamed")' ran for \(duration, default: "an unknown amount of time")")
}

logEvent(name: nil, duration: nil)
// Output: Event 'Unnamed' ran for an unknown amount of time

无需提前拆包、转换类型,插值处直接给默认值。

语法要点速记

特性 说明
可用版本 Swift 6.2+
插值格式 (optional, default: 任意表达式)
类型要求 无;默认值会被直接当 String
与 ??共存 完全兼容,按场景选择

何时用它而非 ??

场景 选 (default:) 选 ??
默认值与 Optional 类型不同
默认值是同类型、已存在 皆可 ✅ 更短
需要复杂表达式/函数调用 ✅ 可读 ❌ 过长

一句话总结

同类型用 ??,异类型或懒得转换时用 (default:)

Swift 6.2 这个小糖让字符串插值兼顾安全与优雅,再不用为“Optional 转 String”写大段模板代码!

窥探 `@Observable` 的“小黑盒”:private 属性到底会不会被观察?

参考原文:Exploring Observation in Swift: What Happens with Private Properties

问题抛出

import Observation
@Observable
final class ViewModel {
    var publicProp  = "A"          // 1️⃣ 公开,可观察
    @ObservationIgnored var ignoredProp = "B"  // 2️⃣ 显式忽略
    private var privateProp = "C"  // 3️⃣ 私有,会参与观察吗?
}
  • 直觉:private = 对外隐藏 = 不生成观察代码?
  • 真相:除非加 @ObservationIgnored,否则一律观察,与可见性无关!

验证工具:SIL(Swift Intermediate Language)

一键导出 SIL

xcrun swiftc -emit-silgen -Onone -parse-as-library \
  -sdk $(xcrun --show-sdk-path --sdk macosx) \
  ViewModel.swift > ViewModel.sil

导出后的文件我存储在了:gitcode.com/unravel/dis…

关键发现(节选)

属性 是否生成 _modify调用方法 结论
publicProp ✅ 有 参与观察
ignoredProp ❌ 无 直接读写,无开销
privateProp ✅ 有 同样被观察

可见:编译器为 所有非忽略属性 生成相同的观察包装,private 也不能幸免。

Xcode 可视化捷径:Expand Macro

不想看 SIL?Xcode 15+ 支持宏展开:

  1. 选中 @Observable 宏,右键选择 “Expand Macro”。

image.png

  1. 在展开的代码里搜索 @ObservationTracked
    • 出现 = 会观察
    • 不出现 = 已忽略(或被 @ObservationIgnored 标记)

(示意:private 属性同样被 @ObservationTracked 包裹)

image.png

实战影响 & 最佳实践

场景 建议
私有缓存、临时变量 不需要观察 显式加 @ObservationIgnored
希望 SwiftUI 不刷新 的辅助属性 加 @ObservationIgnored减少调度开销
确实需要观察私有状态(如内部网络层) 保持默认即可,private 仅对外隐藏,对 Observation 透明

一句话总结

@Observable 世界里,“private” ≠ ‘忽略’;

想真正跳过观察,请用 @ObservationIgnored —— 不论 public 还是 private,编译器都会一视同仁地生成观察代码。

Swift 并发避坑指南:自己动手实现“原子”属性与集合

原文:Atomic properties and collections in Swift

为什么需要“原子”操作?

Swift 没有现成的 atomic 关键字。当多个线程/任务同时读写同一属性或集合时,会出现:

  • 读到中间状态(数组越界、字典重复 key)
  • 丢失更新(值类型复制-修改-写回)

使用Swift 实现Atomic有多种方案,以下从简单到高级介绍iOS17及以下系统的可行方案;最后补充 iOS 18 原生 Synchronization 框架的替代思路。

方案 1:属性包装器 + GCD(入门级)

@propertyWrapper
struct Atomic<Value> {
    private let queue = DispatchQueue(label: "atomic.queue") // 串行队列
    private var storage: Value
    
    init(wrappedValue: Value) { storage = wrappedValue }
    
    var wrappedValue: Value {
        // 串行队列同步执行,类似锁机制,读写都需要排队
        get { queue.sync { storage } }
        set { queue.sync { storage = newValue } }   // 注意:非 barrier
    }
}

使用

class Client {
    @Atomic var counter = 0
}

踩坑:复合运算“非原子”

let client = Client()
client.counter += 1   // 读 + 改 + 写 三步,线程不安全!

方案 2:暴露 modify 闭包(中级)

@propertyWrapper
class Atomic<Value> {          // 改为 class,避免值拷贝
    private let queue = DispatchQueue(label: "atomic.queue") // 串行队列
    private var storage: Value
    
    var projectedValue: Atomic<Value> { self }
    
    init(wrappedValue: Value) { storage = wrappedValue }
    
    var wrappedValue: Value {
        // 串行队列同步执行,类似锁机制,读写都需要排队
        // 这里对应整体取值和整体赋值
        get { queue.sync { storage } }
        set { queue.sync { storage = newValue } }
    }
    
    // 原子“读-改-写”
    // 这里对应复合运算
    func modify(_ block: (inout Value) -> Void) {
        queue.sync { block(&storage) }   // 整个 block 在串行队列里执行
    }
}

使用

client.$counter.modify { $0 += 1 }        // ✅ 线程安全
client.$counter.modify { $0.append(42) }  // 同样适用于数组

方案 3:独立 Atomic<T> 类(高级,API 更友好)

final class Atomic<Value> {
    private var value: Value
    private let queue = DispatchQueue(label: "atomic.queue", attributes: .concurrent) // 并发队列
    
    init(_ value: Value) { self.value = value }
    
    // 1. 只读快照
    func read<T>(_ keyPath: KeyPath<Value, T>) -> T {
        queue.sync { value[keyPath: keyPath] }
    }
    
    // 2. 读并转换
    func read<T>(_ transform: (Value) throws -> T) rethrows -> T {
        try queue.sync { try transform(value) }
    }
    
    // 3. 整体替换
    func modify(_ newValue: Value) {
        queue.async(flags: .barrier) { self.value = newValue }
    }
    
    // 4. 原子读-改-写
    func modify(_ transform: (inout Value) -> Void) {
        queue.sync(flags: .barrier) { transform(&value) }
    }
}

使用

let num = Atomic(0)
num.modify { $0 += 1 }
print(num.read(\.self))   // 1

let arr = Atomic([1, 2, 3])
arr.modify { $0.append(4) }
print(arr.read { $0.contains(4) }) // true

并发队列 + barrier 保证“写”互斥,“读”并行,性能优于串行队列。

方案 4:专用原子集合(线程安全数组)

class AtomicArray<Element> {
    private var storage: [Element] = []
    private let queue = DispatchQueue(label: "atomic.array", attributes: .concurrent)
    
    subscript(index: Int) -> Element {
        get { queue.sync { storage[index] } }
        set { queue.async(flags: .barrier) { self.storage[index] = newValue } }
    }
    
    func append(_ new: Element) {
        queue.async(flags: .barrier) { self.storage.append(new) }
    }
    
    func removeAll() {
        queue.async(flags: .barrier) { self.storage.removeAll() }
    }
    
    func forEach(_ body: (Element) throws -> Void) rethrows {
        try queue.sync { try storage.forEach(body) }
    }
    
    func all() -> [Element] { queue.sync { storage } }
}

压测:10 线程并发 append

Task.detached {
    let arr = AtomicArray<Int>()
    DispatchQueue.concurrentPerform(iterations: 10_000) { @Sendable in  arr.append($0)}
    print(arr.all().count)   // 10_000 ✅
}

方案 5:原子字典(读多写少最优解)

class AtomicDictionary<Key: Hashable, Value> {
    private var dict: [Key: Value] = [:]
    private let queue = DispatchQueue(label: "atomic.dict", attributes: .concurrent)
    
    subscript(key: Key) -> Value? {
        get { queue.sync { dict[key] } }
        set { queue.async(flags: .barrier) { self.dict[key] = newValue } }
    }
    
    subscript(key: Key, default defaultValue: @autoclosure () -> Value) -> Value {
        get {
            queue.sync { dict[key] ?? defaultValue() }
        }
        set {
            queue.async(flags: .barrier) { self.dict[key] = newValue }
        }
    }
}

方案 6 _read / _modify 原生读写访问器

⚠️ 下划线 API,非稳定,仅用于实验。

@propertyWrapper
struct Atomic<Value> {
    private var storage: Value
    private let lock = NSLock()
    
    var wrappedValue: Value {
        get { lock.lock(); defer { lock.unlock() }; return storage }
        
        _modify {              // 原生“产出”引用,零拷贝
            lock.lock()
            defer { lock.unlock() }
            yield &storage
        }
    }
}

优点:

  • 无队列调度,纯锁+直接内存引用,性能更高。

缺点:

  • 未来可能改名或移除;不建议上生产。

iOS 18+ 新选择:Apple Synchronization 框架

import Synchronization

let counter = Atomic<Int>(0)   // 系统级原子类型,无锁、CPU 指令级
counter.wrappingIncrement(by: 1)
  • 真正无锁(使用 std::atomic 底层)。
  • 支持泛型、Sendable、wait/notify 等高级语义。
  • 最低部署版本 iOS 18;若需向下兼容,仍需本文方案。

选型速查表

场景 推荐方案
只支持 iOS 18+ 系统 Synchronization
属性读写,iOS 17 及以下 方案 3 Atomic<Value>
专用集合(数组/字典) 方案 4 / 5 原子集合类
超高性能、实验 方案 6 _read_modify

结论

  • Swift 没有“官方原子”≠ 不能写出线程安全的代码。
  • GCD + barrier + 值类型拷贝 足以覆盖 99 % 业务需求。
  • 提前布局:当 iOS 18 普及后,迁移到 Synchronization 只需换一行 import。

记住口诀:

“读并行、写互斥,修改用 barrier,集合要包类。”

惊!只是 `import Foundation`,`String.contains("")` 的返回值居然变了?

两行代码,两种结果

是否 import Foundation "".contains("") "abc".contains("")
❌ 纯 Swift true true
✅ + Foundation false false

同一个 API,返回值完全相反,而且大多数 iOS 项目会间接 import Foundation,所以你一直用的其实是 ObjC 版本!

幕后黑手:桥接与方法决议

  1. Swift.String 与 NSString 是 toll-free bridged。
  2. 一旦 import Foundation,编译器会把同名方法 优先桥接到 NSString 的实现。
  3. 两个版本的 contains 签名兼容,但 行为不同:
实现 空子串规则
Swift 原生 空字符串是任何字符串的子串 → true
NSString 空子串不存在 → false

实战影响

  • 绝大多数 iOS/SwiftUI 工程都会间接 import Foundation → 你看到的都是 false
  • 纯 Swift Package / Linux Server 没 Foundation → 看到的是 true
  • 跨平台库若依赖空子串行为,务必显式测试两种环境。

避坑清单

场景 建议
写跨平台库 显式单元测试两种 import 状态
需要与 ObjC 对齐 直接使用 Foundation 行为并写注释
想要纯 Swift 行为 自建模块不 import Foundation,或用 Swift-only 方法
不确定当前行为 打印类型或查看 Quick Help 看是 String.contains还是 NSString.contains

一句话总结

只要 import Foundation,你就默认接受了 NSString 的行为。

在写通用逻辑 / 跨平台库 / 纯 Swift Package时,记得给空子串单独写测试,别被“同名不同魂”坑了。

Swift 6.2 新武器:`weak let` —— 既弱引用又不可变的安全魔法

为什么需要 weak let

需求场景 weak var的痛点 weak let的新能力
并发安全的 Sendable类型 weak var无法标记 Sendable ✅ 可以
不可重新赋值的弱引用 仍可能被外部篡改 ✅ 编译期禁止
值类型持有弱引用 无法保证不变性 ✅ 完美支持

一句话:弱引用 + 不可变 = 更安全的所有权图。

语法速览

final class Downloader: Sendable {
    // 1️⃣ 一次性设置,之后不可改指向
    weak let delegate: DownloaderDelegate?
    
    init(delegate: DownloaderDelegate?) {
        self.delegate = delegate
    }
}
  • 仍遵守 ARC:目标释放后自动变 nil

  • 编译器禁止重新赋值:

    downloader.delegate = AnotherVC() // ❌ 直接报错。

完整示例:下载器 + 控制器

protocol DownloaderDelegate: AnyObject {
    func downloadDidUpdate(progress: Double)
}

final class Downloader: Sendable {
    weak let delegate: DownloaderDelegate?
    
    init(delegate: DownloaderDelegate?) {
        self.delegate = delegate
    }
    
    func simulateDownload() {
        delegate?.downloadDidUpdate(progress: 0.5)
    }
}

final class ViewController: DownloaderDelegate {
    func downloadDidUpdate(progress: Double) {
        print("进度:\(progress)")
    }
}

// MARK: - 测试
var vc: ViewController? = ViewController()
let downloader = Downloader(delegate: vc)
downloader.simulateDownload()   // ✅ 打印 0.5

vc = nil
downloader.simulateDownload()   // ✅ delegate 自动 nil,无崩溃

迁移清单:何时把 weak var 换成 weak let

场景 建议
代理/回调 一次性设置 直接替换
单元测试需要多次赋值 保持 weak var
值类型(struct)持有弱引用 立即使用 weak let
actor / TaskGroup 内部 优先 weak let以获得 Sendable资格

实战:值类型快照

struct UserSnapshot {
    let name: String
    weak let avatarLoader: AvatarLoader?   // 不持有加载器
}
  • 即使 avatarLoader 释放,UserSnapshot 依旧安全。
  • 结构体可以跨线程传递,无需担心循环引用。

一句话总结

weak let = “一次性弱引用”,让 不可变性 与 ARC 安全 在同一行代码握手。

在并发、UI、快照场景里,它是 Swift 6.2 给你的“隐形护栏”。

吃透 Swift 的 `@autoclosure`:把“表达式”变“闭包”的延迟利器

什么是 @autoclosure

一句话:把“传入的表达式”自动包成“无参闭包”,实现延迟求值(lazy evaluation)。

语法糖级别:调用方完全无感,只需像传普通值一样写表达式;函数内部拿到的是 () -> T 闭包,想执行才执行。

为什么需要延迟求值?

反例:生产环境也强制计算

class Logger {
    static func log(_ message: String) {
        #if DEBUG
        print(message)
        #endif
    }
}

class DataSource {
    var data: [CustomStringConvertible] = []
    
    func update(with item: CustomStringConvertible) {
        data.append(item)
        // ⚠️ 即使 Release 不打印,description 也会被立即求值
        Logger.log(item.description)
    }
}
  • 浪费 CPU:复杂 description 可能拼接大量字符串。
  • 浪费内存:中间结果在 Release 版毫无用处。

上正菜:用 @autoclosure 实现“真正只在需要时才计算”

改一行签名即可

class Logger {
    // 1. 自动把调用处的表达式包成 () -> String
    static func log(_ message: @autoclosure () -> String) {
        #if DEBUG
        print(message())        // 2. 只有 DEBUG 才执行闭包
        #endif
    }
}

调用方零感知

ds.update(with: Vehicle(name: "BMW"))
// 调用处完全像传值,无需手写大括号

内部流程

阶段 实际行为
编译期 把表达式 item.description包成 { item.description }
运行期 只有 message()被调用时才执行闭包

进阶玩法

?? 运算符同源

标准库定义:

public func ?? <T>(optional: T?, defaultValue: @autoclosure () -> T) -> T {
    optional != nil ? optional! : defaultValue()
}
  • defaultValue 只有在前者为 nil 才执行,避免无谓开销。

自定义断言

func myAssert(_ condition: Bool, _ message: @autoclosure () -> String = "") {
    #if DEBUG
    if !condition {
        print("断言失败: \(message())")
    }
    #endif
}

// 使用
myAssert(score > 0, "分数必须为正,当前值:\(score)")
// 若断言通过,字符串插值不会被执行

短路求值

func logIf(_ condition: Bool, _ msg: @autoclosure () -> String) {
    guard condition else { return }
    print(msg())
}

logIf(isDebug, "昂贵计算结果:\(heavyCompute())")   // 非 Debug 直接短路

使用 checklist

场景 是否适合 @autoclosure
日志、断言、调试信息 ✅ 延迟 + 避免副作用
复杂默认值 ✅ 与 ??同理
需要多次读取的闭包 ❌ 每次调用都会重新求值,缓存请手动处理
需要捕获可变量的闭包 ⚠️ 捕获的是表达式当时的值,注意值语义

一句话总结

@autoclosure 是 Swift 给你的“惰性开关”:

调用方像传值,接收方像拿闭包,只在真正需要时才执行表达式。

把它用在“可能昂贵、可能无效、可能副作用”的参数上,代码立刻更省、更快、更安全。

当液态玻璃计划遭遇反叛者:一场 iOS 26 界面的暗战

在这里插入图片描述

引子

在硅谷的地下代码俱乐部里,流传着一个关于 "液态玻璃" 的传说 —— 那是 Apple 秘密研发的界面改造计划,如同电影《变脸》中那张能改变命运的面具,一旦启用,所有 App 都将被迫换上流光溢彩的新面孔。

在这里插入图片描述

而今天,我们的主角琳恩,一位以守护经典界面为己任的开发者,正面临着职业生涯中最严峻的挑战:她必须在 72 小时内阻止自己开发的 "星图导航"App 被强制换脸,否则整个星际迷航爱好者社区将失去他们最熟悉的操作界面。

在本篇文章中,您将学到如下内容:

  • 引子
    1. 🔍 危机降临:液态玻璃计划的突袭
    1. 🕵️ 发现破绽:藏在 Info.plist 里的救命稻草
    1. ⏳ 倒计时:反杀窗口即将关闭
    1. 🛡️ 终极防御:在变脸狂潮中守住初心

她是否能最终成功?让我们拭目以待!


1. 🔍 危机降临:液态玻璃计划的突袭

"琳恩,紧急情况!" 凌晨三点,搭档杰克的全息投影突然出现在屏幕上,他的虚拟形象因信号急促而闪烁不定,"Apple 刚刚推送了 iOS 26 的终极更新,所有重编译的 App 都会自动启用 ' 液态玻璃 ' 界面 —— 我们的星图坐标系统会彻底错乱!"

在这里插入图片描述

琳恩猛地从睡梦中惊醒,手指在键盘上飞舞如刀。

iOS 26 的 "液态玻璃" redesign 就像反派卡斯托・特洛伊的阴谋,表面光鲜亮丽,实则暗藏杀机:按钮边缘的液态流动效果会遮挡星图的经纬度标识,半透明的层级设计会让深空探测数据变得模糊不清。

更可怕的是,按照 Apple 的规则,只要用 Xcode 26 重新编译,这套新界面就会像病毒一样侵入 App 的每一个角落。

在这里插入图片描述

"他们这是强行换脸!" 琳恩咬牙切齿地调出测试机,屏幕上的星图果然已面目全非 —— 原本棱角分明的星座连线变得弯弯曲曲,像被融化的玻璃随意流淌。这哪是升级,简直是对专业用户的背叛!

2. 🕵️ 发现破绽:藏在 Info.plist 里的救命稻草

就在琳恩快要绝望时,她的导师,人称 "代码幽灵" 的马库斯发来一条加密信息:"查 UIDesignRequiresCompatibility,在 Info.plist 的第 47 行附近。"

在这里插入图片描述

这个神秘的密钥就像《变脸》中肖恩藏在十字架里的微型炸弹,是对抗强制换脸的唯一希望。琳恩立刻打开项目中的 Info.plist 文件 —— 这个相当于 App 身份证的配置文件里,果然藏着玄机。当她添加这行代码时,屏幕仿佛传来一声轻微的 "咔哒" 声,就像解开了某种电子锁:

<!-- Info.plist 关键配置 -->

<key>UIDesignRequiresCompatibility\</key>

<!-- 设为YES,相当于给App装上反变脸装置,阻止液态玻璃界面强制生效 -->

<true/>

在这里插入图片描述

重新编译后,奇迹发生了:液态玻璃效果如同退潮般消失,熟悉的星图界面重现在眼前 —— 棱角分明的按钮、清晰锐利的坐标线,连星座名称的字体都保持着经典样式。

在这里插入图片描述

琳恩长舒一口气,仿佛刚从卡斯托的魔爪中夺回自己的脸,悬在心上的巨石终于落地。

3. ⏳ 倒计时:反杀窗口即将关闭

"别高兴太早," 马库斯的全息影像再次出现,这次他的表情凝重如铁,"Apple 在开发者文档里埋了炸弹 —— 这个密钥将在 Xcode 27 中永久移除。"

在这里插入图片描述

琳恩的心沉了下去。就像电影中那枚有倒计时的炸弹,这个反制措施的有效期只剩下不到一年。Apple 的公告写得明明白白:

UIDesignRequiresCompatibility 主要用于调试和测试,而非长期解决方案。这意味着他们虽然暂时保住了界面,但最终还是要面对液态玻璃的全面接管,临时密钥不过是 "缓兵之计",绝非 "长久之策"。

"我们得提前布局," 杰克在一旁调出 Xcode 27 的预览文档,手指敲出一串数据流,"接下来的 12 个月,我们要做的不是逃避,而是让经典界面与液态玻璃 ' 和平共处 '—— 既不丢老用户的情怀,也不违逆新系统的规则。"

在这里插入图片描述

4. 🛡️ 终极防御:在变脸狂潮中守住初心

接下来的日子里,琳恩团队展开了一场与时间的赛跑。他们没有简单依赖临时密钥,而是像《变脸》中肖恩潜伏在敌人内部一样,深入研究液态玻璃的设计逻辑,在代码世界里搭建起 "双重界面防线":

在这里插入图片描述

  • 核心操作区保留经典样式:星图坐标、星座标注等关键功能模块,坚持使用老用户熟悉的设计,守住 App 的 "灵魂底线";

  • 辅助功能区融入液态元素:设置页面、帮助指南等非核心界面,适度添加液态玻璃的过渡动画和光影效果,让新系统用户也能感受到适配诚意;

  • 开发 "界面切换器":在 App 设置中加入开关,让用户自主选择 "经典模式" 或 "液态模式",把界面选择权交还给用户,真正做到 "以人为本"。

在这里插入图片描述

当 Xcode 27 如期而至,强制启用液态玻璃时,"星图导航" 成为了少数几个没有引发用户暴动的 App。

老用户打开 App,看到熟悉的星图界面时会心一笑;新用户切换到液态模式,也能体验到丝滑的现代设计。琳恩团队用行动证明:开发者面对系统更新,不必像肖恩那样被动接受 "换脸",也不必像卡斯托那样极端反抗,而是能用智慧找到平衡 —— 既顺应技术趋势,又守住用户初心。

在这里插入图片描述

就像《变脸》的结局,肖恩最终接纳了曾带来痛苦的面具,却从未丢失自己的灵魂。在代码的世界里,真正的高手从不是抗拒变化的顽固派,而是在技术浪潮中,始终把用户体验放在首位,用一行行代码守护住那份最珍贵的 "界面情怀"。

在这里插入图片描述

而这份情怀,正是让 App 在无数竞品中脱颖而出的关键,也是开发者对用户最真诚的承诺。

那么,宝子们你们 get 到了吗?感谢观赏,我们下次再会吧!8-)

❌