@Published и .assign не реагируют на обновление значения - PullRequest
1 голос
/ 13 марта 2020

SwiftUI и Combine Noob здесь, я изолировал на детской площадке проблему, с которой я столкнулся. Вот игровая площадка.

final class ReactiveContainer<T: Equatable> {
    @Published var containedValue: T?
}

class AppContainer {
    static let shared = AppContainer()

    let text = ReactiveContainer<String>()
}

struct TestSwiftUIView: View {

    @State private var viewModel = "test"

    var body: some View {
        Text("\(viewModel)")
    }

    init(textContainer: ReactiveContainer<String>) {

        textContainer.$containedValue.compactMap {
            print("compact map \($0)")
            return $0
        }.assign(to: \.viewModel, on: self)
    }
}

AppContainer.shared.text.containedValue = "init"


var testView = TestSwiftUIView(textContainer: AppContainer.shared.text)
print(testView)

print("Executing network request")
DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
    AppContainer.shared.text.containedValue = "Hello world"
    print(testView)
}

Когда я запускаю игровую площадку, вот что происходит:

compact map Optional("init")
TestSwiftUIView(_viewModel: SwiftUI.State<Swift.String>(_value: "test", _location: nil))
Executing network request
TestSwiftUIView(_viewModel: SwiftUI.State<Swift.String>(_value: "test", _location: nil))

Итак, как вы видите, здесь есть две проблемы:

  • Закрытие компактной карты вызывается только один раз, при подписке, но не при выполнении отправки

  • Оператор присвоения никогда не вызывается

В последние несколько часов я пытался решить эту проблему безуспешно. Может быть, кто-то с высокими знаниями в SwiftUI / Combine может помочь мне, спасибо!

РЕДАКТИРОВАТЬ

Вот рабочее решение:

struct ContentView: View {

    @State private var viewModel = "test"
    let textContainer: ReactiveContainer<String>

    var body: some View {
        Text(viewModel).onReceive(textContainer.$containedValue) { (newContainedValue) in
            self.viewModel = newContainedValue ?? ""
        }
    }

    init(textContainer: ReactiveContainer<String>) {
        self.textContainer = textContainer
    }
}

Ответы [ 2 ]

1 голос
/ 13 марта 2020

Я бы предпочел использовать шаблон ObservableObject/ObservedObject справа внизу, но возможны и другие варианты (как указано далее)

Все тестируются с Xcode 11.2 / iOS 13.2

final class ReactiveContainer<T: Equatable>: ObservableObject {
    @Published var containedValue: T?
}

struct TestSwiftUIView: View {

    @ObservedObject var vm: ReactiveContainer<String>

    var body: some View {
        Text("\(vm.containedValue ?? "<none>")")
    }

    init(textContainer: ReactiveContainer<String>) {
        self._vm = ObservedObject(initialValue: textContainer)
    }
}

Альтернативы:

Следующее исправляет ваш случай (если вы не сохраняете подписчика, издатель немедленно отменяется)

private var subscriber: AnyCancellable?
init(textContainer: ReactiveContainer<String>) {

    subscriber = textContainer.$containedValue.compactMap {
        print("compact map \($0)")
        return $0
    }.assign(to: \.viewModel, on: self)
}

Обратите внимание, что состояние представления связано только в иерархии представления, в Playground, как и у вас, он содержит только начальное значение.

Другой возможный подход, который лучше подходит для иерархии SwiftUI, -

struct TestSwiftUIView: View {

    @State private var viewModel: String = "test"

    var body: some View {
        Text("\(viewModel)")
            .onReceive(publisher) { value in
                self.viewModel = value
            }
    }

    let publisher: AnyPublisher<String, Never>
    init(textContainer: ReactiveContainer<String>) {

        publisher = textContainer.$containedValue.compactMap {
            print("compact map \($0)")
            return $0
        }.eraseToAnyPublisher()
    }
}
0 голосов
/ 13 марта 2020

Я бы сохранил ссылку на AppContainer.

struct TestSwiftUIView: View {

    @State private var viewModel = "test"

    ///I just added this
    var textContainer: AnyCancellable?

    var body: some View {
        Text("\(viewModel)")
    }

    init(textContainer: ReactiveContainer<String>) {

        self.textContainer = textContainer.$containedValue.compactMap {
            print("compact map \(String(describing: $0))")
            return $0
        }.assign(to: \.viewModel, on: self)
    }
}

compact map Optional("init")
TestSwiftUIView(_viewModel: SwiftUI.State<Swift.String>(_value: "test", _location: nil), textContainer: Optional(Combine.AnyCancellable))
Executing network request
compact map Optional("Hello")
TestSwiftUIView(_viewModel: SwiftUI.State<Swift.String>(_value: "test", _location: nil), textContainer: Optional(Combine.AnyCancellable))

...