跳到主要内容

SwiftUI 模态框详解:Modal View、悬浮按钮与 Alert 全面解析

鱼雪

上一章节中,我们已经学习了如何使用 NavigationStack 为用户提供页面间的导航。 本章将探讨另一种常见的内容展示方式——模态视图(Modal View)。

在 iOS 中,模态框常用于提示用户输入或显示新内容,例如创建提醒、填写表单等。 我们还会学习如何在模态视图中加入自定义悬浮按钮(Floating Button)以及如何使用 Alert 发出系统弹窗提示。

目录


什么是模态视图 (Modal View)

模态视图在 iOS 中是一种将新内容或功能覆盖在当前界面之上的展示方式, 它能阻止用户回到之前的内容,直到他们关闭该模态视图。

一般用于以下场景:

  • 需要用户专注于当前任务(如填写表单或阅读重要通知)。
  • 需要用户进行确认或输入才可继续(如新建日历事件、添加提醒)。

Card 式模态的默认外观

iOS 13 开始,系统默认使用卡片式(Card-like)呈现模态视图,而非覆盖全屏。 卡片会从屏幕底部向上滑入,并在顶部留出一部分主视图可见。 当用户完成查看或操作后,可通过下拉手势或关闭按钮来关闭卡片。

SwiftUI 中,我们可以使用 .sheet 修饰器来轻松实现这种卡片式模态视图。

使用 .sheet(isPresented:) 展示模态视图

基本用法

要使用 sheet(isPresented:),我们通常需要一个 @State 修饰的布尔值来控制模态视图是否显示。

例如:

.sheet(isPresented: $showModal) {
DetailView()
}
  • showModal: 用于控制模态视图的展示 (true) 或隐藏 (false)。
  • DetailView(): 在模态视图中展示的具体内容。

实战演示

假设我们有一个文章列表,需要在用户点击文章时,以模态方式展示文章详情。

可以这样做:

  1. ContentView 中添加一个布尔状态变量,和一个用于存储用户选中内容的 selectedArticle

    @State var showDetailView = false
    @State var selectedArticle: Article?
  2. List 的每个 ArticleRow 中添加 onTapGesture,当用户点击时, 设置 showDetailView = true,并记录所选文章:

    .onTapGesture {
    self.showDetailView = true
    self.selectedArticle = article
    }
  3. 最后,使用 sheet(isPresented:) 进行模态展示:

    .sheet(isPresented: $showDetailView) {
    if let selectedArticle = self.selectedArticle {
    ArticleDetailView(article: selectedArticle)
    }
    }

这样,当用户点击文章行时,就会弹出一张卡片式的模态视图,显示 ArticleDetailView

使用可选绑定 .sheet(item:) 展示模态视图

为什么可选绑定更简洁

另一种方式是通过 .sheet(item:),它接受一个可选的绑定对象(如 @State var selectedArticle: Article?)。 只要这个可选对象非空,就会弹出模态视图,空值则不展示。相比使用 Bool 值,这种写法更直接:

.sheet(item: $selectedArticle) { article in
ArticleDetailView(article: article)
}
  • selectedArticle 不为 nil 时,系统会以模态形式展示对应的 ArticleDetailView
  • 在使用该方式时,就无需额外管理一个 showDetailView 布尔变量,简化了代码逻辑。

在模态视图中添加悬浮按钮以便关闭

iOS 13+ 的卡片模态视图支持下拉手势关闭,但对部分新用户来说不够直观。 我们可以在详情页上加入一个悬浮按钮(Floating Button)来手动关闭模态

  1. 在详情视图中引入 @Environment(\.dismiss)

    @Environment(\.dismiss) var dismiss
  2. 在界面中使用 overlay 修饰器,让按钮始终悬浮在右上角:

    .overlay(
    HStack {
    Spacer()
    VStack {
    Button {
    dismiss()
    } label: {
    Image(systemName: "chevron.down.circle.fill")
    .font(.largeTitle)
    .foregroundStyle(.white)
    }
    .padding(.trailing, 20)
    .padding(.top, 40)

    Spacer()
    }
    }
    )

这样,即使用户在浏览列表时向下滚动,按钮也会始终固定在右上角,方便随时关闭模态视图。

使用 Alerts 发出警告或提示

Alert 的基本用法

Alert 在 iOS 中是一种更具阻断性的模态,对用户的交互要求更高。 通过 SwiftUI 中的 .alert 修饰器可以实现该功能。

典型的语法如下:

.alert("Warning", isPresented: $showAlert, actions: {
Button("Confirm") {
// 确定操作
}
Button(role: .cancel) {
// 取消操作
} label: {
Text("Cancel")
}
}, message: {
Text("Are you sure?")
})
  • isPresented: $showAlert: 使用布尔变量控制 Alert 的显示或隐藏。
  • actions: 定义 Alert 中的按钮。
  • message: 显示在标题下方的详细文案。

在模态视图中使用 Alert

可以在关闭按钮处触发一则询问是否关闭的 Alert。例如,当用户点击“关闭”时,弹出 Alert, 用户若选择 “Yes” 则 dismiss(),选择 “No” 则取消关闭:

@State private var showAlert = false

Button(action: {
self.showAlert = true
}) {
Image(systemName: "chevron.down.circle.fill")
...
}
.alert("Reminder", isPresented: $showAlert, actions: {
Button("Yes") {
dismiss()
}
Button(role: .cancel) {
Text("No")
}
}, message: {
Text("Do you want to dismiss this article?")
})

全屏模态视图:.fullScreenCover

如果你想要在 iOS 13 之后的系统中恢复全屏覆盖的模态效果, 可以使用 .fullScreenCover(iOS 14+ 引入)。它的用法与 sheet 类似, 只是呈现形式是全屏占据:

.fullScreenCover(item: $selectedArticle) { article in
ArticleDetailView(article: article)
}

selectedArticle 非空时,会自动加载全屏视图并覆盖整个屏幕。

总结

  • .sheet.fullScreenCover:都能实现模态视图,差别在于 .sheet 默认卡片式,.fullScreenCover 全屏覆盖。
  • 可选绑定与布尔绑定:二者都能控制模态视图的显示,使用可选绑定时可让代码更简洁。
  • 悬浮按钮:可以通过 overlay 修饰器实现,结合 @Environment(\.dismiss) 即可随时关闭模态视图。
  • Alert:更强的阻断式弹窗,需要用户做出选择后才可解除;常用于警告、确认操作等场景。

在了解这套模式后,你可以灵活地在各类应用场景中使用卡片式或全屏模态,让用户以直觉的方式进行交互, 并在需要时辅以 Alert 进行必要的确认或警告提示。

完整示例代码

以下示例代码演示了一个完整的文章列表与详情的交互场景,包含了 全屏模态悬浮按钮 以及 Alert 的使用, 便于你在实际项目中直接引用或二次开发。

import SwiftUI

struct ContentView: View {
@State private var selectedArticle: Article?

var body: some View {
NavigationStack {
List(articles) { article in
ArticleRow(article: article)
.onTapGesture {
self.selectedArticle = article
}
.listRowSeparator(.hidden)
}
.listStyle(.plain)
// 使用 fullScreenCover 让模态全屏显示
.fullScreenCover(item: $selectedArticle) { article in
ArticleDetailView(article: article)
}
.navigationTitle("Your Reading")
}
}
}

struct ArticleDetailView: View {
@Environment(\.dismiss) var dismiss
@State private var showAlert = false
var article: Article

var body: some View {
ScrollView {
VStack(alignment: .leading) {
Image(article.image)
.resizable()
.aspectRatio(contentMode: .fit)

Group {
Text(article.title)
.font(.system(.title, design: .rounded))
.fontWeight(.black)
.lineLimit(3)

Text(article.author)
.font(.subheadline)
.foregroundColor(.secondary)
}
.padding(.bottom, 0)
.padding(.horizontal)

Text(article.content)
.font(.body)
.padding()
.lineLimit(1000)
.multilineTextAlignment(.leading)
}
}
// 悬浮关闭按钮
.overlay(
HStack {
Spacer()
VStack {
Button(action: {
self.showAlert = true
}) {
Image(systemName: "chevron.down.circle.fill")
.font(.largeTitle)
.foregroundStyle(.white)
}
.padding(.trailing, 20)
.padding(.top, 40)

Spacer()
}
}
)
// Alert 提示:用户可选择是否关闭模态
.alert("Reminder", isPresented: $showAlert, actions: {
Button("Yes") {
dismiss()
}
Button(role: .cancel) {
Text("No")
}
}, message: {
Text("Do you want to dismiss this article?")
})
// 让图片内容可以延伸到安全区域外
.ignoresSafeArea(.all, edges: .top)
}
}

struct ArticleRow: View {
var article: Article

var body: some View {
VStack(alignment: .leading, spacing: 6) {
Image(article.image)
.resizable()
.aspectRatio(contentMode: .fit)
.cornerRadius(5)

Text(article.title)
.font(.system(.title, design: .rounded))
.fontWeight(.black)
.lineLimit(3)
.padding(.bottom, 0)

Text("By \(article.author)".uppercased())
.font(.subheadline)
.foregroundColor(.secondary)
.padding(.bottom, 0)

HStack(spacing: 3) {
ForEach(1...(article.rating), id: \.self) { _ in
Image(systemName: "star.fill")
.font(.caption)
.foregroundColor(.yellow)
}
}

Text(article.excerpt)
.font(.subheadline)
.foregroundColor(.secondary)
}
}
}

// 示例数据和 Preview
struct Article: Identifiable {
var id = UUID()
var title: String
var author: String
var rating: Int
var excerpt: String
var image: String
var content: String
}

let articles = [
Article(title: "SwiftUI Basics", author: "John Doe", rating: 5,
excerpt: "A quick introduction to SwiftUI...",
image: "swiftui",
content: "Full article content goes here..."),
// 可以自行添加更多文章...
]

#Preview {
ContentView()
}

至此,你已经掌握了 模态视图(Modal View) 的多种实现方式以及 Alert 的用法, 能够在 SwiftUI 项目中为用户提供更丰富的交互体验。祝开发顺利!