SwiftUI TextField present integer (Int64) for macOS target

NumberFormatter does not function as expected under Xcode 13 betas or macOS 12 betas (currently up to beta 5 for both).

I've logged feedback to Apple FB9423179.

I'm writing a SwiftUI universal app with macOS and iOS targets using Core Data and NSPersistentCloudKitContainer.

A Core Data Entity is an Observed Object in the code for a detail View. e.g.

@ObservedObject var account: Account where Account is the class.

NumberFormatter currently does not function in a TextField on macOS 12, so as a workaround I am currently using with great success:

TextField("Sort Order",
    text: Binding(
        get: { String(account.sortOrder) },
        set: { account.sortOrder = Int64($0.filter{"0123456789".contains($0)})! }
    )
)

where sortOrder an entity attribute of type Optional<Int64>.

This works well for number entry and in my humble opinion is elegant in that it is immediately obvious what the TextField is expected to do to get the information it displays and set the information provided by the user.

The only issue is that when the user makes a mistake entering a number, then backspaces or deletes the number such that the current value of the textfield is nil, then the application crashes because of the force unwrap in the setter.

How can I make this accept a temporary value of nil?

I have tried a number of workarounds, including the use of temporary property contained in a @State wrapper and loading and saving this temporary value using the .onAppear and .onDisappear modifiers, but that doesn't seem very SwiftUI to me and the downside is that the app no longer updates its UI dynamically.

  • could not understand how to search for your feedback on the apple site. What is the problem with NumberFormatter in a textField? It seems to work for me.

  • Probably shouldn't have included a link for the FB. But I figured out why NumberFormatter is not working for me... In my Core Data object graph and using my example above, the entity sortOrder is an Integer 64 type with "Use Scalar Type" checked. So when CodeGen creates an automatically generated property for my Account class, it does so with an entity attribute of type Int64 (or perhaps more correctly int64_t) instead of the (non-scalar) NSNumber. So from this I can only assume that NumberFormatter requires a non-scalar type NSNumber. I have a few reasons why I use scalar types but I expect that this will prompt me to do two things - update my feedback AND consider whether I transition to using non-scalar types in my SwiftUI code.

Add a Comment

Replies

you could try something like this:


            TextField("Sort Order",
                      text: Binding(
                        get: { (account.sortOrder != nil) ? String(account.sortOrder!) : "" },
                        set: {
                            if "0123456789".contains($0), let theInt = Int64($0) {
                                account.sortOrder = theInt
                            } else if $0.isEmpty { account.sortOrder = nil }
                        }
                      )
            )
  • Thanks for your help @workingdogintokyo appreciated. The getter is not the problem - get: { String(account.sortOrder) } always works. It is the setter that causes the crash because I was a little too lazy with my code and used a force unwrap. This was one way of solving the challenge that arises from my understanding (perhaps incorrect) that all Core Data objects created using CodeGen and for use in Swift are optional by default. I'll post my own answer to include more detail around the issue and a very simple solution.

Add a Comment

you could even just do this:

        TextField("Sort Order", text: Binding(
            get: { (account.sortOrder != nil) ? String(account.sortOrder!) : "" },
            set: { account.sortOrder = Int64($0.filter { "0123456789".contains($0) }) })
        )

The problem made me realise that I was force unwrapping an Optional, something I knew but didn't fully comprehend in this context. So the entity attribute sortOrder is of type "Integer 64" and checks "Use Scalar Type". On that last point, I want to work with Int64 in my code, not NSNumber.

Albeit that the UI isn't perfect, this works...

    TextField("Sort Order",
              text: Binding(
                get: { String(account.sortOrder) },
                set: { account.sortOrder = Int64($0.filter{ "0123456789".contains($0)}) ?? 0 }
              )
    )

I removed the force unwrap and instead "Coalesce using '??' to provide a default when the optional value contains 'nil'".

This doesn't work...

                set: { account.sortOrder = Int64($0.filter{ "0123456789".contains($0)}) ?? nil }

... because "'nil' is not compatible with expected argument type 'Int64'".

When I traverse my way into the automatically generated property file for the Account entity, I find this line of code for sortOrder...

    @NSManaged public var sortOrder: Int64

This file cannot edited so, using CodeGen = Class Definition and "Use Scalar Type", I am stuck with Int64.

So I guess I need to revisit my understanding of scalar types and how they work with Core Data CodeGen = Class Definition. It would seem that even though I check the "Optional" type option, the object graph and the automatically generated property file includes an Int64 - not Optional<Int64>.

As noted above, in my Core Data object graph, I check the "Use Scalar Type" for the entity attribute sortOrder of type "Integer 64". I do this for a number of reasons that I will not go into here - mainly because I spent a lot of time researching this years ago and made a decision then but acknowledge it may be outdated now - so this will force me to review my use of this option in the object graph.

FINALLY...

Because CodeGen = Class Definition creates an automatically generated property in my Account property file with an entity attribute of scalar type Int64 instead of the (non-scalar) NSNumber (standard use), I must assume that NumberFormatter requires a non-scalar type NSNumber to function.

Sure enough, when I uncheck the "Use Scalar Type" checkbox, NumberFormatter works perfectly for both macOS and iOS targets. 

Conclusion: NumberFormatter currently (14 Aug 2021) works only with NSNumber.