从子视图到模型属性的弱绑定

问题描述 投票:0回答:1

在使用绑定时,我在 SwiftUI 视图模型中遇到了内存泄漏。

  1. 我创建了 ObservableObject 模型,例如
final class Model: ObservableObject { 
   @Published var selectedValue: String?
}
  1. 我使用此模型创建了 ContentView
struct ContentView: View { 
  @StateObject private var model = Model() 

  var body: some View { 
     SelectButton(selection: $model.selectedValue) 
  } 
}
  1. 我用这种方式实现了SelectButton
 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") 
}

不幸的是,只要您的视图没有对父视图或视图模型的任何引用,它就可以工作。

swiftui memory-leaks binding operator-overloading weak-references
1个回答
0
投票

你已经很接近了,但你需要解决一些问题:

该对象通常称为 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 }

© www.soinside.com 2019 - 2024. All rights reserved.