Search code examples
iosswiftalamofirecompletion

Swift completion block with success and failure handler


I'm working on a Swift app's login process and I'd like to know if I should include both success and failure handlers to the function, which checks if the user has successfully logged in or not.

What I want to do here is trigger the successCompletion() block only when the user successfully logged in and otherwise display an Alert with a message. I feel the login function takes multiple arguments, and I think it is kind of hard to figure out it handles two completion handlers from LoginViewController. So, should I include both error and success handlers in the function or better to separate them somehow?

In my LoginViewController,

class LoginViewController: UIViewController {

    private let loginView = LoginView()

    @objc func loginButtonAction() {
        let idInputValue = loginView.loginIdTextField.text!
        let passInputValue = loginView.passwordTextField.text!
        
        LoginAPI().login(with: idInputValue, and: passInputValue, errorCompletion: show(message:)) { _ in
            let sceneDelegate = UIApplication.shared.connectedScenes.first?.delegate as! SceneDelegate
            sceneDelegate.setRootVC(to: HomeViewController())
        }

    func showAlert(message: String) {
        let okAction = UIAlertAction(title: Alert.ButtonTitle.ok, style: .default, handler: nil)
        showAlert(title: Alert.Title.loginError, message: message, actions: [okAction], style: .alert)
    }

}

and my LoginAPI class,

import Alamofire
class LoginAPI {
    
    static var accountInfo: AccountInfo?
    private let loginEndpoint = "https://example.com"
    
    func login(with id: String, and password: String, errorCompletion: @escaping (_ m: String) -> Void?, successCompletion: @escaping () -> Void) {
        let parameter: [String: Any] = ["id": id, "password": password]
        
        AF.request(loginEndpoint, method: .post, parameters: parameter)
            .validate()
            .responseJSON { response in
                guard let data = response.data else {
                    errorCompletion(response.error)
                    return
                }
                
                let decoder: JSONDecoder = JSONDecoder()
                var accountInfo: AccountInfo?
                
                do {
                    accountInfo = try decoder.decode(AccountInfo.self, from: data)
                } catch {
                    errorCompletion(error)
                }
               
                LoginAPI.accountInfo = accountInfo
                
                DispatchQueue.main.async {
                    successCompletion()
                }
            }
    }
}

updated the function

func login(with id: String, and password: String, completed: @escaping (Result<AccountInfo, LoginError>) -> Void) {
    
    let parameter: [String: Any] = ["id": id, "password": password]
    AF.request(loginEndpoint, method: .post, parameters: parameter)
        .responseDecodable(of: AccountInfo.self) { response in

            switch(response.result) {
            case .success(let data):
                completed(.success(data))
            case .failure(let error):
                completed(.failure(.test))
            }
        }
}

Solution

  • It's common to use Result. By returning the result you keep the responsibility of login() tight and handle the result somewhere else. And it indeed also avoids the unclarity of two completion blocks.

    Other remarks: You don't need DispatchQueue.main.async because AF.request completion block is already on the main thread.