在我用MVVM-pattern设计的应用程序中,我有一个loginView。如果存在网络或身份验证问题,登录可能会失败。我的目标是捕获错误并显示相应的警报。我定义了警报的枚举,如下所示:
enum Alerts: Identifiable {
var id: Int {
return self.hashValue
}
case networkError
case authenticationError
}
视图的实现是:
struct LoginView: View {
@ObservedObject var viewModel = LoginViewModel()
var body: some View {
VStack {
TextField("Enter e-mail address", text: $viewModel.email)
SecureField("Enter password", text: $viewModel.password)
Button("Log In") {
viewModel.login()
}
}.alert(item: $viewModel.errorAlert, content: { alert in
switch alert {
case .networkError:
return Alert(title: Text("Error"), message: Text("Check internet Connection"), dismissButton: .default(Text("Ok")))
case .authenticationError:
return Alert(title: Text("Error"), message: Text("Some error occured, please try again"), dismissButton: .default(Text("Ok")))
}
})
}
}
viewModel是:
class LoginViewModel: ObservableObject {
@Published var email: String = ""
@Published var password: String = ""
@Published var errorAlert: Alerts? = nil
@Published var token: Token? = nil
var authentication = PassthroughSubject<User, WebserviceError>()
var cancellables = Set<AnyCancellable>()
init() {
authentication.map { Webservice().authenticate($0) }.switchToLatest().print().sink { error in
self.errorAlert = Alerts.networkError
} receiveValue: { token in
self.token = token
}.store(in: &cancellables)
token.map { KeychainWrapper.save(token: $0)}?.sink(receiveCompletion: { error in
self.errorAlert = Alerts.authenticationError
}, receiveValue: { _ in
//
}).store(in: &cancellables)
}
func login() {
authentication.send(User(username: email, password: password))
}
}
Webservice的实现
class Webservice {
func authenticate(_ user: User) -> AnyPublisher<Token, WebserviceError> {
return Future<Token, WebserviceError> { promis in
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
if user.username.lowercased() == "root" && user.password == "1234" {
promis(.success(Token(access: "asdasda", refresh: "sdfsdfsdf", exprationDate: Date().addingTimeInterval(120))))
} else {
promis(.failure(.unknown))
}
}
}.eraseToAnyPublisher()
}
}
考虑以下场景:用户第一次运行应用程序。在第一次尝试中,她/他输入了错误的用户名/密码。
发生的情况是,用户将看到等同的警告,并可以单击ok将其关闭。问题是,从第二次开始,点击登录按钮后,什么也没有发生。看起来像authentication.map { ....Viewmodel中的}将被永久取消。为什么会这样呢?
发布于 2021-03-18 12:34:24
根据documentation的说法
发布者继续发出元素,直到它正常完成或失败。
这意味着如果您的发布者(authentication: PassthroughSubject
)失败,它将无法再向其订阅者发送值。
解决此问题的一种方法可能是手动调用Webservice().authenticate($0)
,并直接使用它返回的发布者(并完全删除身份验证主题)。
注:我不是合并专家,我不确定这是否是管理发布者的正确方式(如果经常存储可取消的内容存在某种性能问题),您可能需要深入研究一下。
class LoginViewModel: ObservableObject {
// [...]
func login() {
Webservice()
.authenticate(User(username: email, password: password))
.print()
.sink { error in
self.errorAlert = Alerts.networkError
} receiveValue: { token in
self.token = token
}
.store(in: &cancellables)
}
}
发布于 2021-03-18 13:33:32
Combine publishes会发布值,直到完成或出错--这是设计好的。
在您的用例中,您似乎希望“处理”sink
订阅中的错误。换句话说,你的管道永远不会出错。要实现这一点,您可以将值或错误“打包”到一个Result
中,并将其作为一个值发出,而就组合管道而言,它具有一个Never
故障类型。
为方便起见,让我们创建一个操作符asResult()
extension Publisher {
func asResult() -> AnyPublisher<Result<Output, Failure>, Never> {
self
.map { .success($0) }
.catch { err in Result.Publisher(.failure(err))}
.eraseToAnyPublisher()
}
}
您可以将其应用于authenticate
authentication
.map {
Webservice().authenticate($0).asResult()
}
.switchToLatest()
.sink { result in
// handle the result here
switch result {
case .success(let token):
self.token = token
case .failure(let error):
print(error)
self.errorAlert = Alerts.networkError
}
}
.store(in: &cancellables)
正如你所看到的,上面的管道从来没有失败过--也就是有Failure == Never
发布于 2021-03-18 14:02:51
发布者一旦失败就会停止发送值,这是预期的行为。
您可以通过从登录函数发送身份验证请求来实现此目的。如果发布者失败,您将向用户显示一个错误,他们将能够更改其输入并再次点击登录。这将导致产生新的请求:
class LoginViewModel: ObservableObject {
@Published var email: String = ""
@Published var password: String = ""
@Published private (set) var errorAlert: Alerts? = nil
@Published private (set) var token: Token? = nil
private let webservice: Webservice
private var cancellables = Set<AnyCancellable>()
init(webservice: Webservice) {
self.webservice = webservice
$token
.sink { token in
KeychainWrapper.save(token: token)
}
.store(in: &cancellables)
}
func login() {
let user = User(username: email, password: password)
webservice
.authenticate(user)
.sink(receiveCompletion: { [weak self] completion in
if completion == .failure(_) {
self?.errorAlert = .authenticationError
}
}, receiveValue: { [weak self] token in
self?.token = token
})
.store(in: &cancellables)
}
}
https://stackoverflow.com/questions/66689833
复制