为了获得App代码的清晰外观,我为包含逻辑的每个View创建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,并且此后不保存State。与其他框架(例如Flutter)进行比较时,这是不寻常的。
我认为ViewModel应该保留,或者State应该保留。
如果我使用@State
属性替换ViewModel并直接使用int
(在此示例中),它将保持不变,并且不会重新创建:
struct SomeView: View {
@State var state = 1
var body: some View {
// Code to read and write the State goes here
}
}
这显然不适用于更复杂的国家。而且,如果我为@State
设置了一个类(例如ViewModel),那么越来越多的事情将无法正常工作。
@State
复制@ObservedObject
Propertywrapper?我知道通常在内部View中创建ViewModel是一种不好的做法,但是可以通过使用NavigationLink或Sheet复制此行为。
有时候,当您想到一个非常复杂的TableView时,将State保留在ParentsViewModel中并使用绑定是没有用的,其中Cells本身包含很多逻辑。
对于个别情况总会有一种解决方法,但是我认为如果不重新创建ViewModel会更容易。
我知道有很多关于这个问题的问题,都在讨论非常特定的用例。在这里,我想谈一谈一般性问题,而不必深入探讨自定义解决方案。
具有状态更改的ParentView时,例如来自数据库,API或缓存的列表(考虑一些简单的事情)。通过NavigationLink
,您可能会到达详细信息页面,您可以在其中修改数据。通过更改数据,反应性/声明性模式将告诉我们也更新ListView,然后“重绘” NavigationLink
,然后重新创建ViewModel。
我知道我可以将ViewModel存储在ParentView / ParentView的ViewModel中,但这是IMO的错误方法。而且由于订阅被销毁和/或重新创建-可能会有一些副作用。
答案 0 :(得分:6)
最后,Apple提供了一个解决方案:@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
时,在重绘父视图时保持其值。
答案 1 :(得分: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。
答案 2 :(得分:0)
有没有一种方法不能每次都重新创建ViewModel?
是的,将ViewModel实例保留在SomeView
的外部中,并通过构造函数进行注入
struct SomeView: View {
@ObservedObject var viewModel: SomeViewModel // << only declaration
是否可以为@ObservedObject复制@State Propertywrapper?
不需要。 @ObservedObject
是-已经DynamicProperty
,类似于@State
为什么@State保留重绘的状态?
因为它保持其存储,即。包装的值,外部视图。 (因此,请再次参见上文)
答案 3 :(得分:0)
您需要在PassThroughSubject
类中提供自定义ObservableObject
。看这段代码:
//
// 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
的新值传递给.onReceive(...)
视图中的CustomState
。请注意,在这种情况下,onReceive
得到PassthroughSubject
,而不是ObservableObjectPublisher
。在最后一种情况下,您在Publisher.Output
中将只有perform: closure
,而不是NewValue。在这种情况下,state.text
将具有旧的价值。
第二,看一下ComplexState
类。我做了一个objectWillChange
属性,以使文本更改手动将通知发送给订阅者。它几乎与@Published
包装器一样。但是,当文本更改时,它将同时发送objectWillChange.send()
和textChanged.send(newValue)
。这使您能够精确地选择View
,以应对状态变化。如果您想要普通的行为,只需将状态放入@ObservedObject
视图中的CustomStateContainer
包装器中即可。然后,您将重新创建所有视图,本节也将获取更新的值:
HStack{
Text("ordinary Text View: ")
Text(state.text)
}
如果您不希望全部重新创建,只需删除@ObservedObject。普通文本视图将停止更新,但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。
那是您需要的吗?