问题描述
在使用SwiftUI开发iOS应用《汇率仓库》的过程中,发现每次下拉关闭Sheet视图时,都会出现明显的卡顿。

上面的Gif图演示了这两个关闭方式,当点击调用dismiss的按钮时,Sheet视图是立即关闭的。但是,当逐渐下拉Sheet视图时,就会出现明显的卡顿。
通过Xcode调试应用,也可以在监控资源的过程中发现,CPU在手动下拉关闭Sheet的过程中明显升高至 160% 左右。

排查原因
为了寻找导致CPU过高的代码,使用排除法,将主视图的代码依次隐藏。
问题代码1
计算变量导致卡顿。
HStack(spacing:0){
Text(currencySymbols[appStorage.localCurrency] ?? "USD")
Text(" ")
Text("\(totalAmount.0)") // 问题代码
// 显示小数部分(格式化为两位小数)
Text(String(format: "%.2f", totalAmount.1).dropFirst(1)) // 去掉小数点符号,问题代码
.foregroundColor(.gray) // 小数部分使用灰色字体
}
当隐藏大部分代码,显示totalAmount计算变量时,手动关闭Sheet就会出现卡顿,因此初步定位的问题代码是,计算变量导致。
// 计算总金额的计算属性
var totalAmount: (Int,Double) {
// 获取最新的汇率数据
let latestRates = fetchLatestRates()
rateDict = Dictionary(uniqueKeysWithValues: latestRates.map { ($0.symbol ?? "", $0.rate) })
// 计算所有外币的金额
var total = 0.0
for userCurrency in userForeignCurrencies {
if let symbol = userCurrency.symbol, let rate = rateDict[symbol],let localCurrency = rateDict[appStorage.localCurrency] ,rate > 0, localCurrency > 0 {
total += userCurrency.amount / rate * localCurrency
}
}
currencyCount = total
// 将 totalAmount 拆分为整数部分和小数部分
if total.isFinite {
let integerPart = Int(total)
let decimalPart = total - Double(integerPart)
return (integerPart,decimalPart)
} else {
return (0,0.0)
}
}
问题原因1
因为totalAmount是一个计算属性,它会在每次视图渲染时重新执行,这里面又调用了Core Data的fetch操作。
var totalAmount: (Int, Double) {
let latestRates = fetchLatestRates() // 每次访问 totalAmount 都会执行 Core Data 读取!
...
return (integerPart, decimalPart)
}
在SwiftUI中又调用了这个计算属性:
Text("\(totalAmount.0)")
Text(String(format: "%.2f", totalAmount.1))
每次SwiftUI渲染这两个Text时,都会重新出发totalAmount,从而调用:
1) fetchLatestRates()(磁盘读操作)
2)计算汇率
3)更新 rateDict 和 currencyCount 等变量。
更具体的讲,是当我下拉Sheet关闭时,SwiftUI会快速重绘主视图以实现平滑动画,这里的totalAmount被反复触发调用,就导致:
1)多次 Core Data 访问
2)汇率计算重复执行
3)视图结构重新构建
4)CPU 飙升,性能下降
解决方案1
缓存计算结果,而不是在 var 里每次重新计算
可以把 totalAmount 从计算属性改成普通变量或 @State / @Published 存储值,只在需要的时候更新一次,比如在数据加载或 sheet 关闭时:
@State private var totalAmount: (Int, Double) = (0, 0.0)
func updateTotalAmount() {
let latestRates = fetchLatestRates()
rateDict = Dictionary(uniqueKeysWithValues: latestRates.map { ($0.symbol ?? "", $0.rate) })
var total = 0.0
for userCurrency in userForeignCurrencies {
if let symbol = userCurrency.symbol,
let rate = rateDict[symbol],
let localCurrency = rateDict[appStorage.localCurrency],
rate > 0, localCurrency > 0 {
total += userCurrency.amount / rate * localCurrency
}
}
currencyCount = total
if total.isFinite {
let integerPart = Int(total)
let decimalPart = total - Double(integerPart)
self.totalAmount = (integerPart, decimalPart)
} else {
self.totalAmount = (0, 0.0)
}
}
在视图打开时调用该方法:
.onAppear {
updateTotalAmount()
}
除“管理外币”视图外,其他的视图在手动下拉关闭Sheet并返回主视图时,不再出现卡顿并导致CPU过高的问题。

这里有人可能认为可以给计算属性加上@State,这样只有数据修改时SwiftUI才会重新渲染,但事实上会存在语法错误。
这是因为Swift不允许@State用于计算属性。
现在还剩下“管理外币”视图在关闭的过程中,仍然存在这一卡顿的问题,下面是关于“管理外币”视图的代码分析。
问题代码2

在”管理外币“视图中,涉及到41种外币,所以绑定了41个TextField比较多。
TextField("0.0", text: Binding(get: {
inputAmounts[currency.symbol ?? ""] ?? ""
}, set: { newValue in
inputAmounts[currency.symbol ?? ""] = newValue
}))
.keyboardType(.decimalPad) // 数字小数点键盘
.focused($focusedField, equals: .symbol(currency.symbol ?? "")) // 添加这一行
.multilineTextAlignment(.trailing)
.padding(.leading,10)
.onChange(of: focusedField) { newFocus in
// 当失去焦点,处理文本框关于 CoreData 方法
if newFocus != .symbol(currency.symbol ?? "") {
handleInputChange(for: currency.symbol ?? "", newValue: inputAmounts[currency.symbol ?? ""] ?? "")
}
}
未隐藏TextField时,手动关闭Sheet视图并返回到主视图时,CPU达到100%并且卡顿明显。

当隐藏全部TextField后,虽然 CPU占比仍然高达90%,但是手动关闭Sheet视图时卡顿消除。

问题原因2

当只显示3个TextField并且不添加修饰符时,SwiftUI显示就存在明显卡顿。
TextField("0.0", text: Binding(get: {
inputAmounts[currency.symbol ?? ""] ?? ""
}, set: { newValue in
inputAmounts[currency.symbol ?? ""] = newValue
}))
因此,初步判断卡顿问题的原因,是TextField + Binding + Sheet的组合导致性能卡顿。
虽然只是3个TextField,但是只要绑定的状态是复杂或动态key的字典型Binding,就会导致SwiftUI在Sheet关闭时:
1)尝试强制释放 TextField 的所有绑定状态
2)SwiftUI 内部执行大量 diff + 绑定更新检查(尤其是绑定在 ForEach 中)
3)与 Sheet 的关闭动画同时触发,造成 UI 卡顿
首先是现在TextField显示,使用LazyVStack替代VStack,这样 SwiftUI 可以懒加载只渲染当前屏幕内可见的输入框,极大减少开销。
替代后,手动下拉Sheet的CPU从100%降到了80%左右。
ScrollView(showsIndicators: false) {
LazyVStack { // 懒加载垂直堆叠视图
// 其他代码
}
}

但是卡顿仍然存在。
经过一系列测试发现,这个问题确实比较难规避,因为TextField 是控件中最容易触发 View diff 和 layout 的,因为它:
1)使用 UIKit 的 UITextField 做桥接
2)键盘出现/隐藏时,伴随多个 GeometryReader、SafeAreaInset 等系统层更新
3)会持续读取绑定值,导致每次 diff 都需“拉取最新状态”
所以,当我用 Dictionary 做 Binding,如:
TextField("0.0", text: Binding(get: {
inputAmounts[currency.symbol ?? ""] ?? ""
}, set: { newValue in
inputAmounts[currency.symbol ?? ""] = newValue
}))
SwiftUI 无法精准追踪是哪个 key 改了,它会导致整个 sheet 的状态树做 diff。即便只改动一个 key,SwiftUI 仍然倾向于刷新大块内容。
接着,我逐渐注释代码,最后发现,当TextField的Binding设置为空时,Sheet手动关闭不再卡顿。
TextField("0.0", text: Binding(get: {
return ""
// inputAmounts[currency] ?? ""
}, set: { newValue in
// inputAmounts[currency] = newValue
}))
.keyboardType(.decimalPad) // 数字小数点键盘
.focused($focusedField, equals: .symbol(currency)) // 添加这一行
.multilineTextAlignment(.trailing)
.padding(.leading,10)
.onChange(of: focusedField) { newFocus in
// 当失去焦点,处理文本框关于 CoreData 方法
if newFocus != .symbol(currency) {
handleInputChange(for: currency, newValue: inputAmounts[currency] ?? "")
}
}
因此,判断“管理外币”视图手动下拉关闭时,卡顿的原因就是“TextField 绑定到字典(如 inputAmounts[currency])”导致的,特别是在Sheet的场景下,手动下拉关闭更容易卡顿、动画掉帧或退出延迟。
可能的原因为:字典绑定不是 SwiftUI 的理想数据结构,SwiftUI 的状态系统(@State, @Binding)期望的是结构化、稳定、可跟踪的值变化。
inputAmounts[currency] // 每次都会返回一个新的 Optional 值
SwiftUI 无法精确追踪每个绑定值的变化路径,因此:
1)每次更新都可能让整个 View 或 Sheet 重建。
2)尤其是关闭 Sheet 时的 diff+layout 阶段,会遇到额外重计算和潜在冲突。
解决方案2
针对这个TextField +. Binding + 字典目前还没有比较好的解决方案,如果是iOS16+,可以考虑禁止Sheet下拉:
.sheet(isPresented: $showingSheet) {
YourSheetView()
.interactiveDismissDisabled(true)
}
或者使用FullScreen。
我有尝试改用@State列表数组,而非字典绑定,因为考虑到字典绑定可能导致性能差,所以替代为数组。
创建一个存储货币和金额的结构:
struct CurrencyInput: Identifiable {
let id: String // symbol
var value: String
}
在视图中创建@State列表数组:
@State private var inputs: [CurrencyInput] = []
通过下标id去定位数据:
TextField("0.0", text: Binding(
get: {
inputs.first(where: { $0.id == currency.symbol })?.value ?? ""
},
set: { newValue in
if let index = inputs.firstIndex(where: { $0.id == currency.symbol }) {
inputs[index].value = newValue
} else {
inputs.append(CurrencyInput(id: currency.symbol, value: newValue))
}
}
))

总的来说,及时将绑定字典改为@State列表数组,仍然存在下拉Sheet视图卡顿,虽然CPU在这里只有70%左右的占比。
以上就是全部的内容分析。
相关文章
1、SwiftUI的diff系统(Diffing System):https://fangjunyu.com/2025/05/03/swiftui%e7%9a%84diff%e7%b3%bb%e7%bb%9f%ef%bc%88diffing-system%ef%bc%89/
2、SwiftUI布局系统(layout system)之三步布局:https://fangjunyu.com/2024/12/20/swiftui%e4%b8%89%e6%ad%a5%e5%b8%83%e5%b1%80/
3、SwiftUI全屏视图fullScreenCover:https://fangjunyu.com/2025/01/02/swiftui%e5%85%a8%e5%b1%8f%e8%a7%86%e5%9b%befullscreencover/