Multiple sheet(isPresented:) doesn't work in SwiftUI
Asked Answered
B

21

163

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

Blepharitis answered 13/11, 2019 at 12:19 Comment(7)
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 #58738267Sergo
Is this still an issue in iOS 14?Lowry
@EverUribe not anymoreEllary
This bug was fixed in iOS & iPadOS 14.5 Beta 3 / Xcode 12.5 beta 3Skindeep
@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).Betthezul
@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.Betthezul
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.Epilate
S
186

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

Saharanpur answered 30/7, 2020 at 21:44 Comment(16)
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 cleanerJabiru
@Jabiru you're totally right! it works from now on! thank you, I updated the answer and my project as well :)Saharanpur
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.Underpainting
@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)Saharanpur
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.Foodstuff
@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 viewsSaharanpur
@TobiasTimpe It works if you pass it as a binding to the presented viewMccay
@Mccay 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-proneSaharanpur
Your rep been sayin "Sheeeeeessssh!"Rewarding
You might want to add that it has been resolved on iOS because on macOS this is still an issue.Greenling
@JoakimDanielson really? Gotta check, thank you!Saharanpur
Does this also work with fileImporter now? i cant get that to work.Gaelan
@andredewaard unfortunately fileImporter doesn’t have such an initializer so no, you cannot :( but you can add multiple fileImporters and it will workSaharanpur
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.Urethra
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.Skindeep
@Skindeep it might be very well a bug that folks at Apple will fix some time soonSaharanpur
L
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!")
                }
            }
        }
    }
}
Lawn answered 13/11, 2019 at 12:34 Comment(12)
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?Granulite
Your SheetViews have same type: Text, how about different type? As far as I see, it's not working.Depalma
@QuangHà for that you need to do with another way. or you can take two or more SheetViews as different approach you haveLawn
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).Alumina
This solutions wasn't working on Xcode 11.3 but is working on Xcode 11.4.Bloomington
@Bloomington Let me check and update ans. Thanks for informing.Lawn
@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 && CONDITIONYore
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 trueLowry
@Ever Uribe did you fix it?Precursor
@c-villain The only solution that worked was the one provided by SoNice down belowLowry
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.Childers
@ramzesenok's solution works great in iOS 14. New method sheet(item:) and switch allowed in beta now?Sophistic
R
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))
Romine answered 13/1, 2020 at 23:31 Comment(4)
Especially useful if you have a 'root' level .sheet in the WindowGroup. (e.g. a SplashScreen)Aiden
That nailed it. Strange workaround but I guess that's SwiftUIJessejessee
very useful. love itCasares
I had a problem updating between two .sheets but as soon as I placed the second in the background everything became leakyCasares
B
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)
    }
}
Buna answered 13/11, 2019 at 12:35 Comment(1)
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 AlertWayfarer
E
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
Emmerich answered 2/2, 2020 at 22:56 Comment(2)
I've found this method to be the cleanestThanh
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:)'.Dunleavy
I
10

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)
        }
    }
}
Irrevocable answered 19/6, 2020 at 14:28 Comment(2)
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.Lowry
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 wellLowry
W
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)

Wallaby answered 2/3, 2021 at 23:1 Comment(1)
thank you! source: developer.apple.com/documentation/ios-ipados-release-notes/…Tramway
S
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.

Somnambulism answered 4/4, 2020 at 2:2 Comment(0)
D
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")
                })
        }

    }
}
Daviddavida answered 25/7, 2020 at 18:29 Comment(2)
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.Eggleston
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)Tarantula
J
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!")
                }
            }
        }
    }
}
Jobie answered 23/1, 2021 at 19:31 Comment(1)
Item is supposed to be a struct that is the datasource for the sheet. See the code sample in the header for this method.Skindeep
C
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()
    }
}
Cuirassier answered 11/8, 2020 at 14:56 Comment(3)
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.Cuirassier
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.Foodstuff
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 ForEachFoodstuff
P
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

Preadamite answered 17/9, 2020 at 17:33 Comment(1)
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.Stansbury
S
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)
        }
    }
}
Skindeep answered 23/2, 2021 at 10:32 Comment(0)
K
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.

Krantz answered 24/3, 2021 at 6:19 Comment(0)
M
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.

Mentalist answered 7/8, 2022 at 19:27 Comment(0)
C
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()
    }
}
Cuirassier answered 12/8, 2020 at 11:7 Comment(1)
Shouldn’t return View from custom computed property you need a View struct with a body property for SwiftUI to work correctly.Skindeep
C
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

Cohe answered 12/10, 2020 at 12:32 Comment(0)
I
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
            )
        )
    }
}
Idellaidelle answered 7/11, 2020 at 16:55 Comment(0)
M
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")
                }
            }
        }
    }
Melvinamelvyn answered 23/4, 2022 at 20:39 Comment(0)
A
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.

Ambassadress answered 2/8, 2022 at 0:41 Comment(0)
P
-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.

Pashto answered 27/1, 2021 at 16:14 Comment(8)
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.Skindeep
SwiftUI can work absolutely fine with view models, not sure where you’re getting that from - would you like to explain?Pashto
Read what I said I already gave a reference.Skindeep
Also I recommend watching Introduction to SwiftUI WWDC 2020. There is no doubt view models in SwiftUI is completely wrong.Skindeep
You're entitled to your point of view, however many would disagree - see nalexn.github.io/clean-architecture-swiftui or vadimbulavin.com/… or raywenderlich.com/4161005-mvvm-with-combine-tutorial-for-ios to name but a fewPashto
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?Skindeep
@Skindeep 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: 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.Wallaby
Another example of Apple using a view model is here: github.com/apple/cloudkit-sample-privatedb/blob/…. They don't view SwiftUI as lending itself to any particular pattern.Wallaby

© 2022 - 2024 — McMap. All rights reserved.