【问题标题】:How to use isLoading with a viewModel and an Environment Object如何将 isLoading 与 viewModel 和环境对象一起使用
【发布时间】:2021-02-03 00:00:45
【问题描述】:

我在名为 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 都会呈现父视图。

有人能说明如何最好地解决这个问题吗?

【问题讨论】:

  • 您没有提供足够的代码来诊断这个问题
  • 我很抱歉。我仍然知道要分享多少/很少。谢谢!

标签: mvvm swiftui combine


【解决方案1】:

看看您的示例中的以下代码:

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 中,当身份验证状态更改时,子视图将从层次结构中删除。

【讨论】:

  • 感谢您的周到回复。对此,我真的非常感激。实际上,过去几天我一直在尝试阅读不同的架构方法,但我还没有完全理解它们。我会把你的建议铭记在心,并继续坚持下去。再次感谢!
  • 重新阅读您的答案后,我又想到了另一个问题。 isLoading 特定于 SelectUserTypeView。但是,它的值(真或假)基于 authState 环境对象内的 authStateMode 枚举。您能否指出一些有关如何配置基于环境对象(如 authStateMode)中的信息的视图特定变量(如 isLoading)的信息?
  • 我不知道有专门关于这个的文章,但这是一个很好的资源,关于何时选择 StateObject、ObservableObject 和 EnvironmentObject:swiftwithmajid.com/2020/07/02/…
  • 我喜欢采用的一般做法是拥有一个事实来源(即一个状态对象),该来源被传递/注入到我的视图层次结构中。在多个地方存储状态会导致问题(正如您已经看到的)。以单向数据流为目标
  • 完成!我访问了您推荐的博客,事实证明这很有帮助。我发现了另一个对初学者来说非常清楚的资源:@​​987654322@。
【解决方案2】:

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()

【讨论】:

  • 感谢您抽出宝贵时间做出如此详细的回复。非常感激。关于使用 ObservedObject,这是一个很好的观点。我会试一试的。
猜你喜欢
  • 2018-11-29
  • 2019-02-22
  • 2019-02-25
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多