SwiftUI:ObservableObject 在重绘时不会保持其状态
Posted
技术标签:
【中文标题】SwiftUI:ObservableObject 在重绘时不会保持其状态【英文标题】:SwiftUI: ObservableObject does not persist its State over being redrawn 【发布时间】:2020-05-08 10:15:13 【问题描述】:问题
为了实现应用代码的简洁外观,我为每个包含逻辑的视图创建了 ViewModel。
一个普通的 ViewModel 看起来有点像这样:
class SomeViewModel: ObservableObject
@Published var state = 1
// Logic and calls of Business Logic goes here
并像这样使用:
struct SomeView: View
@ObservedObject var viewModel = SomeViewModel()
var body: some View
// Code to read and write the State goes here
当 Views Parent 没有更新时,这可以正常工作。如果父级的状态发生变化,这个视图会被重绘(在声明性框架中很正常)。 但是 ViewModel 也会被重新创建并且之后不会保持状态。与其他框架(例如:Flutter)相比,这是不寻常的。
在我看来,ViewModel 应该保留,或者 State 应该保留。
如果我将 ViewModel 替换为 @State
属性并直接使用 int
(在此示例中),它将保持持久状态并且不会重新创建:
struct SomeView: View
@State var state = 1
var body: some View
// Code to read and write the State goes here
这显然不适用于更复杂的状态。而且,如果我为@State
设置一个类(如 ViewModel),就会有越来越多的事情无法按预期工作。
问题
有没有办法不每次都重新创建 ViewModel? 有没有办法为@ObservedObject
复制@State
Propertywrapper?
为什么@State 在重绘时保留 State?
我知道通常在内部视图中创建 ViewModel 是不好的做法,但可以通过使用 NavigationLink 或 Sheet 来复制这种行为。 有时,当您想到一个非常复杂的 TableView 时,将 State 保留在 ParentsViewModel 中并使用绑定是没有用的,其中 Cell 本身包含很多逻辑。 对于个别情况,总有一种解决方法,但我认为如果不重新创建 ViewModel 会更容易。
重复问题
我知道有很多关于这个问题的问题,都在谈论非常具体的用例。在这里我想谈谈一般问题,而不是太深入地讨论自定义解决方案。
编辑(添加更详细的示例)
当拥有一个改变状态的 ParentView 时,比如来自数据库、API 或缓存的列表(想想一些简单的事情)。通过NavigationLink
,您可能会到达一个详细信息页面,您可以在其中修改数据。通过更改数据,反应式/声明式模式会告诉我们也更新 ListView,然后“重绘”NavigationLink
,这将导致重新创建 ViewModel。
我知道我可以将 ViewModel 存储在 ParentView / ParentView 的 ViewModel 中,但这是 IMO 错误的做法。而且由于订阅被销毁和/或重新创建 - 可能会有一些副作用。
【问题讨论】:
您能找到解决方案吗?我最终创建了一个 environmentObject,因此它只有一个视图模型实例。 【参考方案1】:终于有苹果提供的解决方案:@StateObject
。
通过将@ObservedObject
替换为@StateObject
,我最初的帖子中提到的一切都正常了。
很遗憾,这仅适用于 ios 14+。
这是我的 Xcode 12 Beta 代码(2020 年 6 月 23 日发布)
struct ContentView: View
@State var title = 0
var body: some View
NavigationView
VStack
Button("Test")
self.title = Int.random(in: 0...1000)
TestView1()
TestView2()
.navigationTitle("\(self.title)")
struct TestView1: View
@ObservedObject var model = ViewModel()
var body: some View
VStack
Button("Test1: \(self.model.title)")
self.model.title += 1
class ViewModel: ObservableObject
@Published var title = 0
struct TestView2: View
@StateObject var model = ViewModel()
var body: some View
VStack
Button("StateObject: \(self.model.title)")
self.model.title += 1
如您所见,StateObject
在重绘父视图时保持其值,而 ObservedObject
正在重置。
【讨论】:
您好!我想知道您是否找到适用于 iOS 13 的任何解决方案?我真的需要解决这个问题,但我尝试的任何方法都没有奏效。 @AbdulelahHajjar,我认为唯一的方法是在视图层次结构中创建视图模型。在某些时候可能会很棘手。或者更新到14+,这将是最简单的解决方案 感谢@KonDeichmann 的回复,您能否详细说明“在视图层次结构中向下创建 ViewModel”的含义?非常感谢您的宝贵时间。 @AbdulelahHajjar,已经在父视图(或更改状态的视图)中创建了 ViewModel。当您必须将数据传递到该 ViewModel 时,这有时不起作用。我可能会建议更新您的部署目标。 如果StateObject
在其init
中使用参数,例如:MyViewModel(status: 2)
,假设我们必须从视图外部创建此视图模型,我怎样才能使视图保留其在这种情况下状态?与 ObservableObjects
相比,Apple 不鼓励从视图外部传递 StateObject
,但它们不会保留视图重绘时的状态。【参考方案2】:
我同意你的观点,我认为这是 SwiftUI 的许多主要问题之一。这就是我发现自己在做的事情,虽然很恶心。
struct MyView: View
@State var viewModel = MyViewModel()
var body : some View
MyViewImpl(viewModel: viewModel)
fileprivate MyViewImpl : View
@ObservedObject var viewModel : MyViewModel
var body : some View
...
您可以就地构建视图模型或将其传入,它会为您提供一个视图,该视图将在重建过程中维护您的 ObservableObject。
【讨论】:
嘿,你完全正确。我想我在我的问题中也提到了这个解决方案。问题是MyView
的父视图可能会被重绘,我们最终会遇到同样的问题。但实际上我目前以这种方式实施解决方案。但作为一种非常通用的方法,也存在问题。
@KonDeichmann,我认为这不应该是真的。如果修改了父结构,则必须复制/重新创建所有子结构,但其中的任何@State
都应保存在单独的存储中,并在子视图结构 init() 之后恢复。这个“包装器”模式相当于写@State @ObservedObject var viewModel : MyViewModel
。它可以防止您的视图模型在重绘时丢失。父级仅引用MyView
,这是一个维护MyViewImpl
状态的包装器。【参考方案3】:
有没有办法不每次都重新创建 ViewModel?
是的,保持 ViewModel 实例 在SomeView
之外 并通过构造函数注入
struct SomeView: View
@ObservedObject var viewModel: SomeViewModel // << only declaration
有没有办法为@ObservedObject 复制@State Propertywrapper?
不需要。 @ObservedObject
已经是DynamicProperty
类似于@State
为什么@State 在重绘时保持状态?
因为它保留了它的存储空间,即。包装值,视图之外。 (所以,再次参见上面的第一个)
【讨论】:
有时您不需要在每次发生变化时重绘(初始化)整个视图。在我的例子中,init 中有大量的形状计算,我不想在每次发生变化时都执行它。特别是当你有“状态”单例时。您更改了一个属性 - 所有视图都取决于重绘的状态。即使它们依赖于另一个属性。我认为主题启动器处理类似问题。 将状态保留在视图之外确实是一种解决方案,但在某些情况下,它是不切实际的!考虑一个复杂的详细信息页面,您可以在其中更改对象,然后更新父视图上的项目列表(状态更改)。此外,在这种情况下,'@State' 属性保持状态,'@ObservedObject' 不会。 @KonDeichmann,它确实 - 被广泛使用,并且在这里......此外,两者的组合解决方案也是可能的。 但是组合解决方案(@State 和 @ObservedObject 的组合)无法在重绘时保留订阅。当然我可以在视图中“存储”某个状态(这不是一个好习惯,当状态应该隐藏在视图模型中时),但绝对不是订阅。 您好!遗憾的是,我尝试了这个解决方案,但没有成功。我想知道我是否做错了什么。我在视图中有声明,并且我正在从父视图的 View 调用中传递 ViewModel。【参考方案4】:您需要在 ObservableObject
类中提供自定义 PassThroughSubject
。看这段代码:
//
// Created by Франчук Андрей on 08.05.2020.
// Copyright © 2020 Франчук Андрей. All rights reserved.
//
import SwiftUI
import Combine
struct TextChanger
var textChanged = PassthroughSubject<String,Never>()
public func changeText(newValue: String)
textChanged.send(newValue)
class ComplexState: ObservableObject
var objectWillChange = ObservableObjectPublisher()
let textChangeListener = TextChanger()
var text: String = ""
willSet
objectWillChange.send()
self.textChangeListener.changeText(newValue: newValue)
struct CustomState: View
@State private var text: String = ""
let textChangeListener: TextChanger
init(textChangeListener: TextChanger)
self.textChangeListener = textChangeListener
print("did init")
var body: some View
Text(text)
.onReceive(textChangeListener.textChanged)newValue in
self.text = newValue
struct CustomStateContainer: View
//@ObservedObject var state = ComplexState()
var state = ComplexState()
var body: some View
VStack
HStack
Text("custom state View: ")
CustomState(textChangeListener: state.textChangeListener)
HStack
Text("ordinary Text View: ")
Text(state.text)
HStack
Text("text input: ")
TextInput().environmentObject(state)
struct TextInput: View
@EnvironmentObject var state: ComplexState
var body: some View
TextField("input", text: $state.text)
struct CustomState_Previews: PreviewProvider
static var previews: some View
return CustomStateContainer()
首先,我使用TextChanger
在CustomState
视图中将.text
的新值传递给.onReceive(...)
。请注意,在这种情况下,onReceive
得到PassthroughSubject
,而不是ObservableObjectPublisher
。在最后一种情况下,perform: closure
中只有 Publisher.Output
,而不是 NewValue。 state.text
在这种情况下将具有旧值。
其次,查看ComplexState
类。我创建了一个objectWillChange
属性,以使文本更改手动向订阅者发送通知。它几乎和@Published
wrapper 一样。但是,当文本更改时,它将同时发送objectWillChange.send()
和textChanged.send(newValue)
。这使您能够准确地选择View
,如何对状态变化做出反应。如果您想要普通行为,只需将状态放入 @ObservedObject
包装器中的 CustomStateContainer
视图中。然后,您将重新创建所有视图,并且此部分也将获得更新的值:
HStack
Text("ordinary Text View: ")
Text(state.text)
如果您不想重新创建所有这些,只需删除 @ObservedObject。普通文本 View 会停止更新,但 CustomState 会。无需重新创建。
更新: 如果您想要更多控制权,您可以在更改值时决定您希望通知谁来了解该更改。 查看更复杂的代码:
//
//
// Created by Франчук Андрей on 08.05.2020.
// Copyright © 2020 Франчук Андрей. All rights reserved.
//
import SwiftUI
import Combine
struct TextChanger
// var objectWillChange: ObservableObjectPublisher
// @Published
var textChanged = PassthroughSubject<String,Never>()
public func changeText(newValue: String)
textChanged.send(newValue)
class ComplexState: ObservableObject
var onlyPassthroughSend = false
var objectWillChange = ObservableObjectPublisher()
let textChangeListener = TextChanger()
var text: String = ""
willSet
if !onlyPassthroughSend
objectWillChange.send()
self.textChangeListener.changeText(newValue: newValue)
struct CustomState: View
@State private var text: String = ""
let textChangeListener: TextChanger
init(textChangeListener: TextChanger)
self.textChangeListener = textChangeListener
print("did init")
var body: some View
Text(text)
.onReceive(textChangeListener.textChanged)newValue in
self.text = newValue
struct CustomStateContainer: View
//var state = ComplexState()
@ObservedObject var state = ComplexState()
var body: some View
VStack
HStack
Text("custom state View: ")
CustomState(textChangeListener: state.textChangeListener)
HStack
Text("ordinary Text View: ")
Text(state.text)
HStack
Text("text input with full state update: ")
TextInput().environmentObject(state)
HStack
Text("text input with no full state update: ")
TextInputNoUpdate().environmentObject(state)
struct TextInputNoUpdate: View
@EnvironmentObject var state: ComplexState
var body: some View
TextField("input", text: Binding( get: self.state.text,
set: newValue in
self.state.onlyPassthroughSend.toggle()
self.state.text = newValue
self.state.onlyPassthroughSend.toggle()
))
struct TextInput: View
@State private var text: String = ""
@EnvironmentObject var state: ComplexState
var body: some View
TextField("input", text: Binding(
get: self.text,
set: newValue in
self.state.text = newValue
// self.text = newValue
))
.onAppear()
self.text = self.state.text
.onReceive(state.textChangeListener.textChanged)newValue in
self.text = newValue
struct CustomState_Previews: PreviewProvider
static var previews: some View
return CustomStateContainer()
我做了一个手动绑定来停止广播 objectWillChange。但是您仍然需要在更改此值以保持同步的所有地方获取新值。这也是我修改 TextInput 的原因。
这是你需要的吗?
【讨论】:
一般来说,我认为@State
是供Views内部使用的。 @ObservedObject
用于在视图之间更改数据。因此ObservableObject
仅适用于类——它们是可以在其他视图中使用的链接。就像您将它们放在init()
中一样作为输入参数。你需要做什么,你不确切知道什么,发生了什么变化?重绘批发View
。
我认为我们谈论的不是同一件事。我知道@State
和@ObservedObject
的用途。当我正确理解您时,您只是尝试使用 ObservableObjects 重建@State
。我看到的问题是保持订阅和状态超过父视图的重绘 in 和ObservableObject
!您可能会明白我的意思,如果您使用“父视图”构建一个非常简单的应用程序,它会在按下按钮时更改其状态,并且子视图做同样的事情,将两个状态保持在ObservableObjects
(使用@Published)并将prints
放入init
和deinit
。
您的问题是关于使用@State
变量的默认值。当您在此视图之外更改某些内容(无论是@ObservedObject
中的另一个属性,还是@EnvironmentObject
或通过init()
传递的参数),它将被完全销毁并再次创建。因此,您的 @State
变量设置为其默认值。我向您展示了一种如何通过使用PassthroughSubject
发送更改来避免破坏视图的方法。 ——
请看这个要点:gist.github.com/konDeichmann/9b4e2e7947068ffd906966edddf1d093 尝试替换新创建的 SwiftUI 项目的 ContentView 并使用 3 个按钮。如您所见,@State
将保持其值,而 @ObservedObject
将始终在 ParentView 状态更改时重置。
看起来@State
在重新创建View
后能够恢复其值,而@ObservedObject
则不行。如果你把print()
放入CounterStateView
init()
你也会看到它被重绘,但是在初始化完成后,它会回复旧值。尝试创建状态视图的初始化: init() self._counter = State(initialValue: 1) print("init state (with value of (counter))") 以上是关于SwiftUI:ObservableObject 在重绘时不会保持其状态的主要内容,如果未能解决你的问题,请参考以下文章
SwiftUI/组合监听 ObservableObject 中的多个发布者
SwiftUI = ObservableObject 作为类的选择
SwiftUI 致命错误:未找到“”类型的 ObservableObject
在 SwiftUI 中将 ObservableObject 链接到 DatePicker