bustin
bustin

Reputation: 15

Why does using FetchedResults work for Text and not TextField? SwiftUI, CoreData/ FetchRequest

I am getting CoreData properties from a FetchRequest and want to use it to pre-populate a text field (a user's Email).

Here is the FetchRequest

     @FetchRequest(
       entity: Account.entity(),
       sortDescriptors:[
       NSSortDescriptor(keyPath: \Account.id, ascending: true)
       ]
     )var accounts: FetchedResults<Account>

Then in my View stack I have:

var body: some View{
  ZStack {
    Form {
      Section(header: Text("EMAIL")) {
        ForEach(accounts, id: \.self) {account in
          TextField("Email", text:account.email)    // This is where I get an error (see error below)                         
          }
        }
    }

However is I simply list accounts in a textfield it works. Like so:

var body: some View{
  ZStack {
    Form {
      List(accounts, id: \.self) { account in
         Text(account.email ?? "Unknown")
      }
    }
  }

Why does the second code that uses List not give me the same error?

I thought it had something to do with the ?? operator but after research I realized that it perfectly fine to do given that email in my coredata object is String?.

Now my thought is that I am getting this error here because TextField needs a Binding wrapper? If that is true I'm not sure how to get this to work. All I want to do is have this TextField pre-populated with the single email record the FetchRequest retrieves from my Account Core Data object.

Thanks.

Edit: I want to add that I have found this post https://www.hackingwithswift.com/quick-start/swiftui/how-to-fix-cannot-convert-value-of-type-string-to-expected-argument-type-binding-string

and now I think what I need to do is store this account.email result into a State variable. My question still remains however, I'm not sure how to do this as I am looking for clean way to do it right in the view stack here. Thanks again.

Upvotes: 0

Views: 661

Answers (2)

Adam
Adam

Reputation: 5135

TextField needs a binding to a string variable. Account is and ObservableObject, like all NSManagedObjects, so if you refactor your TextField into a separate View you could do this:

Form {
    Section(header: Text("EMAIL")) {
        ForEach(accounts, id: \.self) { account in
            Row(account: account)
        }
    }
}

...

struct Row: View {
    @ObservedObject var account: Account
    
    var body: some View {
        TextField("Email", text: $account.email)
    }
}

Note the $account.email — the $ turns this into a binding.

Unfortunately, this new code also fails, because email is a String? (i.e., it can be nil) but TextField doesn’t allow nil. Thankfully that’s not too hard to fix, because we can define a custom Binding like so:

struct Row: View {
    @ObservedObject var account: Account
    
    var email: Binding<String> {
        Binding<String>(get: {
            if let email = account.email {
                return email
            } else {
                return "Unknown"
            }
        },
        set: { account.email = $0 })
    }
    
    var body: some View {
        TextField("Email", text: email)
    }
}

Notice we don’t have a $ this time, because email is itself a Binding.

Upvotes: 1

jnpdx
jnpdx

Reputation: 52535

This answer details getting a CoreData request into a @State variable: Update State-variable Whenever CoreData is Updated in SwiftUI

What you'll end up with is something like this:

@State var text = ""

In your view (maybe attached to your ZStack) an onReceive property that tells you when you have new CoreData to look at:

ZStack {
...
}.onReceive(accounts.publisher, perform: { _ in
                //you can reference self.accounts at this point
                //grab the index you want and set self.text equal to it
            })

However, you'll have to do some clever stuff to make sure setting it the first time and then probably not modifying it again once the user starts typing.

This could also get complicated by the fact that you're in a list and have multiple accounts -- at this point, I may split every list item out into its own view with a separate @State variable.

Keep in mind, you can also write custom bindings like this:

Binding<String>(get: {
//return a value from your CoreData model
}, set: { newValue in })

But unless you get more clever with how you're returning in the get section, the user won't be able to edit the test. You could shadow it with another @State variable behind the scenes, too.

Finally, here's a thread on the Apple Developer forums that gets even more in-depth about possible ways to address this: https://developer.apple.com/forums/thread/128195

Upvotes: 0

Related Questions