提问人:Michał Ziobro 提问时间:9/28/2023 最后编辑:Michał Ziobro 更新时间:10/3/2023 访问量:131
从子视图到模型属性的弱绑定
Weak binding to model properties from subview
问:
使用绑定时,我在 SwiftUI 视图的模型中遇到了内存泄漏。
- 我创建了 ObservableObject 模型,例如
final class Model: ObservableObject {
@Published var selectedValue: String?
}
- 我使用这个模型创建了 ContentView
struct ContentView: View {
@StateObject private var model = Model()
var body: some View {
SelectButton(selection: $model.selectedValue)
}
}
- 我以这种方式实现了 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")
}
}
}
答:
你已经很接近了,但你需要解决一些问题:
该对象通常称为 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()
Editor
Model
对于您想对按钮执行的操作,我认为您的模型结构将需要一个字符串作为选项名称,例如“One”和 .如果您的选项名称是唯一的,它甚至可以是可识别的 ID,例如var bool isSelected
var id: String { optionName }
评论
@State
@State
@Observable
Observable
@State
.environmentObject(ModelStore.shared)
评论
@StateObject
.sheet(isPresented: $isPresented, content: { variable })
.sheet(isPresented: $isPresented, content: sheetContent)
.sheet(isPresented: $isPresented, content: { SheetView() })
SheetView(variable: variable, flag: $flag)