2

I basically have the same code as in this question. The problem I have is that when the tapGesture event happens, the sheet shows (the sheet code is called) but debug shows that showUserEditor is false (in that case, how is the sheet showing...?) and that selectedUserId is still nil (and therefore crashes on unwrapping it...)

The view:

struct UsersView: View {
    @Environment(\.managedObjectContext)
    private var viewContext

    @FetchRequest(
        sortDescriptors: [NSSortDescriptor(keyPath: \User.nickname, ascending: true)],
        animation: .default)
    private var users: FetchedResults<User>
    
    @State private var selectedUserId : NSManagedObjectID? = nil
    @State private var showUserEditor = false
    
    var body: some View {
        NavigationView {
            List {
                ForEach(users) { user in
                    UserRowView(user: user)
                        .onTapGesture {
                            self.selectedUserId = user.objectID
                            self.showUserEditor = true
                        }
                    }
                }
            }.sheet(isPresented: $showUserEditor) {
                UserEditorView(userId: self.selectedUserId!)
            }
        }
}

If you want, I can publish the editor and the row but they seem irrelevant to the question as the magic should happen in the view.

11
  • At least set selectedUserId before setting showUserEditor to true, exchange the lines. Commented May 1, 2021 at 18:35
  • Or use the sheet init with item in it Commented May 1, 2021 at 18:36
  • @vadian I did that... that was the obvious way, I changed it and didnt check back. I'll edit the question to reflect that. In any case, to my understanding, it shouldn't matter for the SwiftUI redraw cycle. Also, note that when I debug the sheet code, even the showUserEditor is false when printing the variables so I'm confused there... Perhaps I'm unclear on what I should look for when debugging @State variables, though. Commented May 1, 2021 at 19:00
  • @loremipsum - found what you mean: developer.apple.com/documentation/swiftui/view/… I'll give it a try. Commented May 1, 2021 at 19:00
  • 1
    There is a .sheet init where you pass the item as a parameter. When the @State for the item is not nil it shows a sheet developer.apple.com/documentation/swiftui/text/… Commented May 1, 2021 at 19:04

2 Answers 2

0

So, I still haven't figured out WHY the code posted in the question didn't work, with a pointer from @loremipsum I got a working code by using another .sheet() method, one that takes an optional Item and not a boolean flag. The code now looks like this and works, but still if anyone can explain why the posted code didn't work I'd appreciate it.

struct UsersView: View {
    @Environment(\.managedObjectContext)
    private var viewContext

    @FetchRequest(
        sortDescriptors: [NSSortDescriptor(keyPath: \User.nickname, ascending: true)],
        animation: .default)
    private var users: FetchedResults<User>
    
    @State private var selectedUser : User? = nil
    
    var body: some View {
        NavigationView {
            List {
                ForEach(users) { user in
                    UserRowView(user: user)
                        .onTapGesture {
                            self.selectedUser = user
                        }
                    }.onDelete(perform: deleteItems)
                }
        }.sheet(item: $selectedUser, onDismiss: nil) { user in
            UserEditorView(user: user)
        }
    }
}
Sign up to request clarification or add additional context in comments.

1 Comment

I can type up an answer. And explain better it has to do with a struct being immutable when the sheet is created the variable in already set and the body doesn’t get reloaded because you are showing a sheet.
0

struct == immutable and SwiftUI decides when the struct gets init and reloaded

Working with code that depends on SwiftUI updating non-wrapped variables at a very specific time is not recommended. You have no control over this process.

To make your first setup work you need to use SwiftUI wrappers for the variables

.sheet(isPresented: $showUserEditor) {
            //struct == immutable SwiftUI wrappers load the entire struct when there are changes
            //With your original setup this variable gets created/set when the body is loaded so the orginal value of nil is what is seen in the next View
            UserEditorView1(userId: $selectedUserId)
        }

struct UserEditorView1: View {
    //This is what you orginal View likely looks like it won't work because of the struct being immutable and SwiftUI controlling when the struct is reloaded

    //let userId: NSManagedObjectID? <---- Depends on specific reload steps

    //To make it work you would use a SwiftUI wrapper so the variable gets updated when SwiftUI descides to update it which is invisible to the user
    @Binding var userId: NSManagedObjectID?
    //This setup though now requres you to go fetch the object somehow and put it into the View so you can edit it.
    //It is unnecessary though because SwiftUI provides the .sheet init with item where the item that is set gets passed directly vs waiting for the SwiftUi update no optionals
    var body: some View {
        Text(userId?.description ?? "nil userId")
    }
}

Your answer code doesn't work because your parameter is optional and Binding does not like optionals

struct UserEditorView2: View {
    //This is the setup that you posted in the Answer code and it doesn't work becaue of the ? Bindings do not like nil. You have to create wrappers to compensate for this
    //But unecessary because all CoreData objects are ObservableObjects so you dont need Binding here the Binding is built-in the object for editing the variables
    @Binding var user: User?
    
    var body: some View {
        TextField("nickname", text: $user.nickname)
    }
}

Now for working code with an easily editable CoreData Object

struct UsersView: View {
    @Environment(\.managedObjectContext)
    private var viewContext
    
    @FetchRequest(
        sortDescriptors: [NSSortDescriptor(keyPath: \User.nickname, ascending: true)],
        animation: .default)
    private var users: FetchedResults<User>
    //Your list view would use the CoreData object to trigger a sheet when the new value is available. When nil there will not be a sheet available for showing
    @State private var selectedUser : User? = nil
    
    var body: some View {
        NavigationView {
            List {
                ForEach(users) { user in
                    UserRowView(user: user)
                        .onTapGesture {
                            self.selectedUser = user
                        }
                }
            }
        }.sheet(item: $selectedUser, onDismiss: nil) { user in //This gives you a non-optional user so you don't have to compensate for nil in the next View
            UserEditorView3(user: user)
        }
    }
}

Then the View in the sheet would look like this

struct UserEditorView3: View {
    //I mentioned the ObservedObject in my comment 
    @ObservedObject var user: User
    var body: some View {
        //If your nickname is a String? you have to compensate for that optional but it is much simpler to do it from here
        TextField("nickname", text: $user.nickname.bound)
    }
}


//This comes from another very popular SO question (couldn't find it to quote it) that I could not find and is necessary when CoreData does not let you define a variable as non-optional and you want to use Binding for editing
extension Optional where Wrapped == String {
    var _bound: String? {
        get {
            return self
        }
        set {
            self = newValue
        }
    }
    public var bound: String {
        get {
            //This just give you an empty String when the variable is nil
            return _bound ?? ""
        }
        set {
            _bound = newValue.isEmpty ? nil : newValue
        }
    }
}

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.