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

Weak binding to model properties from subview

提问人:Michał Ziobro 提问时间:9/28/2023 最后编辑:Michał Ziobro 更新时间:10/3/2023 访问量:131

问:

使用绑定时,我在 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 new 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。

最小的可重现 excample(你只需要从根视图推送这个 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") 
}

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

更新 2

创建单独的 SheetView 并直接显示它不会泄露父视图的视图模型。

struct SheetView: View {
    
    var body: some View {
        Text("Sheet content")
    }
}

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: {
                SheetView()
            })
    }
    
    private func contentView() -> some View {
        VStack {
            Text("Sheet content")
        }
    }
}

更新 3

使用环境对象(如建议)也会导致内存泄漏malhal

这是我测试过的代码:

struct SheetView: View {
    
    @EnvironmentObject var viewModel: TestViewModel
    
    var body: some View {
        Text("Sheet content")
    }
}

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
    @State private var flag = false
    
    private let variable = "Test"
    
    var body: some View {
        VStack {
            Text(viewModel.text)
            
            Button {
                isPresented = true
            } label: {
                Text("Open sheet")
            }
        }
        .sheet(isPresented: $isPresented, content: {
            SheetView()
                .environmentObject(viewModel)
        })
    }
    
    private func contentView() -> some View {
        VStack {
            Text("Sheet content")
        }
    }
}
swiftui 内存泄漏绑定 运算符重载 弱引用

评论

1赞 CouchDeveloper 10/1/2023
我确实仅在 iOS 17(而不是 iOS 16)上遇到过类似的情况,其中父视图(通过)创建和拥有的模型 (ObservableObject) 在显示工作表时泄漏(此处,使用自己的模型)。如果工作表不会显示在父视图中,则其模型不会泄漏。父模型与图纸或其模型之间没有任何连接。恕我直言,这是 iOS 17 中的一个严重错误。@StateObject
1赞 Michał Ziobro 10/3/2023
是的,每次我从视图中引用变量或函数以获取工作表内容(如或)时,此工作表都会导致视图模型泄漏.sheet(isPresented: $isPresented, content: { variable }).sheet(isPresented: $isPresented, content: sheetContent)
1赞 Michał Ziobro 10/3/2023
它对我工作的唯一方法是直接在工作表内容闭包中定义 SheetView(),例如 .但只要你不在那里传递任何变量或绑定,它就可以工作。此 SheetView 再次导致内存泄漏.sheet(isPresented: $isPresented, content: { SheetView() })SheetView(variable: variable, flag: $flag)
0赞 CouchDeveloper 10/3/2023
Michał,请向 Apple 报告错误,以便引起高度关注。;)
1赞 Michał Ziobro 10/4/2023
是的,我昨天已经报道过了。我们不能使用新的 xcode,entrie 应用程序有很多内存泄漏:(

答:

-2赞 malhal 9/29/2023 #1

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

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

如果要创建要编辑的临时模型,则只需使用或使用带有 a 作为变量的自定义状态结构即可。@State var editingModel = Model()EditorModel

对于您想对按钮执行的操作,我认为您的模型结构将需要一个字符串作为选项名称,例如“One”和 .如果您的选项名称是唯一的,它甚至可以是可识别的 ID,例如var bool isSelectedvar id: String { optionName }

评论

0赞 CouchDeveloper 10/1/2023
当用于不符合 Observable 的模型存储时,或者用于应用了宏的模型存储时,或者仅使用常规类实例(宏 Observable 或非宏)时,它将在 iOS 17 上运行时泄漏,从您拥有模型存储的视图中显示工作表。这是 iOS 17 中的一个错误。它将始终使用常规类实例(将在 iOS 16 中解除分配)泄漏。@State@State@Observable
0赞 malhal 10/2/2023
最好坚持 SwiftUI 的结构,显然有一些主要的设计缺陷。Observable
0赞 Michał Ziobro 10/3/2023
@CouchDeveloper 是的,我确定这是 iOS 17 中引入的错误。我们有典型的。在 swiftui 中构建(通常不使用 EnvironmentObjects),但 View + ViewModel(苹果将其命名为 model 或 modelstore)并使用 @Published @StateObject @ObservedObject'。由于 iOS 17 工作表会导致视图模型 (ObservableObject) 引用的表单父视图(显示工作表)上的备忘录泄漏。通常你不会看到巨大的副作用,但在一部分中,内存泄漏破坏了应用程序中的完整业务逻辑。 malhal 你的答案是错误的,并没有解决苹果中备忘录泄漏的问题。@State
0赞 Michał Ziobro 10/3/2023
@malhal 在 Update 3 中,我添加了将方法用于环境对象的代码,这也会导致内存泄漏。
0赞 malhal 10/3/2023
@Micha łZiobro update 3 不太对劲,它必须像.environmentObject(ModelStore.shared)