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
This solution removes any non-numeric characters before applying formatting. It returns nil
if the source phone number cannot be formatted according to assumptions.
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])
}
}
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])
}
}
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"))
'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
/// 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"
Really simple solution:
extension String {
func applyPatternOnNumbers(pattern: String, replacmentCharacter: 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(encodedOffset: index)
let patternCharacter = pattern[stringIndex]
guard patternCharacter != replacmentCharacter else { continue }
pureNumber.insert(patternCharacter, at: stringIndex)
}
return pureNumber
}
}
Usage:
guard let text = textField.text else { return }
textField.text = text.applyPatternOnNumbers(pattern: "+# (###) ###-####", replacmentCharacter: "#")
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
}
var formattedPhone = phone
if phone.count == 11 {
let firstChar = phone[..<phone.index(phone.startIndex, offsetBy: 1)]
if firstChar == "1" {
formattedPhone = String(format: "(%@) %@-%@",
String(phone[phone.index(phone.startIndex, offsetBy: 1)..<phone.index(phone.startIndex, offsetBy: 4)]),
String(phone[phone.index(phone.startIndex, offsetBy: 4) ..< phone.index(phone.startIndex, offsetBy: 7)]),
String(phone[phone.index(phone.startIndex, offsetBy: 7) ..< phone.index(phone.startIndex, offsetBy: 11)]))
}
}
Swift 3 but should also be translatable to Swift 4
ErrorHandling
enum PhoneNumberFormattingError: Error {
case wrongCharactersInPhoneNumber
case phoneNumberLongerThanPatternAllowes
}
Create Patterns
enum PhoneNumberFormattingPatterns: String {
case mobile = "+xx (yxx) xxxxxxxxxxx"
case home = "+xx (yxxx) xxxx-xxx"
}
Insert Function
/**
Formats a phone-number to correct format
- Parameter pattern: The pattern to format the phone-number.
- Example:
- x: Says that this should be a digit.
- y: Says that this digit cannot be a "0".
- The length of the pattern restricts also the length of allowed phone-number digits.
- phone-number: "+4306641234567"
- pattern: "+xx (yxx) xxxxxxxxxxx"
- result: "+43 (664) 1234567"
- Throws:
- PhoneNumberFormattingError
- wrongCharactersInPhoneNumber: if phone-number contains other characters than digits.
- phoneNumberLongerThanPatternAllowes: if phone-number is longer than pattern allows.
- Returns:
- The formatted phone-number due to the pattern.
*/
extension String {
func vpToFormattedPhoneNumber(withPattern pattern: PhoneNumberFormattingPatterns) throws -> String {
let phoneNumber = self.replacingOccurrences(of: "+", with: "")
var retVal: String = ""
var index = 0
for char in pattern.rawValue.lowercased().characters {
guard index < phoneNumber.characters.count else {
return retVal
}
if char == "x" {
let charIndex = phoneNumber.index(phoneNumber.startIndex, offsetBy: index)
let phoneChar = phoneNumber[charIndex]
guard "0"..."9" ~= phoneChar else {
throw PhoneNumberFormattingError.wrongCharactersInPhoneNumber
}
retVal.append(phoneChar)
index += 1
} else if char == "y" {
var charIndex = phoneNumber.index(phoneNumber.startIndex, offsetBy: index)
var indexTemp = 1
while phoneNumber[charIndex] == "0" {
charIndex = phoneNumber.index(phoneNumber.startIndex, offsetBy: index + indexTemp)
indexTemp += 1
}
let phoneChar = phoneNumber[charIndex]
guard "0"..."9" ~= phoneChar else {
throw PhoneNumberFormattingError.wrongCharactersInPhoneNumber
}
retVal.append(phoneChar)
index += indexTemp
} else {
retVal.append(char)
}
}
if phoneNumber.endIndex > phoneNumber.index(phoneNumber.startIndex, offsetBy: index) {
throw PhoneNumberFormattingError.phoneNumberLongerThanPatternAllowes
}
return retVal
}
}
Usage
let phoneNumber = "+4306641234567"
let phoneNumber2 = "4343211234567"
do {
print(try phoneNumber.vpToFormattedPhoneNumber(withPattern: .mobile))
print(try phoneNumber2.vpToFormattedPhoneNumber(withPattern: .home))
} catch let error as PhoneNumberFormattingError {
switch error {
case .wrongCharactersInPhoneNumber:
print("wrong characters in phone number")
case .phoneNumberLongerThanPatternAllowes:
print("too long phone number")
default:
print("unknown error")
}
} catch {
print("something other went wrong")
}
// output: +43 (664) 1234567
// output: +43 (4321) 1234-567