SwiftUI:创建自定义警报

gmo*_*eda 5 xcode ios swiftui

我正在尝试抽象一个在我的应用程序的多个地方使用的警报。

我复制并粘贴了它的实现func alert(isPresented: Binding<Bool>, content: () -> Alert) -> some View并对其进行了调整以适应我的使用:

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())
    }
}
Run Code Online (Sandbox Code Playgroud)

我的计划是在类似视图上调用它.externalURLAlert(isPresented: $isPresented, action: someAction),但我无法编译该函数。

我收到的错误如下:

初始化器“init(_:)”要求“Alert”符合“View”

Den*_*Den 18

演示截图

您可以根据自己的设计进行定制。



演示.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
Run Code Online (Sandbox Code Playgroud)

自定义警报.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
Run Code Online (Sandbox Code Playgroud)

自定义AlertButton.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)
    }
}
Run Code Online (Sandbox Code Playgroud)

自定义警报修改器.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
    }
}
Run Code Online (Sandbox Code Playgroud)

视图扩展.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))
    }
}
Run Code Online (Sandbox Code Playgroud)


Emi*_*aez 2

修饰符的工作方式是返回调用它们的视图的修改版本。如果您调用,您会收到一个具有新前景色的Text("").foregroundColor(...)新视图。Text与警报相同,如果您调用Text("").alert(...,您会收到一个Text可以在顶部显示警报的视图。

另一方面,您的修改器完全删除了该层次结构,并将其替换为空视图或警报,但此警报没有关于应在何处显示的信息。

如果您想要显示标准化警报,您应该利用现有的修饰符和您自己的参数,如下所示:

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()
                }
            )
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

请注意 的使用self,因为我们想要维护层次结构,并且.alert(...)因为我们正在使用已经知道如何显示警报的现有系统修饰符。