普通视图

发现新文章,点击刷新页面。
今天 — 2025年11月17日掘金 iOS
昨天以前掘金 iOS

微信与苹果就小程序支付达成和解,iOS用户有望在小程序内直接使用苹果支付

作者 CocoaKier
2025年11月14日 19:19
今日,苹果公司正式发布《小程序合作伙伴计划》,为长期悬而未决的iOS小程序支付问题画上句号。这一官方公告,标志着小程序在苹果生态中的地位获得正式认可,同时也为开发者(宿主App)指明了清晰的合规路径。

SwiftUI 导航

作者 littleplayer
2025年11月11日 17:49

SwiftUI 提供了多种导航方式,让我为你详细介绍主要的导航模式和相关组件。

1. NavigationStack (iOS 16+)

基本用法

struct ContentView: View {
    var body: some View {
        NavigationStack {
            List {
                NavigationLink("前往详情页", value: "详情内容")
                NavigationLink("设置", value: "设置页面")
            }
            .navigationDestination(for: String.self) { value in
                DetailView(content: value)
            }
        }
    }
}

struct DetailView: View {
    let content: String
    
    var body: some View {
        Text("详情: \(content)")
            .navigationTitle("详情页")
    }
}

多类型导航

enum Route: Hashable {
    case product(Int)
    case category(String)
    case settings
}

struct ContentView: View {
    var body: some View {
        NavigationStack {
            VStack {
                NavigationLink("产品123", value: Route.product(123))
                NavigationLink("电子产品", value: Route.category("electronics"))
                NavigationLink("设置", value: Route.settings)
            }
            .navigationDestination(for: Route.self) { route in
                switch route {
                case .product(let id):
                    ProductView(productId: id)
                case .category(let name):
                    CategoryView(category: name)
                case .settings:
                    SettingsView()
                }
            }
        }
    }
}

2. NavigationView (iOS 13-16)

struct ContentView: View {
    var body: some View {
        NavigationView {
            List {
                NavigationLink(destination: DetailView()) {
                    Label("详情页面", systemImage: "star")
                }
                NavigationLink(destination: SettingsView()) {
                    Label("设置", systemImage: "gear")
                }
            }
            .navigationTitle("主页面")
        }
    }
}

3. 编程式导航

使用 NavigationPath

struct ContentView: View {
    @State private var navigationPath = NavigationPath()
    
    var body: some View {
        NavigationStack(path: $navigationPath) {
            VStack(spacing: 20) {
                Button("跳转到产品页") {
                    navigationPath.append(Route.product(456))
                }
                
                Button("跳转到分类页") {
                    navigationPath.append(Route.category("books"))
                }
                
                Button("多层级跳转") {
                    navigationPath.append(Route.category("electronics"))
                    navigationPath.append(Route.product(789))
                }
                
                Button("返回根页面") {
                    navigationPath.removeLast(navigationPath.count)
                }
                
                Button("上一步") {
                    guard !navigationPath.isEmpty else { return }
                    navigationPath.removeLast()
                }
            }
            .navigationDestination(for: Route.self) { route in
                // 路由处理...
            }
        }
    }
}

4. Sheet 和 FullScreenCover

模态展示

struct ContentView: View {
    @State private var showingSheet = false
    @State private var showingFullScreen = false
    
    var body: some View {
        VStack(spacing: 20) {
            Button("显示 Sheet") {
                showingSheet = true
            }
            
            Button("全屏显示") {
                showingFullScreen = true
            }
        }
        .sheet(isPresented: $showingSheet) {
            SheetView()
        }
        .fullScreenCover(isPresented: $showingFullScreen) {
            FullScreenView()
        }
    }
}

struct SheetView: View {
    @Environment(\.dismiss) var dismiss
    
    var body: some View {
        NavigationView {
            Text("这是 Sheet 视图")
                .toolbar {
                    ToolbarItem(placement: .navigationBarTrailing) {
                        Button("完成") {
                            dismiss()
                        }
                    }
                }
        }
    }
}

5. TabView 标签导航

struct MainTabView: View {
    var body: some View {
        TabView {
            HomeView()
                .tabItem {
                    Label("首页", systemImage: "house")
                }
            
            SearchView()
                .tabItem {
                    Label("搜索", systemImage: "magnifyingglass")
                }
            
            ProfileView()
                .tabItem {
                    Label("我的", systemImage: "person")
                }
        }
    }
}

6. 复杂导航示例

带导航栏的完整示例

struct MainView: View {
    @State private var navigationPath = NavigationPath()
    
    var body: some View {
        NavigationStack(path: $navigationPath) {
            ProductListView()
                .navigationTitle("产品列表")
                .toolbar {
                    ToolbarItem(placement: .navigationBarTrailing) {
                        Button("设置") {
                            navigationPath.append(AppRoute.settings)
                        }
                    }
                }
                .navigationDestination(for: AppRoute.self) { route in
                    route.destination
                }
        }
    }
}

enum AppRoute: Hashable {
    case productDetail(Product)
    case category(String)
    case settings
    case profile
    
    @ViewBuilder
    var destination: some View {
        switch self {
        case .productDetail(let product):
            ProductDetailView(product: product)
        case .category(let category):
            CategoryView(category: category)
        case .settings:
            SettingsView()
        case .profile:
            ProfileView()
        }
    }
}

自定义导航栏

struct CustomNavigationView: View {
    @Environment(\.dismiss) var dismiss
    
    var body: some View {
        NavigationView {
            Text("自定义导航栏")
                .navigationTitle("标题")
                .navigationBarTitleDisplayMode(.inline)
                .toolbar {
                    // 左侧按钮
                    ToolbarItem(placement: .navigationBarLeading) {
                        Button("取消") {
                            dismiss()
                        }
                    }
                    
                    // 右侧按钮
                    ToolbarItem(placement: .navigationBarTrailing) {
                        Button("保存") {
                            // 保存操作
                        }
                        .bold()
                    }
                }
        }
    }
}

7. 导航状态管理

class NavigationManager: ObservableObject {
    @Published var path = NavigationPath()
    
    func navigateToProduct(_ id: Int) {
        path.append(Route.product(id))
    }
    
    func navigateToCategory(_ name: String) {
        path.append(Route.category(name))
    }
    
    func popToRoot() {
        path.removeLast(path.count)
    }
}

struct AppView: View {
    @StateObject private var navManager = NavigationManager()
    
    var body: some View {
        NavigationStack(path: $navManager.path) {
            ContentView()
                .navigationDestination(for: Route.self) { route in
                    // 路由处理
                }
        }
        .environmentObject(navManager)
    }
}

主要特点总结

  1. NavigationStack: iOS 16+ 推荐使用,支持类型安全的路由
  2. 编程式导航: 通过状态管理控制导航流程
  3. 模态展示: Sheet 和 FullScreenCover 用于临时内容
  4. 标签导航: TabView 用于主要功能模块切换
  5. 灵活的路由系统: 支持复杂导航逻辑和深度链接

这些导航方式可以组合使用,创建出符合你应用需求的完整导航体验。

Swift 并发:我到底该不该用 Actor?——一张决策图帮你拍板

作者 unravel2025
2025年11月11日 08:20

Actor 是什么?(一句话版)

Actor = 自带大门的房间:一次只能进一个人,进门要“等钥匙”(await)。

它存在的唯一理由:保护非 Sendable 的可变状态。

Actor vs Class:只差一个隔离域

维度 Class Actor
引用语义
继承
隔离域 ❌(谁都能同步访问) ✅(必须 await进门)
线程安全 手动锁/队列 编译器保证
同步调用 任意 外部禁止

把 Actor 想成“远程服务”: 数据在“服务器”里,你要发请求(await)才能读写。

决策三要素:缺一不可!

只有同时满足下面 3 条,才值得上 Actor:

  1. 有非 Sendable 状态

    (纯 Sendable 结构体/类 → 无需保护)

  2. 操作必须原子性

    (读-改-写必须打包,不能中途被插)

  3. 这些原子操作

    不能在现有 Actor 上完成(如 MainActor)

缺一条 → 用别的方式 原因
只有 ① 缺 ② 用 Sendable+ 值类型即可
有 ①② 但能在 MainActor 做 直接标 @MainActor,还能同步访问 UI
为了“避开主线程”而造 Actor 反模式!用 @concurrentTask.detached即可

反例集合:这些 Actor 都“师出无名”

❌ 网络客户端 Actor

actor APIClient {
    // 全是 Sendable:URLSession、tokenString
    func request() async -> Data { ... }
}
  • 状态已 Sendable → 无需保护
  • 副作用只是“不想跑主线程”→ 用 @concurrent 函数即可
  • 结果:人为加锁,解码都无法并发

❌ “看不懂并发报错”就套 Actor

@globalActor actor MyRandomActor {
    // 空状态,只为消 Sendable 警告
}

→ 永远别用 Actor 当创可贴!

先理解警告,再选工具(Sendable@MainActor@concurrent)。

正例:真正需要 Actor 的场景

✅ 本地非 Sendable 缓存

actor ImageCache {
    private var store: [URL: UIImage] = [:]   // UIImage 非 Sendable
    func image(for url: URL) async -> UIImage? {
        if let img = store[url] { return img }
        let data = try await URLSession.shared.data(from: url).0
        let img = UIImage(data: data)!
        store[url] = img
        return img
    }
}
  • 状态非 Sendable
  • 读-写-缓存必须原子
  • MainActor 不适合(网络+解码耗时)

✅ 协议强制 Sendable

protocol DataSource: Sendable {
    func fetch() async -> [Item]
}
  • 实现层含非 Sendable 状态 → 只能用 Actor 满足 Sendable

决策流程图

需要共享可变状态?
├─ 否 → 用 struct / class(Sendable)
├─ 是 → 状态 Sendable?
│   ├─ 是 → 用 Sendable 值类型或锁自由类
│   └─ 否 → 操作必须原子?
│       ├─ 否 → 拆成 Sendable 片段
│       └─ 是 → 能在 MainActor 完成?
│           ├─ 是 → @MainActor
│           └─ 否 → **上 Actor** ✅

口诀:

“Sendable 先,MainActor 其次,新 Actor 最后。”

同步访问红线:Actor = “远程服务”

外部调用必须异步:

actor Counter {
    func increment() { value += 1 }
}

// 外部
await counter.increment()   // ✅
counter.increment()         // ❌ 编译失败

→ 如果你无法容忍这种异步接口(例如实时音频回调),

根本不该用 Actor —— 考虑锁、原子类或 @concurrent 函数。

常见误解速答

误解 真相
“Actor 让并发更快” 它更安全而非更快;异步排队可能更慢
“把类改成 actor 就能消并发警告” 治标不治本;先理解 Sendable 要求
“网络层必须 actor” 若状态 Sendable,用 @concurrent函数/任务即可
“actor 里所有代码都异步” 内部可完全同步;只有外部调用需 await

一句话总结

“Actor 是保护‘非 Sendable 可变状态’的昂贵保险箱—— 确认你真的有宝贝,且别处放不下,再把它请回家。”

记住三要素:

  1. 非 Sendable 状态
  2. 必须原子操作
  3. 现有 Actor 帮不上

同时满足 → 用 Actor;缺一条 → 找更简单的工具。

让 Actor 留在真正需要串行大门的地方,别把远程服务的复杂度,带进本可并行的小花园。

深入理解 DispatchQueue.sync 的死锁陷阱:原理、案例与最佳实践

作者 unravel2025
2025年11月11日 08:06

为什么要谈“死锁”

在 Swift 并发编程中,DispatchQueue.sync 以“阻塞式同步”著称:简单、直观、线程安全,却也最容易让生产环境直接崩溃。

什么是死锁(Deadlock)

维度 说明
定义 两个(或多个)执行单元互相等待对方释放资源,导致永远阻塞。
在 GCD 中的表现 线程 A 通过 sync 提交任务到队列 Q,而队列 Q 正在等待线程 A 完成 → 循环等待 → 触发 EXC_BAD_INSTRUCTION 崩溃。
常见结果 主线程卡死、App 秒退;Crash 日志中出现 0x8badf00d(应用无响应)或 EXC_I386_INVOP(非法操作)等错误码。

餐厅比喻

  • waiter(服务员)同步下订单给 chef(厨师);
  • chef 需要 waiter 回去问顾客口味,又同步派任务给 waiter;
  • 两人互相等,餐厅停摆 → 死锁。

Swift 最小死锁示例

import Foundation

// 1. 同队列嵌套 sync → 立即崩溃
let queue = DispatchQueue(label: "com.demo.queue")
queue.sync {
    print("外层 sync")
    queue.sync {          // ❌ 在这里死锁
        print("永远进不来")
    }
}

运行后控制台只会打印 外层 sync,随后 App 崩溃。

原因:

  • 外层闭包已占用队列唯一线程;
  • 内层 sync 要求同一条线程再次进入 → 无法满足 → 死锁。

双队列交叉死锁(更接近真实业务)

let waiter = DispatchQueue(label: "waiter")
let chef   = DispatchQueue(label: "chef")

// 模拟下单流程
waiter.sync {
    print("① Waiter:同步下单给 Chef")
    
    chef.sync {
        print("② Chef:同步要求 Waiter 去问口味")
        
        waiter.sync {     // ❌ 交叉等待
            print("③ Waiter:永远无法执行")
        }
    }
}

崩溃点:③ 处 waiter 队列已被①占用,而①又在等② → 循环等待。

如何“一键”解决——把任意一个 sync 改成 async

修改方案 代码片段 是否死锁
① → async waiter.async { … }
② → async chef.async { … }
③ → async waiter.async { … }

结论: 只要打破“循环等待链”中的任意一个环,死锁即刻解除。

在真实项目中,优先把“反向调用”做成 async 即可。

工程中最容易踩的“隐性死锁”

  1. 对外暴露 sync 接口
class ImageCache {
    private let queue = DispatchQueue(label: "cache")
    private var storage: [String: UIImage] = [:]
    
    // ❌ 危险:把内部队列 sync 暴露给外部
    func read<T>(_ closure: () -> T) -> T {
        return queue.sync(execute: closure)   // 闭包里可能再调 read()
    }
}

问题:调用方可能在闭包里再次调用 read() → 递归同步 → 死锁。

解决:

  • 绝不对外暴露 sync;
  1. 主线程 sync 到主队列
DispatchQueue.main.sync {   // ❌ 100 % 死锁
    // 代码永远不会进来
}

场景:在后台线程计算完后,想“立刻”回主线程刷新 UI,却手滑写成 sync

正确姿势:

永远用 DispatchQueue.main.async { ... }

sync 的正确打开方式——“私有队列 + 原子访问”

/// 线程安全的日期格式化器缓存
final class DateFormatterCache {
    private var formatters: [String: DateFormatter] = [:]
    private let queue = DispatchQueue(label: "cache.\(UUID().uuidString)")
    
    func formatter(using format: String) -> DateFormatter {
        // 1. 只在此私有队列里同步,外部无法递归根除
        return queue.sync { [unowned self] in
            if let cached = formatters[format] {
                return cached
            }
            let df = DateFormatter()
            df.locale = Locale(identifier: "en_US_POSIX")
            df.dateFormat = format
            formatters[format] = df
            return df
        }
    }
}

为什么这里不会死锁?

  • queue 私有,外部无法直接往它塞 sync 任务;
  • 函数内部无递归调用;
  • 闭包执行时间极短,不会阻塞用户可见线程。

checklist ✅

使用 sync 前自问 回答
队列是否私有?
闭包里还会 sync 到同队列吗?
阻塞是否影响主线程/用户滑动?

封装一个“防死锁”的读写锁

/// 读写锁:写操作 barrier,读操作并发
final class RWLock<T> {
    private var value: T
    private let queue: DispatchQueue
    
    init(_ initial: T) {
        value = initial
        queue = DispatchQueue(label: "rw.\(UUID().uuidString)", attributes: .concurrent)
    }
    
    // 读:并发
    func read<U>(_ closure: (T) throws -> U) rethrows -> U {
        try queue.sync { try closure(value) }
    }
    
    // 写:barrier
    func write(_ closure: @escaping (inout T) -> Void) {
        queue.async(flags: .barrier) { closure(&self.value) }
    }
}

优点:

  • 读并行、写串行;
  • 外部无法拿到 queue 引用,彻底杜绝递归 sync;
  • 所有写操作是 async,不会阻塞调用方。

总结——一句话记住

除非你在做原子访问,且队列私有、无递归,否则一律用 async。

扩展阅读 & 下一步

  1. 官方文档:DispatchQueue.sync
  2. WWDC 2022 – Visualize and eliminate hangs with Instruments
  3. Swift Concurrency 时代:
    • actor 替代“私有队列 + sync”;
    • AsyncSequence 做“异步回调链”,天然避免死锁。

学习资料

  1. www.donnywals.com/understandi…

Skip Fuse现在对独立开发者免费! -- 肘子的 Swift 周报 #0110

作者 东坡肘子
2025年11月11日 07:51

issue110.webp

📮 想持续关注 Swift 技术前沿?

每周一期《肘子的 Swift 周报》,为你精选本周最值得关注的 Swift、SwiftUI 技术文章、开源项目和社区动态。

一起构建更好的 Swift 应用!🚀

Skip Fuse现在对独立开发者免费!

在 Swift 社区发布官方 Android 版 SDK 不久之后,Skip 宣布其 Skip Fuse 版本将对符合条件的独立开发者免费开放,用于构建 Android 应用。

与过去一年多独立开发者可免费使用的 Skip Lite 相比,Skip Fuse 带来了实质性的技术变革。Skip Lite 的原理是将 Swift 代码转译为 Kotlin,而 Skip Fuse 则直接利用 Swift 官方 Android SDK 进行交叉编译,将 Swift 源码编译为可在 Android 平台上原生运行的 ARM 二进制文件。这意味着开发者不再局限于“具有 Skip 感知”的依赖包,而可以使用任何能在 Android 上编译的 Swift 包。

根据 Swift Everywhere 的统计,目前已有超过 2000 个 Swift Package 支持 Android 平台,其中包括 Alamofire、SwiftSoup、swift-sqlcipher 等常用库。换句话说,Fuse 模式让开发者能够充分利用标准 SwiftPM 生态。这不仅显著拓宽了可用依赖的范围,也降低了项目迁移与维护的复杂度。

在架构层面上,Skip Fuse 采用了混合实现方案:业务逻辑部分由原生 Swift 直接编译执行,而 UI 层的 SwiftUI DSL 则在构建过程中由 Skip Fuse UI 模块映射为 Jetpack Compose 代码,从而在 Android 上呈现出完全原生的用户体验。这种做法既保留了 SwiftUI 的声明式语法,又遵循了 Android 平台的设计规范。

此次政策调整,或许会让许多独立开发者和中小团队在技术选型上更倾向于具备跨平台潜力的方案。即便继续主要面向 Apple 生态进行开发(使用苹果私有框架),也可能开始在架构中加入抽象层,为未来的多平台拓展预留空间。

当然,Skip 对“独立开发者”的定义也有明确限制:仅适用于个人或不超过两人的团队,年收入需低于 30,000 美元,并且免费许可仅允许发布一个闭源商业应用(开源项目数量不限)。即便如此,这一政策仍为 Swift 开发者以近乎零成本的方式进入 Android 市场打开了大门,为他们在这一庞大平台上探索新的可能与收入来源提供了契机。

尽管未来还会出现更多面向 Swift 的跨平台开发方案,但 Skip 已经为 Swift 在 Android 生态中的落地提供了一条清晰、可行的路径。社区也正期待着类似 Skip 这样成熟的跨平台方案能够扩展至 Linux、Windows 乃至嵌入式平台,为 Swift 的多平台发展奠定更坚实的基础。

本期内容 | 前一期内容 | 全部周报列表

近期推荐

SwiftData 的优雅取消编辑方案:从混乱到艺术 (The Art of SwiftData in 2025: From Scattered Pieces to a Masterpiece)

由于取消了父子上下文的概念,SwiftData 在实现“可撤销修改”方面变得更加棘手。Mathis Gaignet 在这篇长文中,通过构建一个专用于编辑状态的独立 ModelContext,阐述了在 SwiftData 中实现“可撤销、可复用、低样板”增改(Upsert)架构的思路,用以取代散乱的表单状态与脆弱的回滚方案。

作者并未直接给出“标准答案”,而是完整展示了从问题发现到方案演进的思考路径。文章深入解析了 @Model@Query 宏的底层机制,揭示了 PersistentIdentifier 的临时与永久状态陷阱,以及上下文同步延迟导致的隐蔽 Bug。这种探索式的写作方式,使读者不仅明白“怎么做”,更理解“为什么这样做”。


从 SPM 项目管理迁移到 Tuist 项目管理 (Back Market x Tuist - Part I: Why We Moved Our iOS Project To Tuist)

在 Swift 项目中,SPM 除了负责代码模块化,也承担了项目结构管理的职责。然而,随着项目规模的扩大,这种方式的局限逐渐显现:依赖解析频繁、缺乏构建设置和自定义阶段、无法运行脚本、xcodeproj 文件易冲突、模块间规则难以统一,且 SPM 插件能力有限。Alberto Salas 在本文中介绍了其团队如何将项目结构管理从 SPM Packages 迁移至 Tuist Targets,并系统梳理了问题成因、探索路径与决策过程。在 Part II 中,他进一步分享了如何在不阻塞日常开发的前提下完成迁移,并量化了构建性能的提升。

本文并非要抛弃 SPM,而是将项目结构与生成工作交由 Tuist 接管,依赖管理依然由 SPM 负责。文章还比较了 SPM、Bazel 与 Tuist 的不同取舍。作者指出,Bazel 功能更强大,但最终选择 Tuist,是因为它更符合团队能力与项目特性。这也提醒我们,工具选型的关键不在“最强”,而在“最合适”——尤其对于中小型团队,可自主掌控、易于维护的方案往往更具长期价值。


让应用更懂用户语言:Language Discovery 的个性化新机制 (Making Apps More Personal with Language Discovery)

传统的“选择主要语言”(Locale.preferredLanguages) 模式假设每位用户都有单一语言偏好,但现实中人们往往在不同情境下使用多种语言——例如在工作中使用英语、在社交中使用法语、在媒体消费中使用西班牙语。苹果在 iOS 26 中添加了 Language Discovery 功能,通过设备端的机器学习,在确保隐私的前提下,基于用户的输入模式、内容消费、沟通语言以及应用偏好等行为数据,自动推断用户的语言使用习惯。Letizia Granata 在文中介绍了这一智能识别用户语言偏好的系统机制。

通过 Language Discovery,应用可以更准确地响应用户的语言与文化背景,从而在本地化层面实现个性化。这项功能标志着苹果在多语言支持上的一次重要转变:从被动配置走向主动理解,从单一语言到多语共存,为开发者提供了打造更包容、更真实应用体验的新基础。


关于 Xcode 26.1 CPU 异常占用的提醒和临时解决方案

iOS 开发者 Artem Mirzabekian 指出,Xcode 26.1 在运行 iOS 26.1 模拟器时会出现异常的 CPU 占用问题。原因是 iOS 26.1 模拟器中与壁纸渲染相关的 MercuryPosterExtension 进程持续崩溃并重启,导致 CPU 异常占用。Xcode 26.2(beta 版)目前也受影响。

临时解决方案:使用 iOS 26.0 模拟器,或在 iOS 26.1 模拟器中更换壁纸并删除默认的纯黑壁纸。


Swift 6 并发模型:技术挑战与社区争议

Michael Tsai 汇总了两组关于 Swift 6 并发的重要讨论。第一篇 关注 Swift 6.2 推出的 “Approachable Concurrency” 改进;第二篇 则聚焦具体技术议题,如 MainActor.assumeIsolated@preconcurrency 的实际使用与限制;

社区观点呈现明显分化。支持者认为,默认主线程隔离有效防止了“意外跑到后台线程”的常见问题,降低了初学者与 UI 密集型项目的上手难度,一些团队已经成功完成大型应用的迁移。质疑者则指出,完整的 UIKit 应用仍难以全面采用 Swift 6 模式,与 Core Data 和第三方框架的集成问题频出,错误信息晦涩难懂,而语言的持续演进也让代码不断老化。更激进的声音甚至认为当前并发模型已成为“无法协同运作的拼凑体系”,需要一次“彻底重置”。

整体共识是:Swift 6 并发机制的收益高度依赖项目的并发复杂度——对单线程或 UI 驱动型应用帮助显著,但对于并发密集或系统耦合度高的项目,迁移仍充满挑战。


深入解析 visionOS 上的动画机制 (Deep Dive into Animation on visionOS)

空间计算不仅改变了用户体验,也对开发者提出了更高的要求——许多在平面界面中行之有效的技巧,在三维空间中已不再适用。Cristian Díaz 从“空间交互的可感知性与舒适性”出发,提出了一个动画决策框架:谁创作动画(设计师预制 vs. 运行时生成)、什么需要动画(SwiftUI 窗口 vs. RealityKit 实体)、复杂度如何(微交互 vs. 编排表演)。在此基础上,他系统梳理了 visionOS 的五条渲染路径与十种动画机制,为每种方案明确列出适用场景、避免情形与实现要点。

即便你并非 visionOS 开发者,也能从这篇文章中受益。Cristian 以“从感知需求推导技术选择”的方式诠释了动画设计思维,这种方法同样适用于其他平台和界面的动态设计。


使用 Instruments 找出 SwiftUI 中更新最频繁的视图 (Find the SwiftUI Views that Update the Most Using Instruments)

Xcode 26 为 Instruments 新增了 SwiftUI 专用的分析工具,可统计视图的更新次数与耗时,并通过 All Updates Summary 与 Cause & Effect Graph 定位哪些视图“更新过于频繁”以及具体触发链路。Mark Szymczyk 以实操示例展示了如何创建 SwiftUI Profiling 会话、按更新次数/耗时排序视图,并用因果图追踪更新来源。

排查症状之外,更应理解 SwiftUI 的刷新原理,才能从源头减少无效重绘。在我的文章理解 SwiftUI 的视图刷新机制:从 TimelineView 刷新问题谈起中,借由 TimelineView 个案系统阐述了视图声明、响应机制与递归更新的判定逻辑。只有搞清“为什么视图会更新”、“系统如何决定是否重算视图声明值”,优化才不会沦为补丁式修修补补。

工具

imessage-kit:在 AI Agent 中提供消息集成能力

imessage-kit 是一个由 Photon 开发的功能强大的 iMessage 开源 SDK,非常适合将 iMessage 集成到 AI 智能体或自动化工作流中。其主要功能有:

  • 现代化的 API:提供优雅的链式调用(Fluent API),可以轻松实现“收到消息 A,则回复 B”的逻辑。
  • 功能全面:不仅支持收发文本,还能处理图片、各类文件,并能实时监控新消息。
  • 类型安全与跨运行时:完全使用 TypeScript 编写,类型支持良好,并同时兼容 Node.js 和 Bun。
  • AI 集成友好:官方定位就是为 AI Agent 提供消息集成能力,是连接物理世界和 AI 的一个有趣尝试。

它通过直接读取 iMessage 数据库(chat.db)并结合 AppleScript 实现自动化,因此仅限 macOS 使用,且需要授予应用"完全磁盘访问权限"。

该库采用 SSPL-1.0 许可证,禁止用于创建竞争产品(如其他消息 SDK/API),但允许用于内部业务、个人项目和教育用途。值得注意的是,Photon 还提供高级版本,支持线程回复、消息撤回/编辑、实时输入指示器等功能,以及企业级托管服务。更多内容可以在其官网获取。


SwiftUI-DetectGestureUtil:为单个 SwiftUI 视图绑定多个自定义手势

在 SwiftUI 中,让同一个视图同时识别多个手势一直是个棘手的问题。由 Saw-000 开发的 SwiftUI-DetectGestureUtil 通过引入独立的“检测阶段”,让开发者能在多个自定义手势中只确认其中一个,从而绕开系统默认组合方式(simultaneous、sequenced、exclusive)带来的约束,实现更自然的多手势交互。

它将手势识别流程清晰地拆分为两个阶段:

  • 检测阶段(detectGesture):在手势发生的整个周期内持续更新状态,直到某一自定义规则匹配并返回手势类型
  • 处理阶段(handleGesture):在识别完成后持续追踪手势进展,通过 .yet.finished 明确控制生命周期

这一模型为构建复杂交互(如“双击 + 拖动”、“圆形绘制”、“自定义笔迹检测”)提供了新的思路。借助 DetectGestureState,开发者可在一次手势周期内获得所有触点、时间与几何信息,实现远超 SwiftUI 原生 Gesture API 的表达力与精度。

往期内容

THANK YOU

如果你觉得这份周报或者我的文章对你有所帮助,欢迎 点赞 并将其 转发 给更多的朋友。

📮 想持续关注 Swift 技术前沿?

每周一期《肘子的 Swift 周报》,为你精选本周最值得关注的 Swift、SwiftUI 技术文章、开源项目和社区动态。

一起构建更好的 Swift 应用!🚀

❌
❌