SwiftUI 视图上的 onReceive 方法不会为自定义发布者执行

onReceive method on SwiftUI view won’t execute for a custom publisher

在我的应用程序中,我使用 onChange() 方法和发布者来检测和通知可用的屏幕宽度变化。我还在我的视图模型中使用 innerWorkInProgress 发布的 属性 来控制视图的不透明度。视图的不透明度在屏幕宽度变化时设置为 0。一旦视图的内容被更新,它的不透明度在 onAppear() 方法中被设置回 1。 我的问题是,如果我在 onChange() 方法中将值设置为 innerWorkInProgressonReceive() 方法将不会执行。 如果我使用状态变量而不是已发布的 属性,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()
            }
        }
    }
}