SwiftUI

模态呈现

深入学习 SwiftUI 中的模态呈现机制,包括 Sheet、全屏覆盖、弹出框、警告框和确认对话框的使用方法与定制技巧

概述

模态呈现(Modal presentations)是一种用于吸引用户注意力并专注于特定任务的界面呈现方式。SwiftUI 提供了多种模态呈现形式,包括警告框(alert)、弹出框(popover)、工作表(sheet)和确认对话框(confirmation dialog)。

在 SwiftUI 中,通过视图修饰符创建模态呈现,并使用绑定(Binding)控制呈现条件。当条件满足时,SwiftUI 自动触发呈现;当用户关闭时,SwiftUI 会重置绑定值。

Sheet 工作表

Sheet 是一种从屏幕底部向上滑出的模态视图,适合展示需要用户关注但不完全遮挡底层内容的界面。

基本用法

使用 sheet(isPresented:onDismiss:content:) 修饰符创建 sheet:

struct ContentView: View {
    @State private var showingSheet = false
    
    var body: some View {
        Button("显示 Sheet") {
            showingSheet = true
        }
        .sheet(isPresented: $showingSheet) {
            SheetContentView()
        }
    }
}

struct SheetContentView: View {
    @Environment(\.dismiss) private var dismiss
    
    var body: some View {
        NavigationStack {
            VStack(spacing: 20) {
                Text("这是 Sheet 内容")
                    .font(.title)
                
                Text("可以在这里放置任何视图")
                    .foregroundColor(.secondary)
            }
            .navigationTitle("Sheet 标题")
            .toolbar {
                ToolbarItem(placement: .cancellationAction) {
                    Button("关闭") {
                        dismiss()
                    }
                }
            }
        }
    }
}

使用 Item 绑定

除了布尔值绑定,还可以使用 sheet(item:onDismiss:content:) 传递数据:

struct User: Identifiable {
    let id: UUID
    let name: String
}

struct ContentView: View {
    @State private var selectedUser: User?
    
    var body: some View {
        Button("选择用户") {
            selectedUser = User(id: UUID(), name: "张三")
        }
        .sheet(item: $selectedUser) { user in
            UserDetailView(user: user)
        }
    }
}

struct UserDetailView: View {
    let user: User
    @Environment(\.dismiss) private var dismiss
    
    var body: some View {
        VStack {
            Text("用户: \(user.name)")
                .font(.title)
            
            Button("关闭") {
                dismiss()
            }
            .padding()
        }
    }
}

自定义 Sheet 高度

使用 presentationDetents(_:) 设置 sheet 的可用高度:

struct SettingsView: View {
    @State private var showSettings = false
    
    var body: some View {
        Button("查看设置") {
            showSettings = true
        }
        .sheet(isPresented: $showSettings) {
            SettingsContentView()
                .presentationDetents([.medium, .large])
        }
    }
}

struct SettingsContentView: View {
    var body: some View {
        VStack {
            Text("设置")
                .font(.headline)
            
            List {
                Toggle("通知", isOn: .constant(true))
                Toggle("深色模式", isOn: .constant(false))
            }
        }
        .padding()
    }
}

PresentationDetent 提供了几种预设高度:

  • .medium: 中等高度(约屏幕一半)
  • .large: 大高度(几乎全屏)
  • .fraction(Double): 自定义比例
  • .height(CGFloat): 固定高度

控制拖动指示器

使用 presentationDragIndicator(_:) 控制是否显示拖动指示器:

.sheet(isPresented: $showSettings) {
    SettingsContentView()
        .presentationDetents([.medium, .large])
        .presentationDragIndicator(.visible)
}

全屏覆盖

全屏覆盖(Full Screen Cover)会完全占据屏幕,适合需要用户完全专注的任务。

基本用法

struct ContentView: View {
    @State private var showingFullScreen = false
    
    var body: some View {
        Button("显示全屏") {
            showingFullScreen = true
        }
        .fullScreenCover(isPresented: $showingFullScreen) {
            FullScreenContentView()
        }
    }
}

struct FullScreenContentView: View {
    @Environment(\.dismiss) private var dismiss
    
    var body: some View {
        ZStack {
            Color.blue.ignoresSafeArea()
            
            VStack {
                Text("全屏模态视图")
                    .font(.largeTitle)
                    .foregroundColor(.white)
                
                Text("点击屏幕关闭")
                    .foregroundColor(.white.opacity(0.8))
            }
        }
        .onTapGesture {
            dismiss()
        }
    }
}

禁用交互式关闭

使用 interactiveDismissDisabled(_:) 防止用户通过手势关闭:

.fullScreenCover(isPresented: $showingFullScreen) {
    FullScreenContentView()
        .interactiveDismissDisabled(true)
}

Popover 弹出框

Popover 是一种指向特定 UI 元素的浮动视图,常用于 iPad 和 Mac。

基本用法

struct ContentView: View {
    @State private var showingPopover = false
    
    var body: some View {
        Button("显示 Popover") {
            showingPopover = true
        }
        .popover(isPresented: $showingPopover, arrowEdge: .bottom) {
            PopoverContentView()
        }
    }
}

struct PopoverContentView: View {
    var body: some View {
        VStack(alignment: .leading, spacing: 10) {
            Text("Popover 内容")
                .font(.headline)
            
            Text("这是一个弹出框")
            
            Divider()
            
            Button("选项 1") { }
            Button("选项 2") { }
        }
        .padding()
        .frame(width: 200)
    }
}

自定义锚点

使用 PopoverAttachmentAnchor 精确控制 popover 的附着位置:

.popover(
    isPresented: $showingPopover,
    attachmentAnchor: .rect(.bounds),
    arrowEdge: .top
) {
    PopoverContentView()
}

Alert 警告框

Alert 用于向用户展示重要信息或请求确认操作。

基本 Alert

struct ContentView: View {
    @State private var showingAlert = false
    
    var body: some View {
        Button("显示警告") {
            showingAlert = true
        }
        .alert("警告标题", isPresented: $showingAlert) {
            Button("确定", role: .cancel) { }
        }
    }
}

带多个按钮的 Alert

struct DeleteConfirmation: View {
    @State private var showingAlert = false
    @State private var itemDeleted = false
    
    var body: some View {
        VStack {
            Button("删除项目") {
                showingAlert = true
            }
            
            if itemDeleted {
                Text("已删除")
                    .foregroundColor(.red)
            }
        }
        .alert("确认删除", isPresented: $showingAlert) {
            Button("取消", role: .cancel) { }
            Button("删除", role: .destructive) {
                itemDeleted = true
            }
        } message: {
            Text("此操作无法撤销")
        }
    }
}

带数据的 Alert

使用 alert(_:isPresented:presenting:actions:message:) 传递数据:

struct ErrorAlert: View {
    @State private var error: AppError?
    
    var body: some View {
        Button("触发错误") {
            error = AppError(message: "网络连接失败")
        }
        .alert(
            "错误",
            isPresented: Binding(
                get: { error != nil },
                set: { if !$0 { error = nil } }
            ),
            presenting: error
        ) { error in
            Button("重试") {
                // 重试操作
            }
            Button("取消", role: .cancel) { }
        } message: { error in
            Text(error.message)
        }
    }
}

struct AppError {
    let message: String
}

Confirmation Dialog 确认对话框

确认对话框提供多个操作选项,常用于破坏性操作的确认。

基本用法

struct ContentView: View {
    @State private var showingDialog = false
    
    var body: some View {
        Button("清空回收站") {
            showingDialog = true
        }
        .confirmationDialog(
            "永久删除回收站中的项目?",
            isPresented: $showingDialog
        ) {
            Button("清空回收站", role: .destructive) {
                // 执行清空操作
            }
            Button("取消", role: .cancel) { }
        }
    }
}

带多个选项

struct ExportOptions: View {
    @State private var showingOptions = false
    
    var body: some View {
        Button("导出") {
            showingOptions = true
        }
        .confirmationDialog(
            "选择导出格式",
            isPresented: $showingOptions,
            titleVisibility: .visible
        ) {
            Button("PDF") {
                exportAsPDF()
            }
            Button("图片") {
                exportAsImage()
            }
            Button("文本") {
                exportAsText()
            }
            Button("取消", role: .cancel) { }
        } message: {
            Text("选择一个格式来导出文档")
        }
    }
    
    func exportAsPDF() { }
    func exportAsImage() { }
    func exportAsText() { }
}

自定义呈现样式

背景样式

使用 presentationBackground(_:) 自定义背景:

.sheet(isPresented: $showSettings) {
    SettingsView()
        .presentationBackground(.ultraThinMaterial)
}

支持的背景样式包括:

  • .thinMaterial.regularMaterial.thickMaterial.ultraThinMaterial.ultraThickMaterial
  • 任何符合 ShapeStyle 的类型,如颜色、渐变等

圆角半径

.sheet(isPresented: $showSettings) {
    SettingsView()
        .presentationCornerRadius(30)
}

背景交互

控制用户能否与背景内容交互:

.sheet(isPresented: $showSettings) {
    SettingsView()
        .presentationBackgroundInteraction(.enabled)
}

关闭呈现

使用 Environment 的 dismiss

最常用的方式是通过环境值 dismiss:

struct DetailView: View {
    @Environment(\.dismiss) private var dismiss
    
    var body: some View {
        Button("关闭") {
            dismiss()
        }
    }
}

检查呈现状态

使用 isPresented 环境值检查视图是否被呈现:

struct ContentView: View {
    @Environment(\.isPresented) private var isPresented
    
    var body: some View {
        if isPresented {
            Text("此视图正在被呈现")
        } else {
            Text("此视图未被呈现")
        }
    }
}

禁用交互式关闭

防止用户通过手势或快捷键关闭:

.sheet(isPresented: $showingSheet) {
    UnsavedChangesView()
        .interactiveDismissDisabled(hasUnsavedChanges)
}

紧凑环境适配

在紧凑环境(如 iPhone 横屏)中,sheet 默认会自动变为全屏。使用 presentationCompactAdaptation(_:) 控制此行为:

.sheet(isPresented: $showSettings) {
    SettingsView()
        .presentationCompactAdaptation(.popover)
}

或分别控制水平和垂直方向:

.sheet(isPresented: $showSettings) {
    SettingsView()
        .presentationCompactAdaptation(
            horizontal: .sheet,
            vertical: .fullScreenCover
        )
}

最佳实践

  1. 选择合适的呈现类型:

    • Alert 用于重要通知和简单确认
    • Confirmation Dialog 用于多选项操作
    • Sheet 用于中等复杂度的任务
    • Full Screen Cover 用于需要完全专注的任务
    • Popover 用于上下文相关的辅助信息
  2. 提供关闭方式: 始终为模态视图提供明确的关闭方式,避免用户被困在模态界面中。

  3. 合理使用 interactiveDismissDisabled: 仅在有未保存更改等重要原因时禁用交互式关闭。

  4. 利用 onDismiss 回调: 在模态视图关闭后执行必要的清理或状态更新。

  5. 注意层级: 避免在模态视图上叠加过多层级的模态呈现,这会让用户感到困惑。

  6. 适配不同平台: 在 iPad 和 Mac 上,某些模态呈现(如 confirmationDialog)的表现与 iPhone 不同,应针对性测试。

总结

SwiftUI 的模态呈现系统提供了丰富而灵活的界面呈现方式。通过合理使用 sheet、fullScreenCover、popover、alert 和 confirmationDialog,配合各种定制修饰符,可以创建出既美观又易用的用户界面。关键在于根据具体场景选择合适的呈现类型,并为用户提供清晰的交互路径。

在 GitHub 上编辑

上次更新于