如何将 isLoading 与 viewModel 和环境对象一起使用

How to use isLoading with a viewModel and an Environment Object

我在名为 authState 的环境对象中使用 isLoading Bool。

import Foundation
import FirebaseAuth
import Combine

class AuthState: ObservableObject {
    
    enum AuthStateMode {
        case unregistered
        case signedInAnonymously
        case signedInWithEmail
    }
    
    @Published var user: User?
    @Published var authStateMode: AuthStateMode = .unregistered
    @Published var error: AuthError?
    @Published var isLoading: Bool = false
    
    private var cancellables = Set<AnyCancellable>()

    
    let authService = AuthService()
    
    init() {
        authService.authStateSubject.assign(to: &$user)
        $user.map { user -> AuthStateMode in
            if let user = user {
                if user.email != nil { return .signedInWithEmail }
                else if user.uid != "" { return .signedInAnonymously }
                else { return .unregistered }
            } else { return .unregistered }
        }.assign(to: &$authStateMode)
    
    }
    
    func signIn(email: String, password: String) {
        self.isLoading = true
        authService.signIn(email: email, password: password)
            .sink { completion in
                self.isLoading = false
                switch completion {
                case let .failure(error):
                    return self.error = error
                case .finished: return print("authState.signIn finished")
                }
            } receiveValue: { _ in }
            .store(in: &cancellables)
    }
    
    func signInAnonymously() {
        isLoading = true
        authService.signInAnonymously()
            .sink { completion in
                self.isLoading = false
                switch completion {
                case let .failure(error):
                    return self.error = error
                case .finished: return
                }
            } receiveValue: { user in
                self.user = user
            }.store(in: &cancellables)
    }
    
    func linkAccount(email: String, password: String) {
        isLoading = true
        authService.linkAccount(email: email, password: password)
            .sink { completion in
                self.isLoading = false
                switch completion {
                case let .failure(error):
                    return self.error = error
                case .finished:
                    return print("linkAccount finished")
                }
            } receiveValue: { user in
                self.user = user
            }
            .store(in: &cancellables)
    }
}

我有一个使用 authState.isLoading Bool 的父视图。

import SwiftUI
import Combine

struct SelectUserTypeView: View {
    
    @EnvironmentObject var authState: AuthState

    var body: some View {
        
        NavigationView {
            VStack {
                Spacer()
                if authState.isLoading == true {
                    ProgressView()
                } else if authState.authStateMode == .unregistered {
                    Button { authState.signInAnonymously() }
                        label:{ Text("Connect") }
                } else {
                    NavigationLink(
                        destination: GuestPreviewView(),
                        label: { Text("Join as Guest") })
                    Spacer()
                    NavigationLink(
                        destination: SignInUpView(viewModel: .init(mode: .signUp)),
                        label: { Text("Host a Session") })
                }
                Spacer()
                NavigationLink(destination: RecordingsView(),
                               label: { Text("Browse Recordings") })
                Spacer()
                    .navigationBarTitle(Text("Welcome to Fullres"), displayMode: .inline)
                    .onAppear(perform: authState.signInAnonymously)
            }
        }
    }
}

我有一个子视图也使用 authState.isLoading Bool。

import SwiftUI
import Combine

struct SignInUpView: View {
    
    @EnvironmentObject var authState: AuthState
    @StateObject var viewModel: SignInUpViewModel
    
    var body: some View {
        ZStack {
            if authState.isLoading {
                ProgressView()
            } else if authState.authStateMode == .signedInWithEmail {
                HostPreviewView()
            } else {
                VStack {
                    Spacer()
                    Text(viewModel.heading)
                        .font(.title)
                        .bold()
                        .padding()
                    if let error = authState.error { Text("\(error.localizedDescription)") }
                    TextField("Email", text: $viewModel.email)
                        .autocapitalization(/*@START_MENU_TOKEN@*/.none/*@END_MENU_TOKEN@*/)
                        .disableAutocorrection(true)
                        .padding()
                    SecureField("Password", text: $viewModel.password)
                        .autocapitalization(/*@START_MENU_TOKEN@*/.none/*@END_MENU_TOKEN@*/)
                        .disableAutocorrection(true)
                        .padding()
                    Button(viewModel.signInUpBtnLbl) {signInUpBtnAction()}
                        .font(.title2)
                        .padding()
                    Spacer()
                    Button(viewModel.signInUpModeBtnLbl) {
                        viewModel.mode.toggle()
                    }
                    .padding()
                    Spacer()
                }
            }
        }
    }
    
    func signInUpBtnAction() {
        switch viewModel.mode {
        case .signIn: authState.signIn(email: viewModel.email, password: viewModel.password)
        case .signUp: authState.linkAccount(email: viewModel.email, password: viewModel.password)
        }
    }
}

似乎只要在子视图上 isLoading Bool 切换为 true,UI 就会显示父视图。

有人可以阐明如何最好地解决这个问题吗?

查看示例中的以下代码:

if authState.isLoading == true {
                    ProgressView()
                } else if authState.authStateMode == .unregistered {
                    Button { authState.signInAnonymously() }
                        label:{ Text("Connect") }
                } else {
                    NavigationLink(
                        destination: GuestPreviewView(),
                        label: { Text("Join as Guest") })
                    Spacer()
                    NavigationLink(
                        destination: SignInUpView(viewModel: .init(mode: .signUp)),
                        label: { Text("Host a Session") })
                }

如果authState.isLoading为真,则显示ProgressView()。然后,稍后你有 NavigationLinkSignInUpView。当用户在 SignInUpView 上时 isLoading 设置为 true 时会出现问题 - 您希望它在该页面上呈现 ProgressView,但是因为父级然后切换到它自己的 if authState.isLoading 子句,它从层次结构中删除已呈现给子视图的 NavigationLink,将您踢回到第一页。

你如何解决这个问题是一个架构决定,不一定是一个有明确答案的修复。也许要问的是:您需要在父视图上使用 authState.isLoading 子句吗?只能在子视图页面上加载吗?

可能最简单的方法是不要将 NavigationLink 包装在 if/else 中,因为我认为无论如何它都应该显示在首页上 (?)

类似于:

if authState.isLoading {
  ProgressView()
}
if authState.authStateMode == .unregistered {
  Button...
}
NavigationLink(destination: SignInUpView())...

关键是要以某种方式确保您的 NavigationLink 没有包含在 if/else 中,当身份验证状态更改时,子视图将从层次结构中删除。

jnpdx对问题的解释是正确的。

首先,如果您的状态不应该在视图之间共享,您应该使用 @ObservedObject 而不是 @EnvironmentObject。但如果你仍然坚持,你可以在你的父视图中执行以下操作:

将父视图中的最后一个 else 块替换为:

        Group {
           NavigationLink(
                destination: GuestPreviewView(),
                label: { Text("Join as Guest") })
            Spacer()
            NavigationLink(
                destination: SignInUpView(viewModel: .init(mode: .signUp)),
                label: { Text("Host a Session") })
        }.opacity(authState.isLoading ? 0 : 1)

这不会删除 NavigationLink,从而使您保持在子视图中,但会隐藏按钮。您还可以更改宽度和高度以防止它占用屏幕空间。

更简洁的解决方案是再次在您的子视图中使用 @ObservedObjects,如下所示:

struct SignInUpView: View {
    
    @ObservedObject var authState = AuthState()
    @StateObject var viewModel: SignInUpViewModel
    
    var body: some View {
        ZStack {
            if authState.isLoading {
                ProgressView()