Rust in Everything
#SwiftUI#Swift#iOS

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 项目中为用户提供更丰富的交互体验。祝开发顺利!

评论区

登录后即可发表评论

加载评论中…

💬 关联讨论

发起讨论
加载中...