Formatting Phone number in Swift

79,438

Solution 1

Manipulations with characters in String are not very straightforward. You need following:

Swift 2.1

let s = "05554446677"
let s2 = String(format: "%@ (%@) %@ %@ %@", s.substringToIndex(s.startIndex.advancedBy(1)),
    s.substringWithRange(s.startIndex.advancedBy(1) ... s.startIndex.advancedBy(3)),
    s.substringWithRange(s.startIndex.advancedBy(4) ... s.startIndex.advancedBy(6)),
    s.substringWithRange(s.startIndex.advancedBy(7) ... s.startIndex.advancedBy(8)),
    s.substringWithRange(s.startIndex.advancedBy(9) ... s.startIndex.advancedBy(10))
)

Swift 2.0

let s = "05554446677"
let s2 = String(format: "%@ (%@) %@ %@ %@", s.substringToIndex(advance(s.startIndex, 1)),
    s.substringWithRange(advance(s.startIndex, 1) ... advance(s.startIndex, 3)),
    s.substringWithRange(advance(s.startIndex, 4) ... advance(s.startIndex, 6)),
    s.substringWithRange(advance(s.startIndex, 7) ... advance(s.startIndex, 8)),
    s.substringWithRange(advance(s.startIndex, 9) ... advance(s.startIndex, 10))
)

Code will print 0 (555) 444 66 77

Solution 2

Masked number typing

/// mask example: `+X (XXX) XXX-XXXX`
func format(with mask: String, phone: String) -> String {
    let numbers = phone.replacingOccurrences(of: "[^0-9]", with: "", options: .regularExpression)
    var result = ""
    var index = numbers.startIndex // numbers iterator

    // iterate over the mask characters until the iterator of numbers ends
    for ch in mask where index < numbers.endIndex {
        if ch == "X" {
            // mask requires a number in this place, so take the next one
            result.append(numbers[index])

            // move numbers iterator to the next index
            index = numbers.index(after: index)

        } else {
            result.append(ch) // just append a mask character
        }
    }
    return result
}

Call the above function from the UITextField delegate method:

func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
    guard let text = textField.text else { return false }
    let newString = (text as NSString).replacingCharacters(in: range, with: string)
    textField.text = format(with: "+X (XXX) XXX-XXXX", phone: newString)
    return false
}

So, that is work better.

"" => ""
"0" => "+0"
"412" => "+4 (12"
"12345678901" => "+1 (234) 567-8901"
"a1_b2-c3=d4 e5&f6|g7h8" => "+1 (234) 567-8"

Solution 3

Really simple solution:

extension String {
    func applyPatternOnNumbers(pattern: String, replacementCharacter: Character) -> String {
        var pureNumber = self.replacingOccurrences( of: "[^0-9]", with: "", options: .regularExpression)
        for index in 0 ..< pattern.count {
            guard index < pureNumber.count else { return pureNumber }
            let stringIndex = String.Index(utf16Offset: index, in: pattern)
            let patternCharacter = pattern[stringIndex]
            guard patternCharacter != replacementCharacter else { continue }
            pureNumber.insert(patternCharacter, at: stringIndex)
        }
        return pureNumber
    }
}

Usage:

guard let text = textField.text else { return }
textField.text = text.applyPatternOnNumbers(pattern: "+# (###) ###-####", replacmentCharacter: "#")

Solution 4

Swift 3 & 4

This solution removes any non-numeric characters before applying formatting. It returns nil if the source phone number cannot be formatted according to assumptions.

Swift 4

The Swift 4 solution accounts for the deprecation of CharacterView and Sting becoming a collection of characters as the CharacterView is.

import Foundation

func format(phoneNumber sourcePhoneNumber: String) -> String? {
    // Remove any character that is not a number
    let numbersOnly = sourcePhoneNumber.components(separatedBy: CharacterSet.decimalDigits.inverted).joined()
    let length = numbersOnly.count
    let hasLeadingOne = numbersOnly.hasPrefix("1")

    // Check for supported phone number length
    guard length == 7 || (length == 10 && !hasLeadingOne) || (length == 11 && hasLeadingOne) else {
        return nil
    }

    let hasAreaCode = (length >= 10)
    var sourceIndex = 0

    // Leading 1
    var leadingOne = ""
    if hasLeadingOne {
        leadingOne = "1 "
        sourceIndex += 1
    }

    // Area code
    var areaCode = ""
    if hasAreaCode {
        let areaCodeLength = 3
        guard let areaCodeSubstring = numbersOnly.substring(start: sourceIndex, offsetBy: areaCodeLength) else {
            return nil
        }
        areaCode = String(format: "(%@) ", areaCodeSubstring)
        sourceIndex += areaCodeLength
    }

    // Prefix, 3 characters
    let prefixLength = 3
    guard let prefix = numbersOnly.substring(start: sourceIndex, offsetBy: prefixLength) else {
        return nil
    }
    sourceIndex += prefixLength

    // Suffix, 4 characters
    let suffixLength = 4
    guard let suffix = numbersOnly.substring(start: sourceIndex, offsetBy: suffixLength) else {
        return nil
    }

    return leadingOne + areaCode + prefix + "-" + suffix
}

extension String {
    /// This method makes it easier extract a substring by character index where a character is viewed as a human-readable character (grapheme cluster).
    internal func substring(start: Int, offsetBy: Int) -> String? {
        guard let substringStartIndex = self.index(startIndex, offsetBy: start, limitedBy: endIndex) else {
            return nil
        }

        guard let substringEndIndex = self.index(startIndex, offsetBy: start + offsetBy, limitedBy: endIndex) else {
            return nil
        }

        return String(self[substringStartIndex ..< substringEndIndex])
    }
}

Swift 3

import Foundation

func format(phoneNumber sourcePhoneNumber: String) -> String? {

    // Remove any character that is not a number
    let numbersOnly = sourcePhoneNumber.components(separatedBy: CharacterSet.decimalDigits.inverted).joined()
    let length = numbersOnly.characters.count
    let hasLeadingOne = numbersOnly.hasPrefix("1")

    // Check for supported phone number length
    guard length == 7 || (length == 10 && !hasLeadingOne) || (length == 11 && hasLeadingOne) else {
        return nil
    }

    let hasAreaCode = (length >= 10)
    var sourceIndex = 0

    // Leading 1
    var leadingOne = ""
    if hasLeadingOne {
        leadingOne = "1 "
        sourceIndex += 1
    }

    // Area code
    var areaCode = ""
    if hasAreaCode {
        let areaCodeLength = 3
        guard let areaCodeSubstring = numbersOnly.characters.substring(start: sourceIndex, offsetBy: areaCodeLength) else {
            return nil
        }
        areaCode = String(format: "(%@) ", areaCodeSubstring)
        sourceIndex += areaCodeLength
    }

    // Prefix, 3 characters
    let prefixLength = 3
    guard let prefix = numbersOnly.characters.substring(start: sourceIndex, offsetBy: prefixLength) else {
        return nil
    }
    sourceIndex += prefixLength

    // Suffix, 4 characters
    let suffixLength = 4
    guard let suffix = numbersOnly.characters.substring(start: sourceIndex, offsetBy: suffixLength) else {
        return nil
    }

    return leadingOne + areaCode + prefix + "-" + suffix
}

extension String.CharacterView {
    /// This method makes it easier extract a substring by character index where a character is viewed as a human-readable character (grapheme cluster).
    internal func substring(start: Int, offsetBy: Int) -> String? {
        guard let substringStartIndex = self.index(startIndex, offsetBy: start, limitedBy: endIndex) else {
            return nil
        }

        guard let substringEndIndex = self.index(startIndex, offsetBy: start + offsetBy, limitedBy: endIndex) else {
            return nil
        }

        return String(self[substringStartIndex ..< substringEndIndex])
    }
}

Example

func testFormat(sourcePhoneNumber: String) -> String {
    if let formattedPhoneNumber = format(phoneNumber: sourcePhoneNumber) {
        return "'\(sourcePhoneNumber)' => '\(formattedPhoneNumber)'"
    }
    else {
        return "'\(sourcePhoneNumber)' => nil"
    }
}

print(testFormat(sourcePhoneNumber: "1 800 222 3333"))
print(testFormat(sourcePhoneNumber: "18002223333"))
print(testFormat(sourcePhoneNumber: "8002223333"))
print(testFormat(sourcePhoneNumber: "2223333"))
print(testFormat(sourcePhoneNumber: "18002223333444"))
print(testFormat(sourcePhoneNumber: "Letters8002223333"))
print(testFormat(sourcePhoneNumber: "1112223333"))

Example Output

'1 800 222 3333' => '1 (800) 222-3333'

'18002223333' => '1 (800) 222-3333'

'8002223333' => '(800) 222-3333'

'2223333' => '222-3333'

'18002223333444' => nil

'Letters8002223333' => '(800) 222-3333'

'1112223333' => nil

Solution 5

Swift 4

Create this function and call on text field event Editing Changed

private func formatPhone(_ number: String) -> String {
    let cleanNumber = number.components(separatedBy: CharacterSet.decimalDigits.inverted).joined()
    let format: [Character] = ["X", "X", "X", "-", "X", "X", "X", "-", "X", "X", "X", "X"]

    var result = ""
    var index = cleanNumber.startIndex
    for ch in format {
        if index == cleanNumber.endIndex {
            break
        }
        if ch == "X" {
            result.append(cleanNumber[index])
            index = cleanNumber.index(after: index)
        } else {
            result.append(ch)
        }
    }
    return result
}
Share:
79,438
CAN
Author by

CAN

Updated on July 09, 2022

Comments

  • CAN
    CAN almost 2 years

    I'm formatting my textfiled text once the user start typing the phone number into this format type 0 (555) 444 66 77 and it is working fine but once I get the number from the server I get it like this 05554446677 So please could you tell me how I can edit it in the same format once I get it fro the server?

    My code once I start typing:

    func textField(textField: UITextField, shouldChangeCharactersInRange range: NSRange, replacementString string: String) -> Bool {
    
        if textField == phoneNumberTextField{
            var newString = (textField.text as NSString).stringByReplacingCharactersInRange(range, withString: string)
            var components = newString.componentsSeparatedByCharactersInSet(NSCharacterSet.decimalDigitCharacterSet().invertedSet)
    
            var decimalString = "".join(components) as NSString
            var length = decimalString.length
            var hasLeadingOne = length > 0 && decimalString.characterAtIndex(0) == (1 as unichar)
    
            if length == 0 || (length > 11 && !hasLeadingOne) || length > 12{
                var newLength = (textField.text as NSString).length + (string as NSString).length - range.length as Int
    
                return (newLength > 11) ? false : true
            }
            var index = 0 as Int
            var formattedString = NSMutableString()
    
            if hasLeadingOne{
                formattedString.appendString("1 ")
                index += 1
            }
    
            if (length - index) > 1{
                var zeroNumber = decimalString.substringWithRange(NSMakeRange(index, 1))
                formattedString.appendFormat("%@ ", zeroNumber)
                index += 1
            }
            if (length - index) > 3{
                var areaCode = decimalString.substringWithRange(NSMakeRange(index, 3))
                formattedString.appendFormat("(%@) ", areaCode)
                index += 3
            }
            if (length - index) > 3{
                var prefix = decimalString.substringWithRange(NSMakeRange(index, 3))
                formattedString.appendFormat("%@ ", prefix)
                index += 3
            }
            if (length - index) > 3{
                var prefix = decimalString.substringWithRange(NSMakeRange(index, 2))
                formattedString.appendFormat("%@ ", prefix)
                index += 2
            }
    
            var remainder = decimalString.substringFromIndex(index)
            formattedString.appendString(remainder)
            textField.text = formattedString as String
            return false
        }else{
            return true
        }
    }
    
  • Michał Ziobro
    Michał Ziobro about 6 years
    I don't know why but this doesn't work for me. I set as textInputController.textInput my UITextField. I also make its class TexInputField in Identity inspector in storyboard and make suitable casting. Filed isn't live formatted
  • TuplingD
    TuplingD about 6 years
    This should be the correct solution. Provides more flexibility and can be placed in its own class and used throughout the application.
  • serhanaksut
    serhanaksut over 5 years
    Congrats! The best solution I have ever seen!
  • amin
    amin about 5 years
    best pattern solution
  • amin
    amin about 5 years
    How can I use this for Arabic numbers?
  • Christian Gossain
    Christian Gossain about 5 years
    Definitely my favourite solution to this question. Thank you!
  • Yaroslav Dukal
    Yaroslav Dukal almost 5 years
    init(encodedOffset:)' is deprecated: encodedOffset has been deprecated as most common usage is incorrect.
  • Kamran
    Kamran over 4 years
    Will not work well while editing a number from the middle.
  • sanch
    sanch over 4 years
    I believe this will crash for the input 1112223333. Although it is not a valid phone number, still possible for the user to enter it.
  • Mobile Dan
    Mobile Dan over 4 years
    Thanks @sanch for pointing out the example of a 10 digit number starting with a "1". Such an input will make it past the guard under "// Check for supported phone number length". I had not considered a phone number input like that. I tested the Swift 4 code and thankfully it returns a nil due to the guard statement under the comment "// Suffix, 4 characters".
  • Mobile Dan
    Mobile Dan over 4 years
    @sanch, I improved the code to better handle a 10 number input that starts with a '1'. Now the guard under "// Check for supported phone number length" explicitly rejects an input like this. I added "1112223333" to the examples and to my own unit tests for this code. Thank you.
  • atereshkov
    atereshkov over 4 years
    This will brake iOS feature with phone number suggestions unfortunately.
  • anoo_radha
    anoo_radha over 4 years
    This is an elegant solution. When i wanted to format phonenumbers differently based on number of digits, i just used a different mask based on the phone string count.
  • Admin
    Admin over 4 years
    I get this warning 'init(encodedOffset:)' is deprecated: encodedOffset has been deprecated as most common usage is incorrect. Use String.Index(utf16Offset:in:) to achieve the same behavior. how would I fix this?
  • J A S K I E R
    J A S K I E R over 4 years
    Works for Swift 4.2
  • nr5
    nr5 over 4 years
    Just to add that if you create your own mask such as (XXX) XXX-XXXX, it will work then as well. Also, if you need the plain string back just create a plain mask like XXXXXXXXXX and it will convert the formatted phone number back to normal string.
  • Ahemadabbas Vagh
    Ahemadabbas Vagh about 4 years
    func removeNumberFormat(number: String) -> String { let digits = CharacterSet.decimalDigits var text = "" for char in number.unicodeScalars { if digits.contains(char) { text.append(char.description) } } return text }
  • Daniel Belém Duarte
    Daniel Belém Duarte almost 4 years
    how to use this inside a TextField with a Binding ? I'm not able to set the formatter with the binding value. Example: TextField("",self.$phoneNumber. applyPatternOnNumbers(...))
  • Ali Qaderi
    Ali Qaderi over 3 years
    This a good solution but it has an issue if you have some rules inside TextField.Target function it will prevent to go to target (textFieldChanged()) function.
  • NightCoder
    NightCoder over 3 years
    replace the deprecated expression ('init(encodedOffset:)') by: pattern.index(self.startIndex, offsetBy: index)
  • bdelliott
    bdelliott over 3 years
    Thanks to @NightCoder !!! This was driving me crazy! Now it works!
  • jbcaveman
    jbcaveman about 3 years
    To allow this to work with phone number suggestions, just change the last return = false to: return string == " ". iOS auto-completion always adds an extra space first, then the new value in front of the space. Fortunately, the format() -> String method should remove this extra space for you.
  • shim
    shim over 2 years
    Doesn't this assume the country code is one digit only? (Or always the same length anyhow.)