iOS/개념 & 개발

[iOS] Alert 개선 과정 단계별로 살펴보기

유정주 2023. 8. 30. 01:31
반응형

서론

UIAlertController는 보일러 플레이트가 생기기 쉽습니다.

  1. UIAlertController 생성
  2. UIAlertAction 생성
  3. present

위 세 가지 로직이 반복적으로 사용되기 때문입니다.
메서드로 분리할지라도 UIAlertAction 생성에 중복 코드가 생기고, 그게 아니라면 메서드 파라미터가 많아집니다.
 
이번에는 Wrapper를 사용해 선언형 구조로 Alert를 깔끔하게 정리해 보겠습니다.
많이 고민하며 진행했지만 잘못된 부분이 있을 수 있습니다.
댓글로 피드백 꼭 부탁드립니다.
 
전체 코드는 아래 링크에서 확인 가능합니다.
https://github.com/jeongju9216/SwiftPractice/tree/main/ExampleAlertWrapper/ExampleAlertWrapper/Alert
 
 

오늘 만들 Alert

오늘 만들 Alert입니다.
Alert에 Title, Message가 달려 있고,
OK 버튼과 Cancel 버튼이 있습니다.
 
 

1. 기본 생성법

기본 생성법입니다.

private func showAlert1() {
    let alert = UIAlertController(title: alertTitle, message: message, preferredStyle: .alert)

    let okAction = UIAlertAction(title: "OK", style: .default) { _ in
        print("clicked OK")
    }

    let cancelAction = UIAlertAction(title: "Cancel", style: .cancel)

    alert.addAction(okAction)
    alert.addAction(cancelAction)

    present(alert, animated: true)
}

UIAlertController를 생성하고, 적절한 Action을 생성한 후 설정합니다.
Alert를 사용하는 모든 곳에서 위 코드가 반복돼야 합니다.
 
 

2. 메서드로 분리

다음은 메서드로 분리해 보겠습니다.
 
Extension, Protocol, 상속에서 사용할 수 있도록 의존성이 낮게 작성합니다.
Alert에 필요한 모든 값을 파라미터로 받아서 어디에서든 사용할 수 있어야 합니다.

func showAlert2_1(
    title: String,
    message: String,
    okTitle: String,
    okHandler: ((UIAlertAction) -> Void)? = nil,
    cancelTitle: String,
    cancelHandler: ((UIAlertAction) -> Void)? = nil)
{
    let alert = UIAlertController(title: alertTitle, message: message, preferredStyle: .alert)

    let okAction = UIAlertAction(title: okTitle,
                                 style: .default,
                                 handler: okHandler)
    let cancelAction = UIAlertAction(title: cancelTitle,
                                     style: .cancel,
                                     handler: cancelHandler)

    alert.addAction(okAction)
    alert.addAction(cancelAction)

    present(alert, animated: true)
}

파라미터라 무려 6개나 됩니다.
Cancel 버튼을 사용하지 않는다면 4개로 줄일 수 있겠지만
근본적인 문제는 해결되지 않습니다.
 

showAlert2_1(
    title: alertTitle,
    message: message,
    okTitle: "OK",
    okHandler: { _ in
        print("clicked OK")
    },
    cancelTitle: "Cancel")

많은 파라미터로 인해 호출부도 매우 복잡합니다.
(Handler가 print 한 줄이지만... 실제 로직이 들어간다고 생각해 봅시다... 더 끔찍합니다 ㅠ)
 
만약 파라미터에 기본 값을 설정한다면 호출부 코드는 줄어들겠지만
정의부와 마찬가지로 근본적인 문제는 해결되지 않습니다.
 
 

3. UIAlertAction 배열 전달받기

위 메서드의 파라미터는 대부분 UIAlertAction과 관련된 값입니다.
그렇다면 UIAlertAction 객체를 파라미터로 받으면 어떨까요?
 

func showAlert2_2(title: String, message: String, actions: [UIAlertAction]) {
    let alert = UIAlertController(title: alertTitle, message: message, preferredStyle: .alert)

    for action in actions {
        alert.addAction(action)
    }

    present(alert, animated: true)
}

UIAlertAction 배열을 받아서 반복문으로 등록하고 present 합니다.
정의부는 확실히 깔끔해지고 명확합니다.
 
하지만 호출부는 어떨까요?

let okAction = UIAlertAction(title: okTitle,
                             style: .default,
                             handler: okHandler)
let cancelAction = UIAlertAction(title: cancelTitle,
                                 style: .cancel,
                                 handler: cancelHandler)
                                 
showAlert2_2(
    title: alertTitle,
    message: message,
    actions: [okAction, cancelAction])

Alert를 표시할 때마다 등록할 UIAlertAction 객체를 생성해야 합니다.
객체 생성 코드가 중복될 가능성이 높고 번거롭습니다.
 
UIAlertAction Factory를 만든다면 생성 코드를 메서드로 정리할 수 있고,
이 정도만 해도 충분히 깔끔하겠지만...
한 단계 더 나아가서 선언형 구조로 개선해 봅시다.
 
 

4. AlertWrapper 사용

AlertWrapper를 사용하면 선언형 구조로 Alert를 생성하고 표시할 수 있습니다.

func showAlert3() {
    alert
        .make(title: alertTitle, message: message)
        .addAction(title: "OK") { _ in
            print("clicked OK")
        }
        .addCancelAction()
        .show()
}

 
 
Alert을 생성하고 표시하는 코드입니다.
선언형 구조의 장점대로 표현이 명확하고 깔끔합니다.
굳이 showAlert 메서드로 묶지 않아도 괜찮을 정도입니다.
 
한 가지 단점은 다른 1~3번보다 사전 코드 작업이 많습니다.
지금부터는 그 작업에 대해 알아보죠.
 
 

AlertWrapper

전체 코드는 여기에서 확인할 수 있습니다.
포스팅에서는 중요 포인트만 보죠!
 

/// UIAlertController Wrapper
struct AlertWrapper<Base> {
    let base: Base
    
    init(base: Base) {
        self.base = base
    }
}

/// AlertWrapper와 호환 여부
protocol AlertCompatible: AnyObject {
    associatedtype Base
    var alert: AlertWrapper<Base> { get }
}

AlertWrapper 구조체입니다.
UIViewController에서 alert 프로퍼티를 통해 AlertWrapper 메서드를 사용할 수 있습니다.
 

extension AlertWrapper where Base: UIViewController {
    /// 계산 프로퍼티를 저장 프로퍼티처럼 사용
    /// AlertController 저장
    private var alertController: UIAlertController? {
        get { getAssociatedObject(base, &AlertWrapperAssociatedKeys.alertController) }
        set { setRetainedAssociatedObject(base, &AlertWrapperAssociatedKeys.alertController, newValue) }
    }
    
    ...
    
    /// Alert 표시
    func show() {
        guard let alert = alertController else { return }
        base.present(alert, animated: true)
    }
}

AlertWrapper를 extension 해서 메서드를 구현합니다.
where절을 이용해 Base 타입을 UIViewController로 강제했는데
이러면 base를 이용해 Alert을 present 할 수 있습니다.
 

선언형 구조

사실 선언형 구조는 AlertWrapper와는 큰 연관은 없습니다.
메서드 반환 타입이 중요하니 거기에 집중해 주세요.
 

/// Alert 생성
func make(title: String = "알림", message: String? = nil) -> Self {
    var mutableSelf = self
    mutableSelf.alertController = UIAlertController(title: title,
                                        message: message,
                                        preferredStyle: .alert)
    return mutableSelf
}

Alert 객체를 생성하는 메서드입니다.
 
반환 타입에 주목해 주세요!
Self를 반환하고 있습니다.
AlertWrapper를 반환해서 호출하는 쪽에서 다른 AlertWrapper 메서드를 호출할 수 있도록 합니다.
make( ) 바로 뒤에 addAction( )을 호출할 수 있던 이유가 바로 이거입니다.
 

/// Alert에 Default 액션 추가
func addAction(title: String? = "OK", handler: ((UIAlertAction) -> Void)? = nil) -> Self {
    let action = UIAlertAction(title: title,
                               style: .default,
                               handler: handler)
    self.alertController?.addAction(action)
    return self
}

/// Alert에 Cancel 액션 추가
func addCancelAction(title: String? = "Cancel", handler: ((UIAlertAction) -> Void)? = nil) -> Self {
    let action = UIAlertAction(title: title,
                               style: .cancel,
                               handler: handler)
    self.alertController?.addAction(action)
    return self
}

addAction과 addCancelAction도 마찬가지로 AlertWrapper를 반환합니다.
 
이렇게 "객체를 반환해서 메서드 체이닝 한다"는 것이 선언형 구조의 핵심입니다.
물론 위 예시처럼 Self가 아니어도 됩니다.
프로토콜 타입일 수도 있고, 부모 클래스일 수도 있겠죠.
메서드 체이닝 할 타입 객체를 반환하기만 하면 됩니다.
 
 

마무리

이번 포스팅에서는 단계별로 Alert 코드를 개선해 봤습니다.
중복 코드를 줄이고 편의성을 높인다는 것에 중점을 둬서 개선해 봤는데요.
많이 체감이 되셨나요?
 
또한, 선언형 구조의 원리는 Alert 뿐만 아니라 이곳저곳에서 사용할 수 있으니 익혀두시면 언젠가 써먹을 곳이 많을 거 같습니다.
저도 이번에 처음 써봤는데 너무 매력적이고 만족스러웠습니다.
(개선해야 할 점이 많긴 하지만요 ㅠㅠ)
 
이번 포스팅에서는 제가 학습하며 적용해 본 과정을 다뤘습니다.
그래서 내용이 얕고, 틀린 내용이 있을 수도 있습니다.
아직 취준생인 애기 개발자라는 점 양해해 주시면 감사하겠고, 댓글로 알려주시면 많이 배우겠습니다.
 
감사합니다.


아직은 초보 개발자입니다.
더 효율적인 코드 훈수 환영합니다!
공감 댓글 부탁드립니다.

 
 
 

반응형