Danny의 iOS 컨닝페이퍼
article thumbnail

[TIL #28]  2023 / 06 / 13

간단히 AlertController 사용방법 및 CustomAlertViewController를 만들어 볼 예정입니다.

 

 

UIAlertController

사용자에게 각각의 상황에 맞게 알림 및 경고로 사용자에게 알려 상호작용을 할 수 있는 컨트롤러입니다.

 

 3개의 파라미터와 UIAlertAction만 만들어 주면, UIAlertController를 바로 사용할 수 있습니다.

 

먼저 UIAlertController를 생성 시, 필요한 파라미터들은 title, message, preferredStyle이 있습니다.

 

title

알람의 제목

 

message

알람의 본문 내용 및 추가 설명

 

preferredStyle

알람창의 스타일을 정합니다. 

alert, actionSheet로 각각 사용용도가 조금 다르지만, 크게 상관없는 것 같네요.

 

alert : 경고 메시지 또는 사용자에게 확인 또는 취소와 같은 선택을 요구하는 경우 사용합니다.

(중간에 딱 나타는 형식)

 

actionSheet : 사용자에게 여러 옵션을 제공하고 선택할 수 있게 메뉴형태의 인터페이스입니다.

(아래에서 올라오는 형식)

 

 

 

UIAlertAction

기본적인 UIAlertController의 틀은 알아봤으니, 사용자와 상호작용할 수 있게 버튼을 만들어 줘야 합니다.

 

UIAlertAction은 간단히 버튼이라고 생각할 수 있습니다.

UIAlertAction의 파라미터로의 버튼의 제목, 버튼의 스타일, 동작 핸들러로 버튼을 만들어 주는 거죠.

 

title

버튼의 제목

 

style

버튼의 스타일로는 default, destructive, cancel 이 존재합니다.

 

cancel은 취소 버튼으로 조금 더 Bold 하게 설정되고, HIG에 가이드라인에 따라서 자동으로 배치됩니다.

(alert의 경우 왼쪽에 위치, actionSheet의 경우 맨 하단에 위치)

 

사진으로 각각의 차이점을 비교해 보세요.

 

handler

작업이 선택되면 클로저 블록 내부에서 원하는 동작을 정의하고 실행합니다.

 

 

UIAlertController를 만들어 봅시다

위에서 배운 대로 모양을 만들어주고, UIAlertController의 addAction 메서드를 사용해서 추가해 주면 됩니다.

 

그리고 UIAlertController도 화면에 띄워줘야 하므로 present를 사용해서 화면에 띄워주시면 됩니다.

func setupAlert() {
    let alert = UIAlertController(title: "타이틀",
                                  message: "메세지를 적어주세요.",
                                  preferredStyle: .alert)
    
    let doneAction = UIAlertAction(title: "확인",
                                   style: .default) { action in
        // 동작을 설정
    }

    let cancelAction = UIAlertAction(title: "취소",
                                     style: .cancel) { action in
        // 동작을 설정
    }
    
    alert.addAction(doneAction)
    alert.addAction(cancelAction)
    
    self.present(alert, animated: true)
}

 

 

텍스트 필드 추가하는 방법

신기하게도 UIAlertController의 메서드 중 텍스트필드를 간편하게 만들어주는 메서드가 있습니다.

여러 개의 텍스트필드를 추가할 수 있어요.

 

그런데 여기선 preferredStyle에서 actionSheet에는 사용이 불가능하고 alert에서만 사용이 가능하더라고요.

func setupAlert() {
    let alert = UIAlertController(title: "로그인",
                                  message: "ID와 Password를 입력해주세요.",
                                  preferredStyle: .alert)

    alert.addTextField { textField in
        textField.placeholder = "ID를 입력해주세요."
        textField.tag = 0
    }
    
    alert.addTextField { textField in
        textField.placeholder = "Password를 입력해주세요."
        textField.tag = 1
    }

    let doneAction = UIAlertAction(title: "확인",
                                   style: .default) { action in
        // 동작을 설정
        guard let textFields = alert.textFields else { return }
        for textFiled in textFields {
            
            if textFiled.tag == 0 {
                if let id = textFiled.text {
                    print(id)
                }
            } else {
                if let password = textFiled.text {
                    print(password)
                }
            }
        }
        
    }

    let cancelAction = UIAlertAction(title: "취소",
                                     style: .cancel) { action in
        // 동작을 설정
    }
    
    alert.addAction(doneAction)
    alert.addAction(cancelAction)
    
    self.present(alert, animated: true)
}

 

 

참고, 색상 및 폰트 변경하기

UIAlertController도 일종의 ViewController를 상속받고 있어서, view로 접근하여 속성들을 변경 가능합니다.

 

이런 식으로 alert.view로 접근하여 기본적인 모양을 바꿔 줄 수도 있습니다.

func setupAlert() {
    let alert = UIAlertController(title: "",
                                  message: "",
                                  preferredStyle: .alert)
    // 배경, 틴트 색상                              
    alert.view.tintColor = .red
    alert.view.backgroundColor = .blue
    alert.view.layer.cornerRadius = 18
    alert.view.clipsToBounds = true
    
    // 글자 설정
    let titleAttributedString = NSAttributedString(string: "로그인",
                                                   attributes: [NSAttributedString.Key.foregroundColor : UIColor.green,
                                                                NSAttributedString.Key.font : UIFont.systemFont(ofSize: 30, weight: .bold)])
    let messageAttributedString = NSAttributedString(string: "ID와 Password를 입력해주세요.",
                                                     attributes: [NSAttributedString.Key.foregroundColor : UIColor.orange])
    
    alert.setValue(titleAttributedString, forKey: "attributedTitle")
    alert.setValue(messageAttributedString, forKey: "attributedMessage")
    
    alert.addTextField { textField in
        textField.placeholder = "ID를 입력해주세요."
    }
    
    alert.addTextField { textField in
        textField.placeholder = "Password를 입력해주세요."
    }

    let doneAction = UIAlertAction(title: "확인",
                                   style: .default) { action in
        // 동작을 설정
    }

    let cancelAction = UIAlertAction(title: "취소",
                                     style: .cancel) { action in
        // 동작을 설정
    }
    
    alert.addAction(doneAction)
    alert.addAction(cancelAction)
    
    self.present(alert, animated: true)
}

 

 

CustomAlertController를 만들어보자

UIAlertController를 이용하여 Alert를 만들어 주는 방법은 아니고

UIViewController를 따로 만들어줘, Alert와 같은 동작을 만들어 구현시켜 보려 합니다.

 

다른 분들은 PopupController라고도 하는 거 같네요.

 

일단 기본 UIAlertController 너무 단순해서 많을 정보를 알려주지 못하죠.

정보를 더 추가시킨 Alert를 보여 주고 싶을 때 사용하면 좋을 것 같습니다.

 

CustomAlertController의 객체만 생성해 주면 언제든 재사용할 수 있게 간단히 만들어 봅시다.

 

확인 및 취소 동작을 전달할 때, 델리게이트로도 할 수도 있지만 번거로워서 그냥 클로저로 동작을 넘겨줬습니다.

class CustomAlertController: UIViewController {
    
    private let alertView = UIView()
    private let imageView = UIImageView()
    private let titleLabel = UILabel()
    private let messegeLable = UILabel()
    private let doneButton = UIButton()
    private let cancelButton = UIButton()
    
    private var logoImage: UIImage?
    private var titleText: String?
    private var messegeText: String?
    private var doneButtonTitle: String?
    private var cancelButtonTitle: String?
    
    // 버튼의 동작을 담아서 사용하기 위해 만들어 줌.
    var doneButtonCompletoin: (()-> Void)?
    var cancelButtonCompletoin: (() -> Void)?
    
    override func viewDidLoad() {
        super.viewDidLoad()
        setupUI()
    }
    
    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)
        
        UIView.animate(withDuration: 0.2, delay: 0, options: .curveEaseOut) { [weak self] in
            // 확대된 효과를 처음 크기로 되돌리기
            self?.alertView.transform = .identity
        }
    }

    // 여긴 그냥 레이아웃 및 설정들 (모양은 원하는 대로 만들기)
    private func setupUI() {
        // 이전 VC 배경이 살짝 어두워지는 효과
        self.view.backgroundColor = .black.withAlphaComponent(0.2)
        
        // 처음 생성 될 때, 살짝 커지는 효과(viewWillAppear, viewWillDisappear에서 애니메이션 효과를 만들어 줌)
        self.alertView.transform = CGAffineTransform(scaleX: 1.2, y: 1.2)
        
        alertView.backgroundColor = .white
        alertView.layer.cornerRadius = 15
        alertView.clipsToBounds = true
        
        view.addSubview(alertView)
        alertView.snp.makeConstraints {
            $0.centerY.equalToSuperview()
            $0.leading.trailing.equalToSuperview().inset(30)
            $0.height.equalTo(160)
        }
        
        alertView.addSubview(imageView)
        imageView.snp.makeConstraints {
            $0.top.leading.equalToSuperview().inset(20)
            $0.height.width.equalTo(60)
        }
        imageView.layer.cornerRadius = 30

        alertView.addSubview(titleLabel)
        titleLabel.snp.makeConstraints {
            $0.top.equalToSuperview().offset(20)
            $0.centerX.equalToSuperview()
        }
        titleLabel.font = .preferredFont(forTextStyle: .title2)
        
        alertView.addSubview(messegeLable)
        messegeLable.snp.makeConstraints {
            $0.top.equalTo(titleLabel.snp.bottom).offset(10)
            $0.centerX.equalToSuperview()
        }
        messegeLable.font = .preferredFont(forTextStyle: .body)
        
        let vStack = UIStackView(arrangedSubviews: [cancelButton, doneButton])
        vStack.axis = .horizontal
        vStack.distribution = .fillEqually
        vStack.spacing = 15
        
        cancelButton.layer.cornerRadius = 15
        cancelButton.backgroundColor = .systemRed
        cancelButton.addTarget(self, action: #selector(cancelButtonHandler), for: .touchUpInside)

        doneButton.layer.cornerRadius = 15
        doneButton.backgroundColor = .systemBlue
        doneButton.addTarget(self, action: #selector(doneButtonHandler), for: .touchUpInside)
        
        alertView.addSubview(vStack)
        vStack.snp.makeConstraints {
            $0.leading.trailing.bottom.equalToSuperview().inset(15)
            $0.height.equalTo(45)
        }
    }
}

extension CustomAlertController {
    // 생성 시, 각각의 Label 및 이지미를 설정 할 있게 만듬
    convenience init(logImage: UIImage, titleText: String, messegeText: String, doneButtonTitle: String, cancelButtonTitle: String) {
        self.init()
        
        imageView.image = logImage
        titleLabel.text = titleText
        messegeLable.text = messegeText
        cancelButton.setTitle(cancelButtonTitle, for: .normal)
        doneButton.setTitle(doneButtonTitle, for: .normal)
        
        // 이 부분이 중요하다.
        // crossDissolve는 VC가 생성되는 동시에 나타나서 조금 더 자연스럽게 창이 생성되게 만들 수 있다.
        self.modalTransitionStyle = .crossDissolve
        // ⭐️ 만약 fullScreen을 사용하게 되면, VC가 생성이 완료되면 이전 VC가 제거된다.
        // overFullScreen은 말그대로 덮는 형식으로 생성이 되서 이전 VC가 유지된다.
        self.modalPresentationStyle = .overFullScreen
    }
}

extension CustomAlertController {
    @objc func doneButtonHandler(_ sender: UIButton) {
        doneButtonCompletoin?()
        self.dismiss(animated: true)
    }
    
    @objc func cancelButtonHandler(_ sender: UIButton) {
        cancelButtonCompletoin?()
        self.dismiss(animated: true)
    }
}

 

사용 시

class ViewController: UIViewController {

    // ... 코드 생략
    
    @objc func customAlertButtonHandler(_ sender: UIButton) {
        let customAlert = CustomAlertController(logImage: UIImage(named: "myProfile")!,
                                                titleText: "공지",
                                                messegeText: "많은 댓글 부탁드려요.",
                                                doneButtonTitle: "확인",
                                                cancelButtonTitle: "취소 없음")
        customAlert.doneButtonCompletoin = {
            // 확인 동작을 정의
        }
        
        customAlert.cancelButtonCompletoin = {
            // 취소 동작을 정의
        }
        
        self.present(customAlert, animated: true)
    }
    
    // ... 코드 생략
}

 

기본 Alert 및 CustomAlert의 전체 코드는 GitHub 를 확인해 주세요.

 

 

마무리

현재 진행 중인 프로젝트에서 UIAlertController를 present 했을 때,

뒤에 보이는 MapVC와도 상호작용을 위해서 CustomAlertController를 만든 후, backgroundColor를 clear로 만들어 주면,

MapVC가 터치가 될 줄 알았는데 안되더라고요.

 

이걸 위해 UIAlertController를 공부한 건데... ㅠ.ㅠ

(안 돼서... 위의 예제를 대충 만든 경향이 없지 않아 있습니다...)

 

지도와 터치가 가능하게 만들어야 하는데, 아마도 접근부터 잘못한 것 같습니다.

 

생각해 보면 애당초에 present 메서드의 목적이

present 한 VC와 상호 작용을 하게 만들고 이전 VC와 상호작용을 제한하는 목적으로 사용하는 걸 텐데... 그렇죠?

 

그래서 간단히 해결방법을 생각을 해봤는데, 그냥 MapView위에 UIView를 얹히는 방법과

UISheetPresentationController를 사용하는 방법이 아마도 지도와 상호작용을 할 수 있게 만들 것 같아 보이는데...

 

내일 시도해 봐야겠습니다.

 

추가. UIView를 얹히는 방법도 이전 계층의 뷰와 상호작용이 안되네요.

찾아보니 hitTest와 point란 메서드를 통해 뷰의 이전 계층과도 상호작용이 가능하게 만들 수 있더라고요.

 

일단은 저는 이렇게 해결 했습니다.

 

다른 더 좋을 방법이 있으면, 댓글 남겨주시면 감사하겠습니다.

 

 

 

반응형
profile

Danny의 iOS 컨닝페이퍼

@Danny's iOS

포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!