5

I am attempting to format the data in a SwiftUI TextField with a pattern or mask. (For clarity - NOT a UITextField). One example would be a US phone number. So the user can type 1115551212 and the result in the view is 111-555-1212. I'll use a numberPad in this case, but for future, with the full keyboard, if the user types a non-number, I'd like to be able to replace it with something, say 0. So entering 111abc1212 would result in 111-000-1212. Even though the code below converts the string to numbers, ideally, I want the mask to operate on a String not a number - giving the flexibility to format part numbers etc.

I have been able to do this with a func that operates from a button, but of course I want it to be automatic. I have been completely unsuccessful with SwiftUI modifiers to do the same. And using the built in .textContentType(.telephoneNumber) does absolutely nothing in my tests.

I would expect to have some modifier like .onExitCommand that can execute when the focus leaves the TextField but I don't see a solution.

This code works with the button (I will later add rules to filter for numbers when numbers are expected):

struct ContentView: View {

    @State private var phoneNumber = ""
    @State private var digitArray = [1]

    var body: some View {

        VStack {
            Group {//group one
                Text("Phone Number with Format").font(.title)
                    .padding(.top, 40)
            
                TextField("enter phone number", text: $phoneNumber)
                    .textFieldStyle(RoundedBorderTextFieldStyle())
                    .padding()
                    .keyboardType(.numberPad)

            }//Group one
        
            Group {//group two
                            
                Button(action: {self.populateTextFieldWithPhoneFormat(phoneString: self.phoneNumber)}) {
                    Text("Convert to Phone Format")
                }
            
            }//group two
            Spacer()
        }//VStack
    
    }

    func populateTextFieldWithPhoneFormat(phoneString: String) {

        var digitArray = [Int]()

        let padded = phoneNumber.padding(toLength: 10, withPad: "0", startingAt: 0)
        let paddedArray = padded.map {String($0)}

        for char in paddedArray {
            digitArray.append(Int(char) ?? 0)
        }

        phoneNumber = format(digits: digitArray)

    }//populate

    func format(digits: [Int]) -> String {
        var phone = digits.map(String.init)
            .joined()
        if digits.count > 3 {
            phone.insert("-", at: phone.index(
                phone.startIndex,
                offsetBy: 3)
            )
        }
        if digits.count > 7 {
            phone.insert("-", at: phone.index(
                phone.startIndex,
                offsetBy: 7)
            )
        }
        return phone
    }
}

I also attempted to make my own ViewModifier, but got nowhere.

Xcode Version 11.2.1 (11B500)

1
  • You can use Binding like here Commented Feb 29, 2020 at 9:31

3 Answers 3

9

TextField on SwiftUI has some interesting overloads that could solve your problem very easily. I managed to make a TextField mask for a MAC address formatter like the following (##:##:##:##:##). Below there's a simple snip of code that does the trick:

class MacAddressFormatter: Formatter {
    override func string(for obj: Any?) -> String? {
        if let string = obj as? String {
            return formattedAddress(mac: string)
        }
        return nil
    }
    
    override func getObjectValue(_ obj: AutoreleasingUnsafeMutablePointer<AnyObject?>?, for string: String, errorDescription error: AutoreleasingUnsafeMutablePointer<NSString?>?) -> Bool {
        obj?.pointee = string as AnyObject?
        return true
    }
    
    func formattedAddress(mac: String?) -> String? {
        guard let number = mac else { return nil }
        let mask = "##:##:##"
        var result = ""
        var index = number.startIndex
        for ch in mask where index < number.endIndex {
            if ch == "#" {
                result.append(number[index])
                index = number.index(after: index)
            } else {
                result.append(ch)
            }
        }
        return result
    }
}

In your case you should change the formattedAddress(mac: String) -> String? method in order to return your result as needed.

Below there's a simple SwiftUI implementation of the above:

struct ContentView: View {
    @State private var textFieldValue:String = ""
    var body: some View {
        NavigationView {
            List {
                TextField("First 6 bytes", value: $textFieldValue, formatter: MacAddressFormatter())
            }
            .navigationBarTitle("Mac Lookup", displayMode: .inline)
        }
    }
}

For the more curious: Here You can find a really interesting article about this topic.

Sign up to request clarification or add additional context in comments.

2 Comments

Really cool. For others - I was unaware of the overloads available, including value and formatter as you mention. Also, from the referenced article - you can inline launch closures when editing and committing the field. Highly recommend the referenced article.
There's a multi-run bug, but the SO edit queue is full. You should sanitize the string before each format run. One way is to: mac?.filter("0123456789.".contains)
2

Isn't this constructor of what you are looking for?

/// Creates an instance with a `Text` label generated from a title string.
///
/// - Parameters:
///     - title: The title of `self`, describing its purpose.
///     - text: The text to be displayed and edited.
///     - onEditingChanged: An `Action` that will be called when the user
///     begins editing `text` and after the user finishes editing `text`,
///     passing a `Bool` indicating whether `self` is currently being edited
///     or not.
///     - onCommit: The action to perform when the user performs an action
///     (usually the return key) while the `TextField` has focus.
public init<S>(_ title: S, text: Binding<String>, 
            onEditingChanged: @escaping (Bool) -> Void = { _ in }, 
            onCommit: @escaping () -> Void = {}) where S : StringProtocol

3 Comments

Yes it certainly is. I can't believe I looked for so long but couldn't find this functionality. It works perfectly for me.
For others - added benefit. Using an if-else on the onEditingChange Bool you can clear the TextField when clicking into the field without further code.
You can use Binding like here
0

Based of answer @valvoline I build this, support swift 6.2

  • limited imput 12 hexadecimal chars
  • support ios & macos
  • using ParseableFormatStyle protocol in TextField
nonisolated class MacAddressFormatter: Formatter {
    override func string(for obj: Any?) -> String? {
        if let string = obj as? String {
            return formattedAddress(mac: string)
        }
        return nil
    }
    
    override func getObjectValue(_ obj: AutoreleasingUnsafeMutablePointer<AnyObject?>?,
                                 for string: String,
                                 errorDescription error: AutoreleasingUnsafeMutablePointer<NSString?>?) -> Bool {
        obj?.pointee = string as AnyObject?
        return true
    }
    
    /// Formatea una MAC address como ##:##:##:##:##:##, usando solo dígitos.
    func formattedAddress(mac: String?) -> String? {
        guard let raw = mac else { return nil }
        
        // Filtra solo dígitos hexadecimales
        let hexDigits = raw.filter { $0.isHexDigit }
        
        // Máximo 12 caracteres (6 pares hex)
        let limited = String(hexDigits.prefix(12)).uppercased()
        
        let mask = "##:##:##:##:##:##"
        var result = ""
        var index = limited.startIndex
        
        for ch in mask where index < limited.endIndex {
            if ch == "#" {
                result.append(limited[index])
                index = limited.index(after: index)
            } else {
                result.append(ch)
            }
        }
        
        return result
    }
}
    
struct MacAddressField: View {
    @State private var textFieldValue: String = ""

    var body: some View {
            Form {
                TextField("MAC Address", text: $textFieldValue)
                #if os(iOS)
                    .keyboardType(.asciiCapable)
                    .autocapitalization(.allCharacters)
                #endif
                    .onChange(of: textFieldValue) { _, newValue in
                        let trimmed = String(newValue.filter(\.isHexDigit).prefix(12))

                        let formatter = MacAddressFormatter()
                        textFieldValue = formatter.formattedAddress(mac: trimmed) ?? trimmed

                    }

            }
        }
    
}

#Preview {
    MacAddressField()
}

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.