SwiftUI: ObservableObject не сохраняет свое состояние при перерисовке - PullRequest
7 голосов
/ 08 мая 2020

Проблема

Чтобы добиться чистого внешнего вида кода приложения, я создаю ViewModels для каждого View, содержащего logi c.

Обычная 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 не обновляется. Если состояние родителя изменяется, это представление перерисовывается (это нормально для декларативной платформы). Но также воссоздается 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), все больше и больше вещей не работает должным образом.

Question

  • Есть ли способ не воссоздавать ViewModel каждый раз?
  • Есть ли способ репликации @State Propertywrapper для @ObservedObject?
  • Почему @State сохраняет состояние при перерисовке?

Я знаю, что обычно создание ViewModel во внутреннем представлении - плохая практика, но это поведение можно воспроизвести с помощью NavigationLink или Sheet.
Иногда тогда просто нецелесообразно сохранять State в ParentsViewModel и работать с привязками, когда вы думаете об очень сложном TableView, где сами ячейки содержат много logi c.
В отдельных случаях всегда есть обходной путь, но я думаю, что было бы намного проще, если бы ViewModel не был воссоздан.

Повторяющийся вопрос

Я знаю, что есть много вопросов, касающихся этой проблемы, и все они говорят об очень конкретных * 1 058 * варианты использования. Здесь я хочу поговорить об общей проблеме, не вдаваясь слишком глубоко в индивидуальные решения.

Edit (добавление более подробного примера)

При наличии ParentView, изменяющего состояние, например, список из База данных, API или кеш (подумайте о чем-нибудь простом). Через NavigationLink вы можете попасть на страницу сведений, где можно изменить данные. Изменяя данные, реактивный / декларативный шаблон подсказывал нам также обновить ListView, который затем «перерисовал» NavigationLink, что затем привело бы к воссозданию ViewModel.

Я знаю, что могу сохранить ViewModel в ViewModel ParentView / ParentView, но это неправильный способ сделать это IMO. А поскольку подписки уничтожаются и / или создаются заново - могут быть некоторые побочные эффекты.

Ответы [ 4 ]

2 голосов
/ 23 июня 2020

Наконец, есть Решение, предоставленное Apple: @StateObject.

Заменив @ObservedObject на @StateObject, все, что упомянуто в моем первоначальном сообщении, работает.

К сожалению, это доступен только в ios 14 +.

Это мой код из Xcode 12 Beta (опубликовано 23 июня 2020 г.)

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 голос
/ 25 мая 2020

Я согласен с вами, я думаю, что это одна из многих основных проблем 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 во время реконструкции.

0 голосов
/ 08 мая 2020

Вам необходимо указать индивидуальный 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 View. Обратите внимание, что onReceive в этом случае получает PassthroughSubject, а не ObservableObjectPublisher. В последнем случае у вас будет только Publisher.Output в perform: closure, а не NewValue. state.text в этом случае будет иметь старое значение.

Во-вторых, посмотрите на класс ComplexState. Я сделал свойство objectWillChange, чтобы текстовые изменения отправляли уведомление подписчикам вручную. Это почти то же самое, что и обертка @Published. Но при изменении текста он отправит как objectWillChange.send(), так и textChanged.send(newValue). Это дает вам возможность выбрать с точностью View, как реагировать на изменение состояния. Если вам нужно обычное поведение, просто поместите состояние в оболочку @ObservedObject в CustomStateContainer View. Затем у вас будут воссозданы все представления, и этот раздел также получит обновленные значения:

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.

Это то, что вам нужно?

0 голосов
/ 08 мая 2020

Есть ли способ не воссоздавать ViewModel каждый раз?

Да, сохранить экземпляр ViewModel за пределами из SomeView и вводить через конструктор

struct SomeView: View {
    @ObservedObject var viewModel: SomeViewModel  // << only declaration

Есть ли способ репликации @State Propertywrapper для @ObservedObject?

Нет необходимости. @ObservedObject is-a уже DynamicProperty аналогично @State

Почему @State сохраняет состояние при перерисовке?

Потому что сохраняет свое хранилище, ie. обернутое значение, за пределами обзора. (так что снова см. первое выше)

...