在使用绑定时,我在 SwiftUI 视图模型中遇到了内存泄漏。
final class Model: ObservableObject {
@Published var selectedValue: String?
}
struct ContentView: View {
@StateObject private var model = Model()
var body: some View {
SelectButton(selection: $model.selectedValue)
}
}
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) { option in Text(option) }
}
}
}
}
现在每次我将 ContentView 推到屏幕上时 然后尝试使用 SelectButton 新的 selectedValue 选择 呈现带有列表的表。即使简单地拉动即可关闭此工作表以关闭 然后模型可观察对象泄漏。并从 ContentView 返回 该模型没有被解除分配。 如果我只是进入 ContentView 并且不显示 SelectButton 表,那么就没有泄漏。
当工作表视图不使用 SelectButton 的任何属性时,也不会发生泄漏。但这样的话这个观点就没用了。
例如,我可以通过使用弱绑定来防止泄漏
func weakBinding<Value: ExpressibleByNilLiteral, O: ObservableObject>(_ object: O, keyPath: ReferenceWritableKeyPath<O, Value>) -> Binding<Value> {
Binding(
get: { [weak object] in object?[keyPath: keyPath] ?? nil },
set: { [weak object] in object?[keyPath: keyPath] = $0 }
)
}
和
SelectButton(selection: weakBinding(model keyPath: \.selectedValue))
你知道如何更好地解决这个内存泄漏吗? 它是使用 .sheet()、.fullScreenCover() 发生的,我可以使用自定义 .model() 修饰符来规避(防止)它,但它包装了下面的 UIKit 模式呈现。所以看来 SwiftUI 出了问题。
也许至少可以在 Swift 中实现自定义弱绑定功能,类似于
$viewModel.selectedValue
以及其他前缀,如 #
例如我想要 #viewModel.selectedValue
更新
这似乎是Apple自新的Xcode和iOS 17以来在SwiftUI中引入的错误?
每次您在引用此 ObservableObject 的视图中使用工作表演示时,ObservableObject 上的内存泄漏似乎都会发生。
最小可重现示例(您只需要从根视图推送此 TestView(viewModel: TestViewModel()) 。然后每次打开工作表并移回时都会导致内存泄漏。
final class TestViewModel: ObservableObject {
@Published var text = "Test View"
init() {
print("DEBUG: init TestViewModel")
}
deinit {
print("DEBUG: deinit TestViewModel")
}
}
struct TestView: View {
@StateObject var viewModel: TestViewModel
@State private var isPresented = false
var body: some View {
VStack {
Text(viewModel.text)
Button {
isPresented = true
} label: {
Text("Open sheet")
}
}
.sheet(isPresented: $isPresented, content: contentView)
}
private func contentView() -> some View {
VStack {
Text("Sheet content")
}
}
}
我仍在测试它,但每次您呈现工作表(其中工作表内容在某些中定义)时,似乎都会发生内存泄漏
var sheetContent: some View { }
或
func sheetContent() -> some View { }
只要像这样直接在闭包中指定工作表视图,看起来就不会出现内存泄漏
.sheet(isPresented; $isPresented) {
Text("This doesn't causes memory leak")
}
不幸的是,只要您的视图没有对父视图或视图模型的任何引用,它就可以工作。
你已经很接近了,但你需要解决一些问题:
该对象通常称为 Store,例如
final class ModelStore: ObservableObject {
@Published var models: Model[] = []
static shared = ModelStore()
static preview = ModelStore(preview: true) // fill with sample data for previews
}
其中包含模型类型、结构的数组,因为我们使用的是 Swift:
struct Model: Identifiable {
let id = UUID() // needed for ForEach
var text: String = ""
mutating func someLogic() { // in case you aren't familiar
}
现在使用环境,以便所有视图都可以使用您的商店:
TopMostView()
.environmentObject(ModelStore.shared)
现在,当您绑定到
$model.selectedValue
时,您将不会发生泄漏。
struct ContentView: View {
@EnvironmentObject var modelStore: ModelStore
...
ForEach($modelStore.models) { $model in
DetailView(text: $model.text)
struct DetailView: View {
@Binding var text: String
...
如果您想创建一个临时模型进行编辑,则只需使用
@State var editingModel = Model()
或使用自定义 Editor
状态结构,并将 Model
作为变量。
对于您想要对按钮执行的操作,我认为您的模型结构将需要一个字符串作为选项名称,例如“一”和一个
var bool isSelected
。如果您的选项名称是唯一的,它甚至可以是可识别的 ID,例如var id: String { optionName }