SwiftUI 视图上的 onReceive 方法不会为自定义发布者执行
onReceive method on SwiftUI view won’t execute for a custom publisher
在我的应用程序中,我使用 onChange()
方法和发布者来检测和通知可用的屏幕宽度变化。我还在我的视图模型中使用 innerWorkInProgress
发布的 属性 来控制视图的不透明度。视图的不透明度在屏幕宽度变化时设置为 0。一旦视图的内容被更新,它的不透明度在 onAppear()
方法中被设置回 1。
我的问题是,如果我在 onChange()
方法中将值设置为 innerWorkInProgress
,onReceive()
方法将不会执行。
如果我使用状态变量而不是已发布的 属性,onReceive()
方法效果很好。
我的代码有什么问题?
下面是一个简化的可重现示例。您可以通过将设备从纵向旋转到横向来测试它,反之亦然。
内容视图
import SwiftUI
struct ContentView: View {
@StateObject var viewModel: ViewModel = ViewModel()
var body: some View {
NavigationView {
VStack {
MyView(viewModel: viewModel)
Spacer()
Text("Another view")
}
.navigationBarTitle("Hey!", displayMode: .inline)
}
.navigationViewStyle(StackNavigationViewStyle())
}
}
我的视图
import SwiftUI
import Combine
struct MyView: View {
@ObservedObject var viewModel: ViewModel
@State private var initFlag = false
let widthChangeDetector: CurrentValueSubject<CGFloat, Never>
let widthChangePublisher: AnyPublisher<CGFloat, Never>
init(viewModel: ViewModel){
self.viewModel = viewModel
let wDetector = CurrentValueSubject<CGFloat, Never>(0)
self.widthChangePublisher = wDetector
.debounce(for: .seconds(0.1), scheduler: DispatchQueue.main)
.dropFirst()
.eraseToAnyPublisher()
self.widthChangeDetector = wDetector
}
var body: some View {
GeometryReader { geometry in
ScrollView {
// ForEach along with the viewModel.jIndex published property creates
// a new instance of Text view every time viewModel.jIndex is updated.
// In this way, we can make use of Text's onAppear on each instance newly created.
ForEach((viewModel.jIndex-1..<viewModel.jIndex), id: \.self) { i in
Text(viewModel.textData)
.id(viewModel.jIndex)
.opacity(viewModel.innerWorkInProgress ? 0 : 1)
.onAppear {
withAnimation(.easeInOut(duration: 0.3)) {
viewModel.innerWorkInProgress = false
}
}
.onChange(of: geometry.size.width) { newWidth in
if !initFlag {
initFlag = true
return
}
// Problem: this line prevents onReceive from executing
viewModel.innerWorkInProgress = true
widthChangeDetector.send(newWidth)
}
.onReceive(widthChangePublisher) { finalWidth in
print("onReceive, FINAL WIDTH = \(finalWidth)")
Task {
await viewModel.loadData()
}
}
}
}
.onAppear {
Task {
await viewModel.loadData()
}
}
}
}
}
视图模型
import Foundation
final class ViewModel: ObservableObject {
@Published var innerWorkInProgress = false
@Published private(set) var jIndex = 0
private(set) var textData = ""
func loadData() async {
innerWorkInProgress = true
let i = jIndex + 1
textData = "#\(i) Lorem ipsum dolor sit amet, consectetur adipiscing elit"
jIndex = i
}
}
这是 MyView 的替代版本,使用 onChangeEventInProgress
状态 属性 而不是已发布的 属性。这个版本没有任何问题。
import SwiftUI
import Combine
struct MyView: View {
@ObservedObject var viewModel: ViewModel
// a state property used instead of a published property
@State private var onChangeEventInProgress = false
@State private var initFlag = false
let widthChangeDetector: CurrentValueSubject<CGFloat, Never>
let widthChangePublisher: AnyPublisher<CGFloat, Never>
init(viewModel: ViewModel){
self.viewModel = viewModel
let wDetector = CurrentValueSubject<CGFloat, Never>(0)
self.widthChangePublisher = wDetector
.debounce(for: .seconds(0.1), scheduler: DispatchQueue.main)
.dropFirst()
.eraseToAnyPublisher()
self.widthChangeDetector = wDetector
}
var body: some View {
GeometryReader { geometry in
ScrollView {
ForEach((viewModel.jIndex-1..<viewModel.jIndex), id: \.self) { i in
Text(viewModel.textData)
.id(viewModel.jIndex)
.opacity(onChangeEventInProgress ? 0 : 1)
.onAppear {
withAnimation(.easeInOut(duration: 0.3)) {
onChangeEventInProgress = false
}
}
.onChange(of: geometry.size.width) { newWidth in
if !initFlag {
initFlag = true
return
}
// This line doesn't cause any problems
onChangeEventInProgress = true
widthChangeDetector.send(newWidth)
}
.onReceive(widthChangePublisher) { finalWidth in
print("onReceive, FINAL WIDTH = \(finalWidth)")
Task {
await viewModel.loadData()
}
}
}
}
.onAppear {
Task {
await viewModel.loadData()
}
}
}
}
}
解决方案是将 GeometryReader 块移到 NavigationView 之外。因此,onChange() 方法仅在后代视图上被调用一次。这允许完全摆脱 widthChangeDetector、widthChangePublisher 和 onReceive(widthChangePublisher) 方法。
下面是修改后的代码。
内容视图
import SwiftUI
struct ContentView: View {
@StateObject var viewModel: ViewModel = ViewModel()
var body: some View {
GeometryReader { geometry in
NavigationView {
VStack {
MyView(geometryProxy: geometry, viewModel: viewModel)
Spacer()
Text("Another view")
}
.navigationBarTitle("Hey!", displayMode: .inline)
}
.navigationViewStyle(StackNavigationViewStyle())
}
}
}
我的视图
import SwiftUI
struct MyView: View {
@ObservedObject var viewModel: ViewModel
private var geometryProxy: GeometryProxy
init(geometryProxy: GeometryProxy, viewModel: ViewModel){
self.geometryProxy = geometryProxy
self.viewModel = viewModel
}
var body: some View {
ScrollView {
// ForEach along with the viewModel.jIndex published property creates
// a new instance of Text view every time viewModel.jIndex is updated.
// In this way, we can make use of Text's onAppear on each instance newly created.
ForEach(((viewModel.jIndex == 0 ? 0 : viewModel.jIndex-1)..<viewModel.jIndex), id: \.self) { i in
Text(viewModel.textData)
.id(i)
.opacity(viewModel.innerWorkInProgress ? 0 : 1)
.onAppear {
withAnimation(.easeInOut(duration: 0.3)) {
viewModel.innerWorkInProgress = false
}
}
}
}
.onAppear {
Task {
await viewModel.loadData()
}
}
.onChange(of: geometryProxy.size.width) { newWidth in
viewModel.innerWorkInProgress = true
Task {
await viewModel.loadData()
}
}
}
}
在我的应用程序中,我使用 onChange()
方法和发布者来检测和通知可用的屏幕宽度变化。我还在我的视图模型中使用 innerWorkInProgress
发布的 属性 来控制视图的不透明度。视图的不透明度在屏幕宽度变化时设置为 0。一旦视图的内容被更新,它的不透明度在 onAppear()
方法中被设置回 1。
我的问题是,如果我在 onChange()
方法中将值设置为 innerWorkInProgress
,onReceive()
方法将不会执行。
如果我使用状态变量而不是已发布的 属性,onReceive()
方法效果很好。
我的代码有什么问题?
下面是一个简化的可重现示例。您可以通过将设备从纵向旋转到横向来测试它,反之亦然。
内容视图
import SwiftUI
struct ContentView: View {
@StateObject var viewModel: ViewModel = ViewModel()
var body: some View {
NavigationView {
VStack {
MyView(viewModel: viewModel)
Spacer()
Text("Another view")
}
.navigationBarTitle("Hey!", displayMode: .inline)
}
.navigationViewStyle(StackNavigationViewStyle())
}
}
我的视图
import SwiftUI
import Combine
struct MyView: View {
@ObservedObject var viewModel: ViewModel
@State private var initFlag = false
let widthChangeDetector: CurrentValueSubject<CGFloat, Never>
let widthChangePublisher: AnyPublisher<CGFloat, Never>
init(viewModel: ViewModel){
self.viewModel = viewModel
let wDetector = CurrentValueSubject<CGFloat, Never>(0)
self.widthChangePublisher = wDetector
.debounce(for: .seconds(0.1), scheduler: DispatchQueue.main)
.dropFirst()
.eraseToAnyPublisher()
self.widthChangeDetector = wDetector
}
var body: some View {
GeometryReader { geometry in
ScrollView {
// ForEach along with the viewModel.jIndex published property creates
// a new instance of Text view every time viewModel.jIndex is updated.
// In this way, we can make use of Text's onAppear on each instance newly created.
ForEach((viewModel.jIndex-1..<viewModel.jIndex), id: \.self) { i in
Text(viewModel.textData)
.id(viewModel.jIndex)
.opacity(viewModel.innerWorkInProgress ? 0 : 1)
.onAppear {
withAnimation(.easeInOut(duration: 0.3)) {
viewModel.innerWorkInProgress = false
}
}
.onChange(of: geometry.size.width) { newWidth in
if !initFlag {
initFlag = true
return
}
// Problem: this line prevents onReceive from executing
viewModel.innerWorkInProgress = true
widthChangeDetector.send(newWidth)
}
.onReceive(widthChangePublisher) { finalWidth in
print("onReceive, FINAL WIDTH = \(finalWidth)")
Task {
await viewModel.loadData()
}
}
}
}
.onAppear {
Task {
await viewModel.loadData()
}
}
}
}
}
视图模型
import Foundation
final class ViewModel: ObservableObject {
@Published var innerWorkInProgress = false
@Published private(set) var jIndex = 0
private(set) var textData = ""
func loadData() async {
innerWorkInProgress = true
let i = jIndex + 1
textData = "#\(i) Lorem ipsum dolor sit amet, consectetur adipiscing elit"
jIndex = i
}
}
这是 MyView 的替代版本,使用 onChangeEventInProgress
状态 属性 而不是已发布的 属性。这个版本没有任何问题。
import SwiftUI
import Combine
struct MyView: View {
@ObservedObject var viewModel: ViewModel
// a state property used instead of a published property
@State private var onChangeEventInProgress = false
@State private var initFlag = false
let widthChangeDetector: CurrentValueSubject<CGFloat, Never>
let widthChangePublisher: AnyPublisher<CGFloat, Never>
init(viewModel: ViewModel){
self.viewModel = viewModel
let wDetector = CurrentValueSubject<CGFloat, Never>(0)
self.widthChangePublisher = wDetector
.debounce(for: .seconds(0.1), scheduler: DispatchQueue.main)
.dropFirst()
.eraseToAnyPublisher()
self.widthChangeDetector = wDetector
}
var body: some View {
GeometryReader { geometry in
ScrollView {
ForEach((viewModel.jIndex-1..<viewModel.jIndex), id: \.self) { i in
Text(viewModel.textData)
.id(viewModel.jIndex)
.opacity(onChangeEventInProgress ? 0 : 1)
.onAppear {
withAnimation(.easeInOut(duration: 0.3)) {
onChangeEventInProgress = false
}
}
.onChange(of: geometry.size.width) { newWidth in
if !initFlag {
initFlag = true
return
}
// This line doesn't cause any problems
onChangeEventInProgress = true
widthChangeDetector.send(newWidth)
}
.onReceive(widthChangePublisher) { finalWidth in
print("onReceive, FINAL WIDTH = \(finalWidth)")
Task {
await viewModel.loadData()
}
}
}
}
.onAppear {
Task {
await viewModel.loadData()
}
}
}
}
}
解决方案是将 GeometryReader 块移到 NavigationView 之外。因此,onChange() 方法仅在后代视图上被调用一次。这允许完全摆脱 widthChangeDetector、widthChangePublisher 和 onReceive(widthChangePublisher) 方法。
下面是修改后的代码。
内容视图
import SwiftUI
struct ContentView: View {
@StateObject var viewModel: ViewModel = ViewModel()
var body: some View {
GeometryReader { geometry in
NavigationView {
VStack {
MyView(geometryProxy: geometry, viewModel: viewModel)
Spacer()
Text("Another view")
}
.navigationBarTitle("Hey!", displayMode: .inline)
}
.navigationViewStyle(StackNavigationViewStyle())
}
}
}
我的视图
import SwiftUI
struct MyView: View {
@ObservedObject var viewModel: ViewModel
private var geometryProxy: GeometryProxy
init(geometryProxy: GeometryProxy, viewModel: ViewModel){
self.geometryProxy = geometryProxy
self.viewModel = viewModel
}
var body: some View {
ScrollView {
// ForEach along with the viewModel.jIndex published property creates
// a new instance of Text view every time viewModel.jIndex is updated.
// In this way, we can make use of Text's onAppear on each instance newly created.
ForEach(((viewModel.jIndex == 0 ? 0 : viewModel.jIndex-1)..<viewModel.jIndex), id: \.self) { i in
Text(viewModel.textData)
.id(i)
.opacity(viewModel.innerWorkInProgress ? 0 : 1)
.onAppear {
withAnimation(.easeInOut(duration: 0.3)) {
viewModel.innerWorkInProgress = false
}
}
}
}
.onAppear {
Task {
await viewModel.loadData()
}
}
.onChange(of: geometryProxy.size.width) { newWidth in
viewModel.innerWorkInProgress = true
Task {
await viewModel.loadData()
}
}
}
}