├── .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 | ![PhoneNumberKit](https://cloud.githubusercontent.com/assets/889949/20864386/a1307950-b9ef-11e6-8a58-e9c5103738e7.png) 2 | [![Platform](https://img.shields.io/cocoapods/p/PhoneNumberKit.svg?maxAge=2592000&style=for-the-badge)](http://cocoapods.org/?q=PhoneNumberKit) 3 | ![GitHub Workflow Status (with branch)](https://img.shields.io/github/actions/workflow/status/marmelroy/PhoneNumberKit/pr.yml?branch=master&label=tests&style=for-the-badge) [![Version](http://img.shields.io/cocoapods/v/PhoneNumberKit.svg?style=for-the-badge)](http://cocoapods.org/?q=PhoneNumberKit) 4 | [![Carthage compatible](https://img.shields.io/badge/Carthage-compatible-4BC51D.svg?style=for-the-badge)](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 | ![AsYouTypeFormatter](https://user-images.githubusercontent.com/7651280/67554038-e6512500-f751-11e9-93c9-9111e899a2ef.gif) 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 | --------------------------------------------------------------------------------