You need to enable JavaScript to run this app.
最新活动
大模型
产品
解决方案
定价
生态与合作
支持与服务
开发者
了解我们

SwiftUI中实现可在Sheet/Full Screen Cover等所有视图上方显示的自定义弹窗

SwiftUI中实现可在Sheet/Full Screen Cover等所有视图上方显示的自定义弹窗

太懂你这种困扰了!系统自带的.alert虽然能乖乖显示在所有视图最上层,但想加个自定义图标、改个按钮样式就完全没辙,自己做的弹窗又被Sheet或者Full Screen Cover压在下面,折腾好久都搞不定对吧?

我给你分享一个亲测有效的方案——把自定义弹窗挂载在App根视图的最上层,用状态管理器来控制显示,这样不管你弹出多少层Sheet或者全屏覆盖视图,弹窗都能稳稳顶在最上面,而且完全支持自定义样式。

步骤1:创建弹窗状态管理器

先写一个ObservableObject类,用来统一管理弹窗的显示状态、标题、按钮动作这些内容,这样任何视图都能轻松调用它:

import SwiftUI

class CustomAlertManager: ObservableObject {
    @Published var isShowing = false
    // 可根据需求扩展更多自定义属性,比如图标、按钮颜色等
    var title: String = ""
    var message: String = ""
    var primaryButtonTitle: String = ""
    var primaryAction: (() -> Void)?
    var secondaryButtonTitle: String?
    var secondaryAction: (() -> Void)?
    
    // 封装显示弹窗的方法,按需传参
    func show(title: String, message: String, primaryButtonTitle: String, primaryAction: (() -> Void)? = nil, secondaryButtonTitle: String? = nil, secondaryAction: (() -> Void)? = nil) {
        self.title = title
        self.message = message
        self.primaryButtonTitle = primaryButtonTitle
        self.primaryAction = primaryAction
        self.secondaryButtonTitle = secondaryButtonTitle
        self.secondaryAction = secondaryAction
        isShowing = true
    }
    
    // 隐藏弹窗
    func dismiss() {
        isShowing = false
    }
}

步骤2:实现自定义弹窗视图

接下来写你的自定义弹窗UI,想加图标、改按钮样式都随便来:

struct CustomAlertView: View {
    @ObservedObject var manager: CustomAlertManager
    
    var body: some View {
        if manager.isShowing {
            // 半透明背景,点击可关闭
            Color.black.opacity(0.5)
                .ignoresSafeArea()
                .onTapGesture {
                    manager.dismiss()
                }
                .overlay(
                    // 弹窗主体
                    VStack(spacing: 16) {
                        // 自定义图标,这里用SF Symbol举例,你可以换成自己的图标
                        Image(systemName: "exclamationmark.triangle")
                            .font(.system(size: 40))
                            .foregroundColor(.orange)
                        
                        Text(manager.title)
                            .font(.headline)
                            .foregroundColor(.primary)
                        
                        Text(manager.message)
                            .font(.body)
                            .foregroundColor(.secondary)
                            .multilineTextAlignment(.center)
                            .padding(.horizontal)
                        
                        // 自定义按钮组
                        HStack(spacing: 12) {
                            if let secondaryTitle = manager.secondaryButtonTitle {
                                Button(secondaryTitle) {
                                    manager.secondaryAction?()
                                    manager.dismiss()
                                }
                                .padding(.vertical, 8)
                                .padding(.horizontal, 16)
                                .background(Color.gray.opacity(0.1))
                                .cornerRadius(8)
                            }
                            
                            Button(manager.primaryButtonTitle) {
                                manager.primaryAction?()
                                manager.dismiss()
                            }
                            .padding(.vertical, 8)
                            .padding(.horizontal, 16)
                            .background(Color.blue)
                            .foregroundColor(.white)
                            .cornerRadius(8)
                        }
                    }
                    .padding(24)
                    .background(Color.white)
                    .cornerRadius(16)
                    .shadow(radius: 10)
                    .padding(.horizontal, 24)
                )
                // 加个过渡动画,更丝滑
                .transition(.opacity.combined(with: .scale))
                .animation(.easeInOut, value: manager.isShowing)
        }
    }
}

步骤3:把弹窗挂载到App根视图

在你的App入口里,把弹窗作为overlay加在根视图最外层,同时把状态管理器注入环境变量:

@main
struct YourApp: App {
    @StateObject private var alertManager = CustomAlertManager()
    
    var body: some Scene {
        WindowGroup {
            MainView()
                .environmentObject(alertManager)
                // 把弹窗放在根视图的overlay里,确保层级最高
                .overlay(CustomAlertView(manager: alertManager))
        }
    }
}

步骤4:在任意视图(包括Sheet里)调用弹窗

现在不管是在主视图还是Sheet里的子视图,都能轻松触发这个弹窗了:

// 主视图
struct MainView: View {
    @EnvironmentObject private var alertManager: CustomAlertManager
    @State private var isShowingSheet = false
    
    var body: some View {
        Button("打开Sheet") {
            isShowingSheet = true
        }
        .sheet(isPresented: $isShowingSheet) {
            SheetContentView()
                .environmentObject(alertManager) // 把管理器传给Sheet里的视图
        }
    }
}

// Sheet里的视图
struct SheetContentView: View {
    @EnvironmentObject private var alertManager: CustomAlertManager
    
    var body: some View {
        Button("显示自定义弹窗") {
            alertManager.show(
                title: "搞定啦!",
                message: "这个弹窗终于能显示在Sheet上面了,而且按钮样式、图标都能随便改~",
                primaryButtonTitle: "确认",
                primaryAction: {
                    print("确认按钮被点击")
                },
                secondaryButtonTitle: "取消",
                secondaryAction: {
                    print("取消按钮被点击")
                }
            )
        }
    }
}

原理说明

因为我们把自定义弹窗放在了App根视图的overlay里,它属于整个App视图层级的最顶层。而Sheet、Full Screen Cover这些模态视图都是根视图的子层级,所以弹窗自然能显示在它们上面。用ObservableObject管理状态,能让任何子视图都方便地触发和控制弹窗,完全不用操心层级问题。

备注:内容来源于stack exchange,提问作者tech_human

火山引擎 最新活动