惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

C
CERT Recently Published Vulnerability Notes
U
Unit 42
T
The Blog of Author Tim Ferriss
H
Hackread – Cybersecurity News, Data Breaches, AI and More
B
Blog RSS Feed
Microsoft Azure Blog
Microsoft Azure Blog
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
S
Securelist
L
Lohrmann on Cybersecurity
Blog — PlanetScale
Blog — PlanetScale
Recorded Future
Recorded Future
D
DataBreaches.Net
Spread Privacy
Spread Privacy
T
Threat Research - Cisco Blogs
I
Intezer
P
Palo Alto Networks Blog
Simon Willison's Weblog
Simon Willison's Weblog
cs.CL updates on arXiv.org
cs.CL updates on arXiv.org
I
InfoQ
宝玉的分享
宝玉的分享
Security Latest
Security Latest
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
T
Threatpost
Cisco Talos Blog
Cisco Talos Blog
P
Proofpoint News Feed
博客园 - 司徒正美
H
Hacker News: Front Page
Y
Y Combinator Blog
爱范儿
爱范儿
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
NISL@THU
NISL@THU
月光博客
月光博客
有赞技术团队
有赞技术团队
Cloudbric
Cloudbric
酷 壳 – CoolShell
酷 壳 – CoolShell
G
Google Developers Blog
A
Arctic Wolf
博客园 - 【当耐特】
W
WeLiveSecurity
V
Visual Studio Blog
Exploit-DB.com RSS Feed
Exploit-DB.com RSS Feed
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
cs.CV updates on arXiv.org
cs.CV updates on arXiv.org
V
V2EX
C
Cyber Attacks, Cyber Crime and Cyber Security
S
SegmentFault 最新的问题
The GitHub Blog
The GitHub Blog
The Cloudflare Blog
Stack Overflow Blog
Stack Overflow Blog

肘子的 Swift 记事本 | Fatbobman's Blog

当每一次写入都有了新价格 -- 肘子的 Swift 周报 #144 当 Linux 成为“空气”:容器、Agent 与不再重要的“桌面之争” -- 肘子的 Swift 周报 #143 SPI 加入 Apple,Swift 迈向自举 -- 肘子的 Swift 周报 #142 Swift 还让你 Excited 吗? -- 肘子的 Swift 周报 #141 从 Size Class 到可用空间,horizontalSizeClass 还可靠吗? WWDC 26:AI 帮你看完了,然后呢? -- 肘子的 Swift 周报 #140 WWDC 2026 初印象:符合预期,但更务实 -- 肘子的 Swift 周报 #139 Core Data + Observation:从属性级响应到心智解放 稳定 > 新功能 -- 肘子的 Swift 周报 #138 用自定义 Layout 化解 SwiftUI List 的行高与间距跳变 从社区路标到生态基石:Dave Verwer 的新篇章 -- 肘子的 Swift 周报 #137 消失的 WWDC 愿望单 -- 肘子的 Swift 周报 #136 CocoaPods 正在退场,SwiftPM 才刚到第二章 -- 肘子的 Swift 周报 #135 让 AI 从称手到称心 -- 肘子的 Swift 周报 #134 Swift 并发正被更广泛地接纳 -- 肘子的 Swift 周报 #133 从 OpenSwiftUI 到 DanceUI:换个方式 Dive SwiftUI -- 肘子的 Swift 周报 #132 被 Vibe 摧毁的版权壁垒,与开发者的新护城河 -- 肘子的 Swift 周报 #131 苹果的罕见妥协:当高危漏洞遇上“拒升”潮 -- 肘子的 Swift 周报 #130 一墙之隔,不同的时空 -- 肘子的 Swift 周报 #129 我的 App 审核被卡了? -- 肘子的 Swift 周报 #128 CDE:一次让 Core Data 更像现代 Swift 的尝试 50 岁的苹果和 51 岁的我 -- 肘子的 Swift 周报 #127 2026 年,为什么我仍在思考 Core Data Macbook Neo:苹果重回校园的起点 -- 肘子的 Swift 周报 #126 跨域传递 NSManagedObjectContext 为什么在 Swift 6.2 中不再报错?真正的变化不在编译器 Xcode 26.3 + Claude Agent:模型替换、MCP、Skill 与自适应配置 macOS 录屏软件开发实录:从像素抓取到元数据重现 isolated(any) 与 #isolation:让 Swift 闭包自动继承隔离域 在 tvOS 上活下來:一個非典型播放器的工程實錄 告别“可移植汇编”:我已让 Swift 在 MCU 上运行七年 独立开发者的试炼:Zipic 从 0 到 1 的产品化之路 逃离 Mac App Store:如何从零构建独立应用的分发与售卖体系 为什么父视图无法修改子视图的 @State Partytown 实战:修复 GTM 自定义事件与 dataLayer 失效问题 Swift 6 编译加速:启用 Swift-Syntax 预构建 (Prebuilts) Swift 模块化:使用 package 关键字实现 Target 间安全共享 Monorepo 实战:解决 VSCode/Cursor 中 SwiftLint 配置失效问题 VSCode/Cursor 测试 Core Data:手动编译 xcdatamodeld 指南
两个 SwiftUI 动画 Bug 排查小记
东坡肘子 · 2026-07-01 · via 肘子的 Swift 记事本 | Fatbobman's Blog

易用的动画一直是 SwiftUI 的主要特色之一。但声明式框架的特性是一把双刃剑:动画一旦出现异常,排查往往比命令式框架更棘手。本文记录了过去两周里,我在开发中遇到的两个与 SwiftUI 动画有关的 Bug。与其说是解决方案,不如说是一段排查的心路——希望能为你面对类似问题时开阔一点思路。

Bug 1:显式动画 vs 隐式动画

父视图被意外重建

我的项目中有这样一个列表场景:多个 Cell 依靠状态机实现较为复杂的 UX 效果,视图动画跟随状态机的状态变化而发生。随着场景复杂度不断提高,在 Xcode 26 + iOS 26 中,我偶尔会发现似乎有动画状态被莫名清除。

这种情况出现得非常少,概率大约只有 1~2%,因此很难定位根源,甚至一度让我怀疑是不是自己眼花了。但随着 WWDC 26 的到来,这个问题被彻底放大了——当我在 Xcode 27 + iOS 27 上运行同一套代码时,动画状态被打断的概率几乎超过了 50%。

考虑到相同代码在 macOS 上没有问题,Xcode 27 + iOS 26 的组合也没有明显异常(最多仍是 1~2% 的偶发问题),我最初以为这只是 iOS 27 beta 1 的不稳定。但当问题在 beta 2 中依然存在时,我就不能再坐视不管了。

在用大量埋点观察动画状态后,我将问题锁定为:在关闭动画进行中,SwiftUI 于内部刷新过程中触发了父视图的意外重建,并在原动画事务之外重新提交了子视图的终点状态。由于原来的动画由 withAnimation 所在的 mutation transaction 持有,这次新的非动画更新会让子视图的呈现值直接跳到终点,从而中断在飞动画。

也就是说,我的代码在运行过程中并没有主动执行任何会重建父视图的操作;但由于场景较为复杂,SwiftUI 在某些条件下会自动重建父视图。在 iOS 26 下,对应 SDK 触发这种重建的次数很少;而 iOS 27 似乎大幅调整了 SwiftUI 的内部实现,使这种重建变得更频繁,问题也因此被显著放大。

为什么使用显式动画

我自己也写过不少文章,推荐在合适的情况下优先使用隐式动画。但在我面临的场景中,动画曲线需要根据手势信息动态合成;更关键的是,动画完成后必须立刻执行下一步的状态收尾逻辑,因此 withAnimation 提供的 completion 闭包对我来说至关重要。

不过在上述问题出现后,我不得不重新权衡:是否应该放弃这条路径,让动画声明更靠近动画真正发生的视图位置,从而避免它被系统内部刷新意外中断。

通过 Animatable 构建用于隐式动画的 completion 机制

动画声明越靠近动画的发生地,它受视图树其他部分影响的概率就越低。如今最受推荐的做法,无疑是使用基于闭包的动画范围限定:

.animation(.smooth) {
    $0.foregroundStyle(isActive ? Color.red : Color.blue)
}

但对我的场景来说,状态都由状态机维护,因此只需采用与特定值绑定的动画声明方式:

子视图(动画发生地)
    .animation(animation, value: closeToken)

withAnimation 改成上述基于值的隐式动画后,动画中断问题确实解决了。但随之而来的新问题是:原本依赖 completion 闭包的后续逻辑失去了落脚点。

最终,我通过 Animatable 协议构建了一个 modifier 来弥补这一环。

struct SwipeCellPinnedCloseAnimationModifier: ViewModifier {
    let token: Int
    let animation: Animation
    let onCompletion: (Int) -> Void

    func body(content: Content) -> some View {
        content
            .modifier(
                SwipeCellPinnedCloseCompletionObserver(
                    token: token,
                    onCompletion: onCompletion
                )
            )
            .animation(animation, value: token)
    }
}

private struct SwipeCellPinnedCloseCompletionObserver: ViewModifier, Animatable {
    let token: Int
    let onCompletion: (Int) -> Void
    var progress: CGFloat

    init(token: Int, onCompletion: @escaping (Int) -> Void) {
        self.token = token
        self.onCompletion = onCompletion
        self.progress = CGFloat(token)
    }

    var animatableData: CGFloat {
        get { progress }
        set {
            progress = newValue
            notifyCompletionIfNeeded()
        }
    }

    func body(content: Content) -> some View {
        content
    }

    private func notifyCompletionIfNeeded() {
        guard token > 0 else { return }
        guard abs(progress - CGFloat(token)) < 0.0001 else { return }

        DispatchQueue.main.async {
            onCompletion(token) // 在状态机中重置 token 为 0, 确保只调用一次
        }
    }
}

.animation(_:value:) 解决了动画的归属问题,Animatable 则用来实现 completion 逻辑:当插值进度抵达当前 token 时,再把 token 交回状态机,由状态机完成校验并执行真正的收尾工作。

💡 关于 DispatchQueue.main.async

在 Swift Concurrency 普及的今天,很多人倾向于用 Task { @MainActor in ... }MainActor.run 来替代 DispatchQueue。但在当前场景下,animatableDataset 是在 SwiftUI 的渲染与布局周期内部被调用的,这两个替代方案都不合适。

MainActor.run 在调用方已处于主线程时,可能会同步执行,无法跳出当前的计算周期;Task { @MainActor in ... } 虽然总是异步执行,但它走的是 Swift Concurrency 的协作式调度,执行时机由 executor 决定,并不保证落在主运行循环(Run Loop)的下一拍。

而我们要做的,恰恰是把状态变更(重置 Token)明确推迟到下一个 tick,以稳妥避开 “Publishing changes from within view updates is not allowed” 这一经典运行时警告。在这种特定场景下,DispatchQueue.main.async 是最直接、语义最贴切的工具。

现在,动画曲线以及驱动动画状态的数据,都由状态机统一提供:

子视图
    .modifier(
       SwipeCellPinnedCloseAnimationModifier(
           token: coordinator.closeToken,
           animation: inputs.settlingAnimation,
           onCompletion: { token in
               coordinator.completePinnedClose(token: token)
           }
         )
     )

如此一来,无论是 Xcode 27 + iOS 27,还是 Xcode 26 + iOS 26,动画意外消失的问题都得到了彻底解决。

Bug 2:’?:’ vs ‘if else’

解决了上述 Bug 后,我判断 Xcode 27 + iOS 27 这套组合或许更容易暴露潜在问题,于是在后续开发中让它们成为我的主要搭档。直到完成一个新的动画效果后,剧情却发生了反转——这一次,代码运行在 iOS 26 上反而出现了动画异常中断。

有了上次的经验,我首先怀疑问题仍出在动画声明上。但检查后发现,并非如此。

最终,通过注释大法,我把问题锁定在如下代码中:

Image(systemName: context.isArmed ? "ellipsis" : "heart")

当包含这段代码的 Cell 正在进行动画、且 context.isArmed 发生变化时,List 中的这个 Image 会出现异常,进而中断整个 Cell 的动画。

解决方案简单粗暴:

if context.isArmed {
    Image(systemName: "ellipsis")
} else {
    Image(systemName: "heart")
}

这个修复并非因为 if else 本身更神奇,而是它改变了视图的结构性标识:三元表达式让 SwiftUI 复用同一个 Image 节点、原地切换 SF Symbol 名称;而 if/else 会生成 _ConditionalContent 的两个不同分支,让 SwiftUI 看到两个互不相同的 Image

反直觉之处恰在于此:我们平时反复强调“保持视图标识稳定”,但在这个场景里,主动制造出不同的标识,反而让问题消失了。

真正让人头疼的是问题的复现条件——Image + 三元表达式的异常只出现在 iOS 26 及更早版本,并且只出现在 List 场景中。在 ScrollView 中没有问题;在 macOS 上,无论哪种场景、哪个版本都没有问题。

相比 Bug 1,Bug 2 在技术上几乎没有难度。但恰恰因为 Bug 1 的存在,让我一开始又把注意力拉回到动画声明上,白白浪费了一个多小时。

了解原理外,运气也很重要

SwiftUI 的动画系统无疑是强大的,苹果也提供了不少高级工具来帮助开发者解决问题。例如,geometryGroup 可以将动画插值逻辑上提,Animatable 则能显式要求动画插值,并基于这些插值执行额外操作。

但当问题真正发生时,它可能只出现在个别系统版本、个别 SDK、个别设备,或某个非常具体的场景中。解决这类问题,除了理解原理,确实也需要一点运气。

只是运气并非全靠天意。回头看,埋点与注释大法这类笨办法,本质上是在把模糊的“异常气味”逐步逼成可定位的事实——而这种对异常的敏感、以及把直觉转化为方法的能力,或许正是开发者在 AI Agent 时代仍然拥有的特殊优势。