@Published 和 .assign 对值更新没有反应
@Published and .assign not reacting to value update
这里是 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 \([=10=])")
return [=10=]
}.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 方面具有最高知识的人可以帮助我,thx !
编辑
这是可行的解决方案:
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
}
}
我更愿意使用下面的 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 \([=11=])")
return [=11=]
}.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 \([=12=])")
return [=12=]
}.eraseToAnyPublisher()
}
}
我会保存对 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: [=10=]))")
return [=10=]
}.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))
我们不使用 Combine 在视图之间移动数据,SwiftUI 已经内置了对此的支持。主要问题是您将 TestSwiftUIView
视为 class 但它是一个结构,即一个值。最好将视图简单地视为要显示的数据。当数据发生变化时,SwiftUI 会一遍又一遍地创建这些数据结构。所以解决方案很简单:
struct ContentView: View {
let text: String
var body: some View { // only called if text is different from last time ContentView was created in a parent View's body.
Text(text)
}
}
父体方法可以一遍又一遍地调用ContentView(text:"Test")
,但是ContentView
体方法只有在let text
与上次不同时才会被SwiftUI调用,例如ContentView(text:"Test2")
。我认为这是您尝试使用 Combine 重新创建的内容,但这是不必要的,因为 SwiftUI 已经做到了。
这里是 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 \([=10=])")
return [=10=]
}.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 方面具有最高知识的人可以帮助我,thx !
编辑
这是可行的解决方案:
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
}
}
我更愿意使用下面的 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 \([=11=])")
return [=11=]
}.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 \([=12=])")
return [=12=]
}.eraseToAnyPublisher()
}
}
我会保存对 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: [=10=]))")
return [=10=]
}.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))
我们不使用 Combine 在视图之间移动数据,SwiftUI 已经内置了对此的支持。主要问题是您将 TestSwiftUIView
视为 class 但它是一个结构,即一个值。最好将视图简单地视为要显示的数据。当数据发生变化时,SwiftUI 会一遍又一遍地创建这些数据结构。所以解决方案很简单:
struct ContentView: View {
let text: String
var body: some View { // only called if text is different from last time ContentView was created in a parent View's body.
Text(text)
}
}
父体方法可以一遍又一遍地调用ContentView(text:"Test")
,但是ContentView
体方法只有在let text
与上次不同时才会被SwiftUI调用,例如ContentView(text:"Test2")
。我认为这是您尝试使用 Combine 重新创建的内容,但这是不必要的,因为 SwiftUI 已经做到了。