Need help implementing a custom Formatter

I want to make a custom Formatter to strip any white space. Following Creating a Custom Formatter: need to at least implement these two methods:

class SanSpaceFormatter: Formatter {
    override func string(for obj: Any?) -> String? {
        // what's obj? How to cast Any? to something?
        "placeholder for now"
    }

    override func getObjectValue(_ obj: AutoreleasingUnsafeMutablePointer<AnyObject?>?, for string: String, errorDescription error: AutoreleasingUnsafeMutablePointer<NSString?>?) -> Bool {
        // obj is what?
        false   // placeholder for now
    }
}

they don't look very Swift...

Please help
:pray:

They don't look Swift because they aren't: this is an Objective-C API imported into Swift.

Unless you dealing with other APIs that require Formatter instance, it's usually easier to just start with a (String) -> String closure for your formatting and go from there.

The Swift way is something like this:

extension String.StringInterpolation {
    mutating func appendInterpolation(strip str: String) {
        var strippedStr = str
        // Write code to strip whitespace
        appendLiteral(strippedStr)
    }
}

print("\(strip: "Name")")
1 Like

Sorry I didn't explain fully: I need to implement a Formatter for this:

SwiftUI.TextField.init(_:value:formatter:onEditingChanged:onCommit:)

Still don't know what's the correct thing to do. But this seems to work:

class SanSpaceFormatter: Formatter {
    override func string(for obj: Any?) -> String? {
        if let x = obj {
            return (x as! String).filter { !$0.isWhitespace }
        }
        return "obj is nil?!!"
    }

    override func getObjectValue(_ obj: AutoreleasingUnsafeMutablePointer<AnyObject?>?, for string: String, errorDescription error: AutoreleasingUnsafeMutablePointer<NSString?>?) -> Bool {
        print("in getObjectValue(), string = \(string)")
        obj?.pointee = string.filter { !$0.isWhitespace } as AnyObject
        return true
    }
}
My Test
struct ContentView: View {
    @State var text = ""
    @State var number = 123

    static let formatter = SanSpaceFormatter()
    static let numberFormatter = NumberFormatter()

    var body: some View {
        VStack {
            Text("Hello, world!, you enter: \(text), \(number)")
                .padding()
            TextField("No space allowed", value: $text, formatter: Self.formatter)
            TextField("Number", value: $number, formatter: Self.numberFormatter)
        }
    }
}


struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

Documentation for Formatter

After quickly looking at the documentation for string(for:) and getObjectValue, it seems that string(for:)

  • Returns nil if obj is a wrong class, or simply nil, and
  • Returns a valid String for obj if it is a valid instance (for that formatter instance),

and getObjectValue

  • Returns true if the string can be converted back to a valid instance
    • Set obj to that instance
  • Returns false if the string can't be converted
    • Set error to an error description if it isn't currently nil

Considering that obj is a String sans whitespace, the string(for:) would be more similar to

override func string(for obj: Any?) -> String? {
    obj as? String
}

You don't need to do anything on string(for:) because on obj is already a valid instance of String sans whitespace. If text also comes from other sources, you should ensure it yourself that it is without whitespace. That isn't the job for formatter.

1 Like

Thank you for explaining to me!

The Formatter TextField seems to be different from other:

  1. the Formatter TextField doesn't update on each key stroke, only update on return key
  2. if you don't hit return and click away, the TextField stop updating itself even if the bind to var change.
  3. if you hit return then they change in sync again.

The Formatter TextField is useful for when you don't want your state var change until the user hit return. Could be useful without needing to add some intermediate edit copy.

My Test Code
import SwiftUI

class SanSpaceFormatter: Formatter {
    override func string(for obj: Any?) -> String? {
        return obj as? String
    }

    override func getObjectValue(_ obj: AutoreleasingUnsafeMutablePointer<AnyObject?>?, for string: String, errorDescription error: AutoreleasingUnsafeMutablePointer<NSString?>?) -> Bool {
        print("in getObjectValue(), string = \(string)")
        obj?.pointee = string.filter { !$0.isWhitespace } as AnyObject
        return true
    }
}


struct ContentView: View {
    @State var text = "" {
        didSet { print("text didSet(): \(text)") }
    }
    @State var number = 123

    static let formatter = SanSpaceFormatter()
    static let numberFormatter = NumberFormatter()

    func onEditingChange(_ flag: Bool) {
        if flag {
            // begin editing
        } else {
            // end editiong
            text.removeAll { $0.isWhitespace }
        }
    }

    var body: some View {
        VStack {
            Text("You enter: \(text), \(number)")
                .padding()

            // method A: using a Formatter to strip out  white space
            // no update while typing, only update on hitting return
            Text("This TextField use `Formatter`")
            TextField("No space allowed", value: $text, formatter: Self.formatter)

            Divider()
            // method B using a binding to strip out white space
            // update text on each key stroke, space is strip on each key stroke
            let b = Binding(get: { text }, set: { text = $0.filter { !$0.isWhitespace } })
            TextField("I will trip any space", text: b)

            // method C: strip space onCommit
            // only update on commit
            TextField("onEditingChanged", text: $text, onEditingChanged: onEditingChange)

            // method D: strip space onCommit
            // only update on commit
            TextField("onCommit", text: $text, onCommit: { text.removeAll { $0.isWhitespace } })

            Text("NumberFormatter")
            TextField("Number", value: $number, formatter: Self.numberFormatter)
        }
    }
}


struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

Yes, it is a very different situation when you have a formatter:

When you use a Formatter, the type of the binding target is determined by the formatter (It is Binding<T> where T is the type supported by the formatter). In general, a partially edited string that represents an arbitrary type (say a Date) will not be a valid value of that type (e.g. "1" is not a valid date, but typing a valid date may start with it.)

On the other hand, without a formatter, the type of the binding is fixed: Binding<String>. In this case, a partially edited string is always a valid string and can be used to immediately update the binding.

Stripping a string is not the best use case for a Formatter.

In general, despite Apple's best efforts, there is still significant impedance mismatch between important parts of Apple's legacy APIs (which includes Foundation) with Swift. Dealing with NSFormatter and localization facilities of Foundation is still one of the most annoying things that creep into SwiftUI.

1 Like

I ran into a little bit of problem:

TextField("", value: $value, formatter: Self.formatter, onCommit: validate)
    .keyboardType(.numberPad)

The numberPad keyboard doesn't have a "return" key to commit change on the TextField :(
so user can enter the value but no way to complete...making the Formatter version of TextField un-usable in this case.

TextField is extremely limited and can't add the typical solution, a keyboard accessory with a done button. You're better off wrapping UITextField for real keyboard and responder handling. There are a few examples and libraries out there to do it.

1 Like

Are you saying certain UI things are not possible to implement with SwiftUI? I hope this is not true.

The other versions of SwiftUI.TextField can react to every keystroke: the value binding is updated on every keystroke.

But the version created with Formatter do not update value on every keystroke as explained by @hooman above. It waits for the return key then run the formatter, then update value.

When you need to validate user input on the TextField, you don't want to do it on every keystroke even if you can. You wait until the user ends input by hitting the return key.

I think the .numberPad keyboard should have the return key.

So this is not entirely the fault of SwiftUI.TextField, the problem is the lack of return key on the .numberPad keyboard.

You can implement all of these interactions just fine in SwiftUI, TextField just can’t do it. But people have been able to wrap UITextField, including the responder chain.

1 Like

How does UITextField That uses the same numberPad keyboard with no return key signal “commit”?