SwiftUI:ObservableObject 不会在重绘时保留其状态
SwiftUI: ObservableObject does not persist its State over being redrawn
问题
为了实现应用代码的简洁外观和感觉,我为每个包含逻辑的视图创建了 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
设置一个 class(如 ViewModel),越来越多的东西不会按预期工作。
问题
- 有没有办法不用每次都重新创建 ViewModel?
- 有没有办法为
@ObservedObject
复制 @State
属性 包装器?
- 为什么@State 在重绘时保持状态?
我知道通常情况下,在内部视图中创建 ViewModel 是不好的做法,但可以使用 NavigationLink 或 Sheet.
复制此行为
有时,当您想到一个非常复杂的 TableView,其中 Cells 本身包含很多逻辑时,将 State 保留在 ParentsViewModel 中并使用绑定是没有用的。
对于个别情况总是有解决方法,但我认为如果不重新创建 ViewModel 会更容易。
重复问题
我知道有很多关于这个问题的问题,都在谈论非常具体的用例。在这里我想谈谈普遍的问题,而不是太深入自定义解决方案。
编辑(添加更详细的示例)
当有一个状态改变的父视图时,比如来自数据库的列表,API,或缓存(想一些简单的事情)。通过 NavigationLink
您可能会到达一个详细信息页面,您可以在其中修改数据。通过更改数据,reactive/declarative 模式会告诉我们还要更新 ListView,然后会 "redraw" NavigationLink
,然后会导致重新创建 ViewModel。
我知道我可以将 ViewModel 存储在 ParentView / ParentView 的 ViewModel 中,但在我看来这是错误的做法。由于订阅被销毁 and/or 重新创建 - 可能会有一些副作用。
Is there a way of not recreating the ViewModel every time?
是的,将 ViewModel 实例 放在 SomeView
的 之外,并通过构造函数注入
struct SomeView: View {
@ObservedObject var viewModel: SomeViewModel // << only declaration
Is there a way of replicating the @State Propertywrapper for @ObservedObject?
不需要。 @ObservedObject
已经是 DynamicProperty
类似于 @State
Why is @State keeping the State over the redraw?
因为它保留了它的存储空间,即。包装值,在视图 之外。 (所以,再看上面的第一个)
您需要在 ObservableObject
class 中提供自定义 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
将 .text
的新值传递给 CustomState
视图中的 .onReceive(...)
。请注意,在这种情况下 onReceive
得到 PassthroughSubject
,而不是 ObservableObjectPublisher
。在最后一种情况下,您将只有 Publisher.Output
in perform: closure
,而不是 NewValue。 state.text
在那种情况下会有旧值。
其次,看ComplexState
class。我做了一个 objectWillChange
属性 来让文本更改手动向订阅者发送通知。它几乎和 @Published
wrapper 一样。但是,当文本更改时,它会同时发送 objectWillChange.send()
和 textChanged.send(newValue)
。这使您能够准确选择 View
,如何对状态变化做出反应。如果您想要普通行为,只需将状态放入 CustomStateContainer
视图中的 @ObservedObject
包装器。然后,您将重新创建所有视图,并且此部分也将获得更新的值:
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。
这是你需要的吗?
我同意你的看法,我认为这是 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。
最后Apple提供了一个解决方案:@StateObject
。
通过将 @ObservedObject
替换为 @StateObject
,我最初 post 中提到的所有内容都有效。
不幸的是,这仅适用于 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
正在重置。
我的解决方案是使用 EnvironmentObject 而不要在视图中使用 ObservedObject,它的 viewModel 将被重置,你通过层次结构
.environmentObject(viewModel)
只需在某个地方初始化 viewModel,它不会被重置(例如根视图)。
问题
为了实现应用代码的简洁外观和感觉,我为每个包含逻辑的视图创建了 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
设置一个 class(如 ViewModel),越来越多的东西不会按预期工作。
问题
- 有没有办法不用每次都重新创建 ViewModel?
- 有没有办法为
@ObservedObject
复制@State
属性 包装器? - 为什么@State 在重绘时保持状态?
我知道通常情况下,在内部视图中创建 ViewModel 是不好的做法,但可以使用 NavigationLink 或 Sheet.
复制此行为
有时,当您想到一个非常复杂的 TableView,其中 Cells 本身包含很多逻辑时,将 State 保留在 ParentsViewModel 中并使用绑定是没有用的。
对于个别情况总是有解决方法,但我认为如果不重新创建 ViewModel 会更容易。
重复问题
我知道有很多关于这个问题的问题,都在谈论非常具体的用例。在这里我想谈谈普遍的问题,而不是太深入自定义解决方案。
编辑(添加更详细的示例)
当有一个状态改变的父视图时,比如来自数据库的列表,API,或缓存(想一些简单的事情)。通过 NavigationLink
您可能会到达一个详细信息页面,您可以在其中修改数据。通过更改数据,reactive/declarative 模式会告诉我们还要更新 ListView,然后会 "redraw" NavigationLink
,然后会导致重新创建 ViewModel。
我知道我可以将 ViewModel 存储在 ParentView / ParentView 的 ViewModel 中,但在我看来这是错误的做法。由于订阅被销毁 and/or 重新创建 - 可能会有一些副作用。
Is there a way of not recreating the ViewModel every time?
是的,将 ViewModel 实例 放在 SomeView
的 之外,并通过构造函数注入
struct SomeView: View {
@ObservedObject var viewModel: SomeViewModel // << only declaration
Is there a way of replicating the @State Propertywrapper for @ObservedObject?
不需要。 @ObservedObject
已经是 DynamicProperty
类似于 @State
Why is @State keeping the State over the redraw?
因为它保留了它的存储空间,即。包装值,在视图 之外。 (所以,再看上面的第一个)
您需要在 ObservableObject
class 中提供自定义 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
将 .text
的新值传递给 CustomState
视图中的 .onReceive(...)
。请注意,在这种情况下 onReceive
得到 PassthroughSubject
,而不是 ObservableObjectPublisher
。在最后一种情况下,您将只有 Publisher.Output
in perform: closure
,而不是 NewValue。 state.text
在那种情况下会有旧值。
其次,看ComplexState
class。我做了一个 objectWillChange
属性 来让文本更改手动向订阅者发送通知。它几乎和 @Published
wrapper 一样。但是,当文本更改时,它会同时发送 objectWillChange.send()
和 textChanged.send(newValue)
。这使您能够准确选择 View
,如何对状态变化做出反应。如果您想要普通行为,只需将状态放入 CustomStateContainer
视图中的 @ObservedObject
包装器。然后,您将重新创建所有视图,并且此部分也将获得更新的值:
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。
这是你需要的吗?
我同意你的看法,我认为这是 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。
最后Apple提供了一个解决方案:@StateObject
。
通过将 @ObservedObject
替换为 @StateObject
,我最初 post 中提到的所有内容都有效。
不幸的是,这仅适用于 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
正在重置。
我的解决方案是使用 EnvironmentObject 而不要在视图中使用 ObservedObject,它的 viewModel 将被重置,你通过层次结构
.environmentObject(viewModel)
只需在某个地方初始化 viewModel,它不会被重置(例如根视图)。