Formatting Phone number in Swift
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|g7h8" => "+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
}
CAN
Updated on July 09, 2022Comments
-
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 this05554446677
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 about 6 yearsI 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 about 6 yearsThis should be the correct solution. Provides more flexibility and can be placed in its own class and used throughout the application.
-
serhanaksut over 5 yearsCongrats! The best solution I have ever seen!
-
amin about 5 yearsbest pattern solution
-
amin about 5 yearsHow can I use this for Arabic numbers?
-
Christian Gossain about 5 yearsDefinitely my favourite solution to this question. Thank you!
-
Yaroslav Dukal almost 5 yearsinit(encodedOffset:)' is deprecated: encodedOffset has been deprecated as most common usage is incorrect.
-
Kamran over 4 yearsWill not work well while editing a number from the middle.
-
sanch over 4 yearsI 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 over 4 yearsThanks @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 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 over 4 yearsThis will brake iOS feature with phone number suggestions unfortunately.
-
anoo_radha over 4 yearsThis 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 over 4 yearsI 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 over 4 yearsWorks for Swift 4.2
-
nr5 over 4 yearsJust 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 likeXXXXXXXXXX
and it will convert the formatted phone number back to normal string. -
Ahemadabbas Vagh about 4 yearsfunc 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 almost 4 yearshow 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 over 3 yearsThis 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 over 3 yearsreplace the deprecated expression ('init(encodedOffset:)') by: pattern.index(self.startIndex, offsetBy: index)
-
bdelliott over 3 yearsThanks to @NightCoder !!! This was driving me crazy! Now it works!
-
jbcaveman about 3 yearsTo 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, theformat() -> String
method should remove this extra space for you. -
shim over 2 yearsDoesn't this assume the country code is one digit only? (Or always the same length anyhow.)