관리 메뉴

김종권의 iOS 앱 개발 알아가기

[iOS - swift] API 호출 후 에러가 발생했을 때 Retry 구현 방법 (재시도 기능) 본문

iOS 응용 (swift)

[iOS - swift] API 호출 후 에러가 발생했을 때 Retry 구현 방법 (재시도 기능)

jake-kim 2021. 10. 19. 23:20

* Alamofire를 사용했을 때 정석적인 방법은 retry 방법 참고

* 커스텀 팝업 구현 방법은 해당글 참고

retry 기능 아이디어

  • 클로저 이용: `코드에서 전달하고 사용할 수 있는 2가지의 기능을 가진 자체 블록` - 호출하기 전까지 실행 대기 상태인 특성 이용
  • 커스텀 팝업을 부르는 곳에 retry 클로저를 넘김으로써 retry가 필요할 경우, 버튼을 누른 completion에 적용
  • 팝업은 ViewController 계층 구조상 가장 위쪽에 위치한 인스턴스에 present하는 방식
    • 현재 보여지는 ViewController를 찾기 위해 UIWindow의 extension으로 연산 프로퍼티 정의
    • visibleViewController 이름으로 외부에서 사용할 수 있도록 구현
extension UIWindow {
    public var visibleViewController: UIViewController? {
        return visibleViewControllerFrom(viewController: rootViewController)
    }

    static private var firstRootViewController: UIViewController? {
        return UIApplication.shared.connectedScenes
            .filter { $0.activationState == .foregroundActive }
            .map { $0 as? UIWindowScene }
            .compactMap { $0 }
            .first?.windows
            .first(where: { $0.isKeyWindow })?.rootViewController
    }

    private func visibleViewControllerFrom(viewController: UIViewController? = firstRootViewController) -> UIViewController? {
        if let navigationController = viewController as? UINavigationController {
            return self.visibleViewControllerFrom(viewController: navigationController.visibleViewController)
        } else if let tabBarController = viewController as? UITabBarController {
            return self.visibleViewControllerFrom(viewController: tabBarController.selectedViewController)
        } else {
            if let presentedViewController = viewController?.presentedViewController {
                return visibleViewControllerFrom(viewController: presentedViewController)
            } else {
                return viewController
            }
        }
    }
}
  • extension UIWindow로 정의한 프로퍼티를 `UIApplication.shared.windows.first?.visibleViewController`로 접근
  • 버튼의 이름이 "재시도"이고, 사용하는쪽에서 retry를 위해 completion을 넘긴 경우 right 버튼을 눌렀을 때 해당 클로저가 실행되도록 설계
import UIKit

struct ErrorHandler {

    static func showAlert(_ error: Error?,
                          leftActionCallback: (() -> Void)? = nil,
                          rightActionCallback: (() -> Void)? = nil,
                          completionDidTapRetryButton: (() -> Void)? = nil) {

        guard let error = error as? ErrorType else { return }

        let errorData: ErrorData
        let leftActionTitle: String?
        let rightActionTitle: String

        switch error {
        case .notConnectedToInternet(let data):
            errorData = data
            leftActionTitle = nil
            rightActionTitle = "재시도"
        case .unknown(let data):
            errorData = data
            leftActionTitle = nil
            rightActionTitle = "확인"
        }

        showAlert(errorData,
                  leftActionTitle: leftActionTitle,
                  rightActionTitle: rightActionTitle,
                  leftActionCallback: leftActionCallback,
                  rightActionCallback: rightActionCallback,
                  completionDidTapRetryButton: completionDidTapRetryButton)
    }

    static func showAlert(_ errorData: ErrorData,
                          leftActionTitle: String? = nil,
                          rightActionTitle: String,
                          leftActionCallback: (() -> Void)? = nil,
                          rightActionCallback: (() -> Void)? = nil,
                          completionDidTapRetryButton: (() -> Void)? = nil) {

        var rightActionCallback = rightActionCallback
        if completionDidTapRetryButton != nil, rightActionTitle == "재시도" {
            rightActionCallback = completionDidTapRetryButton
        }

        let rootViewController = UIApplication.shared.windows.first?.visibleViewController
        DispatchQueue.main.async {
            rootViewController?.showPopUp(title: errorData.title,
                                          message: errorData.message,
                                          leftActionTitle: leftActionTitle,
                                          rightActionTitle: rightActionTitle,
                                          leftActionCompletion: leftActionCallback,
                                          rightActionCompletion: rightActionCallback)
        }
    }
}

테스트 용도의 API 정의

  • 보통은 Error타입을 내려주기 때문에 아래처럼 ErrorData로 wrapping하는 작업도 필요하지만 테스트를 위해서 아래처럼 구현
enum ErrorType: Error {
    case notConnectedToInternet(ErrorData)
    case unknown(ErrorData)
}

struct ErrorData {
    let title: String?
    let message: String?
}

struct API {
    static func someAPI(completion: @escaping (Result<Void, ErrorType>) -> ()) {
        DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
            completion(.failure(ErrorType.notConnectedToInternet(ErrorData(title: "네트워크 연결 에러입니다.\n네트워크 설정을 확인해주세요", message: ""))))
        }
    }
}

사용하는 쪽

  • completionDidTapRetryButton에 재실행할 코드를 넘기는 형태
func processBusinessLogic() {
    print("start BusinessLogic !!")

    API.someAPI { result in
        switch result {
        case .success: print("success!!")
        case .failure(let error):
            ErrorHandler.showAlert(error, completionDidTapRetryButton: { [weak self] in self?.processBusinessLogic() } )
        }
    }
}

* 전체 소스코드: https://github.com/JK0369/PopUpViewControllerExample

Comments