多个 sheet(isPresented:) 在 SwiftUI 中不起作用
Multiple sheet(isPresented:) doesn't work in SwiftUI
我的这个 ContentView 有两个不同的模态视图,所以我对两者都使用 sheet(isPresented:)
,但似乎只显示了最后一个。我该如何解决这个问题?或者在 SwiftUI 中不能在一个视图上使用多个工作表?
struct ContentView: View {
@State private var firstIsPresented = false
@State private var secondIsPresented = false
var body: some View {
NavigationView {
VStack(spacing: 20) {
Button("First modal view") {
self.firstIsPresented.toggle()
}
Button ("Second modal view") {
self.secondIsPresented.toggle()
}
}
.navigationBarTitle(Text("Multiple modal view problem"), displayMode: .inline)
.sheet(isPresented: $firstIsPresented) {
Text("First modal view")
}
.sheet(isPresented: $secondIsPresented) {
Text("Only the second modal view works!")
}
}
}
}
以上代码编译时没有警告(Xcode 11.2.1)。
请尝试以下代码
更新答案(iOS 14,Xcode 12)
enum ActiveSheet {
case first, second
var id: Int {
hashValue
}
}
struct ContentView: View {
@State private var showSheet = false
@State private var activeSheet: ActiveSheet? = .first
var body: some View {
NavigationView {
VStack(spacing: 20) {
Button("First modal view") {
self.showSheet = true
self.activeSheet = .first
}
Button ("Second modal view") {
self.showSheet = true
self.activeSheet = .second
}
}
.navigationBarTitle(Text("Multiple modal view problem"), displayMode: .inline)
.sheet(isPresented: $showSheet) {
if self.activeSheet == .first {
Text("First modal view")
}
else {
Text("Only the second modal view works!")
}
}
}
}
}
您的情况可以通过以下方式解决(使用 Xcode 11.2 测试)
var body: some View {
NavigationView {
VStack(spacing: 20) {
Button("First modal view") {
self.firstIsPresented.toggle()
}
.sheet(isPresented: $firstIsPresented) {
Text("First modal view")
}
Button ("Second modal view") {
self.secondIsPresented.toggle()
}
.sheet(isPresented: $secondIsPresented) {
Text("Only the second modal view works!")
}
}
.navigationBarTitle(Text("Multiple modal view problem"), displayMode: .inline)
}
}
也可以将 sheet 添加到放置在视图背景中的 EmptyView。这可以多次完成:
.background(EmptyView()
.sheet(isPresented: isPresented, content: content))
您只需将按钮和 .sheet 调用组合在一起即可完成此操作。如果你有一个前导和一个尾随,那就很简单了。但是,如果您在前导或尾随中有多个导航栏项目,则需要将它们包装在 HStack 中,并将每个按钮及其 sheet 调用包装在 VStack 中。
下面是两个尾随按钮的示例:
trailing:
HStack {
VStack {
Button(
action: {
self.showOne.toggle()
}
) {
Image(systemName: "camera")
}
.sheet(isPresented: self.$showOne) {
OneView().environment(\.managedObjectContext, self.managedObjectContext)
}
}//showOne vstack
VStack {
Button(
action: {
self.showTwo.toggle()
}
) {
Image(systemName: "film")
}
.sheet(isPresented: self.$showTwo) {
TwoView().environment(\.managedObjectContext, self.managedObjectContext)
}
}//show two vstack
}//nav bar button hstack
除了 之外,我找到了一种将 sheet 内容提取到函数中的方法,因为编译器很难对我巨大的 View
进行类型检查。
extension YourView {
enum Sheet {
case a, b
}
@ViewBuilder func sheetContent() -> some View {
if activeSheet == .a {
A()
} else if activeSheet == .b {
B()
}
}
}
你可以这样使用它:
.sheet(isPresented: $isSheetPresented, content: sheetContent)
它使代码更清晰,也减轻了编译器的压力。
创建自定义按钮视图并在其中调用 sheet 解决了这个问题。
struct SheetButton<Content>: View where Content : View {
var text: String
var content: Content
@State var isPresented = false
init(_ text: String, @ViewBuilder content: () -> Content) {
self.text = text
self.content = content()
}
var body: some View {
Button(text) {
self.isPresented.toggle()
}
.sheet(isPresented: $isPresented) {
self.content
}
}
}
ContentView 会更干净。
struct ContentView: View {
var body: some View {
NavigationView {
VStack(spacing: 20) {
SheetButton("First modal view") {
Text("First modal view")
}
SheetButton ("Second modal view") {
Text("Only the second modal view works!")
}
}
.navigationBarTitle(Text("Multiple modal view problem"), displayMode: .inline)
}
}
}
此方法在打开 sheets 时也能正常工作,具体取决于列表行内容。
struct ContentView: View {
var body: some View {
NavigationView {
List(1...10, id: \.self) { row in
SheetButton("\(row) Row") {
Text("\(row) modal view")
}
}
.navigationBarTitle(Text("Multiple modal view problem"), displayMode: .inline)
}
}
}
我知道这个问题已经有很多答案,但我找到了另一个我认为非常有用的解决方案。它在这样的 if 语句中包装了 sheets。对于操作 sheets,我发现在 iPad 上的滚动视图中使用其他解决方案(比如将每个 sheet 及其按钮包装在一个组中)通常会使操作 sheets 去了奇怪的地方,所以这个答案将解决 iPad.
上滚动视图内的操作 sheets 的问题
struct ContentView: View{
@State var sheet1 = false
@State var sheet2 = false
var body: some View{
VStack{
Button(action: {
self.sheet1.toggle()
},label: {
Text("Sheet 1")
}).padding()
Button(action: {
self.sheet2.toggle()
},label: {
Text("Sheet 2")
}).padding()
}
if self.sheet1{
Text("")
.sheet(isPresented: self.$sheet1, content: {
Text("Some content here presenting sheet 1")
})
}
if self.sheet2{
Text("")
.sheet(isPresented: self.$sheet2, content: {
Text("Some content here presenting sheet 2")
})
}
}
}
UPD
从 Xcode 12.5.0 Beta 3(2021 年 3 月 3 日)开始,这个问题不再有意义,因为现在可以有多个 .sheet(isPresented:)
或 .fullScreenCover(isPresented:)
并且问题中提供的代码可以正常工作。
尽管如此,我发现这个答案仍然有效,因为它很好地组织了 sheet 并使代码干净且更具可读性 - 你有一个真实来源而不是几个独立的布尔值
实际答案
最好的方法,也适用于 iOS 14:
enum ActiveSheet: Identifiable {
case first, second
var id: Int {
hashValue
}
}
struct YourView: View {
@State var activeSheet: ActiveSheet?
var body: some View {
VStack {
Button {
activeSheet = .first
} label: {
Text("Activate first sheet")
}
Button {
activeSheet = .second
} label: {
Text("Activate second sheet")
}
}
.sheet(item: $activeSheet) { item in
switch item {
case .first:
FirstView()
case .second:
SecondView()
}
}
}
}
在此处阅读更多内容:https://developer.apple.com/documentation/swiftui/view/sheet(item:ondismiss:content:)
要隐藏 sheet 只需设置 activeSheet = nil
奖金:
如果你想让你的 sheet 全屏,那么使用完全相同的代码,但不是 .sheet
写 .fullScreenCover
这是一个示例,它显示了在同一个 ContentView
中使用 4 个工作表、1 个(或更多)警报和一个 actionSheet。在 iOS 13、iOS 14 中确定。在预览中确定
(来自评论:)目的是使用 sheet(item:onDismiss:content:)
和项目作为 @State
var,以及在枚举中定义的值。这样一来,所有的“生意”就是ContentView
中的self.contained了。这样,工作表或警报的数量就不受限制了。
下面是下面代码的输出:
import SwiftUI
// exemple which show use of 4 sheets,
// 1 (or more) alerts,
// and an actionSheet in the same ContentView
// OK in iOS 13, iOS 14
// OK in Preview
// Any number of sheets, displayed as Views
// can be used for sheets in other views (with unique case values, of course)
enum SheetState {
case none
case AddItem
case PickPhoto
case DocPicker
case ActivityController
}
// Make Identifiable
extension SheetState: Identifiable {
var id: SheetState { self }
}
// the same for Alerts (who are not View, but Alert)
enum AlertState {
case none
case Delete
}
extension AlertState: Identifiable {
var id: AlertState { self }
}
struct ContentView: View {
// Initialized with nil value
@State private var sheetState: SheetState?
@State private var alertState: AlertState?
var body: some View {
NavigationView {
Form {
Text("Hello, world!")
Section(header: Text("sheets")) {
addItemButton
pickDocumentButton
pickPhoto
buttonExportView
}
Section(header: Text("alert")) {
confirmDeleteButton
}
Section(header: Text("Action sheet")) {
showActionSheetButton
}
}
.navigationTitle("Sheets & Alerts")
// ONLY ONE call .sheet(item: ... with required value in enum
// if item become not nil => display sheet
// when dismiss sheet (drag the modal view, or use presentationMode.wrappedValue.dismiss in Buttons) => item = nil
// in other way : if you set item to nil => dismiss sheet
// in closure, look for which item value display which view
// the "item" returned value contains the value passed in .sheet(item: ...
.sheet(item: self.$sheetState) { item in
if item == SheetState.AddItem {
addItemView // SwiftUI view
} else if item == SheetState.DocPicker {
documentPickerView // UIViewControllerRepresentable
} else if item == SheetState.PickPhoto {
imagePickerView // UIViewControllerRepresentable
} else if item == SheetState.ActivityController {
activityControllerView // UIViewControllerRepresentable
}
}
.alert(item: self.$alertState) { item in
if item == AlertState.Delete {
return deleteAlert
} else {
// Not used, but seem to be required
// .alert(item: ... MUST return an Alert
return noneAlert
}
}
}
}
// For cleaner contents : controls, alerts and sheet views are "stocked" in private var
// MARK: - Sheet Views
private var addItemView: some View {
Text("Add item").font(.largeTitle).foregroundColor(.blue)
// drag the modal view set self.sheetState to nil
}
private var documentPickerView: some View {
DocumentPicker() { url in
if url != nil {
DispatchQueue.main.async {
print("url")
}
}
self.sheetState = nil
// make the documentPicker view dismissed
}
}
private var imagePickerView: some View {
ImagePicker() { image in
if image != nil {
DispatchQueue.main.async {
self.logo = Image(uiImage: image!)
}
}
self.sheetState = nil
}
}
private var activityControllerView: some View {
ActivityViewController(activityItems: ["Message to export"], applicationActivities: [], excludedActivityTypes: [])
}
// MARK: - Alert Views
private var deleteAlert: Alert {
Alert(title: Text("Delete?"),
message: Text("That cant be undone."),
primaryButton: .destructive(Text("Delete"), action: { print("delete!") }),
secondaryButton: .cancel())
}
private var noneAlert: Alert {
Alert(title: Text("None ?"),
message: Text("No action."),
primaryButton: .destructive(Text("OK"), action: { print("none!") }),
secondaryButton: .cancel())
}
// In buttons, action set value in item for .sheet(item: ...
// Set self.sheetState value make sheet displayed
// MARK: - Buttons
private var addItemButton: some View {
Button(action: { self.sheetState = SheetState.AddItem }) {
HStack {
Image(systemName: "plus")
Text("Add an Item")
}
}
}
private var pickDocumentButton: some View {
Button(action: { self.sheetState = SheetState.DocPicker }) {
HStack {
Image(systemName: "doc")
Text("Choose Document")
}
}
}
@State private var logo: Image = Image(systemName: "photo")
private var pickPhoto: some View {
ZStack {
HStack {
Text("Pick Photo ->")
Spacer()
}
HStack {
Spacer()
logo.resizable().scaledToFit().frame(height: 36.0)
Spacer()
}
}
.onTapGesture { self.sheetState = SheetState.PickPhoto }
}
private var buttonExportView: some View {
Button(action: { self.sheetState = SheetState.ActivityController }) {
HStack {
Image(systemName: "square.and.arrow.up").imageScale(.large)
Text("Export")
}
}
}
private var confirmDeleteButton: some View {
Button(action: { self.alertState = AlertState.Delete}) {
HStack {
Image(systemName: "trash")
Text("Delete!")
}.foregroundColor(.red)
}
}
@State private var showingActionSheet = false
@State private var foregroundColor = Color.blue
private var showActionSheetButton: some View {
Button(action: { self.showingActionSheet = true }) {
HStack {
Image(systemName: "line.horizontal.3")
Text("Show Action Sheet")
}.foregroundColor(foregroundColor)
}
.actionSheet(isPresented: $showingActionSheet) {
ActionSheet(title: Text("Change foreground"), message: Text("Select a new color"), buttons: [
.default(Text("Red")) { self.foregroundColor = .red },
.default(Text("Green")) { self.foregroundColor = .green },
.default(Text("Blue")) { self.foregroundColor = .blue },
.cancel()
])
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
在一个视图中显示多个 sheet 的另一种简单方法:
每个视图私有变量都有自己的 Bool @State 值和 .sheet(isPresented: ... call
实施简单,所有必要的都集中在一个地方。
确定 iOS 13,iOS 14,预览
import SwiftUI
struct OtherContentView: View {
var body: some View {
Form {
Section {
button1
}
Section {
button2
}
Section {
button3
}
Section {
button4
}
}
}
@State private var showSheet1 = false
private var button1: some View {
Text("Sheet 1")
.onTapGesture { showSheet1 = true }
.sheet(isPresented: $showSheet1) { Text("Modal Sheet 1") }
}
@State private var showSheet2 = false
private var button2: some View {
Text("Sheet 2")
.onTapGesture { showSheet2 = true }
.sheet(isPresented: $showSheet2) { Text("Modal Sheet 2") }
}
@State private var showSheet3 = false
private var button3: some View {
Text("Sheet 3")
.onTapGesture { showSheet3 = true }
.sheet(isPresented: $showSheet3) { Text("Modal Sheet 3") }
}
@State private var showSheet4 = false
private var button4: some View {
Text("Sheet 4")
.onTapGesture { showSheet4 = true }
.sheet(isPresented: $showSheet4) { Text("Modal Sheet 4") }
}
}
struct OtherContentView_Previews: PreviewProvider {
static var previews: some View {
OtherContentView()
}
}
这对我的应用来说效果很好,在 iOS 13.x 上有三种 sheet 的展示可能性。有趣的行为以 iOS 14 开始。由于某种原因,在应用程序启动时,当我 select 显示 sheet 时,状态变量未设置并且 sheet 出现黑屏。如果我保留 select 第一个选择,它会继续显示空白 sheet。一旦我 select 第二个选择(不同于第一个),变量就会被设置并且正确的 sheet 出现。我先 select 哪个 sheet 并不重要,同样的行为也会发生。
错误??还是我错过了什么。除了 3 个 sheet 选项外,我的代码几乎与上面的代码相同,并且我有一个自定义按钮,当按下按钮时,它接受一个参数,() -> Void,到 运行。在 iOS 13.x 中效果很好,但在 iOS 中效果不佳 14.
戴夫
我通过创建一个为我保存和管理状态的可观察 SheetContext
解决了 @State
和多个 sheet 的混乱问题。然后我只需要一个上下文实例并可以告诉它以 sheet.
的形式呈现任何视图
我在这个博客中描述得更详细 post: https://danielsaidi.com/blog/2020/06/06/swiftui-sheets
我认为这不是 SwiftUI 表达任何观点的正确方式。
该范例通过创建在屏幕上显示某些内容的特定视图来工作,因此您可以在 superview 主体中拥有多个需要呈现某些内容的视图。因此 SwiftUI 2,在 iOS 14 上,将不会接受,开发人员应该在某些情况下调用超级视图中可以接受的所有演示文稿,但会有一些时刻如果有具体的view展示内容就更好了
我为此实施了一个解决方案,并在 Swift 5.3 和 Xcode 12.1 在 iOS 14.1
上进行了测试
struct Presentation<Content>: View where Content: View {
enum Style {
case sheet
case popover
case fullScreenCover
}
@State private var isTrulyPresented: Bool = false
@State private var willPresent: Bool = false
@Binding private var isPresented: Bool
let content: () -> Content
let dismissHandler: (() -> Void)?
let style: Style
init(_ style: Style, _ isPresented: Binding<Bool>, onDismiss: (() -> Void)?, content: @escaping () -> Content) {
self._isPresented = isPresented
self.content = content
self.dismissHandler = onDismiss
self.style = style
}
@ViewBuilder
var body: some View {
if !isPresented && !willPresent {
EmptyView()
} else {
switch style {
case .sheet:
EmptyView()
.sheet(isPresented: $isTrulyPresented, onDismiss: dismissHandler, content: dynamicContent)
case .popover:
EmptyView()
.popover(isPresented: $isTrulyPresented, content: dynamicContent)
case .fullScreenCover:
EmptyView()
.fullScreenCover(isPresented: $isTrulyPresented, onDismiss: dismissHandler, content: dynamicContent)
}
}
}
}
extension Presentation {
var dynamicContent: () -> Content {
if isPresented && !isTrulyPresented {
OperationQueue.main.addOperation {
willPresent = true
OperationQueue.main.addOperation {
isTrulyPresented = true
}
}
} else if isTrulyPresented && !isPresented {
OperationQueue.main.addOperation {
isTrulyPresented = false
OperationQueue.main.addOperation {
willPresent = false
}
}
}
return content
}
}
之后,我可以为 SwiftUI
中的所有视图实现这些方法
public extension View {
func _sheet<Content>(
isPresented: Binding<Bool>,
content: @escaping () -> Content
) -> some View where Content: View {
self.background(
Presentation(
.sheet,
isPresented,
onDismiss: nil,
content: content
)
)
}
func _sheet<Content>(
isPresented: Binding<Bool>,
onDismiss: @escaping () -> Void,
content: @escaping () -> Content
) -> some View where Content: View {
self.background(
Presentation(
.sheet,
isPresented,
onDismiss: onDismiss,
content: content
)
)
}
}
public extension View {
func _popover<Content>(
isPresented: Binding<Bool>,
content: @escaping () -> Content
) -> some View where Content: View {
self.background(
Presentation(
.popover,
isPresented,
onDismiss: nil,
content: content
)
)
}
}
public extension View {
func _fullScreenCover<Content>(
isPresented: Binding<Bool>,
content: @escaping () -> Content
) -> some View where Content: View {
self.background(
Presentation(
.fullScreenCover,
isPresented,
onDismiss: nil,
content: content
)
)
}
func _fullScreenCover<Content>(
isPresented: Binding<Bool>,
onDismiss: @escaping () -> Void,
content: @escaping () -> Content
) -> some View where Content: View {
self.background(
Presentation(
.fullScreenCover,
isPresented,
onDismiss: onDismiss,
content: content
)
)
}
}
此解决方案适用于 iOS 14.0
此解决方案使用 .sheet(item:, content:) 构造
struct ContentView: View {
enum ActiveSheet: Identifiable {
case First, Second
var id: ActiveSheet { self }
}
@State private var activeSheet: ActiveSheet?
var body: some View {
NavigationView {
VStack(spacing: 20) {
Button("First modal view") {
activeSheet = .First
}
Button ("Second modal view") {
activeSheet = .Second
}
}
.navigationBarTitle(Text("Multiple modal view problem"), displayMode: .inline)
.sheet(item: $activeSheet) { sheet in
switch sheet {
case .First:
Text("First modal view")
case .Second:
Text("Only the second modal view works!")
}
}
}
}
}
这个派对有点晚了,但是 none 到目前为止的答案已经解决了让 viewModel 完成工作的可能性。因为我绝不是 SwiftUI 的专家(对它还很陌生),所以完全有可能有更好的方法来做到这一点,但我找到的解决方案在这里 -
enum ActiveSheet: Identifiable {
case first
case second
var id: ActiveSheet { self }
}
struct MyView: View {
@ObservedObject private var viewModel: MyViewModel
private var activeSheet: Binding<ActiveSheet?> {
Binding<ActiveSheet?>(
get: { viewModel.activeSheet },
set: { viewModel.activeSheet = [=10=] }
)
}
init(viewModel: MyViewModel) {
self.viewModel = viewModel
}
var body: some View {
HStack {
/// some views
}
.onTapGesture {
viewModel.doSomething()
}
.sheet(item: activeSheet) { _ in
viewModel.activeSheetView()
}
}
}
...并在视图模型中 -
@Published var activeSheet: ActiveSheet?
func activeSheetView() -> AnyView {
switch activeSheet {
case .first:
return AnyView(firstSheetView())
case .second:
return AnyView(secondSheetView())
default:
return AnyView(EmptyView())
}
}
// call this from the view, eg, when the user taps a button
func doSomething() {
activeSheet = .first // this will cause the sheet to be presented
}
其中 firstSheetView() 和 secondSheetView() 提供所需的 actionSheet 内容。
我喜欢这种方法,因为它将所有业务逻辑都排除在视图之外。
编辑:从 iOS 14.5 beta 3 开始,现在已修复:
SwiftUI
已在 iOS 和 iPadOS 14.5 Beta 3
中解决
- 您现在可以在同一视图层次结构中应用多个
sheet(isPresented:onDismiss:content:)
和 fullScreenCover(item:onDismiss:content:)
修饰符。 (74246633)
修复之前,解决方法是对每个按钮应用 sheet 修饰符:
struct ContentView: View {
@State private var firstIsPresented = false
@State private var secondIsPresented = false
var body: some View {
NavigationView {
VStack(spacing: 20) {
Button("First modal view") {
self.firstIsPresented.toggle()
}
.sheet(isPresented: $firstIsPresented) {
Text("First modal view")
}
Button ("Second modal view") {
self.secondIsPresented.toggle()
}
.sheet(isPresented: $secondIsPresented) {
Text("Second modal view")
}
}
.navigationBarTitle(Text("Multiple modal view problem"), displayMode: .inline)
}
}
}
由于 sheet 两者做同样的事情,您可以将重复的功能提取到子视图中:
struct ContentView: View {
var body: some View {
NavigationView {
VStack(spacing: 20) {
ShowSheet(title:"First modal view")
ShowSheet(title:"Second modal view")
}
.navigationBarTitle(Text("Multiple modal view no problem!"), displayMode: .inline)
}
}
}
struct ShowSheet: View {
@State private var isPresented = false
let title: String
var body: some View {
Button(title) {
isPresented.toggle()
}
.sheet(isPresented: $isPresented) {
Text(title)
}
}
}
从 iOS 和 iPadOS 14.5 Beta 3 开始,无论何时公开发布,多张纸都将按预期工作,并且需要其他答案中的 none 解决方法。来自发行说明:
SwiftUI
Resolved in iOS & iPadOS 14.5 Beta 3
You can now apply multiple sheet(isPresented:onDismiss:content:)
and
fullScreenCover(item:onDismiss:content:)
modifiers in the same view
hierarchy. (74246633)
公认的解决方案效果很好,但我想分享一个额外的扩充,以防其他人遇到同样的问题。
我的问题
我遇到了两个按钮合二为一的问题。将两个按钮配对在一起,将整个 VStack
或 HStack
变成一个大按钮。这只允许一个 .sheet
开火,不管是否使用已接受的。
解决方案
对我来说是拼图中缺失的一块。
向每个按钮添加 .buttonStyle(BorderlessButtonStyle())
或 .buttonStyle(PlainButtonStyle())
,使它们像您期望的那样充当单个按钮。
抱歉,如果我在这里添加了任何失礼,但这是我第一次在 StackOverlflow 上发帖。
除以上答案外
- 如果两个 sheet 有顺序关系,您可以替换旧的 sheet
import SwiftUI
struct Sheet1: View {
@Environment(\.dismiss) private var dismiss
@State var text: String = "Text"
var body: some View {
Text(self.text)
if self.text == "Modified Text" {
Button {
dismiss()
} label: {
Text("Close sheet")
}
} else {
Button {
self.text = "Modified Text"
} label: {
Text("Modify Text")
}
}
}
}
struct SheetTester: View {
@State private var isShowingSheet1 = false
var body: some View {
Button(action: {
isShowingSheet1.toggle()
}) {
Text("Show Sheet1")
}
.sheet(isPresented: $isShowingSheet1) {
Sheet1()
}
}
}
或2.使用两个sheet并联
struct SheetTester: View {
@State private var isShowingSheet1 = false
var body: some View {
Button(action: {
isShowingSheet1.toggle()
}) {
Text("Show Sheet1")
}
.sheet(isPresented: $isShowingSheet1) {
Text("Sheet1")
Button {
isShowingSheet1.toggle()
isShowingSheet2.toggle()
} label: {
Text("Show Sheet2")
}
}
.sheet(isPresented: $isShowingSheet2) {
Text("Sheet2")
}
}
}
}
我的这个 ContentView 有两个不同的模态视图,所以我对两者都使用 sheet(isPresented:)
,但似乎只显示了最后一个。我该如何解决这个问题?或者在 SwiftUI 中不能在一个视图上使用多个工作表?
struct ContentView: View {
@State private var firstIsPresented = false
@State private var secondIsPresented = false
var body: some View {
NavigationView {
VStack(spacing: 20) {
Button("First modal view") {
self.firstIsPresented.toggle()
}
Button ("Second modal view") {
self.secondIsPresented.toggle()
}
}
.navigationBarTitle(Text("Multiple modal view problem"), displayMode: .inline)
.sheet(isPresented: $firstIsPresented) {
Text("First modal view")
}
.sheet(isPresented: $secondIsPresented) {
Text("Only the second modal view works!")
}
}
}
}
以上代码编译时没有警告(Xcode 11.2.1)。
请尝试以下代码
更新答案(iOS 14,Xcode 12)
enum ActiveSheet {
case first, second
var id: Int {
hashValue
}
}
struct ContentView: View {
@State private var showSheet = false
@State private var activeSheet: ActiveSheet? = .first
var body: some View {
NavigationView {
VStack(spacing: 20) {
Button("First modal view") {
self.showSheet = true
self.activeSheet = .first
}
Button ("Second modal view") {
self.showSheet = true
self.activeSheet = .second
}
}
.navigationBarTitle(Text("Multiple modal view problem"), displayMode: .inline)
.sheet(isPresented: $showSheet) {
if self.activeSheet == .first {
Text("First modal view")
}
else {
Text("Only the second modal view works!")
}
}
}
}
}
您的情况可以通过以下方式解决(使用 Xcode 11.2 测试)
var body: some View {
NavigationView {
VStack(spacing: 20) {
Button("First modal view") {
self.firstIsPresented.toggle()
}
.sheet(isPresented: $firstIsPresented) {
Text("First modal view")
}
Button ("Second modal view") {
self.secondIsPresented.toggle()
}
.sheet(isPresented: $secondIsPresented) {
Text("Only the second modal view works!")
}
}
.navigationBarTitle(Text("Multiple modal view problem"), displayMode: .inline)
}
}
也可以将 sheet 添加到放置在视图背景中的 EmptyView。这可以多次完成:
.background(EmptyView()
.sheet(isPresented: isPresented, content: content))
您只需将按钮和 .sheet 调用组合在一起即可完成此操作。如果你有一个前导和一个尾随,那就很简单了。但是,如果您在前导或尾随中有多个导航栏项目,则需要将它们包装在 HStack 中,并将每个按钮及其 sheet 调用包装在 VStack 中。
下面是两个尾随按钮的示例:
trailing:
HStack {
VStack {
Button(
action: {
self.showOne.toggle()
}
) {
Image(systemName: "camera")
}
.sheet(isPresented: self.$showOne) {
OneView().environment(\.managedObjectContext, self.managedObjectContext)
}
}//showOne vstack
VStack {
Button(
action: {
self.showTwo.toggle()
}
) {
Image(systemName: "film")
}
.sheet(isPresented: self.$showTwo) {
TwoView().environment(\.managedObjectContext, self.managedObjectContext)
}
}//show two vstack
}//nav bar button hstack
除了 View
进行类型检查。
extension YourView {
enum Sheet {
case a, b
}
@ViewBuilder func sheetContent() -> some View {
if activeSheet == .a {
A()
} else if activeSheet == .b {
B()
}
}
}
你可以这样使用它:
.sheet(isPresented: $isSheetPresented, content: sheetContent)
它使代码更清晰,也减轻了编译器的压力。
创建自定义按钮视图并在其中调用 sheet 解决了这个问题。
struct SheetButton<Content>: View where Content : View {
var text: String
var content: Content
@State var isPresented = false
init(_ text: String, @ViewBuilder content: () -> Content) {
self.text = text
self.content = content()
}
var body: some View {
Button(text) {
self.isPresented.toggle()
}
.sheet(isPresented: $isPresented) {
self.content
}
}
}
ContentView 会更干净。
struct ContentView: View {
var body: some View {
NavigationView {
VStack(spacing: 20) {
SheetButton("First modal view") {
Text("First modal view")
}
SheetButton ("Second modal view") {
Text("Only the second modal view works!")
}
}
.navigationBarTitle(Text("Multiple modal view problem"), displayMode: .inline)
}
}
}
此方法在打开 sheets 时也能正常工作,具体取决于列表行内容。
struct ContentView: View {
var body: some View {
NavigationView {
List(1...10, id: \.self) { row in
SheetButton("\(row) Row") {
Text("\(row) modal view")
}
}
.navigationBarTitle(Text("Multiple modal view problem"), displayMode: .inline)
}
}
}
我知道这个问题已经有很多答案,但我找到了另一个我认为非常有用的解决方案。它在这样的 if 语句中包装了 sheets。对于操作 sheets,我发现在 iPad 上的滚动视图中使用其他解决方案(比如将每个 sheet 及其按钮包装在一个组中)通常会使操作 sheets 去了奇怪的地方,所以这个答案将解决 iPad.
上滚动视图内的操作 sheets 的问题struct ContentView: View{
@State var sheet1 = false
@State var sheet2 = false
var body: some View{
VStack{
Button(action: {
self.sheet1.toggle()
},label: {
Text("Sheet 1")
}).padding()
Button(action: {
self.sheet2.toggle()
},label: {
Text("Sheet 2")
}).padding()
}
if self.sheet1{
Text("")
.sheet(isPresented: self.$sheet1, content: {
Text("Some content here presenting sheet 1")
})
}
if self.sheet2{
Text("")
.sheet(isPresented: self.$sheet2, content: {
Text("Some content here presenting sheet 2")
})
}
}
}
UPD
从 Xcode 12.5.0 Beta 3(2021 年 3 月 3 日)开始,这个问题不再有意义,因为现在可以有多个 .sheet(isPresented:)
或 .fullScreenCover(isPresented:)
并且问题中提供的代码可以正常工作。
尽管如此,我发现这个答案仍然有效,因为它很好地组织了 sheet 并使代码干净且更具可读性 - 你有一个真实来源而不是几个独立的布尔值
实际答案
最好的方法,也适用于 iOS 14:
enum ActiveSheet: Identifiable {
case first, second
var id: Int {
hashValue
}
}
struct YourView: View {
@State var activeSheet: ActiveSheet?
var body: some View {
VStack {
Button {
activeSheet = .first
} label: {
Text("Activate first sheet")
}
Button {
activeSheet = .second
} label: {
Text("Activate second sheet")
}
}
.sheet(item: $activeSheet) { item in
switch item {
case .first:
FirstView()
case .second:
SecondView()
}
}
}
}
在此处阅读更多内容:https://developer.apple.com/documentation/swiftui/view/sheet(item:ondismiss:content:)
要隐藏 sheet 只需设置 activeSheet = nil
奖金:
如果你想让你的 sheet 全屏,那么使用完全相同的代码,但不是 .sheet
写 .fullScreenCover
这是一个示例,它显示了在同一个 ContentView
中使用 4 个工作表、1 个(或更多)警报和一个 actionSheet。在 iOS 13、iOS 14 中确定。在预览中确定
(来自评论:)目的是使用 sheet(item:onDismiss:content:)
和项目作为 @State
var,以及在枚举中定义的值。这样一来,所有的“生意”就是ContentView
中的self.contained了。这样,工作表或警报的数量就不受限制了。
下面是下面代码的输出:
import SwiftUI
// exemple which show use of 4 sheets,
// 1 (or more) alerts,
// and an actionSheet in the same ContentView
// OK in iOS 13, iOS 14
// OK in Preview
// Any number of sheets, displayed as Views
// can be used for sheets in other views (with unique case values, of course)
enum SheetState {
case none
case AddItem
case PickPhoto
case DocPicker
case ActivityController
}
// Make Identifiable
extension SheetState: Identifiable {
var id: SheetState { self }
}
// the same for Alerts (who are not View, but Alert)
enum AlertState {
case none
case Delete
}
extension AlertState: Identifiable {
var id: AlertState { self }
}
struct ContentView: View {
// Initialized with nil value
@State private var sheetState: SheetState?
@State private var alertState: AlertState?
var body: some View {
NavigationView {
Form {
Text("Hello, world!")
Section(header: Text("sheets")) {
addItemButton
pickDocumentButton
pickPhoto
buttonExportView
}
Section(header: Text("alert")) {
confirmDeleteButton
}
Section(header: Text("Action sheet")) {
showActionSheetButton
}
}
.navigationTitle("Sheets & Alerts")
// ONLY ONE call .sheet(item: ... with required value in enum
// if item become not nil => display sheet
// when dismiss sheet (drag the modal view, or use presentationMode.wrappedValue.dismiss in Buttons) => item = nil
// in other way : if you set item to nil => dismiss sheet
// in closure, look for which item value display which view
// the "item" returned value contains the value passed in .sheet(item: ...
.sheet(item: self.$sheetState) { item in
if item == SheetState.AddItem {
addItemView // SwiftUI view
} else if item == SheetState.DocPicker {
documentPickerView // UIViewControllerRepresentable
} else if item == SheetState.PickPhoto {
imagePickerView // UIViewControllerRepresentable
} else if item == SheetState.ActivityController {
activityControllerView // UIViewControllerRepresentable
}
}
.alert(item: self.$alertState) { item in
if item == AlertState.Delete {
return deleteAlert
} else {
// Not used, but seem to be required
// .alert(item: ... MUST return an Alert
return noneAlert
}
}
}
}
// For cleaner contents : controls, alerts and sheet views are "stocked" in private var
// MARK: - Sheet Views
private var addItemView: some View {
Text("Add item").font(.largeTitle).foregroundColor(.blue)
// drag the modal view set self.sheetState to nil
}
private var documentPickerView: some View {
DocumentPicker() { url in
if url != nil {
DispatchQueue.main.async {
print("url")
}
}
self.sheetState = nil
// make the documentPicker view dismissed
}
}
private var imagePickerView: some View {
ImagePicker() { image in
if image != nil {
DispatchQueue.main.async {
self.logo = Image(uiImage: image!)
}
}
self.sheetState = nil
}
}
private var activityControllerView: some View {
ActivityViewController(activityItems: ["Message to export"], applicationActivities: [], excludedActivityTypes: [])
}
// MARK: - Alert Views
private var deleteAlert: Alert {
Alert(title: Text("Delete?"),
message: Text("That cant be undone."),
primaryButton: .destructive(Text("Delete"), action: { print("delete!") }),
secondaryButton: .cancel())
}
private var noneAlert: Alert {
Alert(title: Text("None ?"),
message: Text("No action."),
primaryButton: .destructive(Text("OK"), action: { print("none!") }),
secondaryButton: .cancel())
}
// In buttons, action set value in item for .sheet(item: ...
// Set self.sheetState value make sheet displayed
// MARK: - Buttons
private var addItemButton: some View {
Button(action: { self.sheetState = SheetState.AddItem }) {
HStack {
Image(systemName: "plus")
Text("Add an Item")
}
}
}
private var pickDocumentButton: some View {
Button(action: { self.sheetState = SheetState.DocPicker }) {
HStack {
Image(systemName: "doc")
Text("Choose Document")
}
}
}
@State private var logo: Image = Image(systemName: "photo")
private var pickPhoto: some View {
ZStack {
HStack {
Text("Pick Photo ->")
Spacer()
}
HStack {
Spacer()
logo.resizable().scaledToFit().frame(height: 36.0)
Spacer()
}
}
.onTapGesture { self.sheetState = SheetState.PickPhoto }
}
private var buttonExportView: some View {
Button(action: { self.sheetState = SheetState.ActivityController }) {
HStack {
Image(systemName: "square.and.arrow.up").imageScale(.large)
Text("Export")
}
}
}
private var confirmDeleteButton: some View {
Button(action: { self.alertState = AlertState.Delete}) {
HStack {
Image(systemName: "trash")
Text("Delete!")
}.foregroundColor(.red)
}
}
@State private var showingActionSheet = false
@State private var foregroundColor = Color.blue
private var showActionSheetButton: some View {
Button(action: { self.showingActionSheet = true }) {
HStack {
Image(systemName: "line.horizontal.3")
Text("Show Action Sheet")
}.foregroundColor(foregroundColor)
}
.actionSheet(isPresented: $showingActionSheet) {
ActionSheet(title: Text("Change foreground"), message: Text("Select a new color"), buttons: [
.default(Text("Red")) { self.foregroundColor = .red },
.default(Text("Green")) { self.foregroundColor = .green },
.default(Text("Blue")) { self.foregroundColor = .blue },
.cancel()
])
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
在一个视图中显示多个 sheet 的另一种简单方法:
每个视图私有变量都有自己的 Bool @State 值和 .sheet(isPresented: ... call
实施简单,所有必要的都集中在一个地方。 确定 iOS 13,iOS 14,预览
import SwiftUI
struct OtherContentView: View {
var body: some View {
Form {
Section {
button1
}
Section {
button2
}
Section {
button3
}
Section {
button4
}
}
}
@State private var showSheet1 = false
private var button1: some View {
Text("Sheet 1")
.onTapGesture { showSheet1 = true }
.sheet(isPresented: $showSheet1) { Text("Modal Sheet 1") }
}
@State private var showSheet2 = false
private var button2: some View {
Text("Sheet 2")
.onTapGesture { showSheet2 = true }
.sheet(isPresented: $showSheet2) { Text("Modal Sheet 2") }
}
@State private var showSheet3 = false
private var button3: some View {
Text("Sheet 3")
.onTapGesture { showSheet3 = true }
.sheet(isPresented: $showSheet3) { Text("Modal Sheet 3") }
}
@State private var showSheet4 = false
private var button4: some View {
Text("Sheet 4")
.onTapGesture { showSheet4 = true }
.sheet(isPresented: $showSheet4) { Text("Modal Sheet 4") }
}
}
struct OtherContentView_Previews: PreviewProvider {
static var previews: some View {
OtherContentView()
}
}
这对我的应用来说效果很好,在 iOS 13.x 上有三种 sheet 的展示可能性。有趣的行为以 iOS 14 开始。由于某种原因,在应用程序启动时,当我 select 显示 sheet 时,状态变量未设置并且 sheet 出现黑屏。如果我保留 select 第一个选择,它会继续显示空白 sheet。一旦我 select 第二个选择(不同于第一个),变量就会被设置并且正确的 sheet 出现。我先 select 哪个 sheet 并不重要,同样的行为也会发生。
错误??还是我错过了什么。除了 3 个 sheet 选项外,我的代码几乎与上面的代码相同,并且我有一个自定义按钮,当按下按钮时,它接受一个参数,() -> Void,到 运行。在 iOS 13.x 中效果很好,但在 iOS 中效果不佳 14.
戴夫
我通过创建一个为我保存和管理状态的可观察 SheetContext
解决了 @State
和多个 sheet 的混乱问题。然后我只需要一个上下文实例并可以告诉它以 sheet.
我在这个博客中描述得更详细 post: https://danielsaidi.com/blog/2020/06/06/swiftui-sheets
我认为这不是 SwiftUI 表达任何观点的正确方式。
该范例通过创建在屏幕上显示某些内容的特定视图来工作,因此您可以在 superview 主体中拥有多个需要呈现某些内容的视图。因此 SwiftUI 2,在 iOS 14 上,将不会接受,开发人员应该在某些情况下调用超级视图中可以接受的所有演示文稿,但会有一些时刻如果有具体的view展示内容就更好了
我为此实施了一个解决方案,并在 Swift 5.3 和 Xcode 12.1 在 iOS 14.1
上进行了测试struct Presentation<Content>: View where Content: View {
enum Style {
case sheet
case popover
case fullScreenCover
}
@State private var isTrulyPresented: Bool = false
@State private var willPresent: Bool = false
@Binding private var isPresented: Bool
let content: () -> Content
let dismissHandler: (() -> Void)?
let style: Style
init(_ style: Style, _ isPresented: Binding<Bool>, onDismiss: (() -> Void)?, content: @escaping () -> Content) {
self._isPresented = isPresented
self.content = content
self.dismissHandler = onDismiss
self.style = style
}
@ViewBuilder
var body: some View {
if !isPresented && !willPresent {
EmptyView()
} else {
switch style {
case .sheet:
EmptyView()
.sheet(isPresented: $isTrulyPresented, onDismiss: dismissHandler, content: dynamicContent)
case .popover:
EmptyView()
.popover(isPresented: $isTrulyPresented, content: dynamicContent)
case .fullScreenCover:
EmptyView()
.fullScreenCover(isPresented: $isTrulyPresented, onDismiss: dismissHandler, content: dynamicContent)
}
}
}
}
extension Presentation {
var dynamicContent: () -> Content {
if isPresented && !isTrulyPresented {
OperationQueue.main.addOperation {
willPresent = true
OperationQueue.main.addOperation {
isTrulyPresented = true
}
}
} else if isTrulyPresented && !isPresented {
OperationQueue.main.addOperation {
isTrulyPresented = false
OperationQueue.main.addOperation {
willPresent = false
}
}
}
return content
}
}
之后,我可以为 SwiftUI
中的所有视图实现这些方法public extension View {
func _sheet<Content>(
isPresented: Binding<Bool>,
content: @escaping () -> Content
) -> some View where Content: View {
self.background(
Presentation(
.sheet,
isPresented,
onDismiss: nil,
content: content
)
)
}
func _sheet<Content>(
isPresented: Binding<Bool>,
onDismiss: @escaping () -> Void,
content: @escaping () -> Content
) -> some View where Content: View {
self.background(
Presentation(
.sheet,
isPresented,
onDismiss: onDismiss,
content: content
)
)
}
}
public extension View {
func _popover<Content>(
isPresented: Binding<Bool>,
content: @escaping () -> Content
) -> some View where Content: View {
self.background(
Presentation(
.popover,
isPresented,
onDismiss: nil,
content: content
)
)
}
}
public extension View {
func _fullScreenCover<Content>(
isPresented: Binding<Bool>,
content: @escaping () -> Content
) -> some View where Content: View {
self.background(
Presentation(
.fullScreenCover,
isPresented,
onDismiss: nil,
content: content
)
)
}
func _fullScreenCover<Content>(
isPresented: Binding<Bool>,
onDismiss: @escaping () -> Void,
content: @escaping () -> Content
) -> some View where Content: View {
self.background(
Presentation(
.fullScreenCover,
isPresented,
onDismiss: onDismiss,
content: content
)
)
}
}
此解决方案适用于 iOS 14.0
此解决方案使用 .sheet(item:, content:) 构造
struct ContentView: View {
enum ActiveSheet: Identifiable {
case First, Second
var id: ActiveSheet { self }
}
@State private var activeSheet: ActiveSheet?
var body: some View {
NavigationView {
VStack(spacing: 20) {
Button("First modal view") {
activeSheet = .First
}
Button ("Second modal view") {
activeSheet = .Second
}
}
.navigationBarTitle(Text("Multiple modal view problem"), displayMode: .inline)
.sheet(item: $activeSheet) { sheet in
switch sheet {
case .First:
Text("First modal view")
case .Second:
Text("Only the second modal view works!")
}
}
}
}
}
这个派对有点晚了,但是 none 到目前为止的答案已经解决了让 viewModel 完成工作的可能性。因为我绝不是 SwiftUI 的专家(对它还很陌生),所以完全有可能有更好的方法来做到这一点,但我找到的解决方案在这里 -
enum ActiveSheet: Identifiable {
case first
case second
var id: ActiveSheet { self }
}
struct MyView: View {
@ObservedObject private var viewModel: MyViewModel
private var activeSheet: Binding<ActiveSheet?> {
Binding<ActiveSheet?>(
get: { viewModel.activeSheet },
set: { viewModel.activeSheet = [=10=] }
)
}
init(viewModel: MyViewModel) {
self.viewModel = viewModel
}
var body: some View {
HStack {
/// some views
}
.onTapGesture {
viewModel.doSomething()
}
.sheet(item: activeSheet) { _ in
viewModel.activeSheetView()
}
}
}
...并在视图模型中 -
@Published var activeSheet: ActiveSheet?
func activeSheetView() -> AnyView {
switch activeSheet {
case .first:
return AnyView(firstSheetView())
case .second:
return AnyView(secondSheetView())
default:
return AnyView(EmptyView())
}
}
// call this from the view, eg, when the user taps a button
func doSomething() {
activeSheet = .first // this will cause the sheet to be presented
}
其中 firstSheetView() 和 secondSheetView() 提供所需的 actionSheet 内容。
我喜欢这种方法,因为它将所有业务逻辑都排除在视图之外。
编辑:从 iOS 14.5 beta 3 开始,现在已修复:
SwiftUI 已在 iOS 和 iPadOS 14.5 Beta 3
中解决- 您现在可以在同一视图层次结构中应用多个
sheet(isPresented:onDismiss:content:)
和fullScreenCover(item:onDismiss:content:)
修饰符。 (74246633)
修复之前,解决方法是对每个按钮应用 sheet 修饰符:
struct ContentView: View {
@State private var firstIsPresented = false
@State private var secondIsPresented = false
var body: some View {
NavigationView {
VStack(spacing: 20) {
Button("First modal view") {
self.firstIsPresented.toggle()
}
.sheet(isPresented: $firstIsPresented) {
Text("First modal view")
}
Button ("Second modal view") {
self.secondIsPresented.toggle()
}
.sheet(isPresented: $secondIsPresented) {
Text("Second modal view")
}
}
.navigationBarTitle(Text("Multiple modal view problem"), displayMode: .inline)
}
}
}
由于 sheet 两者做同样的事情,您可以将重复的功能提取到子视图中:
struct ContentView: View {
var body: some View {
NavigationView {
VStack(spacing: 20) {
ShowSheet(title:"First modal view")
ShowSheet(title:"Second modal view")
}
.navigationBarTitle(Text("Multiple modal view no problem!"), displayMode: .inline)
}
}
}
struct ShowSheet: View {
@State private var isPresented = false
let title: String
var body: some View {
Button(title) {
isPresented.toggle()
}
.sheet(isPresented: $isPresented) {
Text(title)
}
}
}
从 iOS 和 iPadOS 14.5 Beta 3 开始,无论何时公开发布,多张纸都将按预期工作,并且需要其他答案中的 none 解决方法。来自发行说明:
SwiftUI
Resolved in iOS & iPadOS 14.5 Beta 3
You can now apply multiple
sheet(isPresented:onDismiss:content:)
andfullScreenCover(item:onDismiss:content:)
modifiers in the same view hierarchy. (74246633)
公认的解决方案效果很好,但我想分享一个额外的扩充,以防其他人遇到同样的问题。
我的问题
我遇到了两个按钮合二为一的问题。将两个按钮配对在一起,将整个 VStack
或 HStack
变成一个大按钮。这只允许一个 .sheet
开火,不管是否使用已接受的。
解决方案
向每个按钮添加 .buttonStyle(BorderlessButtonStyle())
或 .buttonStyle(PlainButtonStyle())
,使它们像您期望的那样充当单个按钮。
抱歉,如果我在这里添加了任何失礼,但这是我第一次在 StackOverlflow 上发帖。
除以上答案外
- 如果两个 sheet 有顺序关系,您可以替换旧的 sheet
import SwiftUI
struct Sheet1: View {
@Environment(\.dismiss) private var dismiss
@State var text: String = "Text"
var body: some View {
Text(self.text)
if self.text == "Modified Text" {
Button {
dismiss()
} label: {
Text("Close sheet")
}
} else {
Button {
self.text = "Modified Text"
} label: {
Text("Modify Text")
}
}
}
}
struct SheetTester: View {
@State private var isShowingSheet1 = false
var body: some View {
Button(action: {
isShowingSheet1.toggle()
}) {
Text("Show Sheet1")
}
.sheet(isPresented: $isShowingSheet1) {
Sheet1()
}
}
}
或2.使用两个sheet并联
struct SheetTester: View {
@State private var isShowingSheet1 = false
var body: some View {
Button(action: {
isShowingSheet1.toggle()
}) {
Text("Show Sheet1")
}
.sheet(isPresented: $isShowingSheet1) {
Text("Sheet1")
Button {
isShowingSheet1.toggle()
isShowingSheet2.toggle()
} label: {
Text("Show Sheet2")
}
}
.sheet(isPresented: $isShowingSheet2) {
Text("Sheet2")
}
}
}
}