I have an initial view in my app that needs to open some modal views. The data I'm working with is form Core Data and I'm populating the list view with the @FetchedRequest
property wrapper. The UI I'm trying to make is similar to the way you create and edit Lists in the Reminders app in iOS 13.
Both of these are sort of working now but there are some issues.
When I close the Add modal, sometimes I can't tap on the Edit button in the top right of the view. Nothing will happen at all when tapping. I tried attaching print state statement and doesn't get called. This happens most of the time, but not always.
When I edit a record using the process described above, when tap Cancel or Done to close this modal it will close. But after this point I cannot use this feature. If I try to open another edit model, it will open, but the data I pass to it is never displayed in the field.
I separated these into two views and sheets because I was having a lot of issues when I had them combined into a single view and sheet.
Update: Issue 1 was addressed by moving both .sheet modifiers outside of the NavigationView and attaching them to some hidden objects, as per this answer.
As for issues 2, I spent some time this evening trying a few different things and I think I can see what the problem is. When the Edit modal is re-opened after the initial use, it is going through any sort of init process. No initialization is called, and even onAppear()
attached to a view is not being called. In this case, the @State variable that the text field uses is never populated with the value from the record, because the text field code is not called again. I have no idea how to deal with this though. In any other environment I would just delete the instance and make a new one, but I don't know how to remove the cached instance of the Edit view created by the modal.
Update: as of 2019.08.27 the second issue was fixed with iOS 13.1. I didn't even have to recompile to see the fix.
Updated to move the sheets outside of the NavigationView
import SwiftUI
struct TimelineListView: View {
@Environment(\.managedObjectContext) var managedObjectContext
@FetchRequest(fetchRequest: Timeline.allTimelinesFetchRequest()) var timelines: FetchedResults<Timeline>
@State var listViewEditMode: EditMode = .inactive
@State var openModalSheetAdd = false
@State var openModalSheetEdit = false
@State var timelineToEdit: Timeline?
var body: some View {
VStack{
NavigationView {
VStack {
List(self.timelines) { timeline in
NavigationLink(destination: TimelineDetailView(timeline: timeline)) {
HStack{
Text(timeline.name ?? "Unnamed List")
if (self.listViewEditMode == .active) {
Spacer()
Image(systemName: "info.circle")
.onTapGesture {
self.timelineToEdit = timeline
self.listViewEditMode = .inactive
self.openModalSheetEdit = true
}
}
}
} // End Navigation Link
} // End List
HStack {
Spacer()
Button("Add List") {
self.openModalSheetAdd = true
}.padding(EdgeInsets(top: 0, leading: 0, bottom: 10, trailing: 20))
}
}
.navigationBarTitle(Text("Lists"), displayMode: .inline)
.navigationBarItems(trailing: EditButton())
.environment(\.editMode, self.$listViewEditMode)
}// End Navigation View
// Edit modal
Text("").hidden()
.sheet(isPresented: $openModalSheetEdit,
onDismiss: {
self.openModalSheetEdit = false
},
content: {
TimelineEditView(timeline: self.timelineToEdit, onDismiss: {
self.openModalSheetEdit = false // this is here because sometimes onDismiss is not called.
})
.environment(\.managedObjectContext, self.managedObjectContext)
})
// Add modal
Text("").hidden()
.sheet(isPresented: $openModalSheetAdd,
onDismiss: {
self.openModalSheetAdd = false
},
content: {
TimelineAddView(onDismiss: {
self.openModalSheetAdd = false // this is here because sometimes onDismiss is not called.
})
.environment(\.managedObjectContext, self.managedObjectContext)
})
}
}
}
struct TimelineAddView: View {
@Environment(\.managedObjectContext) var managedObjectContext
@Environment(\.presentationMode) var presentationMode: Binding<PresentationMode>
@State private var nameEdit: String = ""
// This closure is here to call when we dismiss the modal because onDismiss is not always called
var onDismiss: () -> ()
var body: some View {
VStack {
HStack {
Button(action: ({
self.nameEdit = ""
self.presentationMode.wrappedValue.dismiss()
self.onDismiss()
})) {
Text("Cancel")
}
.padding(EdgeInsets(top: 10, leading: 10, bottom: 0, trailing: 0))
Spacer()
Button(action: ({
let newTimeline = Timeline(context: self.managedObjectContext)
newTimeline.name = self.nameEdit
self.nameEdit = ""
do {
try self.managedObjectContext.save()
} catch {
print(error)
}
self.presentationMode.wrappedValue.dismiss()
self.onDismiss()
})) {
Text("Done")
}
.padding(EdgeInsets(top: 10, leading: 0, bottom: 0, trailing: 10))
}
TextField("Data to edit", text: self.$nameEdit)
.shadow(color: .secondary, radius: 1, x: 0, y: 0)
.textFieldStyle(RoundedBorderTextFieldStyle())
.onAppear {
self.nameEdit = ""
}.padding()
Spacer()
}
}
}
struct TimelineEditView: View {
@Environment(\.managedObjectContext) var managedObjectContext
@Environment(\.presentationMode) var presentationMode: Binding<PresentationMode>
@State private var nameEdit = ""
var timeline : Timeline?
// This closure is here to call when we dismiss the modal because onDismiss is not always called
var onDismiss: () -> ()
var body: some View {
return VStack {
HStack {
Button(action: ({
// Dismiss the modal sheet
self.nameEdit = ""
self.presentationMode.wrappedValue.dismiss()
self.onDismiss()
})) {
Text("Cancel")
}
.padding(EdgeInsets(top: 10, leading: 10, bottom: 0, trailing: 0))
Spacer()
Button(action: ({
self.timeline?.name = self.nameEdit
do {
try self.managedObjectContext.save()
} catch {
print(error)
}
// Dismiss the modal sheet
self.nameEdit = ""
self.presentationMode.wrappedValue.dismiss()
self.onDismiss()
})) {
Text("Done")
}
.padding(EdgeInsets(top: 10, leading: 0, bottom: 0, trailing: 10))
}
TextField("Data to edit", text: self.$nameEdit)
.shadow(color: .secondary, radius: 1, x: 0, y: 0)
.textFieldStyle(RoundedBorderTextFieldStyle())
.onAppear {
self.nameEdit = self.timeline?.name ?? ""
}.padding()
Spacer()
}
}
}
In order for us to be able to use multiple sheets, each modifier has to be applied to the button directly. Using two or more consecutive sheet modifiers does not work in SwiftUI, only the last sheet modifier will be applied and can be used. As a result, we could design our view like this.
In order for a view to be displayed modally in SwiftUI, the sheet modifier can be used. In its simplest form a sheet is being presented if a given condition is true. In this example, the sheet modifier is applied to the VStack. However, it could also be applied to the Button directly.
Watch the WWDC 2020 video Data essentials in SwiftUI. SwiftUI can work absolutely fine with view models, not sure where you’re getting that from - would you like to explain?
In its simplest form a sheet is being presented if a given condition is true. In this example, the sheet modifier is applied to the VStack. However, it could also be applied to the Button directly. What if we wanted to display more than one sheet from a specific view, though?
.sheet
being embedded inside a List
does only open once is a known bug and I hope they fix it. Until then you have to move .sheet
outside of any List
.
But since the .sheet
is not inside a List
but inside a NavigationView
, it might a good idea to try to move the .sheet
outside of it.
But do not attach two .sheet
two the same View, but add them the following way instead:
VStack{
NavigationView{ ... }
Text("").hidden().sheet(...)
Text("").hidden().sheet(...)
}
I have the same problem with ScrollView
.
The sheet
need to be add out of the ScrollView
to be sure we can open more than once.
ScrollView {
[...]
}.sheet([...])
It's really not practical if you extract your views into another views like for a DatePicker for exemple.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With