간단한 커스텀 바인딩으로 만든 코드

custom binding 내용을 알면 다음 코드가 가능하다는 것을 알 수 있습니다. 에러의 값이 바뀔 때 alert의 true, false로 변환하는 코드입니다.

extension Binding where Value == Bool {
    
    init<T>(value: Binding<T?>) {
        self.init(get: {
            return value.wrappedValue != nil ? true : false
        }, set: { newValue in
            value.wrappedValue = nil
        })
    }
}
struct ContentView: View {
    
    @State private var alert: MyCustomAlert? = nil
    
    var body: some View {
        Button("클릭") {
            saveData()
        }
        .alert(alert?.title ?? "에러", isPresented: Binding(value: $alert), actions: {
            alert?.getButtonAlert()
        }) {
            if let subtitle = alert?.subtitle {
                Text(subtitle)
            }
        }
    }

 

위에서 MyCustomAlert는 Error 타입의 enum 열거형이며 enum을 만들게 되면 사용되는 것들을 한번에 볼 수 있어서 편리해지기 때문에 유용하게 사용될 수 있습니다.

private func saveData() {
    let isSuccessful: Bool = false

    if isSuccessful {

    } else {
        alert = .noInternetConnection
    }
}

 

버튼을 누르게 되면 .noInternetConnection으로 Error 값이 설정됩니다.

 

 

enum으로 에러 정의하기

enum에 다음처럼 정의하게 된다면 MyCustomAlert.dataNotFound.title 또는 MyCustomAlert.dataNotFound.subtitle 로 아래 String 데이터들을 접근할 수 있게 됩니다.

enum MyCustomAlert: Error, LocalizedError {
        case noInternetConnection
        case dataNotFound
        case urlError(error: Error)

        var title: String {
            switch self {
            case .noInternetConnection:
                return "No Internet Connection"
            case .dataNotFound:
                return "No Data"
            case .urlError:
                return "Error"
            }
        }

        var subtitle: String? {
            switch self {
            case .noInternetConnection:
                return "Please check your internet connection and try again."
            case .dataNotFound:
                return nil
            case .urlError(error: let error):
                return "Error: \(error.localizedDescription)"
            }
        }

 

@ViewBuilder를 사용해 에러 종류의 따른 버튼 다르게 가져오기

엄청 유용한 기능으로 @ViewBuilder 와 함께 버튼을 만들게 된다면 에러에 따른 다른 버튼들을 사용할 수 있게 됩니다!

@ViewBuilder func getButtonAlert() -> some View {
    switch self {
    case .noInternetConnection:
        Button("확인")     {

        }
    case .dataNotFound:
        Button("재시도") {

        }
    default:
        Button("삭제") {

        }
    }
}

 

좀 더 깊게 파보면 만약 .noInternetConnection을 선택했을 때 @ViewBuilder로 뷰를 만들어 특정 동작을 취하게 하게 할 수도 있습니다. 다음 예시는 noInternetConnection 에서 onOkPressed 와 onRetryPressed 를 받도록 하였습니다.

enum MyCustomAlert: Error, LocalizedError {
    case noInternetConnection(onOkPressed: () -> Void,  onRetryPressed: () -> Void)

 

그리고 위헤서 받은 onOkPressed는 확인 버튼을 눌렀을 때 동작하는 코드를 나타내고 onRetryPressed 는 재시도 버튼을 눌렀을 때 동작하는 코드를 나타냅니다.

@ViewBuilder var getButtonAlert: some View {
    switch self {
    case .noInternetConnection(onOkPressed: let onOkPressed, onRetryPressed: let onRetryPressed):
        Button("확인") {
            onOkPressed()
        }
        Button("재시도") {
            onRetryPressed()
        }

 

아래처럼 사용될 수 있습니다.

alert = .noInternetConnection(onOkPressed: {
    print("확인 버튼 클릭")
}, onRetryPressed: {
    print("재시도 버튼 클릭")
})

 

View extension으로 클린 코드 만들기

다음으로는 코드를 좀 더 클린하게 만들기 위해서 View를 extension으로 showCustomAlert를 호출하면 사용자한테 알람을 보여주는 기존에 했던 코드를 한줄의 코드로 만들게끔 작성한 코드입니다.

wrappedValue는 Binding 값이 찾조하는 값입니다. 즉 Binding<T?>의 값으로 들어온 MyCustomAlert의 값을 의미합니다.
extension View {
    
    func showCustomAlert<T: AppAlert>(alert: Binding<T?>) -> some View {
        self
            .alert(alert.wrappedValue?.title ?? "에러", isPresented: Binding(value: alert), actions: {
                alert.wrappedValue?.buttons
            }) {
                if let subtitle = alert.wrappedValue?.subtitle {
                    Text(subtitle)
                    
                }
            }
    }
}

 

위처럼 구현하게 되면 아래 코드처럼 한출의 코드로 바뀌게 됩니다.

var body: some View {
    Button("클릭") {
        saveData()
    }
    .showCustomAlert(alert: $alert)
}

 

위에서 AppAlert 프로토콜을 준수하는 제너릭 T이 들어가 있는데 이것은 알람에서 주로 사용되는데 MyCustomAlert에서도 들어가는 요소들을 포함하는 프로토콜이며 코드를 좀 더 유연하게 만들어줍니다.

protocol AppAlert {
    var title: String { get }
    var subtitle: String { get }
    var buttons: AnyView { get }
}

전체 코드

import SwiftUI

protocol AppAlert {
    var title: String { get }
    var subtitle: String { get }
    var buttons: AnyView { get }
}

extension Binding where Value == Bool {
    
    init<T>(value: Binding<T?>) {
        self.init(get: {
            return value.wrappedValue != nil ? true : false
        }, set: { newValue in
            value.wrappedValue = nil
        })
    }
}

extension View {
    
    func showCustomAlert<T: AppAlert>(alert: Binding<T?>) -> some View {
        self
            .alert(alert.wrappedValue?.title ?? "에러", isPresented: Binding(value: alert), actions: {
                alert.wrappedValue?.buttons
            }) {
                if let subtitle = alert.wrappedValue?.subtitle {
                    Text(subtitle)
                    
                }
            }
    }
}

struct ContentView: View {
    
    @State private var alert: MyCustomAlert? = nil
    
    var body: some View {
        Button("클릭") {
            saveData()
        }
        .showCustomAlert(alert: $alert)
    }
    

    
        
//    enum MyCustomError: Error, LocalizedError {
//        case noInternetConnection
//        case dataNotFound
//        case urlError(error: Error)
//        
//        var errorDescription: String? {
//            switch self {
//            case .noInternetConnection:
//                return "Please check your internet connection and try again."
//            case .dataNotFound:
//                return "There was an error loading data. Please try again."
//            case .urlError(error: let error):
//                return "Error: \(error.localizedDescription)"
//            }
//        }
//    }
    
    enum MyCustomAlert: Error, LocalizedError, AppAlert {
        case noInternetConnection(onOkPressed: () -> Void,  onRetryPressed: () -> Void)
        case dataNotFound
        case urlError(error: Error)
        
        var errorDescription: String? {
            switch self {
            case .noInternetConnection:
                return "Please check your internet connection and try again."
            case .dataNotFound:
                return "There was an error loading data. Please try again."
            case .urlError(error: let error):
                return "Error: \(error.localizedDescription)"
            }
        }
        
        var title: String {
            switch self {
            case .noInternetConnection:
                return "No Internet Connection"
            case .dataNotFound:
                return "No Data"
            case .urlError:
                return "Error"
            }
        }
        
        var subtitle: String {
            switch self {
            case .noInternetConnection:
                return "Please check your internet connection and try again."
            case .dataNotFound:
                return "Data Not Found check again"
            case .urlError(error: let error):
                return "Error: \(error.localizedDescription)"
            }
        }
        
        var buttons: AnyView {
            AnyView(getButtonAlert)
        }
        
        @ViewBuilder var getButtonAlert: some View {
            switch self {
            case .noInternetConnection(onOkPressed: let onOkPressed, onRetryPressed: let onRetryPressed):
                Button("확인") {
                    onOkPressed()
                }
                Button("재시도") {
                    onRetryPressed()
                }
            case .dataNotFound:
                Button("재시도") {
                    
                }
            default:
                Button("삭제") {
                    
                }
            }
        }
    }
    
    private func saveData() {
        let isSuccessful: Bool = false
        
        if isSuccessful {
            
        } else {
            alert = .noInternetConnection(onOkPressed: {
                print("확인 버튼 클릭")
            }, onRetryPressed: {
                print("재시도 버튼 클릭")
            })
        }
    }
}

#Preview {
    ContentView()
}

 

ytw_developer