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.

Aviad Ben Dov
  • 6,351
  • 2
  • 34
  • 45
  • At least set `selectedUserId` **before** setting `showUserEditor` to true, exchange the lines. – vadian May 01 '21 at 18:35
  • Or use the `sheet` init with `item` in it – lorem ipsum May 01 '21 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. – Aviad Ben Dov May 01 '21 at 19:00
  • @loremipsum - found what you mean: https://developer.apple.com/documentation/swiftui/view/sheet(item:ondismiss:content:) I'll give it a try. – Aviad Ben Dov May 01 '21 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 https://developer.apple.com/documentation/swiftui/text/sheet(item:ondismiss:content:) – lorem ipsum May 01 '21 at 19:04
  • Instead of it being a two-step process it becomes a 1 step process because all you do is set the item. – lorem ipsum May 01 '21 at 19:06
  • @loremipsum - it works! I had to extend `NSManagedObjectID` to conform to `Identifiable` but that was fairly easy, I think - just returned `uriRepresentation().absoluteString`. Think there's a problem with that? Otherwise I'll post it as an answer (or you will, and get the credit..) – Aviad Ben Dov May 01 '21 at 19:21
  • You can post it. I don't have the exact code. you should be able to just set the `User` something like `@State private var userDetail: User?` then just pass `UserEditorView(user: user)` and have an `@ObservedObject user: User` in `UserEditorView` for easy editing since you are using CoreData. Look at the sample code from the site – lorem ipsum May 01 '21 at 19:30
  • @loremipsum - that works of course but I couldn't find a way to send the `User`'s properties as bound properties, for instance `user.$nickname` doesn't compile for some reason (doesn't give a reason.. " file a bug" error.). Anyway, there's an easy workaround for that so its working. Thanks! – Aviad Ben Dov May 02 '21 at 04:31
  • Put the $ before user not the variable $user.variable – lorem ipsum May 02 '21 at 10:05
  • Make sure you are observing the user as well put this @ObservedObject user: User in your edit view vs Binding – lorem ipsum May 02 '21 at 10:19

2 Answers2

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)
        }
    }
}
lorem ipsum
  • 21,175
  • 5
  • 24
  • 48
Aviad Ben Dov
  • 6,351
  • 2
  • 34
  • 45
  • 1
    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. – lorem ipsum May 02 '21 at 10:25
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
        }
    }
}
lorem ipsum
  • 21,175
  • 5
  • 24
  • 48