SwiftUI中Sheet导致ObservableObject内存泄漏的解决方案问询
SwiftUI中Binding关联ViewModel的内存泄漏问题及解决方案
问题场景
在iOS 17+版本的SwiftUI里,用Binding关联ObservableObject类型的ViewModel时,只要通过.sheet()或.fullScreenCover()展示的视图间接引用了父视图的绑定属性,就会导致ViewModel实例无法被释放,出现内存泄漏。复现步骤如下:
- 定义ObservableObject模型:
final class Model: ObservableObject { @Published var selectedValue: String? }
- 父视图使用该模型:
struct ContentView: View { @StateObject private var model = Model() var body: some View { SelectButton(selection: $model.selectedValue) } }
- 带Sheet的子视图:
struct SelectButton: View { @Binding var selection: String? @State private var isPresented = false let options: [String] = ["One", "Two"] var body: some View { Button { isPresented = true } label: { Text(selection ?? "Select") } .sheet(isPresented: $isPresented) { VStack { Text("List with options") ForEach(options, id: \.self) { option in Text(option) } } } } }
泄漏触发条件:弹出并关闭Sheet后返回上一页,Model实例无法被释放;不弹出Sheet或Sheet不引用父属性时则无泄漏。另外,Sheet内容定义为函数/计算属性、用EnvironmentObject传递模型时也会触发泄漏。
解决方案
1. 优化自定义弱绑定函数
你现有的weakBinding函数已经能规避泄漏,这里可以优化兼容性,去掉Value: ExpressibleByNilLiteral的限制,让它支持更多类型:
func weakBinding<O: ObservableObject, Value>(_ object: O, keyPath: ReferenceWritableKeyPath<O, Value>) -> Binding<Value> { Binding( get: { [weak object] in guard let object = object else { // 非可选类型返回默认值,可选类型返回nil return (Value.self as? ExpressibleByNilLiteral.Type)?.init(nilLiteral: ()) as? Value ?? { fatalError("Weak binding object deallocated and Value has no default nil value") }() } return object[keyPath: keyPath] }, set: { [weak object] newValue in object?[keyPath: keyPath] = newValue } ) }
调用方式不变:
SelectButton(selection: weakBinding(model, keyPath: \.selectedValue))
2. 将Sheet内容封装为独立视图并传递弱引用
把Sheet的内容抽成独立视图,通过初始化参数传递弱引用的模型,直接切断绑定的强引用链:
struct OptionSheetView: View { weak var model: Model? let options: [String] var body: some View { VStack { Text("List with options") ForEach(options, id: \.self) { option in Button(option) { model?.selectedValue = option } } } } }
然后调整SelectButton和父视图的调用逻辑:
struct ContentView: View { @StateObject private var model = Model() var body: some View { SelectButton(model: model, selection: $model.selectedValue) } } struct SelectButton: View { weak var model: Model? @Binding var selection: String? @State private var isPresented = false let options: [String] = ["One", "Two"] var body: some View { Button { isPresented = true } label: { Text(selection ?? "Select") } .sheet(isPresented: $isPresented) { OptionSheetView(model: model, options: options) } } }
3. 临时替代方案:自定义弹出视图
在部分场景下,用自定义的弹出视图(比如通过ZStack+opacity控制显示)替代系统.sheet,也能绕过这个Bug,适合快速验证问题。
关于自定义弱绑定语法(如#viewModel.selectedValue)
Swift目前不支持自定义类似$的属性包装器投影语法——$是SwiftUI内置的语言层面特性,没法通过自定义扩展实现#这类投影符号。
不过可以给ObservableObject加扩展,实现更简洁的调用方式:
extension ObservableObject { func weakBinding<Value>(keyPath: ReferenceWritableKeyPath<Self, Value>) -> Binding<Value> { SwiftUI.Binding( get: { [weak self] in guard let self = self else { return (Value.self as? ExpressibleByNilLiteral.Type)?.init(nilLiteral: ()) as? Value ?? { fatalError("Weak binding object deallocated and Value has no default nil value") }() } return self[keyPath: keyPath] }, set: { [weak self] in self?[keyPath: keyPath] = $0 } ) } }
调用时可以简化成:
SelectButton(selection: model.weakBinding(keyPath: \.selectedValue))
虽然不如#model.selectedValue简洁,但已经比原函数调用紧凑很多。
内容的提问来源于stack exchange,提问作者Michał Ziobro
相关产品推荐
相关产品推荐

