skip to Main Content

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


  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"

    Login or Signup to reply.
  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.

    Login or Signup to reply.
  3. 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))
        }
    }
    
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search