这是用户在 2024-3-19 23:23 为 https://www.avanderlee.com/swiftui/presenting-sheets/ 保存的双语快照页面,由 沉浸式翻译 提供双语支持。了解如何保存?
Give your simulator superpowers

RocketSim: 火箭模拟: An Essential Developer Tool
必不可少的开发人员工具

as recommended by Apple 根据 Apple 的推荐

Win a Let's visionOS 2024 conference ticket. Join for free
赢取 Let's visionOS 2024 会议门票。免费加入

Sheets in SwiftUI explained with code examples
SwiftUI 中的工作表通过代码示例进行解释

Sheets in SwiftUI allow you to present views that partly cover the underlying screen. You can present them using view modifiers that respond to a particular state change, like a boolean or an object. Views that partly cover the underlying screen can be a great way to stay in the context while presenting a new flow.
SwiftUI 中的工作表允许您显示部分覆盖底层屏幕的视图。您可以使用响应特定状态更改(如布尔值或对象)的视图修饰符来显示它们。部分覆盖基础屏幕的视图是保留在上下文中同时呈现新流程的好方法。

There can only be one active sheet at a time, so it’s essential to coordinate the presentation. Boolean triggers work great when you have a single presentable view but are less efficient if you have to create a boolean for each presentable. Altogether, I found the need to develop a generic solution that allows you to present multiple views more efficiently.
一次只能有一个活动工作表,因此协调演示文稿至关重要。当您有一个可呈现的视图时,布尔触发器效果很好,但如果您必须为每个可呈现的视图创建一个布尔值,则效率会降低。总而言之,我发现需要开发一个通用解决方案,使您能够更有效地呈现多个视图。

Presenting a sheet in SwiftUI
在 SwiftUI 中显示工作表

You can present a sheet using a view modifier in SwiftUI:
您可以在 SwiftUI 中使用视图修饰符来显示工作表:

/// Present a sheet once `shouldPresentSheet` becomes `true`.
.sheet(isPresented: $shouldPresentSheet) {
    print("Sheet dismissed!")
} content: {
    AddArticleView()
}

The modifier allows you to listen for a dismiss callback and takes a trailing closure in which you can provide the view. An example implementation could look as follows:
修饰符允许您侦听关闭回调,并采用尾随闭包,您可以在其中提供视图。示例实现可能如下所示:

struct ArticlesView: View {
    @State var shouldPresentSheet = false

    var body: some View {
        VStack {
            Button("Present Sheet") {
                shouldPresentSheet.toggle()
            }
                /// Present a sheet once `shouldPresentSheet` becomes `true`.
                .sheet(isPresented: $shouldPresentSheet) {
                    print("Sheet dismissed!")
                } content: {
                    AddArticleView()
                }

        }
            .padding()
            .frame(width: 400, height: 300)
    }
}

After tapping the present button, you’ll see that a modal becomes visible:
点击现在按钮后,您会看到一个模态变得可见:

An example of sheets presented in SwiftUI.
An example of sheets presented in SwiftUI.
SwiftUI 中显示的工作表示例。

Presenting a sheet on macOS
在 macOS 上显示工作表

Note that we’ve configured a frame width and height to support macOS. Sheets on macOS automatically shrink to the minimum size needed, which means that our presented view becomes too small. With the configured dimensions, the same presented view looks as follows on macOS:
请注意,我们已配置框架宽度和高度以支持 macOS。macOS 上的图纸会自动缩小到所需的最小大小,这意味着我们呈现的视图变得太小。使用配置的尺寸,在 macOS 上显示的相同视图如下所示:

An example sheet presented using SwiftUI on macOS.
An example sheet presented using SwiftUI on macOS.
在 macOS 上使用 SwiftUI 显示的示例工作表。

Dismissing a sheet 关闭工作表

You can dismiss a sheet using a so-called DismissAction that’s defined using the environment property wrapper:
可以使用使用环境属性包装器定义的所谓 DismissAction 关闭工作表:

@Environment(\.dismiss) private var dismiss

The dismiss action implements a similar technique to @dynamicCallable and @dynamicMemberLookup, allowing you to call the instance as a function to trigger a dismiss:
关闭操作实现了与@dynamicCallable和@dynamicMemberLookup类似的技术,允许您将实例作为函数调用以触发关闭:

dismiss()

We can put this all together in an example view that allows adding a new article:
我们可以将所有这些放在一个示例视图中,该视图允许添加新文章:

struct AddArticleView: View {
    @Environment(\.dismiss) private var dismiss

    @State var title: String = ""

    var body: some View {
        VStack(spacing: 10) {
            Text("Add a new article")
                .font(.title)
            TextField(text: $title, prompt: Text("Title of the article")) {
                Text("Title")
            }

            HStack {
                Button("Cancel") {
                    // Cancel saving and dismiss.
                    dismiss()
                }
                Spacer()
                Button("Confirm") {
                    // Save the article and dismiss.
                    dismiss()
                }
            }
        }
            .padding(20)
            .frame(width: 300, height: 200)
    }
}

Enum-based sheet presentation
基于枚举的工作表表示

The earlier examples demonstrated how you could present views using a boolean state property. While this works great when working with a single sheet, it can quickly become harder to manage when you have multiple views to present:
前面的示例演示了如何使用布尔状态属性来表示视图。虽然这在处理单个工作表时效果很好,但当您要显示多个视图时,它很快就会变得更难管理:

struct ArticlesView: View {
    @State var presentAddArticleSheet = false
    @State var presentEditArticleSheet = false
    @State var presentArticleCategorySheet = false

    var body: some View {
        VStack {
            Button("Add Article") {
                presentAddArticleSheet.toggle()
            }
            Button("Edit Article") {
                presentEditArticleSheet.toggle()
            }
            Button("Article Categories") {
                presentArticleCategorySheet.toggle()
            }
        }
        .sheet(isPresented: $presentAddArticleSheet, content: {
            AddArticleView()
        })
        .sheet(isPresented: $presentEditArticleSheet, content: {
            EditArticleView()
        })
        .sheet(isPresented: $presentArticleCategorySheet, content: {
            ArticleCategoryView()
        })
            .padding()
            .frame(width: 400, height: 300)
    }
}

As you can see, we have to define a state property and sheet modifier for each presentable view. You can imagine that our view becomes even less readable when we add more elements to enrich our view.
正如你所看到的,我们必须为每个可呈现的视图定义一个状态属性和工作表修饰符。您可以想象,当我们添加更多元素来丰富我们的视图时,我们的视图变得更难读。

We can improve readability and reduce modifiers by using another sheet modifier that takes an identifiable object. In our case, we use an enum identifiable object to define all different presentable views:
我们可以通过使用另一个采用可识别对象的工作表修饰符来提高可读性并减少修饰符。在我们的例子中,我们使用一个枚举可识别对象来定义所有不同的可呈现视图:

enum Sheet: String, Identifiable {
    case addArticle, editArticle, articleCategory

    var id: String { rawValue }
}

We can replace the three view modifiers using a single one that iterates over the current presented value:
我们可以使用一个遍历当前呈现值的视图修饰符来替换三个视图修饰符:

.sheet(item: $presentedSheet, content: { sheet in
    switch sheet {
    case .addArticle:
        AddArticleView()
    case .editArticle:
        EditArticleView()
    case .articleCategory:
        ArticleCategoryView()
    }
})

Altogether, our final view looks as follows:
总而言之,我们的最终视图如下:

struct ArticlesEnumSheetsView: View {
    enum Sheet: String, Identifiable {
        case addArticle, editArticle, articleCategory

        var id: String { rawValue }
    }

    @State var presentedSheet: Sheet?

    var body: some View {
        VStack {
            Button("Add Article") {
                presentedSheet = .addArticle
            }
            Button("Edit Article") {
                presentedSheet = .editArticle
            }
            Button("Article Category") {
                presentedSheet = .articleCategory
            }
        }
        .sheet(item: $presentedSheet, content: { sheet in
            switch sheet {
            case .addArticle:
                AddArticleView()
            case .editArticle:
                EditArticleView()
            case .articleCategory:
                ArticleCategoryView()
            }
        })
            .padding()
            .frame(width: 400, height: 300)
    }
}

We can now present a view using the presentedSheet property, resulting in increased readability in terms of understanding which sheet will be presented:
现在,我们可以使用该 presentedSheet 属性来呈现视图,从而在理解将显示哪个工作表方面提高了可读性:

presentedSheet = .addArticle

This is an excellent solution for most scenarios, but it still doesn’t fulfill all my needs when working with multiple presentable views. There’s no easy way to present another view from within a sheet which can be helpful when working in flows on macOS. Secondly, while we can reuse our enum, we still have to define the sheet modifier on every view that supports presenting. Therefore, I decided to take this solution one step further.
对于大多数方案来说,这是一个很好的解决方案,但在使用多个可呈现的视图时,它仍然不能满足我的所有需求。没有简单的方法可以从工作表中呈现另一个视图,这在 macOS 上的流中工作时会很有帮助。其次,虽然我们可以重用枚举,但我们仍然必须在每个支持表示的视图上定义工作表修饰符。因此,我决定将这个解决方案更进一步。

Creating a coordinator
创建协调器

Creating a presenting coordinator allows sheets to be controlled outside the presenting views. Our code becomes reusable, and we can trigger views from other presented views.
创建演示协调器允许在演示视图之外控制工作表。我们的代码变得可重用,我们可以从其他呈现的视图触发视图。

We start by defining a new SheetEnum protocol that moves the responsibility of view creation to the enum:
我们首先定义一个新 SheetEnum 协议,将视图创建的责任转移到枚举上:

protocol SheetEnum: Identifiable {
    associatedtype Body: View

    @ViewBuilder
    func view(coordinator: SheetCoordinator<Self>) -> Body
}

We can rewrite our earlier defined enum as followed:
我们可以重写我们之前定义的枚举,如下所示:

enum ArticleSheet: String, Identifiable, SheetEnum {
    case addArticle, editArticle, selectArticleCategory

    var id: String { rawValue }

    @ViewBuilder
    func view(coordinator: SheetCoordinator<ArticleSheet>) -> some View {
        switch self {
        case .addArticle:
            AddArticleView()
        case .editArticle:
            EditArticleView()
        case .selectArticleCategory:
            SelectArticleCategoryView(sheetCoordinator: coordinator)
        }
    }
}

Note that we’re injecting the sheet coordinator into the SelectArticleCategoryView to allow that view to control a potential next sheet to be presented.
请注意,我们将工作表协调器注入 SelectArticleCategoryView 中,以允许该视图控制要显示的下一个工作表。

The coordinator looks as follows:
协调器如下所示:

final class SheetCoordinator<Sheet: SheetEnum>: ObservableObject {
    @Published var currentSheet: Sheet?
    private var sheetStack: [Sheet] = []

    @MainActor
    func presentSheet(_ sheet: Sheet) {
        sheetStack.append(sheet)

        if sheetStack.count == 1 {
            currentSheet = sheet
        }
    }

    @MainActor
    func sheetDismissed() {
        sheetStack.removeFirst()

        if let nextSheet = sheetStack.first {
            currentSheet = nextSheet
        }
    }
}

We defined the coordinator as an observable object to ensure containing views get refreshed when the currentSheet property updates. Internally, we keep track of a stack of sheets to allow views to be presented after one was dismissed.
我们将协调器定义为可观察对象,以确保在 currentSheet 属性更新时刷新包含的视图。在内部,我们会跟踪一堆工作表,以便在一个工作表被关闭后显示视图。

We could rely solely on the currentSheet property, but that would transition sheets without dismissing and presenting animations on macOS. Therefore, we make use of the presentSheet(_: ) method to control our stack.
我们可以完全依赖该 currentSheet 属性,但这可以在不关闭和呈现 macOS 动画的情况下转换工作表。因此,我们利用该 presentSheet(_: ) 方法来控制我们的堆栈。

We also introduce a new view modifier to get rid of the sheet modifier within presenting views:
我们还引入了一个新的视图修饰符,以摆脱显示视图中的工作表修饰符:

struct SheetCoordinating<Sheet: SheetEnum>: ViewModifier {
    @StateObject var coordinator: SheetCoordinator<Sheet>

    func body(content: Content) -> some View {
        content
            .sheet(item: $coordinator.currentSheet, onDismiss: {
                coordinator.sheetDismissed()
            }, content: { sheet in
                sheet.view(coordinator: coordinator)
            })
    }
}
extension View {
    func sheetCoordinating<Sheet: SheetEnum>(coordinator: SheetCoordinator<Sheet>) -> some View {
        modifier(SheetCoordinating(coordinator: coordinator))
    }
}

We ensure both coordinator and enum associated types match and connect the coordinator to the view using a state object.
我们确保协调器和枚举关联类型匹配,并使用状态对象将协调器连接到视图。

Finally, we can update our existing view to make use of the new coordinator:
最后,我们可以更新现有视图以使用新的协调器:

struct CoordinatedArticlesView: View {
    @StateObject var sheetCoordinator = SheetCoordinator<ArticleSheet>()

    var body: some View {
        VStack {
            Button("Add Article") {
                sheetCoordinator.presentSheet(.addArticle)
            }
            Button("Edit Article") {
                sheetCoordinator.presentSheet(.editArticle)
            }
            Button("Article Category") {
                sheetCoordinator.presentSheet(.selectArticleCategory)
            }
        }
            .sheetCoordinating(coordinator: sheetCoordinator)
            .padding()
            .frame(width: 400, height: 300)
    }
}

We reduced the code used to present the views by using our coordinator, and we can present the views from any other view.
我们通过使用协调器减少了用于呈现视图的代码,并且我们可以从任何其他视图呈现视图。

Conclusion 结论

You can present sheets using boolean or object state properties. Boolean-based sheets work great when you only have a few sheets but become harder to manage in case you present multiple views. Using a sheet coordinator, we can improve reusability and present sheets from any view.
您可以使用布尔值或对象状态属性来显示工作表。当您只有几张工作表时,基于布尔值的工作表效果很好,但在您呈现多个视图时会变得更难管理。使用工作表协调器,我们可以提高可重用性,并从任何视图呈现工作表。

If you want to improve your SwiftUI knowledge, even more, check out the SwiftUI category page. Feel free to contact me or tweet me on Twitter if you have any additional tips or feedback.
如果您想进一步提高 SwiftUI 知识,请查看 SwiftUI 类别页面。如果您有任何其他提示或反馈,请随时与我联系或在 Twitter 上发推文。

Thanks!  谢谢!

 

Do you know everything about SwiftUI?
你对 SwiftUI 了如指掌吗?

You don't need to master everything, but staying informed is crucial. Join our community of 17,905 developers and stay ahead of the curve:
您不需要掌握所有内容,但随时了解情况至关重要。加入我们由 17,905 名开发人员组成的社区,保持领先地位:


Featured SwiftLee Jobs 精选 SwiftLee 职位

Find your next Swift career step at world-class companies with impressive apps by joining the SwiftLee Talent Collective. I'll match engineers in my collective with exciting app development companies. SwiftLee Jobs
加入 SwiftLee Talent Collective,在拥有出色 app 的世界级公司找到你的下一个 Swift 职业阶梯。我会将我的团队中的工程师与令人兴奋的应用程序开发公司相匹配。SwiftLee职位