Multiple sheet(isPresented:) doesn't work in SwiftUI
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
Multiple Bottom sheets - the content doesn't load SwiftUI
The .sheet modifier will create the sheet view as soon as LogInView() is initialized. In your 'if.. else if..' statement, there is no logic to catch 'else' situations (situations where action == nil). Therefore, since action == nil on init(), the first .sheet that will present will fail your 'if..else if' and an EmptyView will present.
But don't worry! This is a common issue and can be easily solved. Here are 2 easy ways to implement methods to fix this (I prefer the 2nd method bc it's cleaner):
METHOD 1: Present a single view & change that view's content instead of switching between which view to present.
Instead of doing the 'if.. else if..' statement within the .sheet modifier, present a static view (I've called it SecondaryView ) that has a @Binding variable connected to your action. This way, when LogInView() appears, we can ensure that it will definitely render this view and then we can simply modify this view's content by changing the @Binding action.
import SwiftUI
struct LogInView: View {
enum Action{
case resetPW, signUp
}
@State private var showSheet = false
@State private var action: Action?
var body: some View {
LoginEmailView(showSheet: $showSheet, action: $action)
.sheet(isPresented: $showSheet) {
SecondaryView(action: $action)
}
}
}
struct LoginEmailView: View {
@Binding var showSheet: Bool
@Binding var action: LogInView.Action?
var body: some View {
VStack(spacing: 40 ){
Text("Forgot Password")
.onTapGesture {
action = .resetPW
showSheet.toggle()
}
Text("Sign Up")
.onTapGesture {
action = .signUp
showSheet.toggle()
}
}
}
}
struct SecondaryView: View {
@Binding var action: LogInView.Action?
var body: some View {
if action == .signUp {
Text("SIGN UP VIEW HERE")
} else {
Text("FORGOT PASSWORD VIEW HERE")
}
}
}
METHOD 2: Make each Button it's own View, so that it can have it's own .sheet modifier.
In SwiftUI, we are limited to 1 .sheet() modifier per View. However, we can always add Views within Views and each subview is then allowed it's own .sheet() modifier as well. So the easy solution is to make each of your buttons their own view. I prefer this method because we no longer need to pass around the @State/@Binding variables between views.
struct LogInView: View {
var body: some View {
LoginEmailView()
}
}
struct LoginEmailView: View {
var body: some View {
VStack(spacing: 40 ){
ForgotPasswordButton()
SignUpButton()
}
}
}
struct ForgotPasswordButton: View {
@State var showSheet: Bool = false
var body: some View {
Text("Forgot Password")
.onTapGesture {
showSheet.toggle()
}
.sheet(isPresented: $showSheet, content: {
Text("FORGOT PASSWORD VIEW HERE")
})
}
}
struct SignUpButton: View {
@State var showSheet: Bool = false
var body: some View {
Text("Sign Up")
.onTapGesture {
showSheet.toggle()
}
.sheet(isPresented: $showSheet, content: {
Text("SIGN UP VIEW HERE")
})
}
}
Sheet displayed several times in SwiftUI
If you have a .sheet(isPresented:)
modifier in a view that is repeated in a loop, but your boolean state variable is outside the loop, then that one state variable is used for multiple copies of the sheet. Sometimes that may manifest as a sheet showing with details different to the row you clicked on; other times you might see multiple sheets. It sounds like the side effects you're seeing are related to this.
There are a couple of ways you can get around this.
Option 1 - sheet(item:)
Replace your showingBorrowerEditScreen
with a borrowerToEdit
state variable that is an optional object, defaulting to nil:
@State private var borrowerToEdit: Borrower? = nil
In your button action, set this to the borrower of the current row in the loop:
Button {
borrowerToEdit = borrower
} label: {
// etc
And finally, use the item:
form of the sheet modifier outside the ForEach
loop. Note that this form takes a block with a reference to the borrower object concerned.
ForEach(...) { borrower in
// etc.
}
.sheet(item: $borrowerToEdit) { borrower in
EditBorrowerView(
aBorrower: borrower,
firstName: borrower.firstName!,
lastName: borrower.lastName!,
age: Int(borrower.age)
)
}
Option 2 - individual subviews
If you want to stick with booleans to represent whether a modal sheet should be used, you'll need to isolate that boolean to its own subview, by extracting the row details. For example:
List {
if (book.booksBorrowers != nil) {
ForEach (Array(book.booksBorrowers! as! Set<Borrowers>), id: \.self) { borrower in
BorrowerListRow(borrower: borrower)
.onDelete(perform: deleteBorrower)
}
}
}
struct BorrowerListRow: View {
@ObservedObject var borrower: Borrower
@State private var showingBorrowerEditScreen = false
var body: some View {
HStack {
Text(borrower.firstName ?? "unbekannter Vorname")
Text(borrower.lastName ?? "unbekannter Nachname")
Text(String(format: "%.0f", borrower.age))
Spacer()
Button {
showingBorrowerEditScreen.toggle()
} label: {
Image(systemName: "pencil")
.frame(width: 20.0, height: 20.0)
}.multilineTextAlignment(.center).buttonStyle(.borderless)
.sheet(isPresented: $showingBorrowerEditScreen) {
EditBorrowerView(
aBorrower: borrower,
firstName: borrower.firstName!,
lastName: borrower.lastName!,
age: Int(borrower.age)
).environment(\.managedObjectContext, self.viewContext)
}
}
}
}
That way, each row has its own "should I be showing a modal" Boolean, so there can be no confusion about which row "owns" the active sheet, and only one will be able to be shown at a time.
Which option you go for is partly down to personal preference. I tend to favour option 1 in my own code, as it feels more that the modal is "owned" by the list rather than a row, and it reinforces the idea that you can only edit one borrower at a time. But either approach should eliminate the problems you're currently seeing.
SwiftUI - Sheet Dismmis button not working
You can just pass in the Binding
that you use to present the sheet to TopicsExperienceCards
as well.
struct ContentView: View {
@State var isPresented = false
var body: some View {
Button("Present") {
isPresented = true /// set Binding to true to present
}
.sheet(isPresented: $isPresented) {
TopicsExperienceCards(isPresented: $isPresented) /// pass Binding here
}
}
}
struct TopicsExperienceCards: View {
@Binding var isPresented: Bool
var body: some View {
VStack {
HStack {
Spacer()
Button(action: {
isPresented = false /// set Binding back to false
}) {
Image(systemName: "xmark")
.padding()
}
}
Spacer()
}
}
}
Result:
Related Topics
How Does One Generate a Random Number in Swift
Do Swift-Based Applications Work on Os X 10.9/Ios 7 and Lower
How to Make a Weak Protocol Reference in 'Pure' Swift (Without @Objc)
How to Create Array of Unique Object List in Swift
What Does a Module Mean in Swift
Calculate Age from Birth Date Using Nsdatecomponents in Swift
Nsattributedstring, Change the Font Overall But Keep All Other Attributes
Wrong Specialized Generic Function Gets Called in Swift 3 from an Indirect Call
How to Use String.Substringwithrange? (Or, How Do Ranges Work in Swift)
Object X of Class Y Does Not Implement Methodsignatureforselector in Swift
Nsurlsession Concurrent Requests With Alamofire
When Are Argument Labels Required in Swift
How to Encode Enum Using Nscoder in Swift
Arkit - What Do the Different Columns in Transform Matrix Represent