Custom Alert

유호준·2022년 7월 4일
1

iOS

목록 보기
2/8
post-thumbnail

Custom Alert

Apple의 기본 UI는 뛰어나지만, 프로젝트에 따라 Alert의 커스텀을 요하는 경우가 있다.
본인은 주로 SwiftUI를 사용해서 개발을 하고 있는데, 보통 나라면 이런 경우 ZStack을 사용해서 Alert을 띄운다.

하지만 만약 하위 뷰에서 띄워야 한다면? 원하는 데로 동작을 안할 확률이 높다. 내가 원하는 것은 부모 뷰까지 꽉 차게 나오는 것이였기에 UIKit의 도움을 받아야 할 것 같았고 구현을 시도해보았다.

Custom Alert Implementation

struct EndTicketAlert<Content>: View where Content: View{
    let content: Content
    let primaryButton: EndTicketAlertButton
    let secondButton: EndTicketAlertButton?

    init(content: () -> Content, primaryButton: () ->  EndTicketAlertButton, secondButton: (() -> EndTicketAlertButton)? = nil){
        self.content = content()
        self.primaryButton = primaryButton()
        self.secondButton = secondButton?()
    }
    
    var body: some View {
        ZStack{
            Color.black.opacity(0.3).ignoresSafeArea()
            
            content
                .padding(.bottom, 60)
                .frame(maxWidth:315, minHeight: 169)
                .overlay(
                    VStack(spacing:0){
                        if primaryButton.color == nil{
                            Divider()
                        }
                        
                        HStack(){
                            primaryButton
                            if secondButton != nil{
                                Divider()
                                secondButton
                            }
                        }
                        .frame(height:60)
                        .font(.system(size: 16,weight: .bold))
                    }
                    , alignment: .bottom)
                .background(
                    Color.white
                ).cornerRadius(10)
        }
    }
}

나의 프로젝트에서는 버튼 하나는 반드시 들어가고, 버튼은 최대 두개밖에 없었기에 이렇게 구현했다.

Button

SwiftUI의 버튼은 Label의 길이만큼 터치가 가능해 이것 또한 살짝 커스텀을 할 필요가 있을 것 같아 구현했다.

struct EndTicketAlertButton: View {
    typealias Action = () -> ()
    
    private let action: Action
    private let title: Text
    let color: Color?
    
    init(title:Text,color:Color? = nil,action: @escaping Action){
        self.title = title
        self.action = action
        self.color = color
    }
    
    var body: some View {
        Button{
            action()
        }label: {
            title.frame(maxWidth:.infinity, maxHeight: .infinity)
        }.background(color)
    }
}

별 특별한 것은 아니고 label 길이를 무제한으로 준 것 뿐이다.

View Extension

func alert<Content>(isPresented:Binding<Bool>, alert: () -> EndTicketAlert<Content>) -> some View where Content: View{
        let keyWindow = UIApplication.shared.connectedScenes.filter ({$0.activationState == .foregroundActive})
            .map({$0 as? UIWindowScene}).compactMap {$0}.first?.windows.filter { $0.isKeyWindow }.first!

        let vc = UIHostingController(rootView: alert())
        vc.modalTransitionStyle = .crossDissolve
        vc.modalPresentationStyle = .overCurrentContext
        vc.view.backgroundColor = .clear
        vc.definesPresentationContext = true
        
        return self.onChange(of: isPresented.wrappedValue, perform: {
            if $0{
                keyWindow?.rootViewController?.topViewController().present(vc,animated: true)
            }
            else{
                keyWindow?.rootViewController?.topViewController().dismiss(animated: true)
            }
        })
    }

이제 이 alert을 어떤 뷰에서도 띄울 수 있게 하기 위해 extension을 이용했다. 최상위 ViewController를 찾아 alert을 띄우게 했고 Binding으로 Present가 되는 조건을 입력받아 ViewController로 같은 동작을 수행할 수 있도록 구현했다.

결과물


2개의 댓글

comment-user-thumbnail
2022년 12월 28일

혹시 전체 소스를 볼 순 없을까요? 참고해서 적용해보려고하는데 부분소스만있어서 적용하기가 쉽지않네요 ㅠ

1개의 답글