多个工作表(isPresented :)在SwiftUI中不起作用

时间:2019-11-13 12:19:58

标签: swift modal-dialog 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)。

18 个答案:

答案 0 :(得分:42)

最佳方法,它也适用于 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(action: {
                activeSheet = .first
            }) {
                Text("Activate first sheet")
            }

            Button(action: {
                activeSheet = .second
            }) {
                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:)

要隐藏工作表,只需设置activeSheet = nil

答案 1 :(得分:29)

还可以将工作表添加到放置在视图背景中的EmptyView。这可以多次执行:

  .background(EmptyView()
        .sheet(isPresented: isPresented, content: content))

答案 2 :(得分:7)

只需将按钮和.sheet调用组合在一起即可完成此操作。如果您有一个领先者和一个落后者,那就这么简单。但是,如果在开头或结尾有多个导航项,则需要将它们包装在HStack中,并将每个按钮及其工作表调用包装在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

答案 3 :(得分:7)

在其中创建自定义按钮视图和调用表可以解决此问题。

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)
        }
    }
}

当打开工作表取决于列表行的内容时,此方法也可以正常工作。

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)
        }
    }
}

答案 4 :(得分:5)

从 iOS 和 iPadOS 14.5 Beta 3 开始,无论何时公开发布,多张工作表都会按预期工作,并且不需要其他答案中的任何解决方法。来自发行说明:

<块引用>

SwiftUI

在 iOS 和 iPadOS 14.5 Beta 3 中解决

您现在可以应用多个 sheet(isPresented:onDismiss:content:) 和 同一视图中的 fullScreenCover(item:onDismiss:content:) 修饰符 等级制度。 (74246633)

答案 5 :(得分:4)

除了Rohit Makwana's answer之外,我还找到了一种将工作表内容提取到函数中的方法,因为编译器很难检查我巨大的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)

它使代码更整洁,也减轻了编译器的压力。

答案 6 :(得分:4)

请尝试以下代码

enum ActiveSheet {
   case first, second
}

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!")
                }
            }
        }
    }
}

答案 7 :(得分:2)

这对于我的应用程序非常有效,在iOS 13.x上可以进行三种工作表演示。有趣的行为始于iOS14。由于应用程序启动的某些原因,当我选择要显示的工作表时,状态变量未设置,工作表显示为空白屏幕。如果我继续选择第一个选项,它将继续显示空白页。一旦我选择了第二个选择(不同于第一个选择),就设置了变量并显示了正确的工作表。首先选择哪张纸都没有关系,同样的行为也会发生。

错误?还是我错过了一些东西。除了3个工作表选项外,我的代码几乎与上面的代码相同,并且我有一个自定义按钮,该按钮带有一个参数()-> Void,可在按下按钮时运行。在iOS 13.x上运行良好,但在iOS 14中效果不佳。

戴夫

答案 8 :(得分:2)

此解决方案适用于 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!")
                }
            }
        }
    }
}

答案 9 :(得分:1)

另一种在一个视图中显示许多工作表的简单方法:

每个视图私有变量都有其自己的Bool @State值和.sheet(isPresented:...调用

易于实现,所有必要操作都集中在一个地方。 在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()
    }
}

答案 10 :(得分:1)

公认的解决方案效果很好,但我想分享一个额外的补充,以防其他人遇到同样的问题。

我的问题

我遇到了两个按钮合二为一的问题。将两个按钮配对在一起,将整个 VStackHStack 转换为一个大按钮。这仅允许一个 .sheet 触发,而不管是否使用了已接受的。

解决方案

answer here 对我来说就像拼图的缺失部分。

向每个按钮添加 .buttonStyle(BorderlessButtonStyle()).buttonStyle(PlainButtonStyle()),使它们像您期望的那样充当单个按钮。

抱歉,如果我在此处添加此内容犯了任何失礼,但这是我第一次在 StackOverlflow 上发帖。

答案 11 :(得分:1)

编辑:从 iOS 14.5 beta 3 开始,此问题现已修复:

SwiftUI 已在 iOS 和 iPadOS 14.5 Beta 3 中解决

  • 您现在可以在同一视图层次结构中应用多个 with pdfplumber.open(file) as pdf: pages = pdf.pages for page in pdf.pages: text = page.extract_text() for i, line in enumerate(text.split('\n')): print(i, line) elif re.match(r"Error\s*:", line): tot = line.split() # how can I get line on position i+2 sheet(isPresented:onDismiss:content:) 修饰符。 (74246633)

在修复之前,一种解决方法是将工作表修改器应用于每个按钮:

fullScreenCover(item:onDismiss:content:)

由于工作表都做同样的事情,你可以将重复的功能提取到子视图中:

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)
        }
    }
}

答案 12 :(得分:1)

此示例显示在同一ContentView中使用4张纸,1个(或更多)警报和一个actionSheet的示例。在iOS 13,iOS 14中可以确定。在预览中可以确定

(来自注释:)目的是使用sheet(item:onDismiss:content:),并将项作为@State var,并在枚举中定义值。这样,所有“业务”都是自包含在ContentView中的。这样,工作表或警报的数量不受限制。

以下是以下代码的输出:

All in one

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()
    }
}

答案 13 :(得分:1)

我知道这个问题已经有很多答案,但是我发现了另一个非常有用的解决方案。它在这样的if语句中包裹工作表。对于操作表,我发现在iPad上的滚动视图中使用此处的其他解决方案(例如将每个工作表及其按钮包装在一个组中)通常会使操作表进入怪异的位置,因此此答案将解决滚动表中操作表的问题在iPad上观看。

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")
                })
        }

    }
}

答案 14 :(得分:0)

我认为这不是SwiftUI呈现任何视图的正确方法。

该范例通过创建在屏幕上显示某些内容的特定视图来工作,因此您可以在超级视图主体中拥有多个需要呈现某些内容的视图。因此,iOS 14上的SwiftUI 2将不会接受,开发人员应调用在某些情况下可以接受的超级视图中的所有演示,但是如果特定的视图呈现内容,那将有更好的时刻。

我为此实现了一个解决方案,并在iOS 14.1上使用Xcode 12.1在Swift 5.3上进行了测试

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
            )
        )
    }
}

答案 15 :(得分:0)

我通过创建一个可观察的@State来为我保存和管理状态,解决了SheetContext和多张纸的混乱情况。然后,我只需要一个上下文实例,就可以告诉它以表格形式显示任何视图。

我在此博客文章中更详细地描述了它:https://danielsaidi.com/blog/2020/06/06/swiftui-sheets

答案 16 :(得分:0)

您的情况可以通过以下方法解决(已通过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)
    }
}

答案 17 :(得分:-1)

这次聚会有点晚了,但到目前为止,没有一个答案解决了让 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 = $0 }
        )
    }

    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 内容。

我喜欢这种方法,因为它可以将所有业务逻辑排除在视图之外。