├── .github
├── ISSUE_TEMPLATE
│ ├── BUG_REPORT.md
│ └── config.yml
└── workflows
│ ├── pr.yml
│ └── update_metadata.yml
├── .gitignore
├── .swiftformat
├── .swiftlint.auto.yml
├── .swiftlint.yml
├── .swiftpm
└── xcode
│ └── package.xcworkspace
│ └── contents.xcworkspacedata
├── Documentation
└── OXMIGRATIONGUIDE.md
├── LICENSE
├── Package.swift
├── PhoneNumberKit.podspec
├── PhoneNumberKit.xcodeproj
├── project.pbxproj
├── project.xcworkspace
│ ├── contents.xcworkspacedata
│ └── xcshareddata
│ │ └── IDEWorkspaceChecks.plist
└── xcshareddata
│ └── xcschemes
│ ├── PhoneNumberKit-macOS.xcscheme
│ ├── PhoneNumberKit-tvOS.xcscheme
│ ├── PhoneNumberKit-watchOS.xcscheme
│ └── PhoneNumberKit.xcscheme
├── PhoneNumberKit
├── Bundle+Resources.swift
├── Constants.swift
├── Formatter.swift
├── Info.plist
├── MetadataManager.swift
├── MetadataParsing.swift
├── MetadataTypes.swift
├── NSRegularExpression+Swift.swift
├── ParseManager.swift
├── PartialFormatter.swift
├── PhoneNumber+Codable.swift
├── PhoneNumber.swift
├── PhoneNumberFormatter.swift
├── PhoneNumberKit.h
├── PhoneNumberParser.swift
├── PhoneNumberUtility.swift
├── RegexManager.swift
├── Resources
│ ├── .metadata-version
│ ├── Original
│ │ └── PhoneNumberMetadata.xml
│ ├── PhoneNumberMetadata.json
│ ├── PrivacyInfo.xcprivacy
│ ├── README.md
│ └── update_metadata.sh
└── UI
│ ├── CountryCodePickerOptions.swift
│ ├── CountryCodePickerViewController.swift
│ └── PhoneNumberTextField.swift
├── PhoneNumberKitTests
├── Info.plist
├── PartialFormatterTests.swift
├── PhoneNumber+CodableTests.swift
├── PhoneNumberTextFieldTests.swift
├── PhoneNumberUtilityParsingTests.swift
└── PhoneNumberUtilityTests.swift
├── README.md
└── examples
├── AsYouType
├── Sample.xcodeproj
│ ├── project.pbxproj
│ ├── project.xcworkspace
│ │ ├── contents.xcworkspacedata
│ │ └── xcshareddata
│ │ │ └── IDEWorkspaceChecks.plist
│ └── xcshareddata
│ │ └── xcschemes
│ │ └── Sample.xcscheme
├── Sample
│ ├── AppDelegate.swift
│ ├── Assets.xcassets
│ │ └── AppIcon.appiconset
│ │ │ └── Contents.json
│ ├── Base.lproj
│ │ ├── LaunchScreen.storyboard
│ │ └── Main.storyboard
│ ├── Info.plist
│ └── ViewController.swift
└── SampleTests
│ ├── Info.plist
│ └── SampleTests.swift
└── PhoneBook
├── Sample.xcodeproj
├── project.pbxproj
├── project.xcworkspace
│ ├── contents.xcworkspacedata
│ └── xcshareddata
│ │ └── IDEWorkspaceChecks.plist
└── xcshareddata
│ └── xcschemes
│ └── Sample.xcscheme
├── Sample
├── AppDelegate.swift
├── Assets.xcassets
│ └── AppIcon.appiconset
│ │ └── Contents.json
├── Base.lproj
│ ├── LaunchScreen.storyboard
│ └── Main.storyboard
├── Info.plist
└── ViewController.swift
└── SampleTests
├── Info.plist
└── SampleTests.swift
/.github/ISSUE_TEMPLATE/BUG_REPORT.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: 🐛 Bug Report
3 | about: If something isn't working as expected 🤔
4 |
5 | ---
6 |
7 |
8 |
9 | ### New Issue Checklist
10 |
11 | - [ ] Updated PhoneNumberKit to the latest version
12 | - [ ] Phone number formatted correctly on [JavaScript version](https://htmlpreview.github.io/?https://github.com/google/libphonenumber/blob/master/javascript/i18n/phonenumbers/demo-compiled.html)
13 | - [ ] I searched for [existing GitHub issues](https://github.com/marmelroy/PhoneNumberKit)
14 | - [ ] I am aware that this library is not responsible of adding/removing/changing phone number formats and any request should be done at [libphonenumber repo](https://github.com/google/libphonenumber)
15 |
16 | ### Steps to reproduce
17 |
18 |
19 | ##### Expected result
20 |
21 |
22 | ##### Actual result
23 |
24 |
25 | ### Environment
26 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/config.yml:
--------------------------------------------------------------------------------
1 | blank_issues_enabled: false
--------------------------------------------------------------------------------
/.github/workflows/pr.yml:
--------------------------------------------------------------------------------
1 | name: PR Checks
2 |
3 | on:
4 | pull_request:
5 | branches:
6 | - master
7 | push:
8 | branches:
9 | - master
10 |
11 | jobs:
12 | linux-tests:
13 | name: Linux
14 | runs-on: ubuntu-latest
15 |
16 | concurrency:
17 | group: ${{ github.workflow }}-${{ github.ref }}-linux-tests
18 | cancel-in-progress: true
19 |
20 | steps:
21 | - name: Checkout Repository
22 | uses: actions/checkout@v4
23 |
24 | - name: Run tests
25 | run: |
26 | set -o pipefail && swift test
27 |
28 | macos-tests:
29 | name: macOS
30 | runs-on: macos-latest
31 |
32 | concurrency:
33 | group: ${{ github.workflow }}-${{ github.ref }}-macos-tests
34 | cancel-in-progress: true
35 |
36 | steps:
37 | - name: Checkout Repository
38 | uses: actions/checkout@v4
39 |
40 | - name: Run tests
41 | run: |
42 | set -o pipefail && swift test
43 |
44 | macos-carthage-build:
45 | name: Carthage Build
46 | runs-on: macos-latest
47 | needs:
48 | - linux-tests
49 | - macos-tests
50 |
51 | concurrency:
52 | group: ${{ github.workflow }}-${{ github.ref }}-macos-carthage-build
53 | cancel-in-progress: true
54 |
55 | steps:
56 | - name: Checkout Repository
57 | uses: actions/checkout@v4
58 |
59 | - name: Build - iOS
60 | run: |
61 | set -o pipefail && xcodebuild -project "PhoneNumberKit.xcodeproj" -scheme "PhoneNumberKit" -destination "generic/platform=iOS" build
62 |
63 | - name: Build - macOS
64 | run: |
65 | set -o pipefail && xcodebuild -project "PhoneNumberKit.xcodeproj" -scheme "PhoneNumberKit-macOS" -destination "generic/platform=macOS" build
--------------------------------------------------------------------------------
/.github/workflows/update_metadata.yml:
--------------------------------------------------------------------------------
1 | name: Update metadata
2 |
3 | on:
4 | workflow_dispatch:
5 | schedule:
6 | - cron: '0 */12 * * *' # Every 12 hours
7 |
8 | jobs:
9 | build:
10 | runs-on: macos-latest
11 | steps:
12 | - name: Configure Python
13 | uses: actions/setup-python@v4
14 | with:
15 | python-version: '3.12'
16 |
17 | - name: Install XML to JSON converter
18 | run: pip install xmljson
19 |
20 | - name: Checkout
21 | uses: actions/checkout@v4
22 |
23 | - name: Authorize
24 | run: echo "${{ secrets.GITHUB_TOKEN }}" | gh auth login --with-token
25 |
26 | - name: Update Metadata
27 | run: cd PhoneNumberKit/Resources && sh ./update_metadata.sh
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Xcode
2 | #
3 | .build/
4 | *.pbxuser
5 | !default.pbxuser
6 | *.mode1v3
7 | !default.mode1v3
8 | *.mode2v3
9 | !default.mode2v3
10 | *.perspectivev3
11 | !default.perspectivev3
12 | xcuserdata
13 | *.xccheckout
14 | *.moved-aside
15 | DerivedData
16 | *.hmap
17 | *.ipa
18 | *.xcuserstate
19 |
20 | # CocoaPods
21 | #
22 | # We recommend against adding the Pods directory to your .gitignore. However
23 | # you should judge for yourself, the pros and cons are mentioned at:
24 | # http://guides.cocoapods.org/using/using-cocoapods.html#should-i-ignore-the-pods-directory-in-source-control
25 | #
26 | # Pods/
27 |
28 | # Carthage
29 | #
30 | # Add this line if you want to avoid checking in source code from Carthage dependencies.
31 | # Carthage/Checkouts
32 |
33 | Carthage/Build
34 | PhoneNumberKit/Resources/.DS_Store
35 | examples/PhoneBook/.DS_Store
36 | examples/AsYouType/.DS_Store
37 | examples/.DS_Store
38 | .DS_Store
39 | PhoneNumberKit/.DS_Store
40 |
--------------------------------------------------------------------------------
/.swiftformat:
--------------------------------------------------------------------------------
1 | --swiftversion 5.3
2 | --xcodeindentation enabled
3 | --disable unusedArguments, redundantReturn, redundantSelf, trailingCommas, trailingClosures, wrapArguments, wrapMultilineStatementBraces
4 | --enable blockComments, isEmpty
5 | --emptybraces spaced
6 | --ifdef no-indent
7 | --importgrouping testable-first
8 | --ranges no-space
9 |
--------------------------------------------------------------------------------
/.swiftlint.auto.yml:
--------------------------------------------------------------------------------
1 | # rule identifiers to exclude from running
2 | disabled_rules:
3 | - block_based_kvo
4 | - class_delegate_protocol
5 | - closing_brace
6 | - closure_parameter_position
7 | - compiler_protocol_init
8 | - custom_rules
9 | - cyclomatic_complexity
10 | - discarded_notification_center_observer
11 | - discouraged_direct_init
12 | - dynamic_inline
13 | - empty_parentheses_with_trailing_closure
14 | - file_length
15 | - for_where
16 | - force_cast
17 | - force_try
18 | - function_body_length
19 | - function_parameter_count
20 | - generic_type_name
21 | - identifier_name
22 | - implicit_getter
23 | - is_disjoint
24 | - large_tuple
25 | - legacy_cggeometry_functions
26 | - legacy_constant
27 | - legacy_nsgeometry_functions
28 | - line_length
29 | - mark
30 | - multiple_closures_with_trailing_closure
31 | - nesting
32 | - no_fallthrough_only
33 | - notification_center_detachment
34 | - private_over_fileprivate
35 | - private_unit_test
36 | - protocol_property_accessors_order
37 | - redundant_set_access_control
38 | - return_arrow_whitespace
39 | - shorthand_operator
40 | - statement_position
41 | - superfluous_disable_command
42 | - todo
43 | - type_body_length
44 | - type_name
45 | - valid_ibinspectable
46 | - vertical_parameter_alignment
47 | - xctfail_message
48 |
49 |
50 | # paths to ignore during linting. Takes precedence over `included`.
51 | excluded:
52 | - Carthage
53 | - Pods
54 |
--------------------------------------------------------------------------------
/.swiftlint.yml:
--------------------------------------------------------------------------------
1 | # rule identifiers to exclude from running
2 | disabled_rules:
3 | - trailing_whitespace
4 | - line_length # This should be removed from disabled_rules, code needs a lot of refactoring
5 | - cyclomatic_complexity # This should be removed from disabled_rules, code needs a lot of refactoring
6 | - file_length # This should be removed from disabled_rules, code needs a lot of refactoring
7 | - type_body_length # This should be removed from disabled_rules, code needs a lot of refactoring
8 | - todo # This should be removed from disabled_rules, code needs a lot of refactoring
9 | - function_parameter_count # This should be removed from disabled_rules, code needs a lot of refactoring
10 | - force_try # I need this because of realm but this should be removed from disabled_rules
11 | - unused_closure_parameter # Added this for readability purposes of the code.
12 | - function_body_length
13 | - discarded_notification_center_observer # I don't understand this at the moment so I'll disable this.
14 | - statement_position
15 | - nesting
16 | - large_tuple
17 | - private_over_fileprivate
18 |
19 | opt_in_rules:
20 | - empty_count
21 |
22 | # paths to ignore during linting. Takes precedence over `included`.
23 | excluded:
24 | - Carthage
25 | - Pods
26 |
27 | empty_count: warning # implicitly
28 | force_cast: warning # implicitly
29 |
30 | type_name:
31 | max_length:
32 | warning: 99
33 | error: 100
34 | min_length:
35 | warning: 2
36 | error: 3
37 |
38 | identifier_name:
39 | max_length:
40 | warning: 99
41 | error: 100
42 | min_length:
43 | warning: 1
44 | error: 2
45 | excluded: # excluded via string array
46 | - id
47 | - i # for loop iteration
48 | - e # for error e
49 | - x # x axis
50 | - y # y axis
51 |
--------------------------------------------------------------------------------
/.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/Documentation/OXMIGRATIONGUIDE.md:
--------------------------------------------------------------------------------
1 | # PhoneNumberKit 0.x -> 1.0 Migration Guide
2 |
3 | The 1.0 release of PhoneNumberKit took advantage of "The Grand Renaming" of Swift APIs in Swift 3.0 to resolve some issues with the original design that led to confusion, inefficient memory management and possible concurrency issues.
4 |
5 | Unfortunately, this means a few breaking changes.
6 |
7 | ## The PhoneNumberKit object
8 |
9 | To create a simple API, the main object in PhoneNumberKit 0.x was the PhoneNumber object. Number strings were parsed using its initializer, formatting was done via functions declared in an extension.
10 |
11 | The main object of PhoneNumberKit 1.0 is a PhoneNumberKit object. This allows for more granular management of PhoneNumberKit's lifecycle and for immutable PhoneNumber value types.
12 |
13 | ### Before (0.x)
14 | ```swift
15 | do {
16 | let phoneNumber = try PhoneNumber(rawNumber: "+44 20 7031 3000", region: "GB")
17 | let formattedNumber: String = phoneNumber.toInternational()
18 | }
19 | catch {
20 | print("Generic parser error")
21 | }
22 | ```
23 |
24 | ### After (1.0)
25 | ```swift
26 | let phoneNumberKit = PhoneNumberKit()
27 | do {
28 | let phoneNumber = try phoneNumberKit.parse("+44 20 7031 3000", withRegion: "GB")
29 | let formattedNumber: String = phoneNumberKit.format(phoneNumber, toType: .international)
30 | }
31 | catch {
32 | print("Generic parser error")
33 | }
34 | ```
35 | Allocating a PhoneNumberKit object is relatively expensive so reuse is encouraged.
36 |
37 | ## Types and validation
38 |
39 | In PhoneNumberKit 0.x, a PhoneNumber object's ```type``` property was a computed variable. The thinking was that type validation was expensive to perform and not always necessary.
40 |
41 | This choice led to a certain amount of confusion - it meant that PhoneNumber objects were 'lightly' validated. To get a 'strong' validation, an isValid function that checked whether or not the number was of a known type had to be used.
42 |
43 | In PhoneNumberKit 1.0, type validation is part of the parsing process and all PhoneNumber objects are strongly validated.
44 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2018 Roy Marmelstein
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/Package.swift:
--------------------------------------------------------------------------------
1 | // swift-tools-version: 5.4
2 | import PackageDescription
3 |
4 | let package = Package(
5 | name: "PhoneNumberKit",
6 | platforms: [
7 | .iOS(.v12), .macOS(.v10_13), .tvOS(.v12), .watchOS(.v4)
8 | ],
9 | products: [
10 | .library(name: "PhoneNumberKit", targets: ["PhoneNumberKit"]),
11 | .library(name: "PhoneNumberKit-Static", type: .static, targets: ["PhoneNumberKit"]),
12 | .library(name: "PhoneNumberKit-Dynamic", type: .dynamic, targets: ["PhoneNumberKit"])
13 | ],
14 | targets: [
15 | .target(name: "PhoneNumberKit",
16 | path: "PhoneNumberKit",
17 | exclude: ["Resources/Original",
18 | "Resources/README.md",
19 | "Resources/update_metadata.sh",
20 | "Info.plist"],
21 | resources: [
22 | .process("Resources/PhoneNumberMetadata.json"),
23 | .copy("Resources/PrivacyInfo.xcprivacy")
24 | ]),
25 | .testTarget(name: "PhoneNumberKitTests",
26 | dependencies: ["PhoneNumberKit"],
27 | path: "PhoneNumberKitTests",
28 | exclude: ["Info.plist"])
29 | ]
30 | )
31 |
--------------------------------------------------------------------------------
/PhoneNumberKit.podspec:
--------------------------------------------------------------------------------
1 | Pod::Spec.new do |s|
2 | s.name = 'PhoneNumberKit'
3 | s.version = '4.0.2'
4 | s.summary = 'Swift framework for working with phone numbers'
5 | s.description = <<-DESC
6 | A Swift framework for parsing, formatting and validating international phone numbers. Inspired by Google's libphonenumber.
7 | DESC
8 |
9 | s.homepage = 'https://github.com/marmelroy/PhoneNumberKit'
10 | s.license = 'MIT'
11 | s.author = { 'Roy Marmelstein' => 'marmelroy@gmail.com' }
12 | s.source = { git: 'https://github.com/marmelroy/PhoneNumberKit.git', tag: s.version.to_s }
13 |
14 | s.requires_arc = true
15 | s.ios.deployment_target = '12.0'
16 | s.osx.deployment_target = '10.13'
17 | s.tvos.deployment_target = '12.0'
18 | s.watchos.deployment_target = '4.0'
19 |
20 | s.pod_target_xcconfig = { 'SWIFT_VERSION' => '5.0' }
21 | s.swift_version = '5.0'
22 |
23 | s.subspec 'PhoneNumberKitCore' do |core|
24 | core.ios.deployment_target = '12.0'
25 | core.osx.deployment_target = '10.13'
26 | core.tvos.deployment_target = '12.0'
27 | core.watchos.deployment_target = '4.0'
28 | core.source_files = 'PhoneNumberKit/*.{swift}'
29 | core.resources = [
30 | 'PhoneNumberKit/Resources/PhoneNumberMetadata.json'
31 | ]
32 | core.resource_bundles = { 'PhoneNumberKitPrivacy' => ['PhoneNumberKit/Resources/PrivacyInfo.xcprivacy'] }
33 | end
34 |
35 | s.subspec 'UIKit' do |ui|
36 | ui.dependency 'PhoneNumberKit/PhoneNumberKitCore'
37 | ui.ios.deployment_target = '12.0'
38 | ui.source_files = 'PhoneNumberKit/UI/*.{swift}'
39 | end
40 | end
41 |
--------------------------------------------------------------------------------
/PhoneNumberKit.xcodeproj/project.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/PhoneNumberKit.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/PhoneNumberKit.xcodeproj/xcshareddata/xcschemes/PhoneNumberKit-macOS.xcscheme:
--------------------------------------------------------------------------------
1 |
2 |
5 |
8 |
9 |
15 |
21 |
22 |
23 |
24 |
25 |
30 |
31 |
32 |
33 |
43 |
44 |
50 |
51 |
52 |
53 |
59 |
60 |
66 |
67 |
68 |
69 |
71 |
72 |
75 |
76 |
77 |
--------------------------------------------------------------------------------
/PhoneNumberKit.xcodeproj/xcshareddata/xcschemes/PhoneNumberKit-tvOS.xcscheme:
--------------------------------------------------------------------------------
1 |
2 |
5 |
8 |
9 |
15 |
21 |
22 |
23 |
24 |
25 |
30 |
31 |
32 |
33 |
43 |
44 |
50 |
51 |
52 |
53 |
59 |
60 |
66 |
67 |
68 |
69 |
71 |
72 |
75 |
76 |
77 |
--------------------------------------------------------------------------------
/PhoneNumberKit.xcodeproj/xcshareddata/xcschemes/PhoneNumberKit-watchOS.xcscheme:
--------------------------------------------------------------------------------
1 |
2 |
5 |
8 |
9 |
15 |
21 |
22 |
23 |
24 |
25 |
30 |
31 |
32 |
33 |
43 |
44 |
50 |
51 |
52 |
53 |
59 |
60 |
66 |
67 |
68 |
69 |
71 |
72 |
75 |
76 |
77 |
--------------------------------------------------------------------------------
/PhoneNumberKit.xcodeproj/xcshareddata/xcschemes/PhoneNumberKit.xcscheme:
--------------------------------------------------------------------------------
1 |
2 |
5 |
8 |
9 |
15 |
21 |
22 |
23 |
24 |
25 |
30 |
31 |
37 |
38 |
39 |
40 |
42 |
48 |
49 |
50 |
51 |
52 |
62 |
63 |
69 |
70 |
71 |
72 |
78 |
79 |
85 |
86 |
87 |
88 |
90 |
91 |
94 |
95 |
96 |
--------------------------------------------------------------------------------
/PhoneNumberKit/Bundle+Resources.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | private class CurrentBundleFinder { }
4 |
5 | // The custom bundle locator code is needed to work around a bug in Xcode
6 | // where SwiftUI previews in an SPM module will crash if they try to use
7 | // resources in another SPM module that are loaded using the synthesized
8 | // Bundle.module accessor.
9 | //
10 | extension Bundle {
11 | static var phoneNumberKit: Bundle = {
12 | #if DEBUG && SWIFT_PACKAGE
13 | let bundleName = "PhoneNumberKit_PhoneNumberKit"
14 | let candidates = [
15 | // Bundle should be present here when the package is linked into an App.
16 | Bundle.main.resourceURL,
17 | // Bundle should be present here when the package is linked into a framework.
18 | Bundle(for: CurrentBundleFinder.self).resourceURL,
19 | // For command-line tools.
20 | Bundle.main.bundleURL,
21 | // Bundle should be present here when running previews from a different package (this is the path to "…/Debug-iphonesimulator/").
22 | Bundle(for: CurrentBundleFinder.self).resourceURL?.deletingLastPathComponent().deletingLastPathComponent(),
23 | // Bundle should be present here when running previews from a framework which imports framework whick imports PhoneNumberKit package (this is the path to "…/Debug-iphonesimulator/").
24 | Bundle(for: CurrentBundleFinder.self).resourceURL?.deletingLastPathComponent()
25 | ]
26 | for candidate in candidates {
27 | let bundlePath = candidate?.appendingPathComponent(bundleName + ".bundle")
28 | if let bundle = bundlePath.flatMap(Bundle.init(url:)) {
29 | return bundle
30 | }
31 | }
32 | #endif
33 |
34 | #if SWIFT_PACKAGE
35 | return Bundle.module
36 | #else
37 | return Bundle(for: CurrentBundleFinder.self)
38 | #endif
39 | }()
40 | }
41 |
--------------------------------------------------------------------------------
/PhoneNumberKit/Constants.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Constants.swift
3 | // PhoneNumberKit
4 | //
5 | // Created by Roy Marmelstein on 25/10/2015.
6 | // Copyright © 2021 Roy Marmelstein. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | // MARK: Private Enums
12 |
13 | enum PhoneNumberCountryCodeSource {
14 | case numberWithPlusSign
15 | case numberWithIDD
16 | case numberWithoutPlusSign
17 | case defaultCountry
18 | }
19 |
20 | // MARK: Public Enums
21 |
22 | /// Enumeration for parsing error types
23 | ///
24 | /// - GeneralError: A general error occured.
25 | /// - InvalidCountryCode: A country code could not be found or the one found was invalid
26 | /// - InvalidNumber: The string provided is not a number
27 | /// - TooLong: The string provided is too long to be a valid number
28 | /// - TooShort: The string provided is too short to be a valid number
29 | /// - Deprecated: The method used was deprecated
30 | /// - metadataNotFound: PhoneNumberKit was unable to read the included metadata
31 | /// - ambiguousNumber: The string could not be resolved to a single valid number
32 | public enum PhoneNumberError: Error, Equatable {
33 | case generalError
34 | case invalidCountryCode
35 | case invalidNumber
36 | case tooLong
37 | case tooShort
38 | case deprecated
39 | case metadataNotFound
40 | case ambiguousNumber(phoneNumbers: Set)
41 | }
42 |
43 | extension PhoneNumberError: LocalizedError {
44 | public var errorDescription: String? {
45 | switch self {
46 | case .generalError: return NSLocalizedString("An error occured while validating the phone number.", comment: "")
47 | case .invalidCountryCode: return NSLocalizedString("The country code is invalid.", comment: "")
48 | case .invalidNumber: return NSLocalizedString("The number provided is invalid.", comment: "")
49 | case .tooLong: return NSLocalizedString("The number provided is too long.", comment: "")
50 | case .tooShort: return NSLocalizedString("The number provided is too short.", comment: "")
51 | case .deprecated: return NSLocalizedString("This function is deprecated.", comment: "")
52 | case .metadataNotFound: return NSLocalizedString("Valid metadata is missing.", comment: "")
53 | case .ambiguousNumber: return NSLocalizedString("Phone number is ambiguous.", comment: "")
54 | }
55 | }
56 | }
57 |
58 | public enum PhoneNumberFormat {
59 | case e164 // +33689123456
60 | case international // +33 6 89 12 34 56
61 | case national // 06 89 12 34 56
62 | }
63 |
64 | /// Phone number type enumeration
65 | /// - fixedLine: Fixed line numbers
66 | /// - mobile: Mobile numbers
67 | /// - fixedOrMobile: Either fixed or mobile numbers if we can't tell conclusively.
68 | /// - pager: Pager numbers
69 | /// - personalNumber: Personal number numbers
70 | /// - premiumRate: Premium rate numbers
71 | /// - sharedCost: Shared cost numbers
72 | /// - tollFree: Toll free numbers
73 | /// - voicemail: Voice mail numbers
74 | /// - vOIP: Voip numbers
75 | /// - uan: UAN numbers
76 | /// - unknown: Unknown number type
77 | public enum PhoneNumberType: String, Codable {
78 | case fixedLine
79 | case mobile
80 | case fixedOrMobile
81 | case pager
82 | case personalNumber
83 | case premiumRate
84 | case sharedCost
85 | case tollFree
86 | case voicemail
87 | case voip
88 | case uan
89 | case unknown
90 | case notParsed
91 | }
92 |
93 | public enum PossibleLengthType: String, Codable {
94 | case national
95 | case localOnly
96 | }
97 |
98 | // MARK: Constants
99 |
100 | enum PhoneNumberConstants {
101 | static let defaultCountry = "US"
102 | static let defaultExtnPrefix = " ext. "
103 | static let longPhoneNumber = "999999999999999"
104 | static let minLengthForNSN = 2
105 | static let maxInputStringLength = 250
106 | static let maxLengthCountryCode = 3
107 | static let maxLengthForNSN = 16
108 | static let nonBreakingSpace = "\u{00a0}"
109 | static let plusChars = "++"
110 | static let pausesAndWaitsChars = ",;"
111 | static let operatorChars = "*#"
112 | static let validDigitsString = "0-90-9٠-٩۰-۹"
113 | static let digitPlaceholder = "\u{2008}"
114 | static let separatorBeforeNationalNumber = " "
115 | }
116 |
117 | enum PhoneNumberPatterns {
118 | // MARK: Patterns
119 |
120 | static let firstGroupPattern = "(\\$\\d)"
121 | static let fgPattern = "\\$FG"
122 | static let npPattern = "\\$NP"
123 |
124 | static let allNormalizationMappings = ["0": "0", "1": "1", "2": "2", "3": "3", "4": "4", "5": "5", "6": "6", "7": "7", "8": "8", "9": "9", "٠": "0", "١": "1", "٢": "2", "٣": "3", "٤": "4", "٥": "5", "٦": "6", "٧": "7", "٨": "8", "٩": "9", "۰": "0", "۱": "1", "۲": "2", "۳": "3", "۴": "4", "۵": "5", "۶": "6", "۷": "7", "۸": "8", "۹": "9", "*": "*", "#": "#", ",": ",", ";": ";"]
125 | static let capturingDigitPattern = "([0-90-9٠-٩۰-۹])"
126 |
127 | static let extnPattern = "(?:;ext=([0-90-9٠-٩۰-۹]{1,7})|[ \\t,]*(?:e?xt(?:ensi(?:ó?|ó))?n?|e?xtn?|[,xxX##~~;]|int|anexo|int)[:\\..]?[ \\t,-]*([0-90-9٠-٩۰-۹]{1,7})#?|[- ]+([0-90-9٠-٩۰-۹]{1,5})#)$"
128 |
129 | static let iddPattern = "^(?:\\+|%@)"
130 |
131 | static let formatPattern = "^(?:%@)$"
132 |
133 | static let characterClassPattern = "\\[([^\\[\\]])*\\]"
134 |
135 | static let standaloneDigitPattern = "\\d(?=[^,}][^,}])"
136 |
137 | static let nationalPrefixParsingPattern = "^(?:%@)"
138 |
139 | static let prefixSeparatorPattern = "[- ]"
140 |
141 | static let eligibleAsYouTypePattern = "^[-x‐-―−ー--/ ()()[].\\[\\]/~⁓∼~]*(\\$\\d[-x‐-―−ー--/ ()()[].\\[\\]/~⁓∼~]*)+$"
142 |
143 | static let leadingPlusCharsPattern = "^[++]+"
144 |
145 | static let secondNumberStartPattern = "[\\\\\\/] *x"
146 |
147 | static let unwantedEndPattern = "[^0-90-9٠-٩۰-۹A-Za-z#]+$"
148 |
149 | static let validStartPattern = "[++0-90-9٠-٩۰-۹]"
150 |
151 | static let validPhoneNumberPattern = "^[0-90-9٠-٩۰-۹]{2}$|^[++]*(?:[-x\u{2010}-\u{2015}\u{2212}\u{30FC}\u{FF0D}-\u{FF0F} \u{00A0}\u{00AD}\u{200B}\u{2060}\u{3000}()\u{FF08}\u{FF09}\u{FF3B}\u{FF3D}.\\[\\]/~\u{2053}\u{223C}\u{FF5E}*]*[0-9\u{FF10}-\u{FF19}\u{0660}-\u{0669}\u{06F0}-\u{06F9}]){3,}[-x\u{2010}-\u{2015}\u{2212}\u{30FC}\u{FF0D}-\u{FF0F} \u{00A0}\u{00AD}\u{200B}\u{2060}\u{3000}()\u{FF08}\u{FF09}\u{FF3B}\u{FF3D}.\\[\\]/~\u{2053}\u{223C}\u{FF5E}*A-Za-z0-9\u{FF10}-\u{FF19}\u{0660}-\u{0669}\u{06F0}-\u{06F9}]*(?:(?:;ext=([0-90-9٠-٩۰-۹]{1,7})|[ \\t,]*(?:e?xt(?:ensi(?:ó?|ó))?n?|e?xtn?|[,xxX##~~;]|int|anexo|int)[:\\..]?[ \\t,-]*([0-90-9٠-٩۰-۹]{1,7})#?|[- ]+([0-90-9٠-٩۰-۹]{1,5})#)?$)?[,;]*$"
152 |
153 | static let countryCodePattern = "^[a-zA-Z]{2}$"
154 | }
155 |
--------------------------------------------------------------------------------
/PhoneNumberKit/Formatter.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Formatter.swift
3 | // PhoneNumberKit
4 | //
5 | // Created by Roy Marmelstein on 03/11/2015.
6 | // Copyright © 2021 Roy Marmelstein. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | final class Formatter {
12 | weak var regexManager: RegexManager?
13 |
14 | init(regexManager: RegexManager) {
15 | self.regexManager = regexManager
16 | }
17 |
18 | // MARK: Formatting functions
19 |
20 | /// Formats phone numbers for display
21 | ///
22 | /// - Parameters:
23 | /// - phoneNumber: Phone number object.
24 | /// - formatType: Format type.
25 | /// - regionMetadata: Region meta data.
26 | /// - Returns: Formatted Modified national number ready for display.
27 | func format(phoneNumber: PhoneNumber, formatType: PhoneNumberFormat, regionMetadata: MetadataTerritory?) -> String {
28 | var formattedNationalNumber = phoneNumber.adjustedNationalNumber()
29 | if let regionMetadata = regionMetadata {
30 | formattedNationalNumber = self.formatNationalNumber(formattedNationalNumber, regionMetadata: regionMetadata, formatType: formatType)
31 | if let formattedExtension = formatExtension(phoneNumber.numberExtension, regionMetadata: regionMetadata) {
32 | formattedNationalNumber = formattedNationalNumber + formattedExtension
33 | }
34 | }
35 | return formattedNationalNumber
36 | }
37 |
38 | /// Formats extension for display
39 | ///
40 | /// - Parameters:
41 | /// - numberExtension: Number extension string.
42 | /// - regionMetadata: Region meta data.
43 | /// - Returns: Modified number extension with either a preferred extension prefix or the default one.
44 | func formatExtension(_ numberExtension: String?, regionMetadata: MetadataTerritory) -> String? {
45 | if let extns = numberExtension {
46 | if let preferredExtnPrefix = regionMetadata.preferredExtnPrefix {
47 | return "\(preferredExtnPrefix)\(extns)"
48 | } else {
49 | return "\(PhoneNumberConstants.defaultExtnPrefix)\(extns)"
50 | }
51 | }
52 | return nil
53 | }
54 |
55 | /// Formats national number for display
56 | ///
57 | /// - Parameters:
58 | /// - nationalNumber: National number string.
59 | /// - regionMetadata: Region meta data.
60 | /// - formatType: Format type.
61 | /// - Returns: Modified nationalNumber for display.
62 | func formatNationalNumber(_ nationalNumber: String, regionMetadata: MetadataTerritory, formatType: PhoneNumberFormat) -> String {
63 | guard let regexManager = regexManager else { return nationalNumber }
64 | let formats = regionMetadata.numberFormats
65 | var selectedFormat: MetadataPhoneNumberFormat?
66 | for format in formats {
67 | if let leadingDigitPattern = format.leadingDigitsPatterns?.last {
68 | if regexManager.stringPositionByRegex(leadingDigitPattern, string: String(nationalNumber)) == 0 {
69 | if regexManager.matchesEntirely(format.pattern, string: String(nationalNumber)) {
70 | selectedFormat = format
71 | break
72 | }
73 | }
74 | } else {
75 | if regexManager.matchesEntirely(format.pattern, string: String(nationalNumber)) {
76 | selectedFormat = format
77 | break
78 | }
79 | }
80 | }
81 | if let formatPattern = selectedFormat {
82 | guard let numberFormatRule = (formatType == PhoneNumberFormat.international && formatPattern.intlFormat != nil) ? formatPattern.intlFormat : formatPattern.format, let pattern = formatPattern.pattern else {
83 | return nationalNumber
84 | }
85 | var formattedNationalNumber = String()
86 | var prefixFormattingRule = String()
87 | if let nationalPrefixFormattingRule = formatPattern.nationalPrefixFormattingRule, let nationalPrefix = regionMetadata.nationalPrefix {
88 | prefixFormattingRule = regexManager.replaceStringByRegex(PhoneNumberPatterns.npPattern, string: nationalPrefixFormattingRule, template: nationalPrefix)
89 | prefixFormattingRule = regexManager.replaceStringByRegex(PhoneNumberPatterns.fgPattern, string: prefixFormattingRule, template: "\\$1")
90 | }
91 | if formatType == PhoneNumberFormat.national, regexManager.hasValue(prefixFormattingRule) {
92 | let replacePattern = regexManager.replaceFirstStringByRegex(PhoneNumberPatterns.firstGroupPattern, string: numberFormatRule, templateString: prefixFormattingRule)
93 | formattedNationalNumber = regexManager.replaceStringByRegex(pattern, string: nationalNumber, template: replacePattern)
94 | } else {
95 | formattedNationalNumber = regexManager.replaceStringByRegex(pattern, string: nationalNumber, template: numberFormatRule)
96 | }
97 | return formattedNationalNumber
98 | } else {
99 | return nationalNumber
100 | }
101 | }
102 | }
103 |
104 | public extension PhoneNumber {
105 | /// Adjust national number for display by adding leading zero if needed. Used for basic formatting functions.
106 | /// - Returns: A string representing the adjusted national number.
107 | func adjustedNationalNumber() -> String {
108 | if self.leadingZero == true {
109 | return "0" + String(nationalNumber)
110 | } else {
111 | return String(nationalNumber)
112 | }
113 | }
114 | }
115 |
--------------------------------------------------------------------------------
/PhoneNumberKit/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | en
7 | CFBundleExecutable
8 | $(EXECUTABLE_NAME)
9 | CFBundleIdentifier
10 | $(PRODUCT_BUNDLE_IDENTIFIER)
11 | CFBundleInfoDictionaryVersion
12 | 6.0
13 | CFBundleName
14 | $(PRODUCT_NAME)
15 | CFBundlePackageType
16 | FMWK
17 | CFBundleShortVersionString
18 | 3.4.2
19 | CFBundleSignature
20 | ????
21 | CFBundleVersion
22 | 27
23 | NSPrincipalClass
24 |
25 |
26 |
27 |
--------------------------------------------------------------------------------
/PhoneNumberKit/MetadataManager.swift:
--------------------------------------------------------------------------------
1 | //
2 | // MetadataManager.swift
3 | // PhoneNumberKit
4 | //
5 | // Created by Roy Marmelstein on 03/10/2015.
6 | // Copyright © 2021 Roy Marmelstein. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | final class MetadataManager {
12 | private(set) var territories = [MetadataTerritory]()
13 |
14 | private var territoriesByCode = [UInt64: [MetadataTerritory]]()
15 | private var mainTerritoryByCode = [UInt64: MetadataTerritory]()
16 | private var territoriesByCountry = [String: MetadataTerritory]()
17 |
18 | // MARK: Lifecycle
19 |
20 | /// Private init populates metadata territories and the two hashed dictionaries for faster lookup.
21 | ///
22 | /// - Parameter metadataCallback: a closure that returns metadata as JSON Data.
23 | public init(metadataCallback: MetadataCallback) {
24 | self.territories = self.populateTerritories(metadataCallback: metadataCallback)
25 | for item in self.territories {
26 | var currentTerritories: [MetadataTerritory] = self.territoriesByCode[item.countryCode] ?? [MetadataTerritory]()
27 | // In the case of multiple countries sharing a calling code, such as the NANPA countries,
28 | // the one indicated with "isMainCountryForCode" in the metadata should be first.
29 | if item.mainCountryForCode {
30 | currentTerritories.insert(item, at: 0)
31 | } else {
32 | currentTerritories.append(item)
33 | }
34 | self.territoriesByCode[item.countryCode] = currentTerritories
35 | if self.mainTerritoryByCode[item.countryCode] == nil || item.mainCountryForCode == true {
36 | self.mainTerritoryByCode[item.countryCode] = item
37 | }
38 | self.territoriesByCountry[item.codeID] = item
39 | }
40 | }
41 |
42 | deinit {
43 | territories.removeAll()
44 | territoriesByCode.removeAll()
45 | territoriesByCountry.removeAll()
46 | }
47 |
48 | /// Populates the metadata from a metadataCallback.
49 | ///
50 | /// - Parameter metadataCallback: a closure that returns metadata as JSON Data.
51 | /// - Returns: array of MetadataTerritory objects
52 | private func populateTerritories(metadataCallback: MetadataCallback) -> [MetadataTerritory] {
53 | var territoryArray = [MetadataTerritory]()
54 | do {
55 | let jsonData: Data? = try metadataCallback()
56 | let jsonDecoder = JSONDecoder()
57 | if let jsonData = jsonData, let metadata: PhoneNumberMetadata = try? jsonDecoder.decode(PhoneNumberMetadata.self, from: jsonData) {
58 | territoryArray = metadata.territories
59 | }
60 | } catch {
61 | debugPrint("ERROR: Unable to load PhoneNumberMetadata.json resource: \(error.localizedDescription)")
62 | }
63 | return territoryArray
64 | }
65 |
66 | // MARK: Filters
67 |
68 | /// Get an array of MetadataTerritory objects corresponding to a given country code.
69 | ///
70 | /// - parameter code: international country code (e.g 44 for the UK).
71 | ///
72 | /// - returns: optional array of MetadataTerritory objects.
73 | func filterTerritories(byCode code: UInt64) -> [MetadataTerritory]? {
74 | return self.territoriesByCode[code]
75 | }
76 |
77 | /// Get the MetadataTerritory objects for an ISO 3166 compliant region code.
78 | ///
79 | /// - parameter country: ISO 3166 compliant region code (e.g "GB" for the UK).
80 | ///
81 | /// - returns: A MetadataTerritory object.
82 | func filterTerritories(byCountry country: String) -> MetadataTerritory? {
83 | return self.territoriesByCountry[country.uppercased()]
84 | }
85 |
86 | /// Get the main MetadataTerritory objects for a given country code.
87 | ///
88 | /// - parameter code: An international country code (e.g 1 for the US).
89 | ///
90 | /// - returns: A MetadataTerritory object.
91 | func mainTerritory(forCode code: UInt64) -> MetadataTerritory? {
92 | return self.mainTerritoryByCode[code]
93 | }
94 | }
95 |
--------------------------------------------------------------------------------
/PhoneNumberKit/MetadataParsing.swift:
--------------------------------------------------------------------------------
1 | //
2 | // MetadataParsing.swift
3 | // PhoneNumberKit
4 | //
5 | // Created by Roy Marmelstein on 2019-02-10.
6 | // Copyright © 2019 Roy Marmelstein. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | // MARK: - MetadataTerritory
12 |
13 | public extension MetadataTerritory {
14 | enum CodingKeys: String, CodingKey {
15 | case codeID = "id"
16 | case countryCode
17 | case internationalPrefix
18 | case mainCountryForCode
19 | case nationalPrefix
20 | case nationalPrefixFormattingRule
21 | case nationalPrefixForParsing
22 | case nationalPrefixTransformRule
23 | case preferredExtnPrefix
24 | case emergency
25 | case fixedLine
26 | case generalDesc
27 | case mobile
28 | case pager
29 | case personalNumber
30 | case premiumRate
31 | case sharedCost
32 | case tollFree
33 | case voicemail
34 | case voip
35 | case uan
36 | case numberFormats = "numberFormat"
37 | case leadingDigits
38 | case availableFormats
39 | }
40 |
41 | init(from decoder: Decoder) throws {
42 | let container = try decoder.container(keyedBy: CodingKeys.self)
43 |
44 | // Custom parsing logic
45 | codeID = try container.decode(String.self, forKey: .codeID)
46 | let code = try! container.decode(String.self, forKey: .countryCode)
47 | countryCode = UInt64(code)!
48 | mainCountryForCode = container.decodeBoolString(forKey: .mainCountryForCode)
49 | let possibleNationalPrefixForParsing: String? = try container.decodeIfPresent(String.self, forKey: .nationalPrefixForParsing)
50 | let possibleNationalPrefix: String? = try container.decodeIfPresent(String.self, forKey: .nationalPrefix)
51 | nationalPrefix = possibleNationalPrefix
52 | let nationalPrefixForParsing = (possibleNationalPrefixForParsing == nil && possibleNationalPrefix != nil) ? nationalPrefix : possibleNationalPrefixForParsing
53 | self.nationalPrefixForParsing = nationalPrefixForParsing != nil ? nationalPrefixForParsing!.replacingOccurrences(of: "\\", with: #"\\"#) : nil
54 | nationalPrefixFormattingRule = try container.decodeIfPresent(String.self, forKey: .nationalPrefixFormattingRule)
55 | let availableFormats = try? container.nestedContainer(keyedBy: CodingKeys.self, forKey: .availableFormats)
56 | let temporaryFormatList: [MetadataPhoneNumberFormat] = availableFormats?.decodeArrayOrObject(forKey: .numberFormats) ?? [MetadataPhoneNumberFormat]()
57 | numberFormats = temporaryFormatList.withDefaultNationalPrefixFormattingRule(nationalPrefixFormattingRule)
58 |
59 | // Default parsing logic
60 | internationalPrefix = try container.decodeIfPresent(String.self, forKey: .internationalPrefix)
61 | nationalPrefixTransformRule = try container.decodeIfPresent(String.self, forKey: .nationalPrefixTransformRule)
62 | preferredExtnPrefix = try container.decodeIfPresent(String.self, forKey: .preferredExtnPrefix)
63 | emergency = try container.decodeIfPresent(MetadataPhoneNumberDesc.self, forKey: .emergency)
64 | fixedLine = try container.decodeIfPresent(MetadataPhoneNumberDesc.self, forKey: .fixedLine)
65 | generalDesc = try container.decodeIfPresent(MetadataPhoneNumberDesc.self, forKey: .generalDesc)
66 | mobile = try container.decodeIfPresent(MetadataPhoneNumberDesc.self, forKey: .mobile)
67 | pager = try container.decodeIfPresent(MetadataPhoneNumberDesc.self, forKey: .pager)
68 | personalNumber = try container.decodeIfPresent(MetadataPhoneNumberDesc.self, forKey: .personalNumber)
69 | premiumRate = try container.decodeIfPresent(MetadataPhoneNumberDesc.self, forKey: .premiumRate)
70 | sharedCost = try container.decodeIfPresent(MetadataPhoneNumberDesc.self, forKey: .sharedCost)
71 | tollFree = try container.decodeIfPresent(MetadataPhoneNumberDesc.self, forKey: .tollFree)
72 | voicemail = try container.decodeIfPresent(MetadataPhoneNumberDesc.self, forKey: .voicemail)
73 | voip = try container.decodeIfPresent(MetadataPhoneNumberDesc.self, forKey: .voip)
74 | uan = try container.decodeIfPresent(MetadataPhoneNumberDesc.self, forKey: .uan)
75 | leadingDigits = try container.decodeIfPresent(String.self, forKey: .leadingDigits)
76 | }
77 | }
78 |
79 | // MARK: - MetadataPhoneNumberFormat
80 |
81 | public extension MetadataPhoneNumberFormat {
82 | enum CodingKeys: String, CodingKey {
83 | case pattern
84 | case format
85 | case intlFormat
86 | case leadingDigitsPatterns = "leadingDigits"
87 | case nationalPrefixFormattingRule
88 | case nationalPrefixOptionalWhenFormatting
89 | case domesticCarrierCodeFormattingRule = "carrierCodeFormattingRule"
90 | }
91 |
92 | init(from decoder: Decoder) throws {
93 | let container = try decoder.container(keyedBy: CodingKeys.self)
94 |
95 | // Custom parsing logic
96 | leadingDigitsPatterns = container.decodeArrayOrObject(forKey: .leadingDigitsPatterns)
97 | nationalPrefixOptionalWhenFormatting = container.decodeBoolString(forKey: .nationalPrefixOptionalWhenFormatting)
98 |
99 | // Default parsing logic
100 | pattern = try container.decodeIfPresent(String.self, forKey: .pattern)
101 | format = try container.decodeIfPresent(String.self, forKey: .format)
102 | intlFormat = try container.decodeIfPresent(String.self, forKey: .intlFormat)
103 | nationalPrefixFormattingRule = try container.decodeIfPresent(String.self, forKey: .nationalPrefixFormattingRule)
104 | domesticCarrierCodeFormattingRule = try container.decodeIfPresent(String.self, forKey: .domesticCarrierCodeFormattingRule)
105 | }
106 | }
107 |
108 | // MARK: - PhoneNumberMetadata
109 |
110 | extension PhoneNumberMetadata {
111 | enum CodingKeys: String, CodingKey {
112 | case phoneNumberMetadata
113 | case territories
114 | case territory
115 | }
116 |
117 | init(from decoder: Decoder) throws {
118 | let container = try decoder.container(keyedBy: CodingKeys.self)
119 | let metadataObject = try container.nestedContainer(keyedBy: CodingKeys.self, forKey: .phoneNumberMetadata)
120 | let territoryObject = try metadataObject.nestedContainer(keyedBy: CodingKeys.self, forKey: .territories)
121 | territories = try territoryObject.decode([MetadataTerritory].self, forKey: .territory)
122 | }
123 | }
124 |
125 | // MARK: - Parsing helpers
126 |
127 | private extension KeyedDecodingContainer where K: CodingKey {
128 | /// Decodes a string to a boolean. Returns false if empty.
129 | ///
130 | /// - Parameter key: Coding key to decode
131 | func decodeBoolString(forKey key: KeyedDecodingContainer.Key) -> Bool {
132 | guard let value: String = try? self.decode(String.self, forKey: key) else {
133 | return false
134 | }
135 | return Bool(value) ?? false
136 | }
137 |
138 | /// Decodes either a single object or an array into an array. Returns an empty array if empty.
139 | ///
140 | /// - Parameter key: Coding key to decode
141 | func decodeArrayOrObject(forKey key: KeyedDecodingContainer.Key) -> [T] {
142 | guard let array: [T] = try? self.decode([T].self, forKey: key) else {
143 | guard let object: T = try? self.decode(T.self, forKey: key) else {
144 | return [T]()
145 | }
146 | return [object]
147 | }
148 | return array
149 | }
150 | }
151 |
152 | private extension Collection where Element == MetadataPhoneNumberFormat {
153 | func withDefaultNationalPrefixFormattingRule(_ nationalPrefixFormattingRule: String?) -> [Element] {
154 | return self.map { format -> MetadataPhoneNumberFormat in
155 | var modifiedFormat = format
156 | if modifiedFormat.nationalPrefixFormattingRule == nil {
157 | modifiedFormat.nationalPrefixFormattingRule = nationalPrefixFormattingRule
158 | }
159 | return modifiedFormat
160 | }
161 | }
162 | }
163 |
--------------------------------------------------------------------------------
/PhoneNumberKit/MetadataTypes.swift:
--------------------------------------------------------------------------------
1 | //
2 | // MetadataTypes.swift
3 | // PhoneNumberKit
4 | //
5 | // Created by Roy Marmelstein on 02/11/2015.
6 | // Copyright © 2021 Roy Marmelstein. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | /// MetadataTerritory object
12 | /// - Parameter codeID: ISO 3166 compliant region code
13 | /// - Parameter countryCode: International country code
14 | /// - Parameter internationalPrefix: International prefix. Optional.
15 | /// - Parameter mainCountryForCode: Whether the current metadata is the main country for its country code.
16 | /// - Parameter nationalPrefix: National prefix
17 | /// - Parameter nationalPrefixFormattingRule: National prefix formatting rule
18 | /// - Parameter nationalPrefixForParsing: National prefix for parsing
19 | /// - Parameter nationalPrefixTransformRule: National prefix transform rule
20 | /// - Parameter emergency: MetadataPhoneNumberDesc for emergency numbers
21 | /// - Parameter fixedLine: MetadataPhoneNumberDesc for fixed line numbers
22 | /// - Parameter generalDesc: MetadataPhoneNumberDesc for general numbers
23 | /// - Parameter mobile: MetadataPhoneNumberDesc for mobile numbers
24 | /// - Parameter pager: MetadataPhoneNumberDesc for pager numbers
25 | /// - Parameter personalNumber: MetadataPhoneNumberDesc for personal number numbers
26 | /// - Parameter premiumRate: MetadataPhoneNumberDesc for premium rate numbers
27 | /// - Parameter sharedCost: MetadataPhoneNumberDesc for shared cost numbers
28 | /// - Parameter tollFree: MetadataPhoneNumberDesc for toll free numbers
29 | /// - Parameter voicemail: MetadataPhoneNumberDesc for voice mail numbers
30 | /// - Parameter voip: MetadataPhoneNumberDesc for voip numbers
31 | /// - Parameter uan: MetadataPhoneNumberDesc for uan numbers
32 | /// - Parameter leadingDigits: Optional leading digits for the territory
33 | public struct MetadataTerritory: Decodable {
34 | public let codeID: String
35 | public let countryCode: UInt64
36 | public let internationalPrefix: String?
37 | public let mainCountryForCode: Bool
38 | public let nationalPrefix: String?
39 | public let nationalPrefixFormattingRule: String?
40 | public let nationalPrefixForParsing: String?
41 | public let nationalPrefixTransformRule: String?
42 | public let preferredExtnPrefix: String?
43 | public let emergency: MetadataPhoneNumberDesc?
44 | public let fixedLine: MetadataPhoneNumberDesc?
45 | public let generalDesc: MetadataPhoneNumberDesc?
46 | public let mobile: MetadataPhoneNumberDesc?
47 | public let pager: MetadataPhoneNumberDesc?
48 | public let personalNumber: MetadataPhoneNumberDesc?
49 | public let premiumRate: MetadataPhoneNumberDesc?
50 | public let sharedCost: MetadataPhoneNumberDesc?
51 | public let tollFree: MetadataPhoneNumberDesc?
52 | public let voicemail: MetadataPhoneNumberDesc?
53 | public let voip: MetadataPhoneNumberDesc?
54 | public let uan: MetadataPhoneNumberDesc?
55 | public let numberFormats: [MetadataPhoneNumberFormat]
56 | public let leadingDigits: String?
57 | }
58 |
59 | /// MetadataPhoneNumberDesc object
60 | /// - Parameter exampleNumber: An example phone number for the given type. Optional.
61 | /// - Parameter nationalNumberPattern: National number regex pattern. Optional.
62 | /// - Parameter possibleNumberPattern: Possible number regex pattern. Optional.
63 | /// - Parameter possibleLengths: Possible phone number lengths. Optional.
64 | public struct MetadataPhoneNumberDesc: Decodable {
65 | public let exampleNumber: String?
66 | public let nationalNumberPattern: String?
67 | public let possibleNumberPattern: String?
68 | public let possibleLengths: MetadataPossibleLengths?
69 | }
70 |
71 | public struct MetadataPossibleLengths: Decodable {
72 | let national: String?
73 | let localOnly: String?
74 | }
75 |
76 | /// MetadataPhoneNumberFormat object
77 | /// - Parameter pattern: Regex pattern. Optional.
78 | /// - Parameter format: Formatting template. Optional.
79 | /// - Parameter intlFormat: International formatting template. Optional.
80 | ///
81 | /// - Parameter leadingDigitsPatterns: Leading digits regex pattern. Optional.
82 | /// - Parameter nationalPrefixFormattingRule: National prefix formatting rule. Optional.
83 | /// - Parameter nationalPrefixOptionalWhenFormatting: National prefix optional bool. Optional.
84 | /// - Parameter domesticCarrierCodeFormattingRule: Domestic carrier code formatting rule. Optional.
85 | public struct MetadataPhoneNumberFormat: Decodable {
86 | public let pattern: String?
87 | public let format: String?
88 | public let intlFormat: String?
89 | public let leadingDigitsPatterns: [String]?
90 | public var nationalPrefixFormattingRule: String?
91 | public let nationalPrefixOptionalWhenFormatting: Bool?
92 | public let domesticCarrierCodeFormattingRule: String?
93 | }
94 |
95 | /// Internal object for metadata parsing
96 | struct PhoneNumberMetadata: Decodable {
97 | var territories: [MetadataTerritory]
98 | }
99 |
--------------------------------------------------------------------------------
/PhoneNumberKit/NSRegularExpression+Swift.swift:
--------------------------------------------------------------------------------
1 | //
2 | // NSRegularExpression+Swift.swift
3 | // PhoneNumberKit
4 | //
5 | // Created by David Beck on 8/15/16.
6 | // Copyright © 2016 Roy Marmelstein. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | extension String {
12 | func nsRange(from range: Range) -> NSRange {
13 | let utf16view = self.utf16
14 | let from = range.lowerBound.samePosition(in: utf16view) ?? self.startIndex
15 | let to = range.upperBound.samePosition(in: utf16view) ?? self.endIndex
16 | return NSRange(location: utf16view.distance(from: utf16view.startIndex, to: from), length: utf16view.distance(from: from, to: to))
17 | }
18 |
19 | func range(from nsRange: NSRange) -> Range? {
20 | guard
21 | let from16 = utf16.index(utf16.startIndex, offsetBy: nsRange.location, limitedBy: utf16.endIndex),
22 | let to16 = utf16.index(from16, offsetBy: nsRange.length, limitedBy: utf16.endIndex),
23 | let from = String.Index(from16, within: self),
24 | let to = String.Index(to16, within: self)
25 | else { return nil }
26 | return from..? = nil, using block: (NSTextCheckingResult?, NSRegularExpression.MatchingFlags, UnsafeMutablePointer) -> Swift.Void) {
33 | let range = range ?? string.startIndex..? = nil, using block: @escaping (NSTextCheckingResult?, NSRegularExpression.MatchingFlags, UnsafeMutablePointer) -> Swift.Void) {
41 | let range = range ?? string.startIndex..? = nil) -> [NSTextCheckingResult] {
49 | let range = range ?? string.startIndex..? = nil) -> Int {
56 | let range = range ?? string.startIndex..? = nil) -> NSTextCheckingResult? {
63 | let range = range ?? string.startIndex..? = nil) -> Range? {
70 | let range = range ?? string.startIndex..? = nil, withTemplate templ: String) -> String {
79 | let range = range ?? string.startIndex.. PhoneNumber {
28 | guard let metadataManager = metadataManager, let regexManager = regexManager else { throw PhoneNumberError.generalError }
29 | // Make sure region is in uppercase so that it matches metadata (1)
30 | let region = region.uppercased()
31 | // Extract number (2)
32 | var nationalNumber = numberString
33 | let match = try regexManager.phoneDataDetectorMatch(numberString)
34 | let matchedNumber = nationalNumber.substring(with: match.range)
35 | // Replace Arabic and Persian numerals and let the rest unchanged
36 | nationalNumber = regexManager.stringByReplacingOccurrences(matchedNumber, map: PhoneNumberPatterns.allNormalizationMappings, keepUnmapped: true)
37 |
38 | // Strip and extract extension (3)
39 | var numberExtension: String?
40 | if let rawExtension = parser.stripExtension(&nationalNumber) {
41 | numberExtension = self.parser.normalizePhoneNumber(rawExtension)
42 | }
43 | // Country code parse (4)
44 | guard var regionMetadata = metadataManager.filterTerritories(byCountry: region) else {
45 | throw PhoneNumberError.invalidCountryCode
46 | }
47 | let countryCode: UInt64
48 | do {
49 | countryCode = try self.parser.extractCountryCode(nationalNumber, nationalNumber: &nationalNumber, metadata: regionMetadata)
50 | } catch {
51 | let plusRemovedNumberString = regexManager.replaceStringByRegex(PhoneNumberPatterns.leadingPlusCharsPattern, string: nationalNumber as String)
52 | countryCode = try self.parser.extractCountryCode(plusRemovedNumberString, nationalNumber: &nationalNumber, metadata: regionMetadata)
53 | }
54 |
55 | // Normalized number (5)
56 | nationalNumber = self.parser.normalizePhoneNumber(nationalNumber)
57 | if countryCode == 0 {
58 | if nationalNumber.hasPrefix(String(regionMetadata.countryCode)) {
59 | let potentialNationalNumber = String(nationalNumber.dropFirst(String(regionMetadata.countryCode).count))
60 | if let result = try? parse(potentialNationalNumber, withRegion: regionMetadata.codeID, ignoreType: ignoreType) {
61 | return result
62 | }
63 | }
64 |
65 | if let result = try validPhoneNumber(from: nationalNumber, using: regionMetadata, countryCode: regionMetadata.countryCode, ignoreType: ignoreType, numberString: numberString, numberExtension: numberExtension) {
66 | return result
67 | }
68 | throw PhoneNumberError.invalidNumber
69 | }
70 |
71 | // If country code is not default, grab correct metadata (6)
72 | if countryCode != regionMetadata.countryCode, let countryMetadata = metadataManager.mainTerritory(forCode: countryCode) {
73 | regionMetadata = countryMetadata
74 | }
75 |
76 | if let result = try validPhoneNumber(from: nationalNumber, using: regionMetadata, countryCode: countryCode, ignoreType: ignoreType, numberString: numberString, numberExtension: numberExtension) {
77 | return result
78 | }
79 |
80 | // If everything fails, iterate through other territories with the same country code (7)
81 | var possibleResults: Set = []
82 | if let metadataList = metadataManager.filterTerritories(byCode: countryCode) {
83 | for metadata in metadataList where regionMetadata.codeID != metadata.codeID {
84 | if let result = try validPhoneNumber(from: nationalNumber, using: metadata, countryCode: countryCode, ignoreType: ignoreType, numberString: numberString, numberExtension: numberExtension) {
85 | possibleResults.insert(result)
86 | }
87 | }
88 | }
89 |
90 | switch possibleResults.count {
91 | case 0: throw PhoneNumberError.invalidNumber
92 | case 1: return possibleResults.first!
93 | default: throw PhoneNumberError.ambiguousNumber(phoneNumbers: possibleResults)
94 | }
95 | }
96 |
97 | // Parse task
98 |
99 | /// Fastest way to parse an array of phone numbers. Uses custom region code.
100 | /// - Parameter numberStrings: An array of raw number strings.
101 | /// - Parameter region: ISO 3166 compliant region code.
102 | /// - parameter ignoreType: Avoids number type checking for faster performance.
103 | /// - Returns: An array of valid PhoneNumber objects.
104 | func parseMultiple(_ numberStrings: [String], withRegion region: String, ignoreType: Bool, shouldReturnFailedEmptyNumbers: Bool = false) -> [PhoneNumber] {
105 | var hasError = false
106 |
107 | var multiParseArray = [PhoneNumber](unsafeUninitializedCapacity: numberStrings.count) { buffer, initializedCount in
108 | DispatchQueue.concurrentPerform(iterations: numberStrings.count) { [buffer] index in
109 | let numberString = numberStrings[index]
110 | do {
111 | let phoneNumber = try self.parse(numberString, withRegion: region, ignoreType: ignoreType)
112 | buffer.baseAddress!.advanced(by: index).initialize(to: phoneNumber)
113 | } catch {
114 | buffer.baseAddress!.advanced(by: index).initialize(to: PhoneNumber.notPhoneNumber())
115 | hasError = true
116 | }
117 | }
118 | initializedCount = numberStrings.count
119 | }
120 |
121 | if hasError, !shouldReturnFailedEmptyNumbers {
122 | multiParseArray = multiParseArray.filter { $0.type != .notParsed }
123 | }
124 |
125 | return multiParseArray
126 | }
127 |
128 | /// Get correct ISO 3166 compliant region code for a number.
129 | ///
130 | /// - Parameters:
131 | /// - nationalNumber: national number.
132 | /// - countryCode: country code.
133 | /// - leadingZero: whether or not the number has a leading zero.
134 | /// - Returns: ISO 3166 compliant region code.
135 | func getRegionCode(of nationalNumber: UInt64, countryCode: UInt64, leadingZero: Bool) -> String? {
136 | guard let regexManager = regexManager, let metadataManager = metadataManager, let regions = metadataManager.filterTerritories(byCode: countryCode) else { return nil }
137 |
138 | if regions.count == 1 {
139 | return regions[0].codeID
140 | }
141 |
142 | let nationalNumberString = String(nationalNumber)
143 | for region in regions {
144 | if let leadingDigits = region.leadingDigits {
145 | if regexManager.matchesAtStart(leadingDigits, string: nationalNumberString) {
146 | return region.codeID
147 | }
148 | }
149 | if leadingZero, self.parser.checkNumberType("0" + nationalNumberString, metadata: region) != .unknown {
150 | return region.codeID
151 | }
152 | if self.parser.checkNumberType(nationalNumberString, metadata: region) != .unknown {
153 | return region.codeID
154 | }
155 | }
156 | return nil
157 | }
158 |
159 | // MARK: Internal method
160 |
161 | /// Creates a valid phone number given a specifc region metadata, used internally by the parse function
162 | private func validPhoneNumber(from nationalNumber: String, using regionMetadata: MetadataTerritory, countryCode: UInt64, ignoreType: Bool, numberString: String, numberExtension: String?) throws -> PhoneNumber? {
163 | guard let metadataManager = metadataManager, let regexManager = regexManager else { throw PhoneNumberError.generalError }
164 |
165 | var nationalNumber = nationalNumber
166 | var regionMetadata = regionMetadata
167 |
168 | // National Prefix Strip (1)
169 | self.parser.stripNationalPrefix(&nationalNumber, metadata: regionMetadata)
170 |
171 | // Test number against general number description for correct metadata (2)
172 | if let generalNumberDesc = regionMetadata.generalDesc,
173 | regexManager.hasValue(generalNumberDesc.nationalNumberPattern) == false || parser.isNumberMatchingDesc(nationalNumber, numberDesc: generalNumberDesc) == false {
174 | return nil
175 | }
176 | // Finalize remaining parameters and create phone number object (3)
177 | let leadingZero = nationalNumber.hasPrefix("0")
178 | guard let finalNationalNumber = UInt64(nationalNumber) else {
179 | throw PhoneNumberError.invalidNumber
180 | }
181 |
182 | // Check if the number if of a known type (4)
183 | var type: PhoneNumberType = .unknown
184 | if ignoreType == false {
185 | if let regionCode = getRegionCode(of: finalNationalNumber, countryCode: countryCode, leadingZero: leadingZero), let foundMetadata = metadataManager.filterTerritories(byCountry: regionCode) {
186 | regionMetadata = foundMetadata
187 | }
188 | type = self.parser.checkNumberType(String(nationalNumber), metadata: regionMetadata, leadingZero: leadingZero)
189 | if type == .unknown {
190 | throw PhoneNumberError.invalidNumber
191 | }
192 | }
193 |
194 | return PhoneNumber(numberString: numberString, countryCode: countryCode, leadingZero: leadingZero, nationalNumber: finalNationalNumber, numberExtension: numberExtension, type: type, regionID: regionMetadata.codeID)
195 | }
196 | }
197 |
--------------------------------------------------------------------------------
/PhoneNumberKit/PhoneNumber+Codable.swift:
--------------------------------------------------------------------------------
1 | //
2 | // PhoneNumber+Codable.swift
3 | // PhoneNumberKit
4 | //
5 | // Created by David Roman on 16/11/2021.
6 | // Copyright © 2021 Roy Marmelstein. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | /// The strategy used to decode a `PhoneNumber` value.
12 | public enum PhoneNumberDecodingStrategy {
13 | /// Decode `PhoneNumber` properties as key-value pairs. This is the default strategy.
14 | case properties
15 | /// Decode `PhoneNumber` as a E164 formatted string.
16 | case e164
17 | /// The default `PhoneNumber` encoding strategy.
18 | public static var `default` = properties
19 | }
20 |
21 | /// The strategy used to encode a `PhoneNumber` value.
22 | public enum PhoneNumberEncodingStrategy {
23 | /// Encode `PhoneNumber` properties as key-value pairs. This is the default strategy.
24 | case properties
25 | /// Encode `PhoneNumber` as a E164 formatted string.
26 | case e164
27 | /// The default `PhoneNumber` encoding strategy.
28 | public static var `default` = properties
29 | }
30 |
31 | public enum PhoneNumberDecodingUtils {
32 | /// The default `PhoneNumberUtility` instance used for parsing when decoding, if needed.
33 | public static var defaultUtility: () -> PhoneNumberUtility = { .init() }
34 | }
35 |
36 | public enum PhoneNumberEncodingUtils {
37 | /// The default `PhoneNumberUtility` instance used for formatting when encoding, if needed.
38 | public static var defaultUtility: () -> PhoneNumberUtility = { .init() }
39 | }
40 |
41 | public extension JSONDecoder {
42 | /// The strategy used to decode a `PhoneNumber` value.
43 | var phoneNumberDecodingStrategy: PhoneNumberDecodingStrategy {
44 | get {
45 | return userInfo[.phoneNumberDecodingStrategy] as? PhoneNumberDecodingStrategy ?? .default
46 | }
47 | set {
48 | userInfo[.phoneNumberDecodingStrategy] = newValue
49 | }
50 | }
51 |
52 | /// The `PhoneNumberUtility` instance used for parsing when decoding, if needed.
53 | var phoneNumberUtility: () -> PhoneNumberUtility {
54 | get {
55 | return userInfo[.phoneNumberUtility] as? () -> PhoneNumberUtility ?? PhoneNumberDecodingUtils.defaultUtility
56 | }
57 | set {
58 | userInfo[.phoneNumberUtility] = newValue
59 | }
60 | }
61 | }
62 |
63 | public extension JSONEncoder {
64 | /// The strategy used to encode a `PhoneNumber` value.
65 | var phoneNumberEncodingStrategy: PhoneNumberEncodingStrategy {
66 | get {
67 | return userInfo[.phoneNumberEncodingStrategy] as? PhoneNumberEncodingStrategy ?? .default
68 | }
69 | set {
70 | userInfo[.phoneNumberEncodingStrategy] = newValue
71 | }
72 | }
73 |
74 | /// The `PhoneNumberUtility` instance used for formatting when encoding, if needed.
75 | var phoneNumberUtility: () -> PhoneNumberUtility {
76 | get {
77 | return userInfo[.phoneNumberUtility] as? () -> PhoneNumberUtility ?? PhoneNumberEncodingUtils.defaultUtility
78 | }
79 | set {
80 | userInfo[.phoneNumberUtility] = newValue
81 | }
82 | }
83 | }
84 |
85 | extension PhoneNumber: Codable {
86 | public init(from decoder: Decoder) throws {
87 | let strategy = decoder.userInfo[.phoneNumberDecodingStrategy] as? PhoneNumberDecodingStrategy ?? .default
88 | switch strategy {
89 | case .properties:
90 | let container = try decoder.container(keyedBy: CodingKeys.self)
91 | try self.init(
92 | numberString: container.decode(String.self, forKey: .numberString),
93 | countryCode: container.decode(UInt64.self, forKey: .countryCode),
94 | leadingZero: container.decode(Bool.self, forKey: .leadingZero),
95 | nationalNumber: container.decode(UInt64.self, forKey: .nationalNumber),
96 | numberExtension: container.decodeIfPresent(String.self, forKey: .numberExtension),
97 | type: container.decode(PhoneNumberType.self, forKey: .type),
98 | regionID: container.decodeIfPresent(String.self, forKey: .regionID)
99 | )
100 | case .e164:
101 | let container = try decoder.singleValueContainer()
102 | let e164String = try container.decode(String.self)
103 | let utility = decoder.userInfo[.phoneNumberUtility] as? () -> PhoneNumberUtility ?? PhoneNumberDecodingUtils.defaultUtility
104 | self = try utility().parse(e164String, ignoreType: true)
105 | }
106 | }
107 |
108 | public func encode(to encoder: Encoder) throws {
109 | let strategy = encoder.userInfo[.phoneNumberEncodingStrategy] as? PhoneNumberEncodingStrategy ?? .default
110 | switch strategy {
111 | case .properties:
112 | var container = encoder.container(keyedBy: CodingKeys.self)
113 | try container.encode(numberString, forKey: .numberString)
114 | try container.encode(countryCode, forKey: .countryCode)
115 | try container.encode(leadingZero, forKey: .leadingZero)
116 | try container.encode(nationalNumber, forKey: .nationalNumber)
117 | try container.encode(numberExtension, forKey: .numberExtension)
118 | try container.encode(type, forKey: .type)
119 | try container.encode(regionID, forKey: .regionID)
120 | case .e164:
121 | var container = encoder.singleValueContainer()
122 | let utility = encoder.userInfo[.phoneNumberUtility] as? () -> PhoneNumberUtility ?? PhoneNumberEncodingUtils.defaultUtility
123 | let e164String = utility().format(self, toType: .e164)
124 | try container.encode(e164String)
125 | }
126 | }
127 |
128 | private enum CodingKeys: String, CodingKey {
129 | case numberString
130 | case countryCode
131 | case leadingZero
132 | case nationalNumber
133 | case numberExtension
134 | case type
135 | case regionID
136 | }
137 | }
138 |
139 | extension CodingUserInfoKey {
140 | static let phoneNumberDecodingStrategy = Self(rawValue: "com.roymarmelstein.PhoneNumberKit.decoding-strategy")!
141 | static let phoneNumberEncodingStrategy = Self(rawValue: "com.roymarmelstein.PhoneNumberKit.encoding-strategy")!
142 |
143 | static let phoneNumberUtility = Self(rawValue: "com.roymarmelstein.PhoneNumberKit.instance")!
144 | }
145 |
--------------------------------------------------------------------------------
/PhoneNumberKit/PhoneNumber.swift:
--------------------------------------------------------------------------------
1 | //
2 | // PhoneNumber.swift
3 | // PhoneNumberKit
4 | //
5 | // Created by Roy Marmelstein on 26/09/2015.
6 | // Copyright © 2021 Roy Marmelstein. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | /// Parsed phone number object
12 | ///
13 | /// - numberString: String used to generate phone number struct
14 | /// - countryCode: Country dialing code as an unsigned. Int.
15 | /// - leadingZero: Some countries (e.g. Italy) require leading zeros. Bool.
16 | /// - nationalNumber: National number as an unsigned. Int.
17 | /// - numberExtension: Extension if available. String. Optional
18 | /// - type: Computed phone number type on access. Returns from an enumeration - PNPhoneNumberType.
19 | public struct PhoneNumber {
20 | public let numberString: String
21 | public let countryCode: UInt64
22 | public let leadingZero: Bool
23 | public let nationalNumber: UInt64
24 | public let numberExtension: String?
25 | public let type: PhoneNumberType
26 | public let regionID: String?
27 | }
28 |
29 | extension PhoneNumber: Equatable {
30 | public static func == (lhs: PhoneNumber, rhs: PhoneNumber) -> Bool {
31 | return (lhs.countryCode == rhs.countryCode)
32 | && (lhs.leadingZero == rhs.leadingZero)
33 | && (lhs.nationalNumber == rhs.nationalNumber)
34 | && (lhs.numberExtension == rhs.numberExtension)
35 | }
36 | }
37 |
38 | extension PhoneNumber: Hashable {
39 | public func hash(into hasher: inout Hasher) {
40 | hasher.combine(self.countryCode)
41 | hasher.combine(self.nationalNumber)
42 | hasher.combine(self.leadingZero)
43 | if let numberExtension = numberExtension {
44 | hasher.combine(numberExtension)
45 | } else {
46 | hasher.combine(0)
47 | }
48 | }
49 | }
50 |
51 | public extension PhoneNumber {
52 | static func notPhoneNumber() -> PhoneNumber {
53 | return PhoneNumber(numberString: "", countryCode: 0, leadingZero: false, nationalNumber: 0, numberExtension: nil, type: .notParsed, regionID: nil)
54 | }
55 |
56 | func notParsed() -> Bool {
57 | return self.type == .notParsed
58 | }
59 |
60 | /// Get a callable URL from the number.
61 | /// - Returns: A callable URL.
62 | var url: URL? {
63 | return URL(string: "tel://" + numberString)
64 | }
65 | }
66 |
67 | /// In past versions of PhoneNumberKit you were able to initialize a PhoneNumber object to parse a String. Please use a PhoneNumberUtility object's methods.
68 | public extension PhoneNumber {
69 | /// DEPRECATED.
70 | /// Parse a string into a phone number object using default region. Can throw.
71 | /// - Parameter rawNumber: String to be parsed to phone number struct.
72 | @available(*, unavailable, message: "use PhoneNumberUtility instead to produce PhoneNumbers")
73 | init(rawNumber: String) throws {
74 | assertionFailure(PhoneNumberError.deprecated.localizedDescription)
75 | throw PhoneNumberError.deprecated
76 | }
77 |
78 | /// DEPRECATED.
79 | /// Parse a string into a phone number object using custom region. Can throw.
80 | /// - Parameter rawNumber: String to be parsed to phone number struct.
81 | /// - Parameter region: ISO 3166 compliant region code.
82 | @available(*, unavailable, message: "use PhoneNumberUtility instead to produce PhoneNumbers")
83 | init(rawNumber: String, region: String) throws {
84 | throw PhoneNumberError.deprecated
85 | }
86 | }
87 |
--------------------------------------------------------------------------------
/PhoneNumberKit/PhoneNumberFormatter.swift:
--------------------------------------------------------------------------------
1 | //
2 | // PhoneNumberFormatter.swift
3 | // PhoneNumberKit
4 | //
5 | // Created by Jean-Daniel.
6 | // Copyright © 2019 Xenonium. All rights reserved.
7 | //
8 |
9 | #if canImport(ObjectiveC)
10 | import Foundation
11 |
12 | open class PhoneNumberFormatter: Foundation.Formatter {
13 | public let utility: PhoneNumberUtility
14 |
15 | private let partialFormatter: PartialFormatter
16 |
17 | // We declare all properties as @objc, so we can configure them though IB (using custom property)
18 | @objc public dynamic
19 | var generatesPhoneNumber = false
20 |
21 | /// Override region to set a custom region. Automatically uses the default region code.
22 | @objc public dynamic
23 | var defaultRegion = PhoneNumberUtility.defaultRegionCode() {
24 | didSet {
25 | self.partialFormatter.defaultRegion = self.defaultRegion
26 | }
27 | }
28 |
29 | @objc public dynamic
30 | var withPrefix: Bool = true {
31 | didSet {
32 | self.partialFormatter.withPrefix = self.withPrefix
33 | }
34 | }
35 |
36 | @objc public dynamic
37 | var currentRegion: String {
38 | return self.partialFormatter.currentRegion
39 | }
40 |
41 | // MARK: Lifecycle
42 |
43 | public init(utility: PhoneNumberUtility = PhoneNumberUtility(), defaultRegion: String = PhoneNumberUtility.defaultRegionCode(), withPrefix: Bool = true) {
44 | self.utility = utility
45 | self.partialFormatter = PartialFormatter(utility: self.utility, defaultRegion: defaultRegion, withPrefix: withPrefix)
46 | super.init()
47 | }
48 |
49 | public required init?(coder aDecoder: NSCoder) {
50 | self.utility = PhoneNumberUtility()
51 | self.partialFormatter = PartialFormatter(utility: self.utility, defaultRegion: self.defaultRegion, withPrefix: self.withPrefix)
52 | super.init(coder: aDecoder)
53 | }
54 | }
55 |
56 | // MARK: -
57 |
58 | // MARK: NSFormatter implementation
59 |
60 | extension PhoneNumberFormatter {
61 | override open func string(for obj: Any?) -> String? {
62 | if let pn = obj as? PhoneNumber {
63 | return self.utility.format(pn, toType: self.withPrefix ? .international : .national)
64 | }
65 | if let str = obj as? String {
66 | return self.partialFormatter.formatPartial(str)
67 | }
68 | return nil
69 | }
70 |
71 | override open func getObjectValue(_ obj: AutoreleasingUnsafeMutablePointer?, for string: String, errorDescription error: AutoreleasingUnsafeMutablePointer?) -> Bool {
72 | if self.generatesPhoneNumber {
73 | do {
74 | obj?.pointee = try self.utility.parse(string) as AnyObject?
75 | return true
76 | } catch let e {
77 | error?.pointee = e.localizedDescription as NSString
78 | return false
79 | }
80 | } else {
81 | obj?.pointee = string as NSString
82 | return true
83 | }
84 | }
85 |
86 | // MARK: Phone number formatting
87 |
88 | /// To keep the cursor position, we find the character immediately after the cursor and count the number of times it repeats in the remaining string as this will remain constant in every kind of editing.
89 | private struct CursorPosition {
90 | let numberAfterCursor: unichar
91 | let repetitionCountFromEnd: Int
92 | }
93 |
94 | private func extractCursorPosition(from text: NSString, selection selectedTextRange: NSRange) -> CursorPosition? {
95 | var repetitionCountFromEnd = 0
96 |
97 | // The selection range is based on NSString representation
98 | var cursorEnd = selectedTextRange.location + selectedTextRange.length
99 |
100 | guard cursorEnd < text.length else {
101 | // Cursor at end of string
102 | return nil
103 | }
104 |
105 | // Get the character after the cursor
106 | var char: unichar
107 | repeat {
108 | char = text.character(at: cursorEnd) // should work even if char is start of compound sequence
109 | cursorEnd += 1
110 | // We consider only digit as other characters may be inserted by the formatter (especially spaces)
111 | } while !char.isDigit() && cursorEnd < text.length
112 |
113 | guard cursorEnd < text.length else {
114 | // Cursor at end of string
115 | return nil
116 | }
117 |
118 | // Look for the next valid number after the cursor, when found return a CursorPosition struct
119 | for i in cursorEnd.. Action {
134 | // If origin range length > 0, this is a delete or replace action
135 | if range.length == 0 {
136 | return .insert
137 | }
138 |
139 | // If proposed length = orig length - orig range length -> this is delete action
140 | if origString.length - range.length == proposedString.length {
141 | return .delete
142 | }
143 | // If proposed length > orig length - orig range length -> this is replace action
144 | return .replace
145 | }
146 |
147 | override open func isPartialStringValid(
148 | _ partialStringPtr: AutoreleasingUnsafeMutablePointer,
149 | proposedSelectedRange proposedSelRangePtr: NSRangePointer?,
150 | originalString origString: String,
151 | originalSelectedRange origSelRange: NSRange,
152 | errorDescription error: AutoreleasingUnsafeMutablePointer?
153 | ) -> Bool {
154 | guard let proposedSelRangePtr = proposedSelRangePtr else {
155 | // I guess this is an annotation issue. I can't see a valid case where the pointer can be null
156 | return true
157 | }
158 |
159 | // We want to allow space deletion or insertion
160 | let orig = origString as NSString
161 | let action = self.action(for: orig, range: origSelRange, proposedString: partialStringPtr.pointee, proposedRange: proposedSelRangePtr.pointee)
162 | if action == .delete && orig.isWhiteSpace(in: origSelRange) {
163 | // Deleting white space
164 | return true
165 | }
166 |
167 | // Also allow to add white space ?
168 | if action == .insert || action == .replace {
169 | // Determine the inserted text range. This is the range starting at orig selection index and with length = ∆length
170 | let length = partialStringPtr.pointee.length - orig.length + origSelRange.length
171 | if partialStringPtr.pointee.isWhiteSpace(in: NSRange(location: origSelRange.location, length: length)) {
172 | return true
173 | }
174 | }
175 |
176 | let text = partialStringPtr.pointee as String
177 | let formattedNationalNumber = self.partialFormatter.formatPartial(text)
178 | guard formattedNationalNumber != text else {
179 | // No change, no need to update the text
180 | return true
181 | }
182 |
183 | // Fix selection
184 |
185 | // The selection range is based on NSString representation
186 | let formattedTextNSString = formattedNationalNumber as NSString
187 | if let cursor = extractCursorPosition(from: partialStringPtr.pointee, selection: proposedSelRangePtr.pointee) {
188 | var remaining = cursor.repetitionCountFromEnd
189 | for i in stride(from: formattedTextNSString.length - 1, through: 0, by: -1) {
190 | if formattedTextNSString.character(at: i) == cursor.numberAfterCursor {
191 | if remaining > 0 {
192 | remaining -= 1
193 | } else {
194 | // We are done
195 | proposedSelRangePtr.pointee = NSRange(location: i, length: 0)
196 | break
197 | }
198 | }
199 | }
200 | } else {
201 | // assume the pointer is at end of string
202 | proposedSelRangePtr.pointee = NSRange(location: formattedTextNSString.length, length: 0)
203 | }
204 |
205 | partialStringPtr.pointee = formattedNationalNumber as NSString
206 | return false
207 | }
208 | }
209 |
210 | private extension NSString {
211 | func isWhiteSpace(in range: NSRange) -> Bool {
212 | return rangeOfCharacter(from: CharacterSet.whitespacesAndNewlines.inverted, options: [.literal], range: range).location == NSNotFound
213 | }
214 | }
215 |
216 | private extension unichar {
217 | func isDigit() -> Bool {
218 | return self >= 0x30 && self <= 0x39 // '0' < '9'
219 | }
220 | }
221 | #endif
222 |
--------------------------------------------------------------------------------
/PhoneNumberKit/PhoneNumberKit.h:
--------------------------------------------------------------------------------
1 | //
2 | // PhoneNumberKit.h
3 | // PhoneNumberKit
4 | //
5 | // Created by Roy Marmelstein on 26/09/2015.
6 | // Copyright © 2021 Roy Marmelstein. All rights reserved.
7 | //
8 |
9 | @import Foundation;
10 |
11 | //! Project version number for PhoneNumberKit.
12 | FOUNDATION_EXPORT double PhoneNumberKitVersionNumber;
13 |
14 | //! Project version string for PhoneNumberKit.
15 | FOUNDATION_EXPORT const unsigned char PhoneNumberKitVersionString[];
16 |
17 | // In this header, you should import all the public headers of your framework using statements like #import
18 |
19 |
20 |
--------------------------------------------------------------------------------
/PhoneNumberKit/PhoneNumberParser.swift:
--------------------------------------------------------------------------------
1 | //
2 | // PhoneNumberParser.swift
3 | // PhoneNumberKit
4 | //
5 | // Created by Roy Marmelstein on 26/09/2015.
6 | // Copyright © 2021 Roy Marmelstein. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | /// Parser. Contains parsing functions.
12 | final class PhoneNumberParser {
13 | let metadata: MetadataManager
14 | let regex: RegexManager
15 |
16 | init(regex: RegexManager, metadata: MetadataManager) {
17 | self.regex = regex
18 | self.metadata = metadata
19 | }
20 |
21 | // MARK: Normalizations
22 |
23 | /// Normalize a phone number (e.g +33 612-345-678 to 33612345678).
24 | /// - Parameter number: Phone number string.
25 | /// - Returns: Normalized phone number string.
26 | func normalizePhoneNumber(_ number: String) -> String {
27 | let normalizationMappings = PhoneNumberPatterns.allNormalizationMappings
28 | return self.regex.stringByReplacingOccurrences(number, map: normalizationMappings)
29 | }
30 |
31 | // MARK: Extractions
32 |
33 | /// Extract country code (e.g +33 612-345-678 to 33).
34 | /// - Parameter number: Number string.
35 | /// - Parameter nationalNumber: National number string - inout.
36 | /// - Parameter metadata: Metadata territory object.
37 | /// - Returns: Country code is UInt64.
38 | func extractCountryCode(_ number: String, nationalNumber: inout String, metadata: MetadataTerritory) throws -> UInt64 {
39 | var fullNumber = number
40 | guard let possibleCountryIddPrefix = metadata.internationalPrefix else {
41 | return 0
42 | }
43 | let countryCodeSource = self.stripInternationalPrefixAndNormalize(&fullNumber, possibleIddPrefix: possibleCountryIddPrefix)
44 | if countryCodeSource != .defaultCountry {
45 | if fullNumber.count <= PhoneNumberConstants.minLengthForNSN {
46 | throw PhoneNumberError.tooShort
47 | }
48 | if let potentialCountryCode = extractPotentialCountryCode(fullNumber, nationalNumber: &nationalNumber), potentialCountryCode != 0 {
49 | return potentialCountryCode
50 | } else {
51 | return 0
52 | }
53 | } else {
54 | let defaultCountryCode = String(metadata.countryCode)
55 | if fullNumber.hasPrefix(defaultCountryCode) {
56 | var potentialNationalNumber = String(fullNumber.dropFirst(defaultCountryCode.count))
57 | guard let validNumberPattern = metadata.generalDesc?.nationalNumberPattern, let possibleNumberPattern = metadata.generalDesc?.possibleNumberPattern else {
58 | return 0
59 | }
60 | self.stripNationalPrefix(&potentialNationalNumber, metadata: metadata)
61 | let potentialNationalNumberStr = potentialNationalNumber
62 | if (!self.regex.matchesEntirely(validNumberPattern, string: fullNumber) && self.regex.matchesEntirely(validNumberPattern, string: potentialNationalNumberStr)) || self.regex.testStringLengthAgainstPattern(possibleNumberPattern, string: fullNumber as String) == false {
63 | nationalNumber = potentialNationalNumberStr
64 | if let countryCode = UInt64(defaultCountryCode) {
65 | return UInt64(countryCode)
66 | }
67 | }
68 | }
69 | }
70 | return 0
71 | }
72 |
73 | /// Extract potential country code (e.g +33 612-345-678 to 33).
74 | /// - Parameter fullNumber: Full number string.
75 | /// - Parameter nationalNumber: National number string.
76 | /// - Returns: Country code is UInt64. Optional.
77 | func extractPotentialCountryCode(_ fullNumber: String, nationalNumber: inout String) -> UInt64? {
78 | let nsFullNumber = fullNumber as NSString
79 | if nsFullNumber.length == 0 || nsFullNumber.substring(to: 1) == "0" {
80 | return 0
81 | }
82 | let numberLength = nsFullNumber.length
83 | let maxCountryCode = PhoneNumberConstants.maxLengthCountryCode
84 | var startPosition = 0
85 | if fullNumber.hasPrefix("+") {
86 | if nsFullNumber.length == 1 {
87 | return 0
88 | }
89 | startPosition = 1
90 | }
91 | for i in 1...min(numberLength - startPosition, maxCountryCode) {
92 | let stringRange = NSRange(location: startPosition, length: i)
93 | let subNumber = nsFullNumber.substring(with: stringRange)
94 | if let potentialCountryCode = UInt64(subNumber), metadata.filterTerritories(byCode: potentialCountryCode) != nil {
95 | nationalNumber = nsFullNumber.substring(from: i)
96 | return potentialCountryCode
97 | }
98 | }
99 | return 0
100 | }
101 |
102 | // MARK: Validations
103 |
104 | func checkNumberType(_ nationalNumber: String, metadata: MetadataTerritory, leadingZero: Bool = false) -> PhoneNumberType {
105 | if leadingZero {
106 | let type = self.checkNumberType("0" + String(nationalNumber), metadata: metadata)
107 | if type != .unknown {
108 | return type
109 | }
110 | }
111 |
112 | guard let generalNumberDesc = metadata.generalDesc else {
113 | return .unknown
114 | }
115 | if self.regex.hasValue(generalNumberDesc.nationalNumberPattern) == false || self.isNumberMatchingDesc(nationalNumber, numberDesc: generalNumberDesc) == false {
116 | return .unknown
117 | }
118 | if self.isNumberMatchingDesc(nationalNumber, numberDesc: metadata.pager) {
119 | return .pager
120 | }
121 | if self.isNumberMatchingDesc(nationalNumber, numberDesc: metadata.premiumRate) {
122 | return .premiumRate
123 | }
124 | if self.isNumberMatchingDesc(nationalNumber, numberDesc: metadata.tollFree) {
125 | return .tollFree
126 | }
127 | if self.isNumberMatchingDesc(nationalNumber, numberDesc: metadata.sharedCost) {
128 | return .sharedCost
129 | }
130 | if self.isNumberMatchingDesc(nationalNumber, numberDesc: metadata.voip) {
131 | return .voip
132 | }
133 | if self.isNumberMatchingDesc(nationalNumber, numberDesc: metadata.personalNumber) {
134 | return .personalNumber
135 | }
136 | if self.isNumberMatchingDesc(nationalNumber, numberDesc: metadata.uan) {
137 | return .uan
138 | }
139 | if self.isNumberMatchingDesc(nationalNumber, numberDesc: metadata.voicemail) {
140 | return .voicemail
141 | }
142 | if self.isNumberMatchingDesc(nationalNumber, numberDesc: metadata.fixedLine) {
143 | if metadata.fixedLine?.nationalNumberPattern == metadata.mobile?.nationalNumberPattern {
144 | return .fixedOrMobile
145 | } else if self.isNumberMatchingDesc(nationalNumber, numberDesc: metadata.mobile) {
146 | return .fixedOrMobile
147 | } else {
148 | return .fixedLine
149 | }
150 | }
151 | if self.isNumberMatchingDesc(nationalNumber, numberDesc: metadata.mobile) {
152 | return .mobile
153 | }
154 | return .unknown
155 | }
156 |
157 | /// Checks if number matches description.
158 | /// - Parameter nationalNumber: National number string.
159 | /// - Parameter numberDesc: MetadataPhoneNumberDesc of a given phone number type.
160 | /// - Returns: True or false.
161 | func isNumberMatchingDesc(_ nationalNumber: String, numberDesc: MetadataPhoneNumberDesc?) -> Bool {
162 | return self.regex.matchesEntirely(numberDesc?.nationalNumberPattern, string: nationalNumber)
163 | }
164 |
165 | /// Checks and strips if prefix is international dialing pattern.
166 | /// - Parameter number: Number string.
167 | /// - Parameter iddPattern: iddPattern for a given country.
168 | /// - Returns: True or false and modifies the number accordingly.
169 | func parsePrefixAsIdd(_ number: inout String, iddPattern: String) -> Bool {
170 | if self.regex.stringPositionByRegex(iddPattern, string: number) == 0 {
171 | do {
172 | guard let matched = try regex.regexMatches(iddPattern as String, string: number as String).first else {
173 | return false
174 | }
175 | let matchedString = number.substring(with: matched.range)
176 | let matchEnd = matchedString.count
177 | let remainString = (number as NSString).substring(from: matchEnd)
178 | let capturingDigitPatterns = try NSRegularExpression(pattern: PhoneNumberPatterns.capturingDigitPattern, options: NSRegularExpression.Options.caseInsensitive)
179 | let matchedGroups = capturingDigitPatterns.matches(in: remainString as String)
180 | if let firstMatch = matchedGroups.first {
181 | let digitMatched = remainString.substring(with: firstMatch.range) as NSString
182 | if digitMatched.length > 0 {
183 | let normalizedGroup = self.regex.stringByReplacingOccurrences(digitMatched as String, map: PhoneNumberPatterns.allNormalizationMappings)
184 | if normalizedGroup == "0" {
185 | return false
186 | }
187 | }
188 | }
189 | number = remainString as String
190 | return true
191 | } catch {
192 | return false
193 | }
194 | }
195 | return false
196 | }
197 |
198 | // MARK: Strip helpers
199 |
200 | /// Strip an extension (e.g +33 612-345-678 ext.89 to 89).
201 | /// - Parameter number: Number string.
202 | /// - Returns: Modified number without extension and optional extension as string.
203 | func stripExtension(_ number: inout String) -> String? {
204 | do {
205 | let matches = try regex.regexMatches(PhoneNumberPatterns.extnPattern, string: number)
206 | if let match = matches.first {
207 | let adjustedRange = NSRange(location: match.range.location + 1, length: match.range.length - 1)
208 | let matchString = number.substring(with: adjustedRange)
209 | let stringRange = NSRange(location: 0, length: match.range.location)
210 | number = number.substring(with: stringRange)
211 | return matchString
212 | }
213 | return nil
214 | } catch {
215 | return nil
216 | }
217 | }
218 |
219 | /// Strip international prefix.
220 | /// - Parameter number: Number string.
221 | /// - Parameter possibleIddPrefix: Possible idd prefix for a given country.
222 | /// - Returns: Modified normalized number without international prefix and a PNCountryCodeSource enumeration.
223 | func stripInternationalPrefixAndNormalize(_ number: inout String, possibleIddPrefix: String?) -> PhoneNumberCountryCodeSource {
224 | if self.regex.matchesAtStart(PhoneNumberPatterns.leadingPlusCharsPattern, string: number as String) {
225 | number = self.regex.replaceStringByRegex(PhoneNumberPatterns.leadingPlusCharsPattern, string: number as String)
226 | return .numberWithPlusSign
227 | }
228 | number = self.normalizePhoneNumber(number as String)
229 | guard let possibleIddPrefix = possibleIddPrefix else {
230 | return .numberWithoutPlusSign
231 | }
232 | let prefixResult = self.parsePrefixAsIdd(&number, iddPattern: possibleIddPrefix)
233 | if prefixResult == true {
234 | return .numberWithIDD
235 | } else {
236 | return .defaultCountry
237 | }
238 | }
239 |
240 | /// Strip national prefix.
241 | /// - Parameter number: Number string.
242 | /// - Parameter metadata: Final country's metadata.
243 | /// - Returns: Modified number without national prefix.
244 | func stripNationalPrefix(_ number: inout String, metadata: MetadataTerritory) {
245 | guard let possibleNationalPrefix = metadata.nationalPrefixForParsing else {
246 | return
247 | }
248 | #if canImport(ObjectiveC)
249 | let prefixPattern = String(format: "^(?:%@)", possibleNationalPrefix)
250 | #else
251 | // FIX: String format with %@ doesn't work without ObjectiveC (e.g. Linux)
252 | let prefixPattern = "^(?:\(possibleNationalPrefix))"
253 | #endif
254 | do {
255 | let matches = try regex.regexMatches(prefixPattern, string: number)
256 | if let firstMatch = matches.first {
257 | let nationalNumberRule = metadata.generalDesc?.nationalNumberPattern
258 | let firstMatchString = number.substring(with: firstMatch.range)
259 | let numOfGroups = firstMatch.numberOfRanges - 1
260 | var transformedNumber = String()
261 | let firstRange = firstMatch.range(at: numOfGroups)
262 | let firstMatchStringWithGroup = (firstRange.location != NSNotFound && firstRange.location < number.count) ? number.substring(with: firstRange) : String()
263 | let firstMatchStringWithGroupHasValue = self.regex.hasValue(firstMatchStringWithGroup)
264 | if let transformRule = metadata.nationalPrefixTransformRule, firstMatchStringWithGroupHasValue == true {
265 | transformedNumber = self.regex.replaceFirstStringByRegex(prefixPattern, string: number, templateString: transformRule)
266 | } else {
267 | let index = number.index(number.startIndex, offsetBy: firstMatchString.count)
268 | transformedNumber = String(number[index...])
269 | }
270 | if self.regex.hasValue(nationalNumberRule), self.regex.matchesEntirely(nationalNumberRule, string: number), self.regex.matchesEntirely(nationalNumberRule, string: transformedNumber) == false {
271 | return
272 | }
273 | number = transformedNumber
274 | return
275 | }
276 | } catch {
277 | return
278 | }
279 | }
280 | }
281 |
--------------------------------------------------------------------------------
/PhoneNumberKit/RegexManager.swift:
--------------------------------------------------------------------------------
1 | //
2 | // RegexManager.swift
3 | // PhoneNumberKit
4 | //
5 | // Created by Roy Marmelstein on 04/10/2015.
6 | // Copyright © 2021 Roy Marmelstein. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | final class RegexManager {
12 | public init() {
13 | var characterSet = CharacterSet(charactersIn: PhoneNumberConstants.nonBreakingSpace)
14 | characterSet.formUnion(.whitespacesAndNewlines)
15 | spaceCharacterSet = characterSet
16 | }
17 |
18 | // MARK: Regular expression pool
19 |
20 | var regularExpressionPool = [String: NSRegularExpression]()
21 |
22 | private let regularExpressionPoolQueue = DispatchQueue(label: "com.phonenumberkit.regexpool", target: .global())
23 |
24 | var spaceCharacterSet: CharacterSet
25 |
26 | // MARK: Regular expression
27 |
28 | func regexWithPattern(_ pattern: String) throws -> NSRegularExpression {
29 | var cached: NSRegularExpression?
30 | cached = regularExpressionPoolQueue.sync {
31 | regularExpressionPool[pattern]
32 | }
33 |
34 | if let cached {
35 | return cached
36 | }
37 |
38 | do {
39 | let regex = try NSRegularExpression(pattern: pattern, options: .caseInsensitive)
40 |
41 | regularExpressionPoolQueue.sync {
42 | regularExpressionPool[pattern] = regex
43 | }
44 |
45 | return regex
46 | } catch {
47 | throw PhoneNumberError.generalError
48 | }
49 | }
50 |
51 | func regexMatches(_ pattern: String, string: String) throws -> [NSTextCheckingResult] {
52 | do {
53 | let internalString = string
54 | let currentPattern = try regexWithPattern(pattern)
55 | let matches = currentPattern.matches(in: internalString)
56 | return matches
57 | } catch {
58 | throw PhoneNumberError.generalError
59 | }
60 | }
61 |
62 | func phoneDataDetectorMatch(_ string: String) throws -> NSTextCheckingResult {
63 | let fallBackMatches = try regexMatches(PhoneNumberPatterns.validPhoneNumberPattern, string: string)
64 | if let firstMatch = fallBackMatches.first {
65 | return firstMatch
66 | } else {
67 | throw PhoneNumberError.invalidNumber
68 | }
69 | }
70 |
71 | // MARK: Match helpers
72 |
73 | func matchesAtStart(_ pattern: String, string: String) -> Bool {
74 | guard
75 | let matches = try? regexMatches(pattern, string: string),
76 | matches.first(where: { $0.range.location == 0 }) != nil else {
77 | return false
78 | }
79 | return true
80 | }
81 |
82 | func stringPositionByRegex(_ pattern: String, string: String) -> Int {
83 | do {
84 | let matches = try regexMatches(pattern, string: string)
85 | if let match = matches.first {
86 | return match.range.location
87 | }
88 | return -1
89 | } catch {
90 | return -1
91 | }
92 | }
93 |
94 | func matchesExist(_ pattern: String?, string: String) -> Bool {
95 | guard let pattern else {
96 | return false
97 | }
98 | do {
99 | let matches = try regexMatches(pattern, string: string)
100 | return !matches.isEmpty
101 | } catch {
102 | return false
103 | }
104 | }
105 |
106 | func matchesEntirely(_ pattern: String?, string: String) -> Bool {
107 | guard var pattern else {
108 | return false
109 | }
110 | pattern = "^(\(pattern))$"
111 | return matchesExist(pattern, string: string)
112 | }
113 |
114 | func matchedStringByRegex(_ pattern: String, string: String) throws -> [String] {
115 | guard let matches = try? regexMatches(pattern, string: string) else {
116 | return []
117 | }
118 | return matches.map { string.substring(with: $0.range) }
119 | }
120 |
121 | // MARK: String and replace
122 |
123 | func replaceStringByRegex(_ pattern: String, string: String, template: String = "") -> String {
124 | do {
125 | var replacementResult = string
126 | let regex = try regexWithPattern(pattern)
127 | let matches = regex.matches(in: string)
128 | if matches.count == 1 {
129 | let range = regex.rangeOfFirstMatch(in: string)
130 | if range != nil {
131 | replacementResult = regex.stringByReplacingMatches(
132 | in: string,
133 | options: [],
134 | range: range,
135 | withTemplate: template
136 | )
137 | }
138 | return replacementResult
139 | } else if matches.count > 1 {
140 | replacementResult = regex.stringByReplacingMatches(in: string, withTemplate: template)
141 | }
142 | return replacementResult
143 | } catch {
144 | return string
145 | }
146 | }
147 |
148 | func replaceFirstStringByRegex(_ pattern: String, string: String, templateString: String) -> String {
149 | do {
150 | let regex = try regexWithPattern(pattern)
151 | let range = regex.rangeOfFirstMatch(in: string)
152 | if range != nil {
153 | return regex.stringByReplacingMatches(
154 | in: string,
155 | options: [],
156 | range: range,
157 | withTemplate: templateString
158 | )
159 | }
160 | return string
161 | } catch {
162 | return String()
163 | }
164 | }
165 |
166 | func stringByReplacingOccurrences(_ string: String, map: [String: String], keepUnmapped: Bool = false) -> String {
167 | var targetString = String()
168 | for i in 0.. Bool {
183 | if let valueString = value {
184 | if valueString.trimmingCharacters(in: spaceCharacterSet).isEmpty {
185 | return false
186 | }
187 | return true
188 | } else {
189 | return false
190 | }
191 | }
192 |
193 | func testStringLengthAgainstPattern(_ pattern: String, string: String) -> Bool {
194 | if matchesEntirely(pattern, string: string) {
195 | return true
196 | } else {
197 | return false
198 | }
199 | }
200 | }
201 |
202 | // MARK: Extensions
203 |
204 | extension String {
205 | func substring(with range: NSRange) -> String {
206 | let nsString = self as NSString
207 | return nsString.substring(with: range)
208 | }
209 | }
210 |
--------------------------------------------------------------------------------
/PhoneNumberKit/Resources/.metadata-version:
--------------------------------------------------------------------------------
1 | 9.0.6
2 |
--------------------------------------------------------------------------------
/PhoneNumberKit/Resources/PrivacyInfo.xcprivacy:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | NSPrivacyTracking
6 |
7 | NSPrivacyTrackingDomains
8 |
9 | NSPrivacyCollectedDataTypes
10 |
11 | NSPrivacyAccessedAPITypes
12 |
13 |
14 |
--------------------------------------------------------------------------------
/PhoneNumberKit/Resources/README.md:
--------------------------------------------------------------------------------
1 | # Metadata
2 | PhoneNumberKit is using metadata from Google's libphonenumber.
3 |
4 | The metadata exists in PhoneNumberMetadata.json and the original XML can be found at [Original/PhoneNumberMetadata.xml](https://github.com/marmelroy/PhoneNumberKit/blob/master/PhoneNumberKit/Resources/Original/PhoneNumberMetadata.xml)
5 |
6 | ## Updating the metadata
7 |
8 | We try to keep the metadata of PhoneNumberKit up to date and making sure you are running on the latest release will be sufficient for most apps
9 |
10 | However, you can also update the metadata youself by following these steps:
11 | 1. Download a newer version of the XML metadata file from [libPhoneNumber](https://github.com/googlei18n/libphonenumber/blob/master/resources/)
12 | 2. Replace the XML file in your PhoneNumberKit projects.
13 | 3. Run
14 | ```bash
15 | ./update.sh
16 | ```
17 |
18 | You will need a python library called 'xmljson' installed. You can install it with pip
19 | ```bash
20 | pip install xmljson
21 | ```
--------------------------------------------------------------------------------
/PhoneNumberKit/Resources/update_metadata.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | get_version () {
4 | # Regex to find the version number, assumes semantic versioning
5 | grep -Eo '[0-9]+\.[0-9]+\.[0-9]+' |
6 | # Take the first match in the regex
7 | head -1 || echo '0.0.0'
8 | }
9 |
10 | latest_release_number () {
11 | # Github cli to get the latest release
12 | gh release list --repo $1 --limit 1 | get_version
13 | }
14 |
15 | current_metadata_version () {
16 | cat .metadata-version | get_version
17 | }
18 |
19 | create_scratch () {
20 | # Create temporary directory
21 | scratch=$(mktemp -d -t TemporaryDirectory)
22 | if [[ $debug ]]; then open $scratch; fi
23 | # Run cleanup on exit
24 | trap "if [[ \$debug ]]; then read -p \"\"; fi; rm -rf \"$scratch\"" EXIT
25 | }
26 |
27 | commit_changes() {
28 | branch="$1"
29 | git checkout -b $branch
30 | git add .
31 | git commit -m "Updated metadata to version $branch"
32 | git push -u origin $branch
33 | gh pr create --fill
34 | }
35 |
36 | # Exit when any command fails
37 | set -e
38 | set -o pipefail
39 |
40 | # Repos
41 | libphonenumber_repo="https://github.com/google/libphonenumber"
42 | phonenumberkit_repo="https://github.com/marmelroy/PhoneNumberKit"
43 |
44 | # Release versions
45 | latest=$(latest_release_number $libphonenumber_repo)
46 | current=$(current_metadata_version)
47 |
48 | # Args
49 | debug=$(echo $@ || "" | grep debug)
50 | skip_release=$(echo $@ || "" | grep skip-release)
51 |
52 | if [[ $latest != $current ]]; then
53 | echo "$current is out of date. Updating to $latest..."
54 |
55 | create_scratch
56 | (
57 | cd $scratch
58 | home=$OLDPWD
59 | echo "Downloading latest release..."
60 | gh release download --archive zip --repo $libphonenumber_repo
61 | echo "Unzipping..."
62 | lib_name="libphonenumber"
63 | unzip -q *.zip
64 | for _dir in *"${lib_name}"*; do
65 | [ -d "${_dir}" ] && dir="${_dir}" && break
66 | done
67 | echo "Copying original metadata..."
68 | cp -r "$scratch/$dir/resources/PhoneNumberMetadata.xml" "$home/Original/"
69 | cd $home
70 | )
71 |
72 | echo "Generating JSON file..."
73 | python -m xmljson -o PhoneNumberMetadataTemp.json -d yahoo "$(pwd)/Original/PhoneNumberMetadata.xml"
74 | cat PhoneNumberMetadataTemp.json | sed 's/\\n//g' | sed 's/ \{3,\}//g' | sed 's/ //g' | tr -d "\n" > PhoneNumberMetadata.json
75 | rm PhoneNumberMetadataTemp.json
76 |
77 | echo "Updating version file..."
78 | echo $latest > .metadata-version
79 |
80 | echo "Testing new metadata..."
81 | cd ../..
82 | swift test
83 | rm -rf .build
84 |
85 | # Skips deploy
86 | if [[ $skip_release ]]; then echo "Done"; exit 0; fi
87 |
88 | # Commit, push and create PR
89 | echo "Merging changes to Github..."
90 | commit_changes "metadata/$latest"
91 |
92 | else
93 | echo "$current is up to date."
94 | fi
95 |
96 | echo "Done"
--------------------------------------------------------------------------------
/PhoneNumberKit/UI/CountryCodePickerOptions.swift:
--------------------------------------------------------------------------------
1 | //
2 | // CountryCodePickerOptions.swift
3 | // PhoneNumberKit
4 | //
5 | // Created by Joao Vitor Molinari on 19/09/23.
6 | // Copyright © 2021 Roy Marmelstein. All rights reserved.
7 | //
8 |
9 | #if os(iOS)
10 | import UIKit
11 |
12 | /// CountryCodePickerOptions object
13 | /// - Parameter backgroundColor: UIColor used for background
14 | /// - Parameter separatorColor: UIColor used for the separator line between cells
15 | /// - Parameter textLabelColor: UIColor for the TextLabel (Country code)
16 | /// - Parameter textLabelFont: UIFont for the TextLabel (Country code)
17 | /// - Parameter detailTextLabelColor: UIColor for the DetailTextLabel (Country name)
18 | /// - Parameter detailTextLabelFont: UIFont for the DetailTextLabel (Country name)
19 | /// - Parameter tintColor: Default TintColor used on the view
20 | /// - Parameter cellBackgroundColor: UIColor for the cell background
21 | /// - Parameter cellBackgroundColorSelection: UIColor for the cell selectedBackgroundView
22 | public struct CountryCodePickerOptions {
23 | public init() { }
24 |
25 | public init(backgroundColor: UIColor? = nil,
26 | separatorColor: UIColor? = nil,
27 | textLabelColor: UIColor? = nil,
28 | textLabelFont: UIFont? = nil,
29 | detailTextLabelColor: UIColor? = nil,
30 | detailTextLabelFont: UIFont? = nil,
31 | tintColor: UIColor? = nil,
32 | cellBackgroundColor: UIColor? = nil,
33 | cellBackgroundColorSelection: UIColor? = nil) {
34 | self.backgroundColor = backgroundColor
35 | self.separatorColor = separatorColor
36 | self.textLabelColor = textLabelColor
37 | self.textLabelFont = textLabelFont
38 | self.detailTextLabelColor = detailTextLabelColor
39 | self.detailTextLabelFont = detailTextLabelFont
40 | self.tintColor = tintColor
41 | self.cellBackgroundColor = cellBackgroundColor
42 | self.cellBackgroundColorSelection = cellBackgroundColorSelection
43 | }
44 |
45 | public var backgroundColor: UIColor?
46 | public var separatorColor: UIColor?
47 | public var textLabelColor: UIColor?
48 | public var textLabelFont: UIFont?
49 | public var detailTextLabelColor: UIColor?
50 | public var detailTextLabelFont: UIFont?
51 | public var tintColor: UIColor?
52 | public var cellBackgroundColor: UIColor?
53 | public var cellBackgroundColorSelection: UIColor?
54 | }
55 | #endif
56 |
--------------------------------------------------------------------------------
/PhoneNumberKit/UI/CountryCodePickerViewController.swift:
--------------------------------------------------------------------------------
1 | #if os(iOS)
2 |
3 | import UIKit
4 |
5 | public protocol CountryCodePickerDelegate: AnyObject {
6 | func countryCodePickerViewControllerDidPickCountry(_ country: CountryCodePickerViewController.Country)
7 | }
8 |
9 | public class CountryCodePickerViewController: UITableViewController {
10 | lazy var searchController: UISearchController = {
11 | let searchController = UISearchController(searchResultsController: nil)
12 | searchController.searchBar.placeholder = NSLocalizedString(
13 | "PhoneNumberKit.CountryCodePicker.SearchBarPlaceholder",
14 | value: "Search Country Codes",
15 | comment: "Placeholder for country code search field")
16 |
17 | return searchController
18 | }()
19 |
20 | public let utility: PhoneNumberUtility
21 |
22 | public let options: CountryCodePickerOptions
23 |
24 | let commonCountryCodes: [String]
25 |
26 | var shouldRestoreNavigationBarToHidden = false
27 |
28 | var hasCurrent = true
29 | var hasCommon = true
30 |
31 | lazy var allCountries = utility
32 | .allCountries()
33 | .compactMap({ Country(for: $0, with: self.utility) })
34 | .sorted(by: { $0.name.localizedCaseInsensitiveCompare($1.name) == .orderedAscending })
35 |
36 | lazy var countries: [[Country]] = {
37 | let countries = allCountries
38 | .reduce([[Country]]()) { collection, country in
39 | var collection = collection
40 | guard var lastGroup = collection.last else { return [[country]] }
41 | let lhs = lastGroup.first?.name.folding(options: .diacriticInsensitive, locale: nil)
42 | let rhs = country.name.folding(options: .diacriticInsensitive, locale: nil)
43 | if lhs?.first == rhs.first {
44 | lastGroup.append(country)
45 | collection[collection.count - 1] = lastGroup
46 | } else {
47 | collection.append([country])
48 | }
49 | return collection
50 | }
51 |
52 | let popular = commonCountryCodes.compactMap({ Country(for: $0, with: utility) })
53 |
54 | var result: [[Country]] = []
55 | // Note we should maybe use the user's current carrier's country code?
56 | if hasCurrent, let current = Country(for: PhoneNumberUtility.defaultRegionCode(), with: utility) {
57 | result.append([current])
58 | }
59 | hasCommon = hasCommon && !popular.isEmpty
60 | if hasCommon {
61 | result.append(popular)
62 | }
63 | return result + countries
64 | }()
65 |
66 | var filteredCountries: [Country] = []
67 |
68 | public weak var delegate: CountryCodePickerDelegate?
69 |
70 | lazy var cancelButton = UIBarButtonItem(barButtonSystemItem: .cancel, target: self, action: #selector(dismissAnimated))
71 |
72 | /// Init with a phone number kit instance. Because a `PhoneNumberUtility` initialization is expensive you can must pass a pre-initialized instance to avoid incurring perf penalties.
73 | ///
74 | /// - parameter utility: A `PhoneNumberUtility` instance to be used by the text field.
75 | /// - parameter commonCountryCodes: An array of country codes to display in the section below the current region section. defaults to `PhoneNumberUtility.CountryCodePicker.commonCountryCodes`
76 | public init(
77 | utility: PhoneNumberUtility,
78 | options: CountryCodePickerOptions?,
79 | commonCountryCodes: [String] = CountryCodePicker.commonCountryCodes) {
80 | self.utility = utility
81 | self.commonCountryCodes = commonCountryCodes
82 | self.options = options ?? CountryCodePickerOptions()
83 | super.init(style: .grouped)
84 | self.commonInit()
85 | }
86 |
87 | required init?(coder aDecoder: NSCoder) {
88 | self.utility = PhoneNumberUtility()
89 | self.commonCountryCodes = CountryCodePicker.commonCountryCodes
90 | self.options = CountryCodePickerOptions()
91 | super.init(coder: aDecoder)
92 | self.commonInit()
93 | }
94 |
95 | func commonInit() {
96 | self.title = NSLocalizedString("PhoneNumberKit.CountryCodePicker.Title", value: "Choose your country", comment: "Title of CountryCodePicker ViewController")
97 |
98 | tableView.register(Cell.self, forCellReuseIdentifier: Cell.reuseIdentifier)
99 | searchController.searchResultsUpdater = self
100 | searchController.obscuresBackgroundDuringPresentation = false
101 | searchController.searchBar.backgroundColor = .clear
102 |
103 | navigationItem.searchController = searchController
104 | navigationItem.hidesSearchBarWhenScrolling = !CountryCodePicker.alwaysShowsSearchBar
105 |
106 | definesPresentationContext = true
107 |
108 | if let tintColor = options.tintColor {
109 | view.tintColor = tintColor
110 | navigationController?.navigationBar.tintColor = tintColor
111 | }
112 |
113 | if let backgroundColor = options.backgroundColor {
114 | tableView.backgroundColor = backgroundColor
115 | }
116 |
117 | if let separator = options.separatorColor {
118 | tableView.separatorColor = separator
119 | }
120 | }
121 |
122 | override public func viewWillAppear(_ animated: Bool) {
123 | super.viewWillAppear(animated)
124 | if let nav = navigationController {
125 | shouldRestoreNavigationBarToHidden = nav.isNavigationBarHidden
126 | nav.setNavigationBarHidden(false, animated: true)
127 | }
128 | if let nav = navigationController, nav.isBeingPresented, nav.viewControllers.count == 1 {
129 | navigationItem.setRightBarButton(cancelButton, animated: true)
130 | }
131 | }
132 |
133 | override public func viewWillDisappear(_ animated: Bool) {
134 | super.viewWillDisappear(animated)
135 | navigationController?.setNavigationBarHidden(shouldRestoreNavigationBarToHidden, animated: true)
136 | }
137 |
138 | @objc func dismissAnimated() {
139 | dismiss(animated: true)
140 | }
141 |
142 | func country(for indexPath: IndexPath) -> Country {
143 | isFiltering ? filteredCountries[indexPath.row] : countries[indexPath.section][indexPath.row]
144 | }
145 |
146 | override public func numberOfSections(in tableView: UITableView) -> Int {
147 | isFiltering ? 1 : countries.count
148 | }
149 |
150 | override public func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
151 | isFiltering ? filteredCountries.count : countries[section].count
152 | }
153 |
154 | override public func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
155 | let cell = tableView.dequeueReusableCell(withIdentifier: Cell.reuseIdentifier, for: indexPath)
156 | let country = self.country(for: indexPath)
157 |
158 | if let cellBackgroundColor = options.cellBackgroundColor {
159 | cell.backgroundColor = cellBackgroundColor
160 | }
161 |
162 | cell.textLabel?.text = country.prefix + " " + country.flag
163 |
164 | if let textLabelColor = options.textLabelColor {
165 | cell.textLabel?.textColor = textLabelColor
166 | }
167 |
168 | if let detailTextLabelColor = options.detailTextLabelColor {
169 | cell.detailTextLabel?.textColor = detailTextLabelColor
170 | }
171 |
172 | cell.detailTextLabel?.text = country.name
173 |
174 | if let textLabelFont = options.textLabelFont {
175 | cell.textLabel?.font = textLabelFont
176 | }
177 |
178 | if let detailTextLabelFont = options.detailTextLabelFont {
179 | cell.detailTextLabel?.font = detailTextLabelFont
180 | }
181 |
182 | if let cellBackgroundColorSelection = options.cellBackgroundColorSelection {
183 | let view = UIView()
184 | view.backgroundColor = cellBackgroundColorSelection
185 | cell.selectedBackgroundView = view
186 | }
187 |
188 | return cell
189 | }
190 |
191 | override public func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
192 | if isFiltering {
193 | return nil
194 | } else if section == 0, hasCurrent {
195 | return NSLocalizedString("PhoneNumberKit.CountryCodePicker.Current", value: "Current", comment: "Name of \"Current\" section")
196 | } else if section == 0, !hasCurrent, hasCommon {
197 | return NSLocalizedString("PhoneNumberKit.CountryCodePicker.Common", value: "Common", comment: "Name of \"Common\" section")
198 | } else if section == 1, hasCurrent, hasCommon {
199 | return NSLocalizedString("PhoneNumberKit.CountryCodePicker.Common", value: "Common", comment: "Name of \"Common\" section")
200 | }
201 | return countries[section].first?.name.first.map(String.init)
202 | }
203 |
204 | override public func sectionIndexTitles(for tableView: UITableView) -> [String]? {
205 | guard !isFiltering else {
206 | return nil
207 | }
208 | var titles: [String] = []
209 | if hasCurrent {
210 | titles.append("•") // NOTE: SFSymbols are not supported otherwise we would use
211 | }
212 | if hasCommon {
213 | titles.append("★") // This is a classic unicode star
214 | }
215 | return titles + countries.suffix(countries.count - titles.count).map { group in
216 | group.first?.name.first
217 | .map(String.init)?
218 | .folding(options: .diacriticInsensitive, locale: nil) ?? ""
219 | }
220 | }
221 |
222 | override public func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
223 | let country = self.country(for: indexPath)
224 | delegate?.countryCodePickerViewControllerDidPickCountry(country)
225 | tableView.deselectRow(at: indexPath, animated: true)
226 | }
227 | }
228 |
229 | extension CountryCodePickerViewController: UISearchResultsUpdating {
230 | var isFiltering: Bool {
231 | searchController.isActive && !isSearchBarEmpty
232 | }
233 |
234 | var isSearchBarEmpty: Bool {
235 | searchController.searchBar.text?.isEmpty ?? true
236 | }
237 |
238 | public func updateSearchResults(for searchController: UISearchController) {
239 | let searchText = searchController.searchBar.text ?? ""
240 | filteredCountries = allCountries.filter { country in
241 | country.name.lowercased().contains(searchText.lowercased()) ||
242 | country.code.lowercased().contains(searchText.lowercased()) ||
243 | country.prefix.lowercased().contains(searchText.lowercased())
244 | }
245 | tableView.reloadData()
246 | }
247 | }
248 |
249 | // MARK: Types
250 |
251 | public extension CountryCodePickerViewController {
252 | struct Country {
253 | public var code: String
254 | public var flag: String
255 | public var name: String
256 | public var prefix: String
257 |
258 | public init?(for countryCode: String, with utility: PhoneNumberUtility) {
259 | let flagBase = UnicodeScalar("🇦").value - UnicodeScalar("A").value
260 | guard
261 | let name = (Locale.current as NSLocale).localizedString(forCountryCode: countryCode),
262 | let prefix = utility.countryCode(for: countryCode)?.description
263 | else {
264 | return nil
265 | }
266 |
267 | self.code = countryCode
268 | self.name = name
269 | self.prefix = "+" + prefix
270 | self.flag = ""
271 | countryCode.uppercased().unicodeScalars.forEach {
272 | if let scaler = UnicodeScalar(flagBase + $0.value) {
273 | flag.append(String(describing: scaler))
274 | }
275 | }
276 | if flag.count != 1 { // Failed to initialize a flag ... use an empty string
277 | return nil
278 | }
279 | }
280 | }
281 |
282 | class Cell: UITableViewCell {
283 | static let reuseIdentifier = "Cell"
284 |
285 | override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
286 | super.init(style: .value2, reuseIdentifier: Self.reuseIdentifier)
287 | }
288 |
289 | @available(*, unavailable)
290 | required init?(coder: NSCoder) {
291 | fatalError("init(coder:) has not been implemented")
292 | }
293 | }
294 | }
295 |
296 | #endif
297 |
--------------------------------------------------------------------------------
/PhoneNumberKitTests/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | en
7 | CFBundleExecutable
8 | $(EXECUTABLE_NAME)
9 | CFBundleIdentifier
10 | $(PRODUCT_BUNDLE_IDENTIFIER)
11 | CFBundleInfoDictionaryVersion
12 | 6.0
13 | CFBundleName
14 | $(PRODUCT_NAME)
15 | CFBundlePackageType
16 | BNDL
17 | CFBundleShortVersionString
18 | 3.4.2
19 | CFBundleSignature
20 | ????
21 | CFBundleVersion
22 | 27
23 |
24 |
25 |
--------------------------------------------------------------------------------
/PhoneNumberKitTests/PhoneNumber+CodableTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // PhoneNumber+CodableTests.swift
3 | // PhoneNumberKitTests
4 | //
5 | // Created by David Roman on 16/11/2021.
6 | // Copyright © 2021 Roy Marmelstein. All rights reserved.
7 | //
8 |
9 | import PhoneNumberKit
10 | import XCTest
11 |
12 | final class PhoneNumberCodableTests: XCTestCase {
13 | private var utility: PhoneNumberUtility!
14 |
15 | override func setUp() {
16 | super.setUp()
17 | utility = PhoneNumberUtility()
18 | }
19 |
20 | override func tearDown() {
21 | utility = nil
22 | super.tearDown()
23 | }
24 | }
25 |
26 | extension PhoneNumberCodableTests {
27 | func testDecode_defaultStrategy() throws {
28 | try assertDecode(
29 | """
30 | {
31 | "countryCode" : 44,
32 | "leadingZero" : false,
33 | "nationalNumber" : 1632960015,
34 | "numberExtension" : null,
35 | "numberString" : "+441632960015",
36 | "regionID" : "GB",
37 | "type" : "unknown"
38 | }
39 | """,
40 | utility.parse("+441632960015", ignoreType: true),
41 | strategy: nil
42 | )
43 | try assertDecode(
44 | """
45 | {
46 | "countryCode" : 34,
47 | "leadingZero" : false,
48 | "nationalNumber" : 646990213,
49 | "numberExtension" : null,
50 | "numberString" : "+34646990213",
51 | "regionID" : "ES",
52 | "type" : "unknown"
53 | }
54 | """,
55 | utility.parse("+34646990213", ignoreType: true),
56 | strategy: nil
57 | )
58 | }
59 |
60 | func testDecode_propertiesStrategy() throws {
61 | try assertDecode(
62 | """
63 | {
64 | "countryCode" : 44,
65 | "leadingZero" : false,
66 | "nationalNumber" : 1632960015,
67 | "numberExtension" : null,
68 | "numberString" : "+441632960015",
69 | "regionID" : "GB",
70 | "type" : "unknown"
71 | }
72 | """,
73 | utility.parse("+441632960015", ignoreType: true),
74 | strategy: .properties
75 | )
76 | try assertDecode(
77 | """
78 | {
79 | "countryCode" : 34,
80 | "leadingZero" : false,
81 | "nationalNumber" : 646990213,
82 | "numberExtension" : null,
83 | "numberString" : "+34646990213",
84 | "regionID" : "ES",
85 | "type" : "unknown"
86 | }
87 | """,
88 | utility.parse("+34646990213", ignoreType: true),
89 | strategy: .properties
90 | )
91 | }
92 |
93 | func testDecode_e164Strategy() throws {
94 | try assertDecode(
95 | """
96 | "+441632960015"
97 | """,
98 | utility.parse("+441632960015", ignoreType: true),
99 | strategy: .e164
100 | )
101 | try assertDecode(
102 | """
103 | "+441632960015"
104 | """,
105 | utility.parse("01632960015", withRegion: "GB", ignoreType: true),
106 | strategy: .e164
107 | )
108 | try assertDecode(
109 | """
110 | "+34646990213"
111 | """,
112 | utility.parse("+34646990213", ignoreType: true),
113 | strategy: .e164
114 | )
115 | try assertDecode(
116 | """
117 | "+34646990213"
118 | """,
119 | utility.parse("646990213", withRegion: "ES", ignoreType: true),
120 | strategy: .e164
121 | )
122 | }
123 | }
124 |
125 | extension PhoneNumberCodableTests {
126 | func testEncode_defaultStrategy() throws {
127 | try assertEncode(
128 | utility.parse("+441632960015", ignoreType: true),
129 | """
130 | {
131 | "countryCode" : 44,
132 | "leadingZero" : false,
133 | "nationalNumber" : 1632960015,
134 | "numberExtension" : null,
135 | "numberString" : "+441632960015",
136 | "regionID" : "GB",
137 | "type" : "unknown"
138 | }
139 | """,
140 | strategy: nil
141 | )
142 | try assertEncode(
143 | utility.parse("+34646990213", ignoreType: true),
144 | """
145 | {
146 | "countryCode" : 34,
147 | "leadingZero" : false,
148 | "nationalNumber" : 646990213,
149 | "numberExtension" : null,
150 | "numberString" : "+34646990213",
151 | "regionID" : "ES",
152 | "type" : "unknown"
153 | }
154 | """,
155 | strategy: nil
156 | )
157 | }
158 |
159 | func testEncode_propertiesStrategy() throws {
160 | try assertEncode(
161 | utility.parse("+441632960015", ignoreType: true),
162 | """
163 | {
164 | "countryCode" : 44,
165 | "leadingZero" : false,
166 | "nationalNumber" : 1632960015,
167 | "numberExtension" : null,
168 | "numberString" : "+441632960015",
169 | "regionID" : "GB",
170 | "type" : "unknown"
171 | }
172 | """,
173 | strategy: .properties
174 | )
175 | try assertEncode(
176 | utility.parse("+34646990213", ignoreType: true),
177 | """
178 | {
179 | "countryCode" : 34,
180 | "leadingZero" : false,
181 | "nationalNumber" : 646990213,
182 | "numberExtension" : null,
183 | "numberString" : "+34646990213",
184 | "regionID" : "ES",
185 | "type" : "unknown"
186 | }
187 | """,
188 | strategy: .properties
189 | )
190 | }
191 |
192 | func testEncode_e164Strategy() throws {
193 | try assertEncode(
194 | utility.parse("+441632960015", ignoreType: true),
195 | """
196 | "+441632960015"
197 | """,
198 | strategy: .e164
199 | )
200 | try assertEncode(
201 | utility.parse("01632960015", withRegion: "GB", ignoreType: true),
202 | """
203 | "+441632960015"
204 | """,
205 | strategy: .e164
206 | )
207 | try assertEncode(
208 | utility.parse("+34646990213", ignoreType: true),
209 | """
210 | "+34646990213"
211 | """,
212 | strategy: .e164
213 | )
214 | try assertEncode(
215 | utility.parse("646990213", withRegion: "ES", ignoreType: true),
216 | """
217 | "+34646990213"
218 | """,
219 | strategy: .e164
220 | )
221 | }
222 | }
223 |
224 | private extension PhoneNumberCodableTests {
225 | func assertDecode(
226 | _ json: String,
227 | _ expectedPhoneNumber: PhoneNumber,
228 | strategy: PhoneNumberDecodingStrategy?,
229 | file: StaticString = #file,
230 | line: UInt = #line
231 | ) throws {
232 | let decoder = JSONDecoder()
233 | if let strategy {
234 | decoder.phoneNumberDecodingStrategy = strategy
235 | }
236 | let data = try XCTUnwrap(json.data(using: .utf8))
237 | let sut = try decoder.decode(PhoneNumber.self, from: data)
238 | XCTAssertEqual(sut, expectedPhoneNumber, file: file, line: line)
239 | }
240 |
241 | func assertEncode(
242 | _ phoneNumber: PhoneNumber,
243 | _ expectedJSON: String,
244 | strategy: PhoneNumberEncodingStrategy?,
245 | file: StaticString = #file,
246 | line: UInt = #line
247 | ) throws {
248 | let encoder = JSONEncoder()
249 | if let strategy {
250 | encoder.phoneNumberEncodingStrategy = strategy
251 | }
252 | encoder.outputFormatting = [.prettyPrinted, .sortedKeys]
253 | let data = try encoder.encode(phoneNumber)
254 | let json = String(decoding: data, as: UTF8.self)
255 | XCTAssertEqual(json, expectedJSON, file: file, line: line)
256 | }
257 | }
258 |
--------------------------------------------------------------------------------
/PhoneNumberKitTests/PhoneNumberTextFieldTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // PhoneNumberTextFieldTests.swift
3 | // PhoneNumberKitTests
4 | //
5 | // Created by Travis Kaufman on 10/4/19.
6 | // Copyright © 2019 Roy Marmelstein. All rights reserved.
7 | //
8 |
9 | #if os(iOS)
10 |
11 | import PhoneNumberKit
12 | import UIKit
13 | import XCTest
14 |
15 | final class PhoneNumberTextFieldTests: XCTestCase {
16 | private var utility: PhoneNumberUtility!
17 |
18 | override func setUp() {
19 | super.setUp()
20 | utility = PhoneNumberUtility()
21 | }
22 |
23 | override func tearDown() {
24 | utility = nil
25 | super.tearDown()
26 | }
27 |
28 | func testWorksWithPhoneNumberKitInstance() {
29 | let textField = PhoneNumberTextField(utility: utility)
30 | textField.partialFormatter.defaultRegion = "US"
31 | textField.text = "4125551212"
32 | XCTAssertEqual(textField.text, "(412) 555-1212")
33 | }
34 |
35 | func testWorksWithFrameAndPhoneNumberKitInstance() {
36 | let frame = CGRect(x: 10.0, y: 20.0, width: 400.0, height: 250.0)
37 | let textField = PhoneNumberTextField(frame: frame, utility: utility)
38 | textField.partialFormatter.defaultRegion = "US"
39 | XCTAssertEqual(textField.frame, frame)
40 | textField.text = "4125551212"
41 | XCTAssertEqual(textField.text, "(412) 555-1212")
42 | }
43 |
44 | func testPhoneNumberProperty() {
45 | let textField = PhoneNumberTextField(utility: utility)
46 | textField.partialFormatter.defaultRegion = "US"
47 | textField.text = "4125551212"
48 | XCTAssertNotNil(textField.phoneNumber)
49 | textField.text = ""
50 | XCTAssertNil(textField.phoneNumber)
51 | }
52 |
53 | func testUSPhoneNumberWithFlag() {
54 | let textField = PhoneNumberTextField(utility: utility)
55 | textField.partialFormatter.defaultRegion = "US"
56 | textField.withFlag = true
57 | textField.text = "4125551212"
58 | XCTAssertNotNil(textField.flagButton)
59 | XCTAssertEqual(textField.flagButton.titleLabel?.text, "🇺🇸 ")
60 | }
61 |
62 | func testNonUSPhoneNumberWithFlag() {
63 | let textField = PhoneNumberTextField(utility: utility)
64 | textField.partialFormatter.defaultRegion = "US"
65 | textField.withFlag = true
66 | textField.text = "5872170177"
67 | XCTAssertNotNil(textField.flagButton)
68 | XCTAssertEqual(textField.flagButton.titleLabel?.text, "🇨🇦 ")
69 | }
70 | }
71 |
72 | #endif
73 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | 
2 | [](http://cocoapods.org/?q=PhoneNumberKit)
3 |  [](http://cocoapods.org/?q=PhoneNumberKit)
4 | [](https://github.com/Carthage/Carthage)
5 |
6 | # PhoneNumberKit
7 |
8 | Swift 5.3 framework for parsing, formatting and validating international phone numbers.
9 | Inspired by Google's libphonenumber.
10 |
11 | ## Features
12 |
13 | | | Features |
14 | | ---------------- | ------------------------------------------------------------------------------------------- |
15 | | :phone: | Validate, normalize and extract the elements of any phone number string. |
16 | | :100: | Simple Swift syntax and a lightweight readable codebase. |
17 | | :checkered_flag: | Fast. 1000 parses -> ~0.4 seconds. |
18 | | :books: | Best-in-class metadata from Google's libPhoneNumber project. |
19 | | :trophy: | Fully tested to match the accuracy of Google's JavaScript implementation of libPhoneNumber. |
20 | | :iphone: | Built for iOS. Automatically grabs the default region code from the phone. |
21 | | 📝 | Editable (!) AsYouType formatter for UITextField. |
22 | | :us: | Convert country codes to country names and vice versa |
23 |
24 | ## Usage
25 |
26 | Import PhoneNumberKit at the top of the Swift file that will interact with a phone number.
27 |
28 | ```swift
29 | import PhoneNumberKit
30 | ```
31 |
32 | All of your interactions with PhoneNumberKit happen through a PhoneNumberUtility object. The first step you should take is to allocate one.
33 |
34 | A PhoneNumberUtility instance is relatively expensive to allocate (it parses the metadata and keeps it in memory for the object's lifecycle), you should try and make sure PhoneNumberUtility is allocated once and deallocated when no longer needed.
35 |
36 | ```swift
37 | let phoneNumberUtility = PhoneNumberUtility()
38 | ```
39 |
40 | To parse a string, use the parse function. The region code is automatically computed but can be overridden if needed. PhoneNumberKit automatically does a hard type validation to ensure that the object created is valid, this can be quite costly performance-wise and can be turned off if needed.
41 |
42 | ```swift
43 | do {
44 | let phoneNumber = try phoneNumberUtility.parse("+33 6 89 017383")
45 | let phoneNumberCustomDefaultRegion = try phoneNumberUtility.parse("+44 20 7031 3000", withRegion: "GB", ignoreType: true)
46 | }
47 | catch {
48 | print("Generic parser error")
49 | }
50 | ```
51 |
52 | If you need to parse and validate a large amount of numbers at once, PhoneNumberKit has a special, lightning fast array parsing function. The default region code is automatically computed but can be overridden if needed. Here you can also ignore hard type validation if it is not necessary. Invalid numbers are ignored in the resulting array.
53 |
54 | ```swift
55 | let rawNumberArray = ["0291 12345678", "+49 291 12345678", "04134 1234", "09123 12345"]
56 | let phoneNumbers = phoneNumberUtility.parse(rawNumberArray)
57 | let phoneNumbersCustomDefaultRegion = phoneNumberUtility.parse(rawNumberArray, withRegion: "DE", ignoreType: true)
58 | ```
59 |
60 | PhoneNumber objects are immutable Swift structs with the following properties:
61 |
62 | ```swift
63 | phoneNumber.numberString
64 | phoneNumber.countryCode
65 | phoneNumber.nationalNumber
66 | phoneNumber.numberExtension
67 | phoneNumber.type // e.g Mobile or Fixed
68 | ```
69 |
70 | Formatting a PhoneNumber object into a string is also very easy
71 |
72 | ```swift
73 | phoneNumberUtility.format(phoneNumber, toType: .e164) // +61236618300
74 | phoneNumberUtility.format(phoneNumber, toType: .international) // +61 2 3661 8300
75 | phoneNumberUtility.format(phoneNumber, toType: .national) // (02) 3661 8300
76 | ```
77 |
78 | ## PhoneNumberTextField
79 |
80 | 
81 |
82 | To use the AsYouTypeFormatter, just replace your UITextField with a PhoneNumberTextField (if you are using Interface Builder make sure the module field is set to PhoneNumberKit).
83 |
84 | You can customize your TextField UI in the following ways
85 |
86 | - `withFlag` will display the country code for the `currentRegion`. The `flagButton` is displayed in the `leftView` of the text field with it's size set based off your text size.
87 | - `withExamplePlaceholder` uses `attributedPlaceholder` to show an example number for the `currentRegion`. In addition when `withPrefix` is set, the country code's prefix will automatically be inserted and removed when editing changes.
88 |
89 | PhoneNumberTextField automatically formats phone numbers and gives the user full editing capabilities. If you want to customize you can use the PartialFormatter directly. The default region code is automatically computed but can be overridden if needed (see the example given below).
90 |
91 | ```swift
92 | class MyGBTextField: PhoneNumberTextField {
93 | override var defaultRegion: String {
94 | get {
95 | return "GB"
96 | }
97 | set {} // exists for backward compatibility
98 | }
99 | }
100 | ```
101 |
102 | ```swift
103 | let textField = PhoneNumberTextField()
104 |
105 | PartialFormatter().formatPartial("+336895555") // +33 6 89 55 55
106 | ```
107 |
108 | You can also query countries for a dialing code or the dialing code for a given country
109 |
110 | ```swift
111 | phoneNumberUtility.countries(withCode: 33)
112 | phoneNumberUtility.countryCode(for: "FR")
113 | ```
114 |
115 | ## Customize Country Picker
116 |
117 | You can customize colors and fonts on the Country Picker View Controller by overriding the property "withDefaultPickerUIOptions"
118 |
119 | ```swift
120 | let options = CountryCodePickerOptions(
121 | backgroundColor: UIColor.systemGroupedBackground
122 | separatorColor: UIColor.opaqueSeparator
123 | textLabelColor: UIColor.label
124 | textLabelFont: .preferredFont(forTextStyle: .callout)
125 | detailTextLabelColor: UIColor.secondaryLabel
126 | detailTextLabelFont: .preferredFont(forTextStyle: .body)
127 | tintColor: UIView().tintColor
128 | cellBackgroundColor: UIColor.secondarySystemGroupedBackground
129 | cellBackgroundColorSelection: UIColor.tertiarySystemGroupedBackground
130 | )
131 | textField.withDefaultPickerUIOptions = options
132 | ```
133 |
134 | Or you can change it directly:
135 |
136 | ```swift
137 | textField.withDefaultPickerUIOptions.backgroundColor = .red
138 | ```
139 |
140 | Please refer to `CountryCodePickerOptions` for more information about usage and how it affects the view.
141 |
142 |
143 | ## Need more customization?
144 |
145 | You can access the metadata powering PhoneNumberKit yourself, this enables you to program any behaviours as they may be implemented in PhoneNumberKit itself. It does mean you are exposed to the less polished interface of the underlying file format. If you program something you find useful please push it upstream!
146 |
147 | ```swift
148 | phoneNumberUtility.metadata(for: "AU")?.mobile?.exampleNumber // 412345678
149 | ```
150 |
151 | ### [Preferred] Setting up with [Swift Package Manager](https://swiftpm.co/?query=PhoneNumberKit)
152 |
153 | The [Swift Package Manager](https://swift.org/package-manager/) is now the preferred tool for distributing PhoneNumberKit.
154 |
155 | From Xcode 11+ :
156 |
157 | 1. Select File > Swift Packages > Add Package Dependency. Enter `https://github.com/marmelroy/PhoneNumberKit.git` in the "Choose Package Repository" dialog.
158 | 2. In the next page, specify the version resolving rule as "Up to Next Major" from "4.0.0".
159 | 3. After Xcode checked out the source and resolving the version, you can choose the "PhoneNumberKit" library and add it to your app target.
160 |
161 | For more info, read [Adding Package Dependencies to Your App](https://developer.apple.com/documentation/xcode/adding_package_dependencies_to_your_app) from Apple.
162 |
163 | Alternatively, you can also add PhoneNumberKit to your `Package.swift` file:
164 |
165 | ```swift
166 | dependencies: [
167 | .package(url: "https://github.com/marmelroy/PhoneNumberKit", from: "4.0.0")
168 | ]
169 | ```
170 |
171 | ### Setting up with Carthage
172 |
173 | [Carthage](https://github.com/Carthage/Carthage) is a decentralized dependency manager that automates the process of adding frameworks to your Cocoa application.
174 |
175 | You can install Carthage with [Homebrew](http://brew.sh/) using the following command:
176 |
177 | ```bash
178 | $ brew update
179 | $ brew install carthage
180 | ```
181 |
182 | To integrate PhoneNumberKit into your Xcode project using Carthage, specify it in your `Cartfile`:
183 |
184 | ```ogdl
185 | github "marmelroy/PhoneNumberKit"
186 | ```
187 |
188 | ### Setting up with [CocoaPods](http://cocoapods.org/?q=PhoneNumberKit)
189 |
190 | ```ruby
191 | pod 'PhoneNumberKit', '~> 4.0'
192 | ```
193 |
--------------------------------------------------------------------------------
/examples/AsYouType/Sample.xcodeproj/project.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/examples/AsYouType/Sample.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/examples/AsYouType/Sample.xcodeproj/xcshareddata/xcschemes/Sample.xcscheme:
--------------------------------------------------------------------------------
1 |
2 |
5 |
8 |
9 |
15 |
21 |
22 |
23 |
24 |
25 |
30 |
31 |
37 |
38 |
39 |
40 |
41 |
42 |
52 |
54 |
60 |
61 |
62 |
63 |
69 |
71 |
77 |
78 |
79 |
80 |
82 |
83 |
86 |
87 |
88 |
--------------------------------------------------------------------------------
/examples/AsYouType/Sample/AppDelegate.swift:
--------------------------------------------------------------------------------
1 | //
2 | // AppDelegate.swift
3 | // Sample
4 | //
5 | // Created by Roy Marmelstein on 27/09/2015.
6 | // Copyright © 2015 Roy Marmelstein. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | @main
12 | class AppDelegate: UIResponder, UIApplicationDelegate {
13 | var window: UIWindow?
14 |
15 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
16 | // Override point for customization after application launch.
17 | return true
18 | }
19 |
20 | func applicationWillResignActive(_ application: UIApplication) {
21 | // Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state.
22 | // Use this method to pause ongoing tasks, disable timers, and invalidate graphics rendering callbacks. Games should use this method to pause the game.
23 | }
24 |
25 | func applicationDidEnterBackground(_ application: UIApplication) {
26 | // Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later.
27 | // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits.
28 | }
29 |
30 | func applicationWillEnterForeground(_ application: UIApplication) {
31 | // Called as part of the transition from the background to the active state; here you can undo many of the changes made on entering the background.
32 | }
33 |
34 | func applicationDidBecomeActive(_ application: UIApplication) {
35 | // Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface.
36 | }
37 |
38 | func applicationWillTerminate(_ application: UIApplication) {
39 | // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:.
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/examples/AsYouType/Sample/Assets.xcassets/AppIcon.appiconset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "iphone",
5 | "size" : "29x29",
6 | "scale" : "2x"
7 | },
8 | {
9 | "idiom" : "iphone",
10 | "size" : "29x29",
11 | "scale" : "3x"
12 | },
13 | {
14 | "idiom" : "iphone",
15 | "size" : "40x40",
16 | "scale" : "2x"
17 | },
18 | {
19 | "idiom" : "iphone",
20 | "size" : "40x40",
21 | "scale" : "3x"
22 | },
23 | {
24 | "idiom" : "iphone",
25 | "size" : "60x60",
26 | "scale" : "2x"
27 | },
28 | {
29 | "idiom" : "iphone",
30 | "size" : "60x60",
31 | "scale" : "3x"
32 | }
33 | ],
34 | "info" : {
35 | "version" : 1,
36 | "author" : "xcode"
37 | }
38 | }
--------------------------------------------------------------------------------
/examples/AsYouType/Sample/Base.lproj/LaunchScreen.storyboard:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
--------------------------------------------------------------------------------
/examples/AsYouType/Sample/Base.lproj/Main.storyboard:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
93 |
94 |
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
109 |
110 |
111 |
112 |
113 |
114 |
115 |
116 |
117 |
118 |
119 |
120 |
121 |
122 |
123 |
124 |
125 |
126 |
127 |
128 |
129 |
130 |
131 |
132 |
133 |
134 |
135 |
136 |
137 |
138 |
139 |
140 |
141 |
142 |
143 |
144 |
145 |
146 |
147 |
148 |
149 |
--------------------------------------------------------------------------------
/examples/AsYouType/Sample/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | en
7 | CFBundleExecutable
8 | $(EXECUTABLE_NAME)
9 | CFBundleIdentifier
10 | $(PRODUCT_BUNDLE_IDENTIFIER)
11 | CFBundleInfoDictionaryVersion
12 | 6.0
13 | CFBundleName
14 | $(PRODUCT_NAME)
15 | CFBundlePackageType
16 | APPL
17 | CFBundleShortVersionString
18 | 1.0
19 | CFBundleSignature
20 | ????
21 | CFBundleVersion
22 | 1
23 | LSRequiresIPhoneOS
24 |
25 | UILaunchStoryboardName
26 | LaunchScreen
27 | UIMainStoryboardFile
28 | Main
29 | UIRequiredDeviceCapabilities
30 |
31 | armv7
32 |
33 | UISupportedInterfaceOrientations
34 |
35 | UIInterfaceOrientationPortrait
36 | UIInterfaceOrientationLandscapeLeft
37 | UIInterfaceOrientationLandscapeRight
38 |
39 |
40 |
41 |
--------------------------------------------------------------------------------
/examples/AsYouType/Sample/ViewController.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ViewController.swift
3 | // Sample
4 | //
5 | // Created by Roy Marmelstein on 27/09/2015.
6 | // Copyright © 2015 Roy Marmelstein. All rights reserved.
7 | //
8 |
9 | import ContactsUI
10 | import Foundation
11 | import PhoneNumberKit
12 | import UIKit
13 |
14 | final class ViewController: UIViewController, CNContactPickerDelegate {
15 | @IBOutlet var textField: PhoneNumberTextField!
16 | @IBOutlet var withPrefixSwitch: UISwitch!
17 | @IBOutlet var withFlagSwitch: UISwitch!
18 | @IBOutlet var withExamplePlaceholderSwitch: UISwitch!
19 | @IBOutlet var withDefaultPickerUISwitch: UISwitch!
20 |
21 | override func viewDidLoad() {
22 | super.viewDidLoad()
23 |
24 | PhoneNumberKit.CountryCodePicker.commonCountryCodes = ["US", "CA", "MX", "AU", "GB", "DE"]
25 | PhoneNumberKit.CountryCodePicker.alwaysShowsSearchBar = true
26 |
27 | self.textField.becomeFirstResponder()
28 | self.withPrefixSwitch.isOn = self.textField.withPrefix
29 | self.withFlagSwitch.isOn = self.textField.withFlag
30 | self.withExamplePlaceholderSwitch.isOn = self.textField.withExamplePlaceholder
31 | self.withDefaultPickerUISwitch.isOn = self.textField.withDefaultPickerUI
32 |
33 | if #available(iOS 13.0, *) {
34 | self.view.backgroundColor = .systemBackground
35 | }
36 | }
37 |
38 | @IBAction func didTapView(_ sender: Any) {
39 | self.textField.resignFirstResponder()
40 | }
41 |
42 | @IBAction func withPrefixDidChange(_ sender: Any) {
43 | self.textField.withPrefix = self.withPrefixSwitch.isOn
44 | }
45 |
46 | @IBAction func withFlagDidChange(_ sender: Any) {
47 | self.textField.withFlag = self.withFlagSwitch.isOn
48 | }
49 |
50 | @IBAction func withExamplePlaceholderDidChange(_ sender: Any) {
51 | self.textField.withExamplePlaceholder = self.withExamplePlaceholderSwitch.isOn
52 | if !self.textField.withExamplePlaceholder {
53 | self.textField.placeholder = "Enter phone number"
54 | }
55 | }
56 |
57 | @IBAction func withDefaultPickerUIDidChange(_ sender: Any) {
58 | self.textField.withDefaultPickerUI = self.withDefaultPickerUISwitch.isOn
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/examples/AsYouType/SampleTests/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | en
7 | CFBundleExecutable
8 | $(EXECUTABLE_NAME)
9 | CFBundleIdentifier
10 | $(PRODUCT_BUNDLE_IDENTIFIER)
11 | CFBundleInfoDictionaryVersion
12 | 6.0
13 | CFBundleName
14 | $(PRODUCT_NAME)
15 | CFBundlePackageType
16 | BNDL
17 | CFBundleShortVersionString
18 | 1.0
19 | CFBundleSignature
20 | ????
21 | CFBundleVersion
22 | 1
23 |
24 |
25 |
--------------------------------------------------------------------------------
/examples/AsYouType/SampleTests/SampleTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SampleTests.swift
3 | // SampleTests
4 | //
5 | // Created by Roy Marmelstein on 27/09/2015.
6 | // Copyright © 2015 Roy Marmelstein. All rights reserved.
7 | //
8 |
9 | @testable import Sample
10 | import XCTest
11 |
12 | class SampleTests: XCTestCase {
13 | override func setUp() {
14 | super.setUp()
15 | // Put setup code here. This method is called before the invocation of each test method in the class.
16 | }
17 |
18 | override func tearDown() {
19 | // Put teardown code here. This method is called after the invocation of each test method in the class.
20 | super.tearDown()
21 | }
22 |
23 | func testExample() {
24 | // This is an example of a functional test case.
25 | // Use XCTAssert and related functions to verify your tests produce the correct results.
26 | }
27 |
28 | func testPerformanceExample() {
29 | // This is an example of a performance test case.
30 | self.measureBlock {
31 | // Put the code you want to measure the time of here.
32 | }
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/examples/PhoneBook/Sample.xcodeproj/project.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/examples/PhoneBook/Sample.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/examples/PhoneBook/Sample.xcodeproj/xcshareddata/xcschemes/Sample.xcscheme:
--------------------------------------------------------------------------------
1 |
2 |
5 |
8 |
9 |
15 |
21 |
22 |
23 |
24 |
25 |
30 |
31 |
37 |
38 |
39 |
40 |
41 |
42 |
52 |
54 |
60 |
61 |
62 |
63 |
69 |
71 |
77 |
78 |
79 |
80 |
82 |
83 |
86 |
87 |
88 |
--------------------------------------------------------------------------------
/examples/PhoneBook/Sample/AppDelegate.swift:
--------------------------------------------------------------------------------
1 | //
2 | // AppDelegate.swift
3 | // Sample
4 | //
5 | // Created by Roy Marmelstein on 27/09/2015.
6 | // Copyright © 2015 Roy Marmelstein. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | @main
12 | class AppDelegate: UIResponder, UIApplicationDelegate {
13 | var window: UIWindow?
14 |
15 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
16 | // Override point for customization after application launch.
17 | return true
18 | }
19 |
20 | func applicationWillResignActive(_ application: UIApplication) {
21 | // Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state.
22 | // Use this method to pause ongoing tasks, disable timers, and invalidate graphics rendering callbacks. Games should use this method to pause the game.
23 | }
24 |
25 | func applicationDidEnterBackground(_ application: UIApplication) {
26 | // Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later.
27 | // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits.
28 | }
29 |
30 | func applicationWillEnterForeground(_ application: UIApplication) {
31 | // Called as part of the transition from the background to the active state; here you can undo many of the changes made on entering the background.
32 | }
33 |
34 | func applicationDidBecomeActive(_ application: UIApplication) {
35 | // Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface.
36 | }
37 |
38 | func applicationWillTerminate(_ application: UIApplication) {
39 | // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:.
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/examples/PhoneBook/Sample/Assets.xcassets/AppIcon.appiconset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "iphone",
5 | "size" : "29x29",
6 | "scale" : "2x"
7 | },
8 | {
9 | "idiom" : "iphone",
10 | "size" : "29x29",
11 | "scale" : "3x"
12 | },
13 | {
14 | "idiom" : "iphone",
15 | "size" : "40x40",
16 | "scale" : "2x"
17 | },
18 | {
19 | "idiom" : "iphone",
20 | "size" : "40x40",
21 | "scale" : "3x"
22 | },
23 | {
24 | "idiom" : "iphone",
25 | "size" : "60x60",
26 | "scale" : "2x"
27 | },
28 | {
29 | "idiom" : "iphone",
30 | "size" : "60x60",
31 | "scale" : "3x"
32 | }
33 | ],
34 | "info" : {
35 | "version" : 1,
36 | "author" : "xcode"
37 | }
38 | }
--------------------------------------------------------------------------------
/examples/PhoneBook/Sample/Base.lproj/LaunchScreen.storyboard:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
--------------------------------------------------------------------------------
/examples/PhoneBook/Sample/Base.lproj/Main.storyboard:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
31 |
37 |
43 |
49 |
55 |
61 |
62 |
63 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
--------------------------------------------------------------------------------
/examples/PhoneBook/Sample/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | en
7 | CFBundleExecutable
8 | $(EXECUTABLE_NAME)
9 | CFBundleIdentifier
10 | $(PRODUCT_BUNDLE_IDENTIFIER)
11 | CFBundleInfoDictionaryVersion
12 | 6.0
13 | CFBundleName
14 | $(PRODUCT_NAME)
15 | CFBundlePackageType
16 | APPL
17 | CFBundleShortVersionString
18 | 1.0
19 | CFBundleSignature
20 | ????
21 | CFBundleVersion
22 | 1
23 | LSRequiresIPhoneOS
24 |
25 | UILaunchStoryboardName
26 | LaunchScreen
27 | UIMainStoryboardFile
28 | Main
29 | UIRequiredDeviceCapabilities
30 |
31 | armv7
32 |
33 | UISupportedInterfaceOrientations
34 |
35 | UIInterfaceOrientationPortrait
36 | UIInterfaceOrientationLandscapeLeft
37 | UIInterfaceOrientationLandscapeRight
38 |
39 |
40 |
41 |
--------------------------------------------------------------------------------
/examples/PhoneBook/Sample/ViewController.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ViewController.swift
3 | // Sample
4 | //
5 | // Created by Roy Marmelstein on 27/09/2015.
6 | // Copyright © 2015 Roy Marmelstein. All rights reserved.
7 | //
8 |
9 | import ContactsUI
10 | import Foundation
11 | import PhoneNumberKit
12 | import UIKit
13 |
14 | class ViewController: UIViewController, CNContactPickerDelegate {
15 | let phoneNumberUtility = PhoneNumberUtility()
16 |
17 | @IBOutlet var parsedNumberLabel: UILabel!
18 | @IBOutlet var parsedCountryCodeLabel: UILabel!
19 | @IBOutlet var parsedCountryLabel: UILabel!
20 |
21 | let notAvailable = "NA"
22 |
23 | override func viewDidLoad() {
24 | super.viewDidLoad()
25 | self.clearResults()
26 |
27 | if #available(iOS 13.0, *) {
28 | self.view.backgroundColor = .systemBackground
29 | }
30 | }
31 |
32 | override func didReceiveMemoryWarning() {
33 | super.didReceiveMemoryWarning()
34 | // Dispose of any resources that can be recreated.
35 | }
36 |
37 | @IBAction func selectFromContacts(_ sender: AnyObject) {
38 | let controller = CNContactPickerViewController()
39 | controller.delegate = self
40 | self.present(
41 | controller,
42 | animated: true, completion: nil
43 | )
44 | }
45 |
46 | func contactPicker(_ picker: CNContactPickerViewController, didSelect contact: CNContact) {
47 | guard let firstPhoneNumber = contact.phoneNumbers.first else {
48 | self.clearResults()
49 | return
50 | }
51 | let phoneNumber = firstPhoneNumber.value
52 | self.parseNumber(phoneNumber.stringValue)
53 | }
54 |
55 | func parseNumber(_ number: String) {
56 | do {
57 | let phoneNumber = try phoneNumberUtility.parse(number, ignoreType: true)
58 | self.parsedNumberLabel.text = phoneNumberUtility.format(phoneNumber, toType: .international)
59 | self.parsedCountryCodeLabel.text = String(phoneNumber.countryCode)
60 | if let regionCode = phoneNumberUtility.mainCountry(forCode: phoneNumber.countryCode) {
61 | let country = Locale.current.localizedString(forRegionCode: regionCode)
62 | self.parsedCountryLabel.text = country
63 | }
64 | } catch {
65 | self.clearResults()
66 | print("Something went wrong")
67 | }
68 | }
69 |
70 | func clearResults() {
71 | self.parsedNumberLabel.text = self.notAvailable
72 | self.parsedCountryCodeLabel.text = self.notAvailable
73 | self.parsedCountryLabel.text = self.notAvailable
74 | }
75 | }
76 |
--------------------------------------------------------------------------------
/examples/PhoneBook/SampleTests/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | en
7 | CFBundleExecutable
8 | $(EXECUTABLE_NAME)
9 | CFBundleIdentifier
10 | $(PRODUCT_BUNDLE_IDENTIFIER)
11 | CFBundleInfoDictionaryVersion
12 | 6.0
13 | CFBundleName
14 | $(PRODUCT_NAME)
15 | CFBundlePackageType
16 | BNDL
17 | CFBundleShortVersionString
18 | 1.0
19 | CFBundleSignature
20 | ????
21 | CFBundleVersion
22 | 1
23 |
24 |
25 |
--------------------------------------------------------------------------------
/examples/PhoneBook/SampleTests/SampleTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SampleTests.swift
3 | // SampleTests
4 | //
5 | // Created by Roy Marmelstein on 27/09/2015.
6 | // Copyright © 2015 Roy Marmelstein. All rights reserved.
7 | //
8 |
9 | @testable import Sample
10 | import XCTest
11 |
12 | class SampleTests: XCTestCase {
13 | override func setUp() {
14 | super.setUp()
15 | // Put setup code here. This method is called before the invocation of each test method in the class.
16 | }
17 |
18 | override func tearDown() {
19 | // Put teardown code here. This method is called after the invocation of each test method in the class.
20 | super.tearDown()
21 | }
22 |
23 | func testExample() {
24 | // This is an example of a functional test case.
25 | // Use XCTAssert and related functions to verify your tests produce the correct results.
26 | }
27 |
28 | func testPerformanceExample() {
29 | // This is an example of a performance test case.
30 | self.measureBlock {
31 | // Put the code you want to measure the time of here.
32 | }
33 | }
34 | }
35 |
--------------------------------------------------------------------------------