6

I'm trying to abstract an Alert which is being used in multiple places across my app.

I copied and pasted the implementation of func alert(isPresented: Binding<Bool>, content: () -> Alert) -> some View and tweaked it to adapt it to my usage:

extension View {
    func externalURLAlert(isPresented: Binding<Bool>, action: ()) -> some View {
        isPresented.wrappedValue ? AnyView(Alert(
            title: Text("alert.externalURL.title".localized),
            message: Text("alert.externalURL.message".localized),
            primaryButton: .cancel(),
            secondaryButton: .default(Text("alert.externalURL.openAction.title".localized)) {
                action
            }
        )) : AnyView(EmptyView())
    }
}

My plan is to call it on a View like .externalURLAlert(isPresented: $isPresented, action: someAction) but I'm failing to get the function to compile.

The error I'm getting is the following one:

Initializer 'init(_:)' requires that 'Alert' conform to 'View'

3 Answers 3

18

Demo screenshot

You can customize to your own design.



Demo.swift

import SwiftUI

struct DemoView: View {

    // MARK: - Value
    // MARK: Private
    @State private var isAlertPresented = false


    // MARK: - View
    // MARK: Public
    var body: some View {
        ZStack {
            Button {
                isAlertPresented = true

            } label: {
                Text("Alert test")
            }
        }
        .alert(title: "title", message: "message",
           primaryButton: CustomAlertButton(title: "Yes", action: { }),
           secondaryButton: CustomAlertButton(title: "No", action: {  }),
           isPresented: $isAlertPresented)
    }
}

#if DEBUG
struct DemoView_Previews: PreviewProvider {

    static var previews: some View {
        DemoView()
            .previewDevice("iPhone 11 Pro")
    }
}
#endif


CustomAlert.swift

import SwiftUI

struct CustomAlert: View {

    // MARK: - Value
    // MARK: Public
    let title: String
    let message: String
    let dismissButton: CustomAlertButton?
    let primaryButton: CustomAlertButton?
    let secondaryButton: CustomAlertButton?
    
    // MARK: Private
    @State private var opacity: CGFloat           = 0
    @State private var backgroundOpacity: CGFloat = 0
    @State private var scale: CGFloat             = 0.001

    @Environment(\.dismiss) private var dismiss


    // MARK: - View
    // MARK: Public
    var body: some View {
        ZStack {
            dimView
    
            alertView
                .scaleEffect(scale)
                .opacity(opacity)
        }
        .ignoresSafeArea()
        .transition(.opacity)
        .task {
            animate(isShown: true)
        }
    }

    // MARK: Private
    private var alertView: some View {
        VStack(spacing: 20) {
            titleView
            messageView
            buttonsView
        }
        .padding(24)
        .frame(width: 320)
        .background(.white)
        .cornerRadius(12)
        .shadow(color: Color.black.opacity(0.4), radius: 16, x: 0, y: 12)
    }

    @ViewBuilder
    private var titleView: some View {
        if !title.isEmpty {
            Text(title)
                .font(.system(size: 18, weight: .bold))
                .foregroundColor(.black)
                .lineSpacing(24 - UIFont.systemFont(ofSize: 18, weight: .bold).lineHeight)
                .multilineTextAlignment(.leading)
                .frame(maxWidth: .infinity, alignment: .leading)
        }
    }

    @ViewBuilder
    private var messageView: some View {
        if !message.isEmpty {
            Text(message)
                .font(.system(size: title.isEmpty ? 18 : 16))
                .foregroundColor(title.isEmpty ? .black : .gray)
                .lineSpacing(24 - UIFont.systemFont(ofSize: title.isEmpty ? 18 : 16).lineHeight)
                .multilineTextAlignment(.leading)
                .frame(maxWidth: .infinity, alignment: .leading)
        }
    }

    private var buttonsView: some View {
        HStack(spacing: 12) {
            if dismissButton != nil {
                dismissButtonView
    
            } else if primaryButton != nil, secondaryButton != nil {
                secondaryButtonView
                primaryButtonView
            }
        }
        .padding(.top, 23)
    }

    @ViewBuilder
    private var primaryButtonView: some View {
        if let button = primaryButton {
            CustomAlertButton(title: button.title) {
                animate(isShown: false) {
                    dismiss()
                }
            
                DispatchQueue.main.asyncAfter(deadline: .now() + 0.7) {
                    button.action?()
                }
            }
        }
    }

    @ViewBuilder
    private var secondaryButtonView: some View {
        if let button = secondaryButton {
            CustomAlertButton(title: button.title) {
                animate(isShown: false) {
                    dismiss()
                }
        
                DispatchQueue.main.asyncAfter(deadline: .now() + 0.7) {
                    button.action?()
                }
            }
        }
    }

    @ViewBuilder
    private var dismissButtonView: some View {
        if let button = dismissButton {
            CustomAlertButton(title: button.title) {
                animate(isShown: false) {
                    dismiss()
                }
        
                DispatchQueue.main.asyncAfter(deadline: .now() + 0.7) {
                    button.action?()
                }
            }
        }
    }

    private var dimView: some View {
        Color.gray
            .opacity(0.88)
            .opacity(backgroundOpacity)
    }


    // MARK: - Function
    // MARK: Private
    private func animate(isShown: Bool, completion: (() -> Void)? = nil) {
        switch isShown {
        case true:
            opacity = 1
    
            withAnimation(.spring(response: 0.3, dampingFraction: 0.9, blendDuration: 0).delay(0.5)) {
                backgroundOpacity = 1
                scale             = 1
            }
    
            DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
                completion?()
            }
    
        case false:
            withAnimation(.easeOut(duration: 0.2)) {
                backgroundOpacity = 0
                opacity           = 0
            }
    
            DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) {
                completion?()
            }
        }
    }
}

#if DEBUG
struct CustomAlert_Previews: PreviewProvider {

    static var previews: some View {
        let dismissButton   = CustomAlertButton(title: "OK")
        let primaryButton   = CustomAlertButton(title: "OK")
        let secondaryButton = CustomAlertButton(title: "Cancel")

        let title = "This is your life. Do what you want and do it often."
        let message = """
                    If you don't like something, change it.
                    If you don't like your job, quit.
                    If you don't have enough time, stop watching TV.
                    """

        return VStack {
            CustomAlert(title: title, message: message, dismissButton: nil,           primaryButton: nil,           secondaryButton: nil)
            CustomAlert(title: title, message: message, dismissButton: dismissButton, primaryButton: nil,           secondaryButton: nil)
            CustomAlert(title: title, message: message, dismissButton: nil,           primaryButton: primaryButton, secondaryButton: secondaryButton)
        }
        .previewDevice("iPhone 13 Pro Max")
        .preferredColorScheme(.light)
    }
}
#endif


CustomAlertButton.swift

import SwiftUI

struct CustomAlertButton: View {

    // MARK: - Value
    // MARK: Public
    let title: LocalizedStringKey
    var action: (() -> Void)? = nil
    
    
    // MARK: - View
    // MARK: Public
    var body: some View {
        Button {
          action?()
        
        } label: {
            Text(title)
                .font(.system(size: 14, weight: .medium))
                .foregroundColor(.white)
                .padding(.horizontal, 10)
        }
        .frame(height: 30)
        .background(Color.purple)
        .cornerRadius(15)
    }
}


CustomAlertModifier.swift

import SwiftUI

struct CustomAlertModifier {

    // MARK: - Value
    // MARK: Private
    @Binding private var isPresented: Bool

    // MARK: Private
    private let title: String
    private let message: String
    private let dismissButton: CustomAlertButton?
    private let primaryButton: CustomAlertButton?
    private let secondaryButton: CustomAlertButton?
}


extension CustomAlertModifier: ViewModifier {

    func body(content: Content) -> some View {
        content
            .fullScreenCover(isPresented: $isPresented) {
                CustomAlert(title: title, message: message, dismissButton: dismissButton, primaryButton: primaryButton, secondaryButton: secondaryButton)
            }
    }
}

extension CustomAlertModifier {

    init(title: String = "", message: String = "", dismissButton: CustomAlertButton, isPresented: Binding<Bool>) {
        self.title         = title
        self.message       = message
        self.dismissButton = dismissButton
    
        self.primaryButton   = nil
        self.secondaryButton = nil
    
        _isPresented = isPresented
    }

    init(title: String = "", message: String = "", primaryButton: CustomAlertButton, secondaryButton: CustomAlertButton, isPresented: Binding<Bool>) {
        self.title           = title
        self.message         = message
        self.primaryButton   = primaryButton
        self.secondaryButton = secondaryButton
    
        self.dismissButton = nil
    
        _isPresented = isPresented
    }
}


ViewExtension.swift

import SwiftUI

extension View {

    func alert(title: String = "", message: String = "", dismissButton: CustomAlertButton = CustomAlertButton(title: "ok"), isPresented: Binding<Bool>) -> some View {
        let title   = NSLocalizedString(title, comment: "")
        let message = NSLocalizedString(message, comment: "")
    
        return modifier(CustomAlertModifier(title: title, message: message, dismissButton: dismissButton, isPresented: isPresented))
    }

    func alert(title: String = "", message: String = "", primaryButton: CustomAlertButton, secondaryButton: CustomAlertButton, isPresented: Binding<Bool>) -> some View {
        let title   = NSLocalizedString(title, comment: "")
        let message = NSLocalizedString(message, comment: "")
    
        return modifier(CustomAlertModifier(title: title, message: message, primaryButton: primaryButton, secondaryButton: secondaryButton, isPresented: isPresented))
    }
}
Sign up to request clarification or add additional context in comments.

4 Comments

You have missed the StyleButton class
@NishadArora Sorry for the missing part. CustomAlertButton is a minimized version of StyleButton and the code has been modified.
@Den, on iOS 14 you need to use: .onAppear { animate(isShown: true) }
I know the track from which you took text (message) to put into the alert :)
2

The way modifiers work is by returning a modified version of the view they are called on. If you call Text("").foregroundColor(...), you receive a new Text view with a new foreground color. It's the same with an alert, if you call Text("").alert(..., you receive a Text view that can display an alert on top.

Your modifier, on the other hand, completely erases that hierarchy and replaces it with either an empty view, or an alert, but this alert has no information on where it should be presented on.

If what you want is to display a standardized alert, you should leverage the existing modifier with your own parameter, like this:

extension View {
    func externalURLAlert(isPresented: Binding<Bool>, action: ()) -> some View {
        self.alert(isPresented: isPresented) {
            Alert(
                title: Text("alert.externalURL.title".localized),
                message: Text("alert.externalURL.message".localized),
                primaryButton: .cancel(),
                secondaryButton: .default(Text("alert.externalURL.openAction.title".localized)) {
                    action()
                }
            )
        }
    }
}

Notice the use of self, because we want to maintain the hierarchy, and .alert(...) because we're using the existing system modifier that already knows how to display an alert.

Comments

1

try the following:

extension View {

    @ViewBuilder
    func externalURLAlert(isPresented: Binding<Bool>, action: ()) -> some View {
       if isPresented.wrappedValue {
           Alert(
              title: Text("alert.externalURL.title".localized),
              message: Text("alert.externalURL.message".localized),
              primaryButton: .cancel(),
              secondaryButton: .default(Text("alert.externalURL.openAction.title".localized)) { action }
           )
      }

}

Plus as written in the error you need to make Alert conform to View.

Also try to use .sheet() -- this looks like you search for.

Usage sample: https://www.hackingwithswift.com/quick-start/swiftui/how-to-present-a-new-view-using-sheets

or any that you find by google -> "swiftUI usage .sheet"

3 Comments

This won't work because you're just replacing the existing hierarchy. If you call this on a NavigationView, for example, the result is either EmptyView or Alert, but the NavigationView is gone.
@EmilioPelaez I just show how to write his code by the correct way. What about the main question -- sheet() will work as he expected. And I have write about sheet in my answer.
It's not really the "correct way" if it doesn't compile (because Alert does not conform to View) and doesn't work (because even if it did, as I already said, you're clearing the view hierarchy).

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.