阅读视图

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

从开放平台到受控生态:谷歌宣布 Android 开发者验证政策 - 肘子的 Swift 周报 #101

谷歌宣布从 2026 年 9 月起,将 Play 商店的开发者验证要求扩展到所有 Android 应用安装方式,这从根本上改变了该平台的开放分发模式。这项政策要求所有在 Google Play 之外分发应用的开发者必须向谷歌注册、提供政府身份证明并支付费用。该政策将首先在巴西、印度尼西亚、新加坡和泰国实施,2027 年扩展至全球。这代表着 Android 自诞生以来对其开放生态系统原则的最重大背离。

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 的更新。你几乎不需要手动处理订阅的生命周期,框架为你搞定了一切。

lld 21 ELF changes

LLVM 21.1 have been released. As usual, I maintain lld/ELF and haveadded some notes to https://github.com/llvm/llvm-project/blob/release/21.x/lld/docs/ReleaseNotes.rst.I've meticulously reviewed nearly all the patches that are not authoredby me. I'll delve into some of the key changes.

  • Added -z dynamic-undefined-weak to make undefined weaksymbols dynamic when the dynamic symbol table is present. (#143831)
  • For -z undefs (default for -shared),relocations referencing undefined strong symbols now behave likerelocations referencing undefined weak symbols.
  • --why-live=<glob> prints for each symbol matching<glob> a chain of items that kept it live duringgarbage collection. This is inspired by the Mach-O LLD feature of thesame name.
  • --thinlto-distributor= and--thinlto-remote-compiler= options are added to supportIntegrated Distributed ThinLTO. (#142757)
  • Linker script OVERLAY descriptions now support virtualmemory regions (e.g. >region) andNOCROSSREFS.
  • When the last PT_LOAD segment is executable andincludes BSS sections, its p_memsz member is now correct.(#139207)
  • Spurious ASSERT errors before the layout converges arenow fixed.
  • For ARM and AArch64, --xosegment and--no-xosegment control whether to place executable-only andreadable-executable sections in the same segment. The default option is--no-xosegment. (#132412)
  • For AArch64, added support for the SHF_AARCH64_PURECODEsection flag, which indicates that the section only contains programcode and no data. An output section will only have this flag set if allinput sections also have it set. (#125689, #134798)
  • For AArch64 and ARM, added -zexecute-only-report, whichchecks for missing SHF_AARCH64_PURECODE andSHF_ARM_PURECODE section flags on executable sections. (#128883)
  • For AArch64, -z nopac-plt has been added.
  • For AArch64 and X86_64, added --branch-to-branch, whichrewrites branches that point to another branch instruction to insteadbranch directly to the target of the second instruction. Enabled bydefault at -O2.
  • For AArch64, added support for -zgcs-report-dynamic,enabling checks for GNU GCS Attribute Flags in Dynamic Objects when GCSis enabled. Inherits value from -zgcs-report (capped atwarning level) unless user-defined, ensuring compatibilitywith GNU ld linker.
  • The default Hexagon architecture version in ELF object filesproduced by lld is changed to v68. This change is only effective whenthe version is not provided in the command line by the user and cannotbe inferred from inputs.
  • For LoongArch, the initial-exec to local-exec TLS optimization hasbeen implemented.
  • For LoongArch, several relaxation optimizations are supported,including relaxation for R_LARCH_PCALA_HI20/LO12 andR_LARCH_GOT_PC_HI20/LO12 relocations, instructionrelaxation for R_LARCH_CALL36, TLS local-exec(LE)/global dynamic (GD)/ local dynamic(LD) model relaxation, and TLSDESC code sequencerelaxation.
  • For RISCV, an oscillation bug due to call relaxation is now fixed.(#142899)
  • For x86-64, the .ltext section is now placed before.rodata.

Link: lld 20 ELFchanges

老司机 iOS 周报 #350 | 2025-09-08

ios-weekly
老司机 iOS 周报,只为你呈现有价值的信息。

你也可以为这个项目出一份力,如果发现有价值的信息、文章、工具等可以到 Issues 里提给我们,我们会尽快处理。记得写上推荐的理由哦。有建议和意见也欢迎到 Issues 提出。

文章

🐕 What's New in the Lambda V2 Runtime (Beta)

@Kyle-Ye: Lambda V2 Runtime 是 AWS Lambda Swift 运行时的重大升级,核心改进是使用 Swift 结构化并发重写了内部实现。新版本引入了更灵活的执行模型,开发者可以控制 main 函数入口,支持后台执行(LambdaWithBackgroundProcessingHandler)和流式响应(StreamingLambdaHandler)等高级特性。同时提供了统一的服务生命周期管理机制,让 Lambda 函数能够优雅地处理初始化和清理工作。对于需要在 AWS Lambda 上运行 Swift 代码的开发者来说,V2 运行时带来了更强大的控制能力和更好的性能表现,值得关注并适时迁移。

🐎 Flutter 小技巧之有趣的 UI 骨架屏框架 skeletonizer

@Crazy:骨架图是 app 中经常用来等待网络加载的一种方式,这种方式可以更友好的让用户知道 app 的状态。Skeletonizer 可以让使用者快速的完成骨架图的功能开发,只需把布局包裹在 Skeletonizer 外层,开启 enabled: true 即可得到骨架屏动画。简单来说 skeletonizer 就是通过自定义 PaintingContext 拦截处理 child 的渲染,让原 UI 直接转化为骨架,而不是手写一份骨架版 UI。还可以通过配置来实现更精细的控制骨架化逻辑,比如跳过、合并等。但是越复杂的界面实现起来也就越困难,很多时候需要自己来实现骨架图。这篇文章主要是给大家一个思路,这种实现方案不仅可以用于骨架图,也可以用来其他的功能开发。

🐕 果味儿幽灵 -- Xcode 新 AI 助手深度解析

@Smallfly:这篇文章深度拆解了 Xcode 26 全新 AI 助手的技术内核,从源码到行为策略揭示其「苹果味儿」的设计逻辑。核心内容包括:

  • 架构设计:采用 Planner-Executor 模型,planner 负责意图分类与方案规划,executor 调用 edit_file 等工具执行代码修改,确保行为确定性。
  • 策略驱动:通过外部 .idechatprompttemplate 文件定义「苹果优先」规则,强制使用 Swift Concurrency、Swift Testing 等技术,甚至要求回答「苹果新特性」前必须调用 search_additional_documentation 检索内部文档。
  • 扩展潜力:prompts 外置、工具动态加载的设计,为开发者定制助手提供可能——修改文本模板或注入自定义工具即可调整其「灵魂」。
  • 对比价值:与 Cursor、Claude Code 等工具对比,突出 Xcode AI 在 IDE 深度集成、企业合规上的优势。

文章结合框架源码与实践案例,为开发者理解这一「执拗」助手的行为逻辑,甚至是二次定制提供了关键视角。

🐎 iOS 26 正式版即将发布,Flutter 完成全新 devicectl + lldb 的 Debug JIT 运行支持

@david-clang:Apple 在 iOS 26 中禁止 Debug 时 mprotectRX 权限,将导致真机无法在 Debug Mode 编译成功的错误,Flutter 之前采取的 hack 方案会导致容易出现 Timed out *** to update 错误,现在的方案是在  iOS 17+ 和 Xcode 26+ 上利用 devicectllldb 突破 RX 权限限制, 完成了全新 LLDB 调试的适配迁移。

🐎 What makes Claude Code so damn good (and how to recreate that magic in your agent)!?

@zhangferry:在编程领域为什么总感觉 Claude Code 比其他 Agent 要强不少?除了模型本身的差异,更重要的是对 Agent 的设计逻辑上,本文罗列了几点值得借鉴的 Agent 设计原则。

  1. 控制逻辑设计
  • 只保留一个主循环,最多增加一个处理分支。过多分支路径下既容易出错,还不容易调试
  • 一些简单工作比如读文件、总结记录等交给小模型处理
  1. Prompts 设计
  • 要有一个整体的配置文件(类似 claude.md)以记录用户偏好,比如跳过某些文件夹,使用特定的库。来自 MinusX 的实践,在 AI 首次遇到一些不了解的专有名词时,会自动提醒要不要补充信息到该配置文件。
  • 一些标记或者示例,可以使用 XML 标签或者 Markdown 语法,以强化提醒
  1. Tools 设计
  • 在内容搜索时使用 jq、find 这类轻量工具而不是 RAG 这种重工具
  • 给工具分类,低级工具(bash 命令、读文件)、中级工具(grep、glob)、高级工具(网页搜索等 MCP),每个工具都注明其适用场景
  • 让 AI 自己管理 todo list。长任务里,AI 容易忘事,就让它自己写待办、随时看,还能中途改,避免做到一半 “跑偏”
  1. 可控性设计
  • 语气和风格限制,使得 AI 回答更简洁
  • 对于重要的规则,使用这些关键字 IMPORTANTNEVER 会很有效
  • 「什么时候该做什么,不该做什么」不易过多,因为他们容易冲突,处理逻辑的关键步骤需补充一些启发式的方法和处理策略

代码

awesome-nano-bananaawesome-gpt4o-images

@EyreFree:当下 AI 生图技术发展迅猛,Nano Banana 和 GPT-4o 备受关注。Nano Banana 基于谷歌 Gemini 2.5 Flash Image 架构,解决了角色一致性等难题,能深度理解物理逻辑,支持对话式编辑,在商业、营销及个人创作等领域应用广泛。GPT-4o 是 OpenAI 的多模态大模型,风格多样,构图真实,便于再编辑且响应迅速,常用于广告、艺术、设计等场景。相关的 JimmyLv/awesome-nano-bananajamez-bondos/awesome-gpt4o-images 仓库,收集了大量图像案例与提示词,为探索 AI 生图技术提供了丰富资源。

内推

重新开始更新「iOS 靠谱内推专题」,整理了最近明确在招人的岗位,供大家参考

具体信息请移步:https://www.yuque.com/iosalliance/article/bhutav 进行查看(如有招聘需求请联系 iTDriverr)

关注我们

我们是「老司机技术周报」,一个持续追求精品 iOS 内容的技术公众号,欢迎关注。

关注有礼,关注【老司机技术周报】,回复「2024」,领取 2024 及往年内参

同时也支持了 RSS 订阅:https://github.com/SwiftOldDriver/iOS-Weekly/releases.atom

说明

🚧 表示需某工具,🌟 表示编辑推荐

预计阅读时间:🐎 很快就能读完(1 - 10 mins);🐕 中等 (10 - 20 mins);🐢 慢(20+ mins)

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时,记得给空子串单独写测试,别被“同名不同魂”坑了。

❌