如何将 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()
。然后,稍后你有 NavigationLink
到 SignInUpView
。当用户在 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()
我在名为 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()
。然后,稍后你有 NavigationLink
到 SignInUpView
。当用户在 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()