159

I have this ContentView with two different modal views, so I'm using sheet(isPresented:) for both, but as it seems only the last one gets presented. How could I solve this issue? Or is it not possible to use multiple sheets on a view in 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!")
            }
        }
    }
}

The above code compiles without warnings (Xcode 11.2.1).

turingtested
  • 6,356
  • 7
  • 32
  • 47
  • You can only have one sheet. This solution shows how to have different alerts which is similar to your situation and could probably be easily repurposed https://stackoverflow.com/questions/58737767/how-to-show-different-alerts-based-on-a-condition-after-clicking-a-button-in-swi/58738292#58738292 – Andrew Nov 13 '19 at 12:28
  • Is this still an issue in iOS 14? – Ever Uribe Jul 11 '20 at 01:10
  • @EverUribe not anymore – Arnav Motwani Nov 09 '20 at 10:49
  • 4
    This bug was fixed in iOS & iPadOS 14.5 Beta 3 / Xcode 12.5 beta 3 – malhal Mar 04 '21 at 15:20
  • 2
    @EverUribe I'm not running the 14.5 betas right now, and am still having this fail as of 14.4.2 on multiple test devices (current and past generation). – bemental Apr 12 '21 at 08:51
  • @EverUribe The accepted answer with the `ActiveSheet` enum and switch _does_ work well as a solution, if you're looking to still have two sheet views as I am. – bemental Apr 12 '21 at 09:20
  • Having more than one modal view presented by the same view makes no sense to me. Which one will be presented over the other? Or one after the other? Or next to each other? Can the user switch focus from one modal view to the other modal? Which one receives keyboard input? @mahal How is this "fixed"? So, the only reasonable solution is to have a sheet itself show a (_one_) modal. – CouchDeveloper Aug 11 '21 at 11:39

21 Answers21

183

UPD

Starting from Xcode 12.5.0 Beta 3 (3 March 2021) this question makes no sense anymore as it is possible now to have multiple .sheet(isPresented:) or .fullScreenCover(isPresented:) in a row and the code presented in the question will work just fine.

Nevertheless I find this answer still valid as it organizes the sheets very well and makes the code clean and much more readable - you have one source of truth instead of a couple of independent booleans

The actual answer

Best way to do it, which also works for 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()
            }
        }
    }
}

Read more here: https://developer.apple.com/documentation/swiftui/view/sheet(item:ondismiss:content:)

To hide the sheet just set activeSheet = nil

Bonus: If you want your sheet to be fullscreen, then use the very same code, but instead of .sheet write .fullScreenCover

ramzesenok
  • 5,469
  • 4
  • 30
  • 41
  • 6
    This solution works really great on iOS 14 and it is quite elegant. I would recommend to use a switch instead if (now is allowed), specially if there is more than two views to make it cleaner – jarnaez Aug 22 '20 at 10:11
  • 1
    @jarnaez you're totally right! it works from now on! thank you, I updated the answer and my project as well :) – ramzesenok Aug 22 '20 at 19:12
  • 1
    I love this solution - but for two things....1) couldn't make it work with a switch, it was much happier with a series of ifs. 2) And do you dismiss a sheet programmatically? In the traditional approach with a single sheet you toggle the isPresented flag, that's not here. Thank you. – Rob Cohen Sep 12 '20 at 23:51
  • @RobCohen you just should set the `self.activeSheet` to `nil`. Switch works with the last betas and will work in Xcode 12 GM (state 13.09.20) – ramzesenok Sep 13 '20 at 03:19
  • Is it possible to pass the activeSheet to one of the view to let it dismiss itself? It doe not seem to work on iOS 14. – Tobias Timpe Sep 16 '20 at 13:45
  • 2
    @TobiasTimpe you can pass a closure, that sets `activeSheet` to `nil` (e.g. `SheetToShow(onCompleteBlock: { self.activeSheet = nil })` and then just call that closure from the sheet). You could also pass `activeSheet` to the sheet and set it to `nil` inside the sheet, but I would recommend against exposing this parameter to other views – ramzesenok Sep 16 '20 at 13:54
  • @TobiasTimpe It works if you pass it as a binding to the presented view – Marcio Sep 17 '20 at 15:29
  • 1
    @Marcio yes it does, but as I said it’s not optimal - to expose the property. Better pass the closure, that will change it. It’s better testable and less error-prone – ramzesenok Sep 17 '20 at 15:30
  • Your rep been sayin "Sheeeeeessssh!" – xTwisteDx Apr 07 '21 at 22:35
  • You might want to add that it has been resolved on _iOS_ because on macOS this is still an issue. – Joakim Danielson Apr 22 '21 at 08:14
  • @JoakimDanielson really? Gotta check, thank you! – ramzesenok Apr 22 '21 at 08:15
  • Does this also work with fileImporter now? i cant get that to work. – andre de waard May 12 '21 at 13:23
  • @andredewaard unfortunately fileImporter doesn’t have such an initializer so no, you cannot :( but you can add multiple fileImporters and it will work – ramzesenok Jul 25 '21 at 18:25
  • This is a great solution when you need one sheet to close so another can open and then reopen the first when the first one closes. – dbDev Jun 30 '22 at 13:27
  • There is a new problem that when different presenters , e.g. popover, sheet, confirmationDialog, fullScreenCover and Menu, are used together they break in the same way like when before multiple sheets were possible. – malhal Mar 25 '23 at 13:30
  • @malhal it might be very well a bug that folks at Apple will fix some time soon – ramzesenok Mar 25 '23 at 22:30
79

Please try below code

Update Answer (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!")
                }
            }
        }
    }
}
Rohit Makwana
  • 4,337
  • 1
  • 21
  • 29
  • Tried this code with switch statement instead of if...else and got the error "Closure containing control flow statement cannot be used with function builder 'ViewBuilder'" which was baffling because isn't if...else is control flow? – Aaron Surrain Jan 16 '20 at 23:03
  • Your SheetViews have same type: Text, how about different type? As far as I see, it's not working. – Quang Hà Mar 02 '20 at 04:25
  • @QuangHà for that you need to do with another way. or you can take two or more SheetViews as different approach you have – Rohit Makwana Mar 23 '20 at 06:01
  • I encountered the same problem and this solution worked for me although I think it is not best practice because I need to sync between two parameters (showSheet, activeSheet). – evya Apr 10 '20 at 15:57
  • This solutions wasn't working on Xcode 11.3 but is working on Xcode 11.4. – Genki Apr 29 '20 at 03:39
  • @Genki Let me check and update ans. Thanks for informing. – Rohit Makwana Apr 29 '20 at 03:57
  • 1
    @AaronSurrain I'm sure you've solved it by now but that happens when you use if CONDITION, CONDITION or if let x = optional. You have to use a single expression like if CONDITION && CONDITION – ethoooo May 10 '20 at 01:09
  • 29
    This doesn't work on iOS14 it seems. It tries to load the default active sheet (ie, .first) even if the '@State' variable is changed to a different one before 'showSheet' is assigned true – Ever Uribe Jul 15 '20 at 04:07
  • 1
    @Ever Uribe did you fix it? – Alex Kraev Aug 04 '20 at 18:39
  • @c-villain The only solution that worked was the one provided by *SoNice* down below – Ever Uribe Aug 19 '20 at 16:35
  • 2
    This solution worked for me only after changing `@State private var activeSheet: ActiveSheet = .first` too `@State private var activeSheet: ActiveSheet?`, otherwise it would it would show .first on either button until first then second had been clicked. – MwcsMac Aug 25 '20 at 16:41
  • 2
    @ramzesenok's solution works great in iOS 14. New method sheet(item:) and switch allowed in beta now? – leftysauce Sep 13 '20 at 17:55
47

Can also add the sheet to an EmptyView placed in the view's background. This can be done multiple times:

  .background(EmptyView()
        .sheet(isPresented: isPresented, content: content))
Tylerc230
  • 2,883
  • 1
  • 27
  • 31
43

You're case can be solved by the following (tested with 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)
    }
}
Martijn Pieters
  • 1,048,767
  • 296
  • 4,058
  • 3,343
Asperi
  • 228,894
  • 20
  • 464
  • 690
  • I have one Alert but multiple Bool that is false but once true (outside of the body) then I want my Alert to know which Bool is true and to show a certain Alert – Dewan Mar 05 '20 at 01:03
11

You can accomplish this simply by grouping the button and the .sheet calls together. If you have one leading and one trailing it is that simple. However, if you have multiple navigationbaritems in either the leading or trailing you need to wrap them in an HStack and also wrap each button with its sheet call in a VStack.

Here's an example of two trailing buttons:

            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
JohnSF
  • 3,736
  • 3
  • 36
  • 72
  • 1
    I've found this method to be the cleanest – iOSDevil Aug 23 '20 at 12:36
  • This answer is correct!! The modifier '.sheet(isPresented:)' is not working if multiple modifier exists in same node or its ancestor. If we need to use multiple sheet in same node tree, we have to use the modifier '.sheet(item:)'. – Brownsoo Han Nov 24 '21 at 03:28
9

Creating custom Button view and call sheet in it solve this problem.

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

The ContentView will be more cleaner.

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

This method also works fine when opening sheets depends on List row content.

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)
        }
    }
}
SoNice
  • 121
  • 1
  • 3
  • Does the method on List have a performance impact? I'm not sure how sheet works but I would think there is some loading done in the background for each sheet even prior to activating it. – Ever Uribe Jul 15 '20 at 07:58
  • 1
    Tested in iOS 14 Beta 2. This only works if the parent view doesn't have a sheet modifier, otherwise the parent sheet modifier seems to override the SheetButtons. Note that a Navigation Bar button can encapsulate a sheet modifier separate from anything in the body view as well – Ever Uribe Jul 15 '20 at 18:24
7

As of iOS & iPadOS 14.5 Beta 3, and whenever they will be publicly released, multiple sheets will work as expected and none of the workarounds in the other answers will be needed. From the release notes:

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)

Curiosity
  • 544
  • 1
  • 15
  • 29
  • 1
    thank you! source: https://developer.apple.com/documentation/ios-ipados-release-notes/ios-ipados-14_5-release-notes – joliejuly Apr 26 '21 at 16:09
4

In addition to Rohit Makwana's answer, I found a way to extract the sheet content to a function because the compiler was having a hard time type-checking my gigantic View.

extension YourView {
    enum Sheet {
        case a, b
    }

    @ViewBuilder func sheetContent() -> some View {
        if activeSheet == .a {
            A()
        } else if activeSheet == .b {
            B()
        }
    }
}

You can use it this way:

.sheet(isPresented: $isSheetPresented, content: sheetContent)

It makes the code cleaner and also relieves the stress of your compiler.

cayZ
  • 111
  • 1
  • 4
3

I know that this question already has many answers, but I found another possible solution to this problem that I find extremely useful. It is wrapping sheets inside if statements like this. For action sheets, I find that using other solutions here (like wrapping each sheet and its button inside a group) inside a scroll view on the iPad often makes action sheets go to weird places so this answer will fix that problem for action sheets inside scroll views on the 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")
                })
        }

    }
}
ryandu
  • 617
  • 8
  • 17
  • this solution worked best for me as I dont use a direct button to cause the sheet to show but things like number of times run to show a welcome screen. – Prasanth Apr 23 '21 at 02:59
  • this is a great point, particularly if you have different sources for `item` with different types (such as if the action sheet item is in a `ViewModifier`) – Cenk Bilgen May 17 '21 at 17:42
3

This solution is working for iOS 14.0

This solution is using .sheet(item:, content:) construct

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!")
                }
            }
        }
    }
}
Pramodya Abeysinghe
  • 1,098
  • 17
  • 13
  • Item is supposed to be a struct that is the datasource for the sheet. See the code sample in the header for this method. – malhal Feb 23 '21 at 10:35
2

This is an example which shows the use of 4 sheets, 1 (or more) alerts, and an actionSheet in the same ContentView. OK in iOS 13, iOS 14. OK in Preview

(From comments:) The purpose is the use of sheet(item:onDismiss:content:) with item as @State var, and values defined in an enum. With that, all the "business" is self.contained in the ContentView. In that manner, the number of sheets or alerts is not limited.

Here is the output of the below code:

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()
    }
}
José María
  • 2,835
  • 5
  • 27
  • 42
u0cram
  • 89
  • 6
  • 1
    The purpose is the use of sheet(item:onDismiss:content:) with item as @State var, and values defined in an enum. With that, all the "business" is self.contained in the ContentView. In that manner, the **number of sheets or alerts is not limited**. – u0cram Aug 12 '20 at 10:14
  • I am passing the sheetState variable to the view inside the sheet to dismiss it programatically afterwards. I am using a custom initializer for this, however the sheet doesn't show up on iOS 14, only on iOS 13. – Tobias Timpe Sep 16 '20 at 13:41
  • This does not seem to work for me. I am calling the sheet modifier on a subview, could this be the problem? Like RowView.sheet() inside a ForEach – Tobias Timpe Sep 16 '20 at 14:36
2

This worked well for my App with three sheet presentation possibilities on iOS 13.x. Funny behavior began with iOS 14. For some reason on app launch when I select a sheet to be presented the state variables do not get set and the sheet appears with a blank screen. If I keep selecting the first choice it continues to present a blank sheet. As soon as I select a second choice (different from the first) the variables are set and the proper sheet presents. It doesn't matter which sheet I select first, the same bahavior happens.

Bug?? or am I missing something. My code is almost identicle to the above except for 3 sheet options and I have a custom button that takes an argument, () -> Void, to run when the button is pressed. Works great in iOS 13.x but not in iOS 14.

Dave

  • Dave i have the same problem with sheet in iOS 14 ,my app in a view have 1 sheet and 1 action,is ok until iOS 13.x but in iOS 14 come bypass. – PosF Sep 18 '20 at 14:56
2

Edit2: on the current latest iOS 16.4 there is a major bug that sheet, confirmationDialog, popover all conflict with each other. E.g. if a popover is showing and you try to show a sheet, the sheet breaks and can never be shown again.

Edit: as of iOS 14.5 beta 3 this is now fixed:

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)

Before the fix, a workaround was to apply the sheet modifier to each Button:

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

Since the sheets both do the same thing you could extract that duplicated functionality to a sub View:

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)
        }
    }
}
malhal
  • 26,330
  • 7
  • 115
  • 133
1

The accepted solution works great, but I wanted to share an additional augmentation just in case anyone else runs into this same problem.

My problem

I was having an issue where two buttons were acting as one. Pairing two buttons together, transformed the entire VStack or HStack into a single, large button. This was only allowing one .sheet to fire, regardless of using the accepted.

Solution

The answer here acted as the missing piece of the puzzle for me.

Adding either .buttonStyle(BorderlessButtonStyle()) or .buttonStyle(PlainButtonStyle()) to each button, made them act as single buttons as you would expect.

Sorry if I committed any faux pas by adding this here, but this is my first time posting on StackOverlflow.

Hunt
  • 11
  • 1
1

On SwiftUI the sheet(isPresented) is a view modifier.

A modifier that you apply to a view or another view modifier, producing a different version of the original value.

Apple Documentation.

This means that you are modifying the same view twice. On SwfitUI, the order is important, so only the last one is visible.

What you need to do, is to apply the modifiers to different views or make your sheet look different according to your needs.

Daniel
  • 783
  • 1
  • 5
  • 16
0

Another simple way to display many sheets in one view :

Each view private var has its own Bool @State value and .sheet(isPresented: ... call

Simple to implement, all necessary in one place. OK in iOS 13, iOS 14, Preview

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()
    }
}
u0cram
  • 89
  • 6
  • Shouldn’t return View from custom computed property you need a View struct with a body property for SwiftUI to work correctly. – malhal Feb 23 '21 at 09:58
0

I solved the messiness of @State and multiple sheets by creating an observable SheetContext that holds and manages the state for me. I then only need a single context instance and can tell it to present any view as a sheet.

I describe it in more details in this blog post: https://danielsaidi.com/blog/2020/06/06/swiftui-sheets

Daniel Saidi
  • 6,079
  • 4
  • 27
  • 29
0

I don't think that is the right way for SwiftUI to present any kind of view.

The paradigm works by creating specific views that show some content on the screen, so you can have more than one view inside the body of superview that needs to present something. So the SwiftUI 2, on iOS 14, will not accept that and the developer should call all presentations in the superview that can be accepted in some cases, but will have moments that will be better if the specific views present the content.

I implemented a solution for that and test on Swift 5.3 with Xcode 12.1 on 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
    }
}

After that, I can implement these methods for all views in 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
            )
        )
    }
}
0

In addition to the above answers

  1. You can replace the old sheet if two sheets have sequential relationships
    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()
            }
        }
    }

Or 2. Use two sheets parallel

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

I will be honest with you and I imagine the easy solution like that. You put the sheet as you have done, and then inside this sheet putting a Text inside some Stacks (not necessary) and make another sheet inside of it and then using a second boolean to open another one. Just like a matrioska.

Davencode
  • 75
  • 6
-1

Bit late to this party, but none of the answers so far have addressed the possibility of having a viewModel do the work. As I'm by no means an expert at SwiftUI (being pretty new to it), it's entirely possible that there may be better ways of doing this, but the solution I reached is here -

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

...and in the viewModel -

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

where firstSheetView() & secondSheetView() are providing the required actionSheet content.

I like this approach as it keeps all the business logic out of the views.

SomaMan
  • 4,127
  • 1
  • 34
  • 45
  • SwiftUI can’t use view model pattern because it doesn’t have traditional views or view controllers. There is a lot of magic happening behind the scenes that the MVVM folks don’t understand yet, you have to learn structs and use @ State and @ Binding to make structs behave like objects. Watch the WWDC 2020 video Data essentials in SwiftUI. – malhal Feb 23 '21 at 10:03
  • 2
    SwiftUI can work absolutely fine with view models, not sure where you’re getting that from - would you like to explain? – SomaMan Feb 24 '21 at 22:33
  • Read what I said I already gave a reference. – malhal Feb 24 '21 at 22:40
  • Also I recommend watching Introduction to SwiftUI WWDC 2020. There is no doubt view models in SwiftUI is completely wrong. – malhal Feb 24 '21 at 22:46
  • 1
    You're entitled to your point of view, however many would disagree - see https://nalexn.github.io/clean-architecture-swiftui/ or https://www.vadimbulavin.com/modern-mvvm-ios-app-architecture-with-combine-and-swiftui/ or https://www.raywenderlich.com/4161005-mvvm-with-combine-tutorial-for-ios to name but a few – SomaMan Mar 02 '21 at 13:16
  • It's not my point of view I'm just following Apple's point of view. All those you mention disagree with Apple. So who do you agree with, Apple or those random bloggers? – malhal Mar 03 '21 at 14:22
  • 1
    @malhal while it's certainly not common for Apple to use view models, there's at least one example of them using them in SwiftUI for CareKit UI: https://developer.apple.com/videos/play/wwdc2020-10151/?time=590. I remember Josh Schaffer (head of UIKit and SwiftUI) saying in an interview with John Sundell that separation of concerns is what's important, and that Apple doesn't try to prescribe architecture for using SwiftUI. – Curiosity Mar 11 '21 at 22:35
  • 1
    Another example of Apple using a view model is here: https://github.com/apple/cloudkit-sample-privatedb/blob/swift-concurrency/PrivateDatabase/ViewModel.swift. They don't view SwiftUI as lending itself to any particular pattern. – Curiosity Jun 23 '21 at 22:41