In my app which is designed in MVVM-pattern I have a loginView. Login may fail if there is either a network or authentication problem. My goal is to catch errors and display the corresponding alerts. I defined and enum for alerts as follow:
enum Alerts: Identifiable {
var id: Int {
return self.hashValue
}
case networkError
case authenticationError
}
the implementation of the view is:
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")))
}
})
}
}
and the viewModel is:
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))
}
}
implementation of the 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()
}
}
considering the following scenario: user runs the app for the first time. in the first try she/he puts the wrong username/password.
what happens is that the user will be shown the equivalent alert and can click ok to dismiss it. The problem is that from the 2nd time so on, after clicking the login button, nothing happen. looks like the authentication.map { .... } in Viewmodel is cancelled forever. why is that so?