sheet 介绍#
在 SwiftUI 中,.sheet 是一个视图修饰符,用于展示一个模态弹窗(Modal Presentation)。它的特点是与某个视图绑定,用于呈现一个子视图。以下是关于 .sheet 的使用规范和它可以添加在哪些视图下面的详细解答。
.sheet 可以加在哪些视图下面?#
.sheet 可以添加到任意支持修饰符的视图下面。例如:
Text、Button、Image等基础视图。- 容器视图:如
VStack、HStack、ZStack。 - 复杂视图:如
List、ScrollView等。
示例 1:添加到基础视图#
struct SheetExample: View {
@State private var showSheet = false
var body: some View {
Button("Show Sheet") {
showSheet = true
}
.sheet(isPresented: $showSheet) { // Sheet 可以直接加在 Button 视图上
Text("This is a sheet")
.font(.largeTitle)
.padding()
}
}
}示例 2:添加到容器视图#
struct SheetExample: View {
@State private var showSheet = false
var body: some View {
VStack {
Text("Hello, SwiftUI!")
.padding()
Button("Show Sheet") {
showSheet = true
}
}
.sheet(isPresented: $showSheet) { // 添加到 VStack 容器上
Text("This is a sheet")
.font(.largeTitle)
.padding()
}
}
}示例 3:添加到 List 视图#
struct SheetExample: View {
@State private var showSheet = false
var body: some View {
List {
Button("Show Sheet") {
showSheet = true
}
}
.sheet(isPresented: $showSheet) { // 添加到 List 上
Text("Inside the sheet!")
}
}
}.sheet 的触发条件#
.sheet 的触发条件必须满足以下两种之一:
- 基于布尔值的弹窗触发(常见模式)。
- 基于绑定的视图模型或可选值的传递。
1. 基于布尔值的 .sheet#
这是最常见的场景,通过 @State 或 @Binding 的布尔值来控制 sheet 是否显示。
示例#
struct SheetWithBoolExample: View {
@State private var isSheetPresented = false
var body: some View {
Button("Present Sheet") {
isSheetPresented = true
}
.sheet(isPresented: $isSheetPresented) { // 绑定布尔值,控制视图呈现
VStack {
Text("Hello, this is a sheet!")
Button("Dismiss") {
isSheetPresented = false // 手动关闭 sheet
}
}
.padding()
}
}
}2. 基于模型或可选值的 .sheet(推荐用于复杂场景)#
sheet 可以绑定到一个 对象模型(optional 类型),当模型不为空时自动展示弹窗。
示例#
struct SheetWithOptionalExample: View {
@State private var selectedItem: String? = nil
var body: some View {
VStack {
Button("Select Item 1") {
selectedItem = "Item 1"
}
Button("Select Item 2") {
selectedItem = "Item 2"
}
}
.sheet(item: $selectedItem) { item in // 绑定到 Optional 类型
Text("Selected item: \(item)")
.font(.largeTitle)
.padding()
}
}
}解释:
- 当
selectedItem不为nil时,sheet自动弹出; - 传递的
item(一个字符串)用于展示不同的内容。
注意:多个 .sheet 注意不要冲突#
如果多个 .sheet 同时作用于同一个视图层级(比如都加在同一个容器或顶层视图上),它们可能会导致冲突 或者只有一个生效。
解决方案:#
- 确保只对触发
.sheet的视图绑定一个状态值。 - 如果多个弹窗需要共存,可以将
.sheet放置到子视图中。
示例:多个 .sheet#
struct MultipleSheetExample: View {
@State private var showFirstSheet = false
@State private var showSecondSheet = false
var body: some View {
VStack {
Button("Show First Sheet") {
showFirstSheet = true
}
.sheet(isPresented: $showFirstSheet) {
Text("First Sheet")
}
Button("Show Second Sheet") {
showSecondSheet = true
}
.sheet(isPresented: $showSecondSheet) {
Text("Second Sheet")
}
}
}
}专注点:
- 每个
.sheet使用独立的状态值(showFirstSheet和showSecondSheet)。
实践中不要做的操作(错误用法)#
- 多个
.sheet修饰同一个视图。
Button("Tap Me") {
// 尝试加载两个 `.sheet`,可能会发生冲突
}
.sheet(isPresented: $someCondition) {
Text("Sheet 1")
}
.sheet(isPresented: $anotherCondition) {
Text("Sheet 2")
}问题: 这种情况下,SwiftUI 的运行时将无法正确决定哪个 .sheet 生效。
解决方案#
- 将不同的
.sheet修饰符分配到不同的视图上; - 如果需要动态判断,可以用
@ViewBuilder来动态加载。
总结#
.sheet可以加在哪些视图下?- 任意支持修饰符的视图,包括
Text、Button、布局容器(如VStack)和列表(如List/ScrollView)。
- 任意支持修饰符的视图,包括
通过两种主要触发方式实现弹窗:
- 布尔值绑定(简单、常见)。
- 对象或可选值绑定(灵活、推荐用于复杂场景)。
注意事项:
- 避免在同一视图上绑定多个
.sheet。 - 对于复杂需求,建议采用
ViewBuilder或条件逻辑。
- 避免在同一视图上绑定多个
PresentationDetent#
PresentationDetent 的定义#
在 SwiftUI 中,PresentationDetent 是 SwiftUI 5.0(iOS 15 和更高版本)引入的一个功能,用于控制 sheet 的弹出高度。通过 PresentationDetent,开发者可以设置 sheet 的不同高度,并允许用户在这些高度之间切换,如 .medium 和 .large。这一功能使得 sheet 的使用更加灵活和直观。
标准高度#
PresentationDetent 提供了几个预定义的标准高度:
.medium:高度覆盖屏幕的一半,大约 50%。.large:高度覆盖整个屏幕(全屏)。.fraction(CGFloat)(iOS 16 及更高版本):使用屏幕高度的一个特定百分比。
此外,在 UIKit 的 UISheetPresentationController 中,还可以自定义高度。
用法#
要使用 PresentationDetent,你需要在 sheet 修饰符中配置 presentationDetents 参数。
示例代码#
import SwiftUI
struct ContentView: View {
@State private var isSheetPresented = false
var body: some View {
VStack {
Button("Present Sheet") {
isSheetPresented.toggle()
}
}
.sheet(isPresented: $isSheetPresented) {
SheetView()
.presentationDetents([.medium, .large]) // 定义预设的高度
}
}
}
struct SheetView: View {
var body: some View {
VStack {
Text("This is a sheet")
.font(.title)
.padding()
Spacer()
}
.background(Color.white)
}
}自定义高度#
在 iOS 16 及更高版本中,PresentationDetent 提供了一种更加灵活的方式来设置 sheet 的高度,即通过 .fraction(CGFloat),允许你定义基于屏幕高度的百分比。
示例代码:自定义高度#
import SwiftUI
struct ContentView: View {
@State private var isSheetPresented = false
var body: some View {
VStack {
Button("Present Custom Height Sheet") {
isSheetPresented.toggle()
}
}
.sheet(isPresented: $isSheetPresented) {
SheetView()
.presentationDetents([.fraction(0.4)]) // 用屏幕高度的40%作为自定义高度
}
}
}
struct SheetView: View {
var body: some View {
VStack {
Text("This is a custom height sheet")
.font(.title)
.padding()
Spacer()
}
.background(Color.white)
}
}多种高度支持和动态调整#
你可以将多种高度结合使用,并允许用户通过拖动在这些高度之间进行切换。
示例代码:多种高度#
import SwiftUI
struct ContentView: View {
@State private var isSheetPresented = false
var body: some View {
VStack {
Button("Present Resizable Sheet") {
isSheetPresented.toggle()
}
}
.sheet(isPresented: $isSheetPresented) {
SheetView()
.presentationDetents([.fraction(0.2), .medium, .large]) // 支持多种高度
.presentationDragIndicator(.visible) // 显示可拖动指示器
}
}
}
struct SheetView: View {
var body: some View {
VStack {
Text("This sheet supports multiple heights")
.font(.title)
.padding()
Spacer()
}
.background(Color.white)
}
}解释:#
presentationDetents([.fraction(0.2), .medium, .large]):提供用户20%屏幕高度、50%屏幕高度和全屏高度的选择。presentationDragIndicator(.visible):显示一个可拖动的指示器,让用户知道可以在不同高度之间拖动切换。
动态调整高度#
你可以动态改变 sheet 的高度以响应用户交互或状态变化。
示例代码:动态调整高度#
import SwiftUI
struct ContentView: View {
@State private var isSheetPresented = false
@State private var currentDetent: PresentationDetent = .medium
var body: some View {
VStack {
Button("Present Sheet") {
isSheetPresented.toggle()
}
Button("Toggle Height") {
currentDetent = (currentDetent == .medium) ? .large : .medium
}
}
.sheet(isPresented: $isSheetPresented) {
SheetView()
.presentationDetents([currentDetent])
}
}
}
struct SheetView: View {
var body: some View {
VStack {
Text("This sheet can change height dynamically")
.font(.title)
.padding()
Spacer()
}
.background(Color.white)
}
}解释:#
- 动态变化:通过状态变量
currentDetent控制sheet的高度。 - 交互控制:用户点击按钮可以切换
sheet的高度。
自定义高度的 UISheetPresentationController#
如果需要更多自定义控制,可以通过 UIViewControllerRepresentable 结合 UISheetPresentationController 来实现。
示例代码:自定义 UISheetPresentationController#
import SwiftUI
import UIKit
struct ContentView: View {
@State private var isSheetPresented = false
var body: some View {
VStack {
Button("Present Custom Sheet") {
isSheetPresented.toggle()
}
}
.sheet(isPresented: $isSheetPresented) {
CustomSheetViewController()
}
}
}
struct CustomSheetViewController: UIViewControllerRepresentable {
func makeUIViewController(context: Context) -> UIViewController {
let viewController = UIViewController()
viewController.view.backgroundColor = .white
let sheetController = viewController.sheetPresentationController
sheetController?.detents = [
.medium(),
.large(),
.custom { context in
return 150 // 自定义高度
}
]
sheetController?.prefersGrabberVisible = true // 显示抓手
return viewController
}
func updateUIViewController(_ uiViewController: UIViewController, context: Context) {}
}
struct SheetView: View {
var body: some View {
VStack {
Text("This is a custom height sheet")
.font(.title)
.padding()
Spacer()
}
.background(Color.white)
}
}解释:#
- 集成 UIKit:通过
UIViewControllerRepresentable在 SwiftUI 中嵌入UIViewController。 - 细致控制:使用
UISheetPresentationController提供更细化的高度控制。
总结#
PresentationDetent 使 SwiftUI 中的 sheet 控制更加灵活和强大。它允许开发者轻松设置 sheet 的高度,并且可以利用:
- 标准高度:
.medium和.large预定义高度。 - 自定义高度:通过
.fraction(CGFloat)指定自定义百分比高度。 - 多高度支持:允许用户通过拖动在不同高度之间切换。
- 动态调整:根据用户交互或状态变化动态调整高度。
- 结合 UIKit:使用
UIViewControllerRepresentable和UISheetPresentationController实现更复杂的控制。
通过这些功能,你可以更加灵活地实现和控制 SwiftUI 中的 sheet,满足不同的应用需求和用户体验。
其他#
在 SwiftUI 中,除了 PresentationDetent,还有一些其他的 presentation 修饰符可以用来定制 sheet 的外观和行为。这些修饰符主要用于 sheet,但有些也可以在其他类型的视图展示中使用。
让我们详细介绍一下这些 presentation 修饰符以及它们的用途。
1. PresentationDetent#
PresentationDetent 控制 sheet 的高度,之前已经讲解过。常用的高度包括:
.medium:覆盖一半屏幕。.large:覆盖整个屏幕。.fraction(CGFloat):自定义高度,以屏幕高度的百分比表示。
2. presentationCornerRadius#
presentationCornerRadius 用于设置 sheet 的圆角半径。你可以通过此修饰符设置 sheet 顶部圆角的弧度,使它在视觉上符合应用的设计需求。
示例代码:设置圆角半径#
import SwiftUI
struct ContentView: View {
@State private var isSheetPresented = false
var body: some View {
VStack {
Button("Present Sheet") {
isSheetPresented.toggle()
}
}
.sheet(isPresented: $isSheetPresented) {
SheetView()
.presentationDetents([.medium, .large])
.presentationCornerRadius(20) // 设置圆角半径
}
}
}
struct SheetView: View {
var body: some View {
VStack {
Text("This sheet has rounded corners")
.font(.title)
.padding()
Spacer()
}
.background(Color.white)
}
}解释:#
presentationCornerRadius(20):将sheet的顶部圆角半径设置为 20。
3. presentationDragIndicator#
presentationDragIndicator 用于设置 sheet 是否显示拖动指示器。拖动指示器是一个视觉提示,告诉用户可以对 sheet 进行拖动操作。
示例代码:显示拖动指示器#
import SwiftUI
struct ContentView: View {
@State private var isSheetPresented = false
var body: some View {
VStack {
Button("Present Sheet") {
isSheetPresented.toggle()
}
}
.sheet(isPresented: $isSheetPresented) {
SheetView()
.presentationDetents([.medium, .large])
.presentationDragIndicator(.visible) // 显示拖动指示器
}
}
}
struct SheetView: View {
var body: some View {
VStack {
Text("This sheet has a drag indicator")
.font(.title)
.padding()
Spacer()
}
.background(Color.white)
}
}解释:#
presentationDragIndicator(.visible):显示sheet的拖动指示器。presentationDragIndicator(.hidden):隐藏sheet的拖动指示器。
4. presentationBackgroundInteraction#
在 iOS 17 中,SwiftUI 引入了 presentationBackgroundInteraction 修饰符。该修饰符允许你控制 sheet 背后的交互行为。
示例代码:背景交互#
import SwiftUI
struct ContentView: View {
@State private var isSheetPresented = false
var body: some View {
VStack {
Button("Present Sheet") {
isSheetPresented.toggle()
}
}
.sheet(isPresented: $isSheetPresented) {
SheetView()
.presentationDetents([.medium, .large])
.presentationBackgroundInteraction(.enabled) // 启用背景交互
}
}
}
struct SheetView: View {
var body: some View {
VStack {
Text("This sheet allows background interaction")
.font(.title)
.padding()
Spacer()
}
.background(Color.white)
}
}解释:#
presentationBackgroundInteraction(.enabled):允许用户与sheet背后的视图进行交互。presentationBackgroundInteraction(.disabled):禁用与sheet背后的视图进行交互。
5. 结合使用多个修饰符#
这些 presentation 修饰符通常可以组合使用,以实现更复杂和精细的控制。
示例代码:结合使用多个修饰符#
import SwiftUI
struct ContentView: View {
@State private var isSheetPresented = false
var body: some View {
VStack {
Button("Present Sheet") {
isSheetPresented.toggle()
}
}
.sheet(isPresented: $isSheetPresented) {
SheetView()
.presentationDetents([.medium, .large])
.presentationCornerRadius(20)
.presentationDragIndicator(.visible)
.presentationBackgroundInteraction(.enabled) // 组合多个修饰符
}
}
}
struct SheetView: View {
var body: some View {
VStack {
Text("This sheet is fully customized")
.font(.title)
.padding()
Spacer()
}
.background(Color.white)
}
}在非 sheet 中使用 presentation 修饰符#
虽然这些 presentation 修饰符主要是为 sheet 设计的,但某些修饰符可能也可以用在 popover 等其他弹出式视图中,不过详细的支持情况需要查看具体的 SwiftUI 更新文档。当前这些修饰符在 sheet 中最为常用,并且提供了丰富的自定义选项。
总结#
SwiftUI 提供了一组丰富的 presentation 修饰符,用于定制 sheet 的外观和行为。通过这些修饰符,你可以创建更符合设计需求和用户期望的 sheet 体验:
- PresentationDetent:控制
sheet的高度。 - presentationCornerRadius:设置
sheet顶部的圆角半径。 - presentationDragIndicator:显示或隐藏
sheet的拖动指示器。 - presentationBackgroundInteraction:控制
sheet背后的交互行为(iOS 17)。
这些修饰符提供了高度灵活性和可定制性,允许你精细控制 sheet 的呈现方式,以提升应用的用户体验。不妨在你的项目中尝试使用这些修饰符,看看它们能为你的应用带来怎样的提升。
性能优化#
在 SwiftUI 中优化 sheet 视图的传参逻辑,避免每次弹出时重新初始化视图,可以通过以下方案实现:
一、核心思路:Identifiable 标识符 + 状态管理#
通过将列表项包装成 Identifiable 类型,配合 sheet(item:content:) 方法,使 SwiftUI 能自动追踪视图实例的生命周期。当列表项更新时,系统会复用已有视图而非重新初始化。
示例代码:#
// 定义 Identifiable 数据模型
struct ListItem: Identifiable {
let id: UUID
var content: String
}
// 主视图
struct ContentView: View {
@State var items: [ListItem] = []
@State var selectedItem: ListItem? // 通过 Optional 状态控制 sheet 显示
var body: some View {
List(items) { item in
Text(item.content)
.onTapGesture { selectedItem = item }
}
.sheet(item: $selectedItem) { item in
DetailView(item: item)
}
}
}二、优化传参方式#
1. 使用 ObservableObject 共享数据源#
将列表数据存储在 ObservableObject 中,通过 @EnvironmentObject 或 @ObservedObject 在 sheet 视图中共享数据:
class ListStore: ObservableObject {
@Published var items: [ListItem] = []
}
// Sheet 视图中直接操作共享数据
struct DetailView: View {
@EnvironmentObject var store: ListStore
var item: ListItem
var body: some View {
TextField("内容", text: $store.items[itemIndex].content)
}
private var itemIndex: Int {
store.items.firstIndex(where: { $0.id == item.id })!
}
}2. 通过 Binding 双向绑定(适用于简单场景)#
当不需要全局状态时,可以直接传递 Binding:
.sheet(item: $selectedItem) { item in
DetailView(item: Binding(
get: { item },
set: { newValue in
if let index = items.firstIndex(where: { $0.id == newValue.id }) {
items[index] = newValue
}
}
))
}三、性能优化技巧#
1. 惰性初始化视图#
通过 LazyView 包装器延迟视图的初始化:
struct LazyView<Content: View>: View {
let build: () -> Content
init(_ build: @autoclosure @escaping () -> Content) {
self.build = build
}
var body: Content { build() }
}
// 使用方式
.sheet(item: $selectedItem) { item in
LazyView(DetailView(item: item))
}2. 避免 View 层级重建#
- 确保列表项的
id稳定(如使用 UUID 而非索引) - 对复杂视图使用
EquatableView减少重绘
四、进阶方案:View Model 模式#
将业务逻辑抽离到 View Model 中,通过 @StateObject 保持视图模型的生命周期:
class DetailViewModel: ObservableObject {
@Published var item: ListItem
init(item: ListItem) {
self.item = item
}
func save() {
// 持久化逻辑
}
}
struct DetailView: View {
@StateObject var viewModel: DetailViewModel
var body: some View {
TextField("内容", text: $viewModel.item.content)
}
}
// 调用时初始化一次 ViewModel
.sheet(item: $selectedItem) { item in
DetailView(viewModel: .init(item: item))
}五、注意事项#
- 内存管理:通过
Identifiable和sheet(item:)可确保同一时间只有一个 sheet 实例存在 - 状态隔离:若需要修改原始数据,建议通过回调闭包或共享的 ObservableObject 实现
- 调试工具:使用
onAppear/onDisappear打印日志,验证视图是否被重复初始化
通过上述方案,可有效避免每次弹出 sheet 时的重复初始化问题,同时保持数据双向同步的能力。