Swift global Phone Number Formatter & Mask easy to use

Diney Bomfim
5 min readDec 13, 2022

Have you ever tried (or are trying) to get an easy-to-use, works-everywhere phone number masking in Swift? Here is the answer to that.

Straight to the point

Show me the money! https://gist.github.com/dineybomfim/86bb72209ee525a9014c5ed6719ac489

Detailed version

The problem statement

So in order to solve that problem, we need first to understand the problem and how deep it goes.

  • Step 1. Phone Country Codes

So probably everybody knows, not a secret, each country's nation conveys. As always, any human standard requires an association and agreement upon certain conventions. So the international calling codes are agreed to members of International Telecommunication Union (ITU)

World convention for phone country codes
  • Step 2. Types/Categories of Phone Numbers

Knowing countries agree on some standards, the next natural question is: “What then is NOT covered by the standards?”. Well, also, as expected, turns out that anything that is not covered by the convention will be pretty random and up to each country to define. Things like having a fixed line standard, a mobile phone standard, a toll-free standard, and all other sorts of types and categories will become then a little messy, as each country defines its own!

  • Step 3. Evolution over time (CHANGES)

On any human standard, the governance part is crucial… How do we evolve? How do we make changes and make sure all the previous work will not be thrown out of the window when a future change comes in? Well, there is no easy answer to that. For example, Brazil in 1990 had no Mobile phone numbers, so a subset of the fixed line numbers was fine to use as Mobile Numbers. At that time, the fixed line had 8 digits as standard. Then in 2012, the growth of mobile numbers was so rapid that the country set a new standard, introducing 9 as a prefix for every mobile number while keeping the existing 8 digits. So the country had fixed lines with 8 digits and mobile numbers with either 8 or 9 digits.

This is just one country over the last 10 years. Imagine all the 240+ other countries over the last and next decade that are part of the ITU standards.

The solution statement

So it’s now clear that a global solution for phone masking can’t be done without very close eyes on all those standards and changes. Well, as developers of Mobile applications, we can’t expect each App to manage a database with all those changes, right? So we have to rely on someone else, possibly a large group that can keep track of all those changes and updates us ON THE FLY!

  • The first answer to this problem then was clear, relying on the Google Open Source project for Phone Number Library https://github.com/google/libphonenumber. So that’s what this Swift solution does, consumes the updated files from the Google Project, after some normalization and parsing.
  • Then we need a way to update this file on the fly, in runtime, so users can have always up-to-date phone masks, format, and info.
  • Finally we need a easy to use Swift native API, that we can call at any point from our Apps, with a single point of control and behind the scenes it must be able to understand 1) The country 2) The types of phones 3) The possible changes and updates that a country standard can suffer over years.

Building Time

  • Step 1: Model

It always starts with the Models, as usual:

private struct PhoneNumberMetadata : Codable {

struct PhoneLengths : Codable {
let national: String?
let localOnly: String?
}

struct PhonePattern : Codable {
let exampleNumber: String
let nationalNumberPattern: String
let possibleLengths: PhoneLengths
}

struct GeneralDescription : Codable {
let nationalNumberPattern: String
}

struct NumberFormat : Codable {
let pattern: String
let format: String
let nationalPrefixFormattingRule: String?
let leadingDigits: StringDigits? // Little caveat here
}

struct AvailableFormats : Codable {
let numberFormat: [NumberFormat]
}

struct Territory : Codable {
let id: String
let countryCode: String
let leadingDigits: String?
let nationalPrefix: String?
let internationalPrefix: String?
let availableFormats: AvailableFormats?
let generalDesc: GeneralDescription?
let fixedLine: PhonePattern?
let mobile: PhonePattern?
}

struct Territories : Codable {
let territory: [Territory]
}

struct Metadata : Codable {
let territories: Territories
}

let phoneNumberMetadata: Metadata
}
  • Step 2: Networking

Then the loader. Here is a technique I believe very much, the good smart use of CQRS concepts (Command and Query Responsibility Segregation). It’s the idea that saving data can be done in an ideal way/format to optimize storage and writing, reading is a different responsibility, and reading shouldn’t struggle or have any drawbacks by the way the data was stored, even if you have to use a different technique/format to optimize the data for reading. So the following code uses HashMaps (Hashable Dictionaries) to prepare the data for the fast reading as possible.

public struct PhoneNumberFormatter {

// MARK: - Properties

private static var url: URL? { .init(string: "https://tinyurl.com/phonenumbermetadata") }
private static var territories: [String : PhoneNumberMetadata.Territory] = [:]
private static var codes: [String : String] = [:]

// MARK: - Exposed Methods

public static func load(completion: @escaping () -> Void) {
guard territories.isEmpty || codes.isEmpty else {
completion()
return
}

DispatchQueue.global().async {
guard
let validURL = url,
let data = try? Data(contentsOf: validURL)
else { return }

do {
let formatter = try JSONDecoder().decode(PhoneNumberMetadata.self, from: data)
formatter.phoneNumberMetadata.territories.territory.forEach {
territories[$0.id] = $0
codes[$0.countryCode] = $0.id
}
} catch {
print(error)
}

DispatchQueue.main.async { completion() }
}
}
}
  • Step 3: Public API execution

So at this point, we need to provide the information that most apps may need, like the letters of the country code, the international calling code, the digits isolated, an example of a mobile phone from that region, a mask format, the fully formatted digits, the formatted version with the country code, and a partially formatted version based on the possible mask.

public struct PhoneNumberFormatter {

// MARK: - Properties

...

public let countryCode: String
public let internationalCode: String
public let digits: String
public let example: String
public let mask: String
public let formatted: String
public var formattedWithCountry: String { "+\(internationalCode) \(formatted)" }
public var formattedByMask: String {
var formatted = ""
var index = digits.startIndex
var end = digits.endIndex

for character in mask where index < end {
if character == "X" {
formatted.append(digits[index])
index = digits.index(after: index)
} else {
formatted.append(character)
}
}

if index < end {
formatted.append(contentsOf: digits.suffix(from: index))
}

return formatted
}
...
}

Conclusion

It’s indeed a complex problem, but the solution should be easy and straightforward forward, as you can see in detail at the GIST: https://gist.github.com/dineybomfim/86bb72209ee525a9014c5ed6719ac489

Feel free to take and change the code as per your project needs, and I hope this sets a good foundation for your phone number masking.

Thanks for reading

--

--