Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

SwiftUI Issues with sheet modifier

Tags:

swiftui

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.

  • Add record - Allow the user to create a new List (child objects are later) at any time.
  • Edit record - Toggle the List view into Edit mode, tap an edit button on a row to open a modal to edit the record.

Both of these are sort of working now but there are some issues.

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

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

List view

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

    }
}

Add record view

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

Edit record view

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

like image 570
radicalappdev Avatar asked Aug 23 '19 18:08

radicalappdev


People also ask

How to use multiple sheet modifiers in SwiftUI?

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.

How do you display a sheet in SwiftUI?

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.

Does SwiftUI work with view models?

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?

What is the sheet modifier?

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?


Video Answer


2 Answers

.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(...)
}
like image 50
Fabian Avatar answered Dec 30 '22 15:12

Fabian


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.

like image 21
Pascal Guizard Avatar answered Dec 30 '22 14:12

Pascal Guizard