├── .gitignore ├── Assets ├── Before.jpg ├── Broken.jpg ├── BrokenPerson.jpg ├── Fixed.jpg └── Screenshot.png ├── PhoneInternationaliser.xcodeproj ├── project.pbxproj └── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ └── IDEWorkspaceChecks.plist ├── PhoneInternationaliser ├── AppDelegate.swift ├── Assets.xcassets │ ├── AccentColor.colorset │ │ └── Contents.json │ ├── AppIcon.appiconset │ │ └── Contents.json │ └── Contents.json ├── Base.lproj │ └── Main.storyboard ├── CallingCountry.swift ├── ContactsManager.swift ├── Extensions or Categories │ ├── CNPhoneNumber+Classification.swift │ └── CNPhoneNumber+ThingsThatArePrivateForSomeReason.h ├── Logging │ ├── LogReceivingTextView.swift │ └── UserLogger.swift ├── PhoneInternationaliser-Bridging-Header.h ├── PhoneInternationaliser.entitlements └── ViewController.swift └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | Archives 2 | 3 | # macOS Stuff 4 | .DS_Store 5 | 6 | # Xcode 7 | # 8 | # gitignore contributors: remember to update Global/Xcode.gitignore, 9 | Objective-C.gitignore & Swift.gitignore 10 | 11 | ## Build generated 12 | build/ 13 | DerivedData/ 14 | 15 | ## Various settings 16 | *.pbxuser 17 | !default.pbxuser 18 | *.mode1v3 19 | !default.mode1v3 20 | *.mode2v3 21 | !default.mode2v3 22 | *.perspectivev3 23 | !default.perspectivev3 24 | xcuserdata/ 25 | 26 | ## Other 27 | *.moved-aside 28 | *.xccheckout 29 | *.xcscmblueprint 30 | 31 | ## Obj-C/Swift specific 32 | *.hmap 33 | *.ipa 34 | *.dSYM.zip 35 | *.dSYM 36 | 37 | ## Playgrounds 38 | timeline.xctimeline 39 | playground.xcworkspace 40 | 41 | # Swift Package Manager 42 | # 43 | # Add this line if you want to avoid checking in source code from Swift 44 | Package Manager dependencies. 45 | # Packages/ 46 | # Package.pins 47 | .build/ 48 | 49 | # CocoaPods 50 | # 51 | # We recommend against adding the Pods directory to your .gitignore. 52 | However 53 | # you should judge for yourself, the pros and cons are mentioned at: 54 | # 55 | https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 56 | # 57 | # Pods/ 58 | 59 | # Carthage 60 | # 61 | # Add this line if you want to avoid checking in source code from Carthage 62 | dependencies. 63 | # Carthage/Checkouts 64 | 65 | Carthage/Build 66 | 67 | # fastlane 68 | # 69 | # It is recommended to not store the screenshots in the git repo. Instead, 70 | use fastlane to re-generate the 71 | # screenshots whenever they are needed. 72 | # For more information about the recommended setup visit: 73 | # 74 | https://docs.fastlane.tools/best-practices/source-control/#source-control 75 | 76 | fastlane/report.xml 77 | fastlane/Preview.html 78 | fastlane/screenshots 79 | fastlane/test_output 80 | -------------------------------------------------------------------------------- /Assets/Before.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aydenp/PhoneInternationaliser/8ca627f71399a2279a942083b32361b459195286/Assets/Before.jpg -------------------------------------------------------------------------------- /Assets/Broken.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aydenp/PhoneInternationaliser/8ca627f71399a2279a942083b32361b459195286/Assets/Broken.jpg -------------------------------------------------------------------------------- /Assets/BrokenPerson.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aydenp/PhoneInternationaliser/8ca627f71399a2279a942083b32361b459195286/Assets/BrokenPerson.jpg -------------------------------------------------------------------------------- /Assets/Fixed.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aydenp/PhoneInternationaliser/8ca627f71399a2279a942083b32361b459195286/Assets/Fixed.jpg -------------------------------------------------------------------------------- /Assets/Screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aydenp/PhoneInternationaliser/8ca627f71399a2279a942083b32361b459195286/Assets/Screenshot.png -------------------------------------------------------------------------------- /PhoneInternationaliser.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 55; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | C3139911280B06E0002C2B4D /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3139910280B06E0002C2B4D /* AppDelegate.swift */; }; 11 | C3139913280B06E0002C2B4D /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3139912280B06E0002C2B4D /* ViewController.swift */; }; 12 | C3139915280B06E1002C2B4D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = C3139914280B06E1002C2B4D /* Assets.xcassets */; }; 13 | C3139918280B06E1002C2B4D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = C3139916280B06E1002C2B4D /* Main.storyboard */; }; 14 | C3139920280B0D3B002C2B4D /* UserLogger.swift in Sources */ = {isa = PBXBuildFile; fileRef = C313991F280B0D3B002C2B4D /* UserLogger.swift */; }; 15 | C3139922280B1096002C2B4D /* LogReceivingTextView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3139921280B1096002C2B4D /* LogReceivingTextView.swift */; }; 16 | C3139924280B168A002C2B4D /* CallingCountry.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3139923280B168A002C2B4D /* CallingCountry.swift */; }; 17 | C313992A280B7260002C2B4D /* ContactsManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3139929280B7260002C2B4D /* ContactsManager.swift */; }; 18 | C313992C280B7271002C2B4D /* CNPhoneNumber+Classification.swift in Sources */ = {isa = PBXBuildFile; fileRef = C313992B280B7271002C2B4D /* CNPhoneNumber+Classification.swift */; }; 19 | /* End PBXBuildFile section */ 20 | 21 | /* Begin PBXFileReference section */ 22 | C313990D280B06E0002C2B4D /* PhoneInternationaliser.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = PhoneInternationaliser.app; sourceTree = BUILT_PRODUCTS_DIR; }; 23 | C3139910280B06E0002C2B4D /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 24 | C3139912280B06E0002C2B4D /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; }; 25 | C3139914280B06E1002C2B4D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 26 | C3139917280B06E1002C2B4D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 27 | C3139919280B06E1002C2B4D /* PhoneInternationaliser.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = PhoneInternationaliser.entitlements; sourceTree = ""; }; 28 | C313991F280B0D3B002C2B4D /* UserLogger.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserLogger.swift; sourceTree = ""; }; 29 | C3139921280B1096002C2B4D /* LogReceivingTextView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LogReceivingTextView.swift; sourceTree = ""; }; 30 | C3139923280B168A002C2B4D /* CallingCountry.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallingCountry.swift; sourceTree = ""; }; 31 | C3139925280B5E39002C2B4D /* PhoneInternationaliser-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "PhoneInternationaliser-Bridging-Header.h"; sourceTree = ""; }; 32 | C3139926280B5E3A002C2B4D /* CNPhoneNumber+ThingsThatArePrivateForSomeReason.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "CNPhoneNumber+ThingsThatArePrivateForSomeReason.h"; sourceTree = ""; }; 33 | C3139929280B7260002C2B4D /* ContactsManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactsManager.swift; sourceTree = ""; }; 34 | C313992B280B7271002C2B4D /* CNPhoneNumber+Classification.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CNPhoneNumber+Classification.swift"; sourceTree = ""; }; 35 | /* End PBXFileReference section */ 36 | 37 | /* Begin PBXFrameworksBuildPhase section */ 38 | C313990A280B06E0002C2B4D /* Frameworks */ = { 39 | isa = PBXFrameworksBuildPhase; 40 | buildActionMask = 2147483647; 41 | files = ( 42 | ); 43 | runOnlyForDeploymentPostprocessing = 0; 44 | }; 45 | /* End PBXFrameworksBuildPhase section */ 46 | 47 | /* Begin PBXGroup section */ 48 | C3139904280B06E0002C2B4D = { 49 | isa = PBXGroup; 50 | children = ( 51 | C313990F280B06E0002C2B4D /* PhoneInternationaliser */, 52 | C313990E280B06E0002C2B4D /* Products */, 53 | ); 54 | sourceTree = ""; 55 | }; 56 | C313990E280B06E0002C2B4D /* Products */ = { 57 | isa = PBXGroup; 58 | children = ( 59 | C313990D280B06E0002C2B4D /* PhoneInternationaliser.app */, 60 | ); 61 | name = Products; 62 | sourceTree = ""; 63 | }; 64 | C313990F280B06E0002C2B4D /* PhoneInternationaliser */ = { 65 | isa = PBXGroup; 66 | children = ( 67 | C3139910280B06E0002C2B4D /* AppDelegate.swift */, 68 | C3139912280B06E0002C2B4D /* ViewController.swift */, 69 | C3139929280B7260002C2B4D /* ContactsManager.swift */, 70 | C3139923280B168A002C2B4D /* CallingCountry.swift */, 71 | C3139914280B06E1002C2B4D /* Assets.xcassets */, 72 | C3139916280B06E1002C2B4D /* Main.storyboard */, 73 | C313992E280B7298002C2B4D /* Logging */, 74 | C313992D280B728A002C2B4D /* Extensions or Categories */, 75 | C3139919280B06E1002C2B4D /* PhoneInternationaliser.entitlements */, 76 | C3139925280B5E39002C2B4D /* PhoneInternationaliser-Bridging-Header.h */, 77 | ); 78 | path = PhoneInternationaliser; 79 | sourceTree = ""; 80 | }; 81 | C313992D280B728A002C2B4D /* Extensions or Categories */ = { 82 | isa = PBXGroup; 83 | children = ( 84 | C313992B280B7271002C2B4D /* CNPhoneNumber+Classification.swift */, 85 | C3139926280B5E3A002C2B4D /* CNPhoneNumber+ThingsThatArePrivateForSomeReason.h */, 86 | ); 87 | path = "Extensions or Categories"; 88 | sourceTree = ""; 89 | }; 90 | C313992E280B7298002C2B4D /* Logging */ = { 91 | isa = PBXGroup; 92 | children = ( 93 | C313991F280B0D3B002C2B4D /* UserLogger.swift */, 94 | C3139921280B1096002C2B4D /* LogReceivingTextView.swift */, 95 | ); 96 | path = Logging; 97 | sourceTree = ""; 98 | }; 99 | /* End PBXGroup section */ 100 | 101 | /* Begin PBXNativeTarget section */ 102 | C313990C280B06E0002C2B4D /* PhoneInternationaliser */ = { 103 | isa = PBXNativeTarget; 104 | buildConfigurationList = C313991C280B06E1002C2B4D /* Build configuration list for PBXNativeTarget "PhoneInternationaliser" */; 105 | buildPhases = ( 106 | C3139909280B06E0002C2B4D /* Sources */, 107 | C313990A280B06E0002C2B4D /* Frameworks */, 108 | C313990B280B06E0002C2B4D /* Resources */, 109 | ); 110 | buildRules = ( 111 | ); 112 | dependencies = ( 113 | ); 114 | name = PhoneInternationaliser; 115 | productName = PhoneInternationaliser; 116 | productReference = C313990D280B06E0002C2B4D /* PhoneInternationaliser.app */; 117 | productType = "com.apple.product-type.application"; 118 | }; 119 | /* End PBXNativeTarget section */ 120 | 121 | /* Begin PBXProject section */ 122 | C3139905280B06E0002C2B4D /* Project object */ = { 123 | isa = PBXProject; 124 | attributes = { 125 | BuildIndependentTargetsInParallel = 1; 126 | LastSwiftUpdateCheck = 1330; 127 | LastUpgradeCheck = 1330; 128 | TargetAttributes = { 129 | C313990C280B06E0002C2B4D = { 130 | CreatedOnToolsVersion = 13.3; 131 | LastSwiftMigration = 1330; 132 | }; 133 | }; 134 | }; 135 | buildConfigurationList = C3139908280B06E0002C2B4D /* Build configuration list for PBXProject "PhoneInternationaliser" */; 136 | compatibilityVersion = "Xcode 13.0"; 137 | developmentRegion = en; 138 | hasScannedForEncodings = 0; 139 | knownRegions = ( 140 | en, 141 | Base, 142 | ); 143 | mainGroup = C3139904280B06E0002C2B4D; 144 | productRefGroup = C313990E280B06E0002C2B4D /* Products */; 145 | projectDirPath = ""; 146 | projectRoot = ""; 147 | targets = ( 148 | C313990C280B06E0002C2B4D /* PhoneInternationaliser */, 149 | ); 150 | }; 151 | /* End PBXProject section */ 152 | 153 | /* Begin PBXResourcesBuildPhase section */ 154 | C313990B280B06E0002C2B4D /* Resources */ = { 155 | isa = PBXResourcesBuildPhase; 156 | buildActionMask = 2147483647; 157 | files = ( 158 | C3139915280B06E1002C2B4D /* Assets.xcassets in Resources */, 159 | C3139918280B06E1002C2B4D /* Main.storyboard in Resources */, 160 | ); 161 | runOnlyForDeploymentPostprocessing = 0; 162 | }; 163 | /* End PBXResourcesBuildPhase section */ 164 | 165 | /* Begin PBXSourcesBuildPhase section */ 166 | C3139909280B06E0002C2B4D /* Sources */ = { 167 | isa = PBXSourcesBuildPhase; 168 | buildActionMask = 2147483647; 169 | files = ( 170 | C3139913280B06E0002C2B4D /* ViewController.swift in Sources */, 171 | C313992C280B7271002C2B4D /* CNPhoneNumber+Classification.swift in Sources */, 172 | C3139922280B1096002C2B4D /* LogReceivingTextView.swift in Sources */, 173 | C3139920280B0D3B002C2B4D /* UserLogger.swift in Sources */, 174 | C3139924280B168A002C2B4D /* CallingCountry.swift in Sources */, 175 | C313992A280B7260002C2B4D /* ContactsManager.swift in Sources */, 176 | C3139911280B06E0002C2B4D /* AppDelegate.swift in Sources */, 177 | ); 178 | runOnlyForDeploymentPostprocessing = 0; 179 | }; 180 | /* End PBXSourcesBuildPhase section */ 181 | 182 | /* Begin PBXVariantGroup section */ 183 | C3139916280B06E1002C2B4D /* Main.storyboard */ = { 184 | isa = PBXVariantGroup; 185 | children = ( 186 | C3139917280B06E1002C2B4D /* Base */, 187 | ); 188 | name = Main.storyboard; 189 | sourceTree = ""; 190 | }; 191 | /* End PBXVariantGroup section */ 192 | 193 | /* Begin XCBuildConfiguration section */ 194 | C313991A280B06E1002C2B4D /* Debug */ = { 195 | isa = XCBuildConfiguration; 196 | buildSettings = { 197 | ALWAYS_SEARCH_USER_PATHS = NO; 198 | CLANG_ANALYZER_NONNULL = YES; 199 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 200 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; 201 | CLANG_ENABLE_MODULES = YES; 202 | CLANG_ENABLE_OBJC_ARC = YES; 203 | CLANG_ENABLE_OBJC_WEAK = YES; 204 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 205 | CLANG_WARN_BOOL_CONVERSION = YES; 206 | CLANG_WARN_COMMA = YES; 207 | CLANG_WARN_CONSTANT_CONVERSION = YES; 208 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 209 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 210 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 211 | CLANG_WARN_EMPTY_BODY = YES; 212 | CLANG_WARN_ENUM_CONVERSION = YES; 213 | CLANG_WARN_INFINITE_RECURSION = YES; 214 | CLANG_WARN_INT_CONVERSION = YES; 215 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 216 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 217 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 218 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 219 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 220 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 221 | CLANG_WARN_STRICT_PROTOTYPES = YES; 222 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 223 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 224 | CLANG_WARN_UNREACHABLE_CODE = YES; 225 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 226 | COPY_PHASE_STRIP = NO; 227 | DEBUG_INFORMATION_FORMAT = dwarf; 228 | ENABLE_STRICT_OBJC_MSGSEND = YES; 229 | ENABLE_TESTABILITY = YES; 230 | GCC_C_LANGUAGE_STANDARD = gnu11; 231 | GCC_DYNAMIC_NO_PIC = NO; 232 | GCC_NO_COMMON_BLOCKS = YES; 233 | GCC_OPTIMIZATION_LEVEL = 0; 234 | GCC_PREPROCESSOR_DEFINITIONS = ( 235 | "DEBUG=1", 236 | "$(inherited)", 237 | ); 238 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 239 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 240 | GCC_WARN_UNDECLARED_SELECTOR = YES; 241 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 242 | GCC_WARN_UNUSED_FUNCTION = YES; 243 | GCC_WARN_UNUSED_VARIABLE = YES; 244 | MACOSX_DEPLOYMENT_TARGET = 11.0; 245 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; 246 | MTL_FAST_MATH = YES; 247 | ONLY_ACTIVE_ARCH = YES; 248 | SDKROOT = macosx; 249 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 250 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 251 | }; 252 | name = Debug; 253 | }; 254 | C313991B280B06E1002C2B4D /* Release */ = { 255 | isa = XCBuildConfiguration; 256 | buildSettings = { 257 | ALWAYS_SEARCH_USER_PATHS = NO; 258 | CLANG_ANALYZER_NONNULL = YES; 259 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 260 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; 261 | CLANG_ENABLE_MODULES = YES; 262 | CLANG_ENABLE_OBJC_ARC = YES; 263 | CLANG_ENABLE_OBJC_WEAK = YES; 264 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 265 | CLANG_WARN_BOOL_CONVERSION = YES; 266 | CLANG_WARN_COMMA = YES; 267 | CLANG_WARN_CONSTANT_CONVERSION = YES; 268 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 269 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 270 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 271 | CLANG_WARN_EMPTY_BODY = YES; 272 | CLANG_WARN_ENUM_CONVERSION = YES; 273 | CLANG_WARN_INFINITE_RECURSION = YES; 274 | CLANG_WARN_INT_CONVERSION = YES; 275 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 276 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 277 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 278 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 279 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 280 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 281 | CLANG_WARN_STRICT_PROTOTYPES = YES; 282 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 283 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 284 | CLANG_WARN_UNREACHABLE_CODE = YES; 285 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 286 | COPY_PHASE_STRIP = NO; 287 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 288 | ENABLE_NS_ASSERTIONS = NO; 289 | ENABLE_STRICT_OBJC_MSGSEND = YES; 290 | GCC_C_LANGUAGE_STANDARD = gnu11; 291 | GCC_NO_COMMON_BLOCKS = YES; 292 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 293 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 294 | GCC_WARN_UNDECLARED_SELECTOR = YES; 295 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 296 | GCC_WARN_UNUSED_FUNCTION = YES; 297 | GCC_WARN_UNUSED_VARIABLE = YES; 298 | MACOSX_DEPLOYMENT_TARGET = 11.0; 299 | MTL_ENABLE_DEBUG_INFO = NO; 300 | MTL_FAST_MATH = YES; 301 | SDKROOT = macosx; 302 | SWIFT_COMPILATION_MODE = wholemodule; 303 | SWIFT_OPTIMIZATION_LEVEL = "-O"; 304 | }; 305 | name = Release; 306 | }; 307 | C313991D280B06E1002C2B4D /* Debug */ = { 308 | isa = XCBuildConfiguration; 309 | buildSettings = { 310 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 311 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 312 | CLANG_ENABLE_MODULES = YES; 313 | CODE_SIGN_ENTITLEMENTS = PhoneInternationaliser/PhoneInternationaliser.entitlements; 314 | CODE_SIGN_STYLE = Automatic; 315 | COMBINE_HIDPI_IMAGES = YES; 316 | CURRENT_PROJECT_VERSION = 1; 317 | DEVELOPMENT_TEAM = RHEEZ26ZWB; 318 | ENABLE_HARDENED_RUNTIME = YES; 319 | GENERATE_INFOPLIST_FILE = YES; 320 | INFOPLIST_KEY_NSContactsUsageDescription = "To find local phone numbers and convert them to international phone numbers, as requested by you."; 321 | INFOPLIST_KEY_NSHumanReadableCopyright = ""; 322 | INFOPLIST_KEY_NSMainStoryboardFile = Main; 323 | INFOPLIST_KEY_NSPrincipalClass = NSApplication; 324 | LD_RUNPATH_SEARCH_PATHS = ( 325 | "$(inherited)", 326 | "@executable_path/../Frameworks", 327 | ); 328 | MARKETING_VERSION = 1.0; 329 | PRODUCT_BUNDLE_IDENTIFIER = dev.ayden.macos.PhoneInternationaliser; 330 | PRODUCT_NAME = "$(TARGET_NAME)"; 331 | SWIFT_EMIT_LOC_STRINGS = YES; 332 | SWIFT_OBJC_BRIDGING_HEADER = "PhoneInternationaliser/PhoneInternationaliser-Bridging-Header.h"; 333 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 334 | SWIFT_VERSION = 5.0; 335 | }; 336 | name = Debug; 337 | }; 338 | C313991E280B06E1002C2B4D /* Release */ = { 339 | isa = XCBuildConfiguration; 340 | buildSettings = { 341 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 342 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 343 | CLANG_ENABLE_MODULES = YES; 344 | CODE_SIGN_ENTITLEMENTS = PhoneInternationaliser/PhoneInternationaliser.entitlements; 345 | CODE_SIGN_STYLE = Automatic; 346 | COMBINE_HIDPI_IMAGES = YES; 347 | CURRENT_PROJECT_VERSION = 1; 348 | DEVELOPMENT_TEAM = RHEEZ26ZWB; 349 | ENABLE_HARDENED_RUNTIME = YES; 350 | GENERATE_INFOPLIST_FILE = YES; 351 | INFOPLIST_KEY_NSContactsUsageDescription = "To find local phone numbers and convert them to international phone numbers, as requested by you."; 352 | INFOPLIST_KEY_NSHumanReadableCopyright = ""; 353 | INFOPLIST_KEY_NSMainStoryboardFile = Main; 354 | INFOPLIST_KEY_NSPrincipalClass = NSApplication; 355 | LD_RUNPATH_SEARCH_PATHS = ( 356 | "$(inherited)", 357 | "@executable_path/../Frameworks", 358 | ); 359 | MARKETING_VERSION = 1.0; 360 | PRODUCT_BUNDLE_IDENTIFIER = dev.ayden.macos.PhoneInternationaliser; 361 | PRODUCT_NAME = "$(TARGET_NAME)"; 362 | SWIFT_EMIT_LOC_STRINGS = YES; 363 | SWIFT_OBJC_BRIDGING_HEADER = "PhoneInternationaliser/PhoneInternationaliser-Bridging-Header.h"; 364 | SWIFT_VERSION = 5.0; 365 | }; 366 | name = Release; 367 | }; 368 | /* End XCBuildConfiguration section */ 369 | 370 | /* Begin XCConfigurationList section */ 371 | C3139908280B06E0002C2B4D /* Build configuration list for PBXProject "PhoneInternationaliser" */ = { 372 | isa = XCConfigurationList; 373 | buildConfigurations = ( 374 | C313991A280B06E1002C2B4D /* Debug */, 375 | C313991B280B06E1002C2B4D /* Release */, 376 | ); 377 | defaultConfigurationIsVisible = 0; 378 | defaultConfigurationName = Release; 379 | }; 380 | C313991C280B06E1002C2B4D /* Build configuration list for PBXNativeTarget "PhoneInternationaliser" */ = { 381 | isa = XCConfigurationList; 382 | buildConfigurations = ( 383 | C313991D280B06E1002C2B4D /* Debug */, 384 | C313991E280B06E1002C2B4D /* Release */, 385 | ); 386 | defaultConfigurationIsVisible = 0; 387 | defaultConfigurationName = Release; 388 | }; 389 | /* End XCConfigurationList section */ 390 | }; 391 | rootObject = C3139905280B06E0002C2B4D /* Project object */; 392 | } 393 | -------------------------------------------------------------------------------- /PhoneInternationaliser.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /PhoneInternationaliser.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /PhoneInternationaliser/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // PhoneInternationaliser 4 | // 5 | // Created by Ayden Panhuyzen on 2022-04-16. 6 | // 7 | 8 | import Cocoa 9 | 10 | @main 11 | class AppDelegate: NSObject, NSApplicationDelegate { 12 | func applicationDidFinishLaunching(_ aNotification: Notification) { 13 | // Insert code here to initialize your application 14 | } 15 | 16 | func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { 17 | return true 18 | } 19 | } 20 | 21 | -------------------------------------------------------------------------------- /PhoneInternationaliser/Assets.xcassets/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "idiom" : "universal" 5 | } 6 | ], 7 | "info" : { 8 | "author" : "xcode", 9 | "version" : 1 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /PhoneInternationaliser/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "mac", 5 | "scale" : "1x", 6 | "size" : "16x16" 7 | }, 8 | { 9 | "idiom" : "mac", 10 | "scale" : "2x", 11 | "size" : "16x16" 12 | }, 13 | { 14 | "idiom" : "mac", 15 | "scale" : "1x", 16 | "size" : "32x32" 17 | }, 18 | { 19 | "idiom" : "mac", 20 | "scale" : "2x", 21 | "size" : "32x32" 22 | }, 23 | { 24 | "idiom" : "mac", 25 | "scale" : "1x", 26 | "size" : "128x128" 27 | }, 28 | { 29 | "idiom" : "mac", 30 | "scale" : "2x", 31 | "size" : "128x128" 32 | }, 33 | { 34 | "idiom" : "mac", 35 | "scale" : "1x", 36 | "size" : "256x256" 37 | }, 38 | { 39 | "idiom" : "mac", 40 | "scale" : "2x", 41 | "size" : "256x256" 42 | }, 43 | { 44 | "idiom" : "mac", 45 | "scale" : "1x", 46 | "size" : "512x512" 47 | }, 48 | { 49 | "idiom" : "mac", 50 | "scale" : "2x", 51 | "size" : "512x512" 52 | } 53 | ], 54 | "info" : { 55 | "author" : "xcode", 56 | "version" : 1 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /PhoneInternationaliser/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /PhoneInternationaliser/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 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 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 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 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 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | 236 | 237 | 238 | 239 | 240 | 241 | 242 | 243 | 244 | 245 | 246 | 247 | 248 | 249 | 250 | 251 | 252 | 253 | 254 | 255 | 256 | 257 | 258 | 259 | 260 | 261 | 262 | 263 | 264 | 265 | 266 | 267 | 268 | 269 | 270 | 271 | 272 | 273 | 274 | 275 | 276 | 277 | 278 | 279 | 280 | 281 | 282 | 283 | 284 | 285 | 286 | 287 | 288 | 289 | 290 | 291 | 292 | 293 | 294 | 295 | 296 | 297 | 298 | 299 | 300 | 301 | 302 | 303 | 304 | 305 | 306 | 307 | 308 | 309 | 310 | 311 | 312 | 313 | 314 | 315 | 316 | 317 | 318 | 319 | 320 | 321 | 322 | 323 | 324 | 325 | 326 | 327 | 328 | 329 | 330 | 331 | 332 | 333 | 334 | 335 | 336 | 337 | 338 | 339 | 340 | 341 | 342 | 343 | 344 | 345 | 346 | 347 | 348 | 349 | 350 | 351 | 352 | 353 | 354 | 355 | 356 | 357 | 358 | 359 | 360 | 361 | 362 | 363 | 364 | 365 | 366 | 367 | 368 | 369 | 370 | 371 | 372 | Convert all local phone numbers in your Mac's contacts database to international format, so they continue to work if you move countries. 373 | 374 | 375 | 376 | 377 | 378 | 379 | 380 | 381 | 382 | 383 | 384 | 385 | 386 | 387 | 388 | 389 | 390 | 391 | 392 | 393 | 394 | 395 | 396 | 397 | 398 | 399 | 400 | 401 | 402 | 403 | 404 | 405 | 406 | Country to which local phone numbers will be converted. This is not necessarily where you are currently, but rather that of those phone numbers. 407 | 408 | 409 | 410 | 411 | 421 | 422 | 423 | 424 | 425 | 426 | 427 | 428 | 429 | 430 | 431 | 432 | 433 | 434 | 435 | 436 | 437 | 438 | 439 | 440 | 441 | 442 | 443 | 444 | 445 | 446 | 447 | 448 | 449 | 450 | 451 | 452 | 453 | 454 | 455 | 459 | 460 | 461 | 462 | 463 | 464 | 465 | 466 | 467 | 468 | 469 | 470 | 471 | 472 | 473 | 474 | 475 | 476 | 477 | 478 | 479 | 480 | 481 | 482 | 483 | 484 | 485 | 486 | 487 | 488 | 489 | 490 | -------------------------------------------------------------------------------- /PhoneInternationaliser/CallingCountry.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Countries.swift 3 | // PhoneInternationaliser 4 | // 5 | // Created by Ayden Panhuyzen on 2022-04-16. 6 | // 7 | 8 | import Foundation 9 | 10 | struct CallingCountry { 11 | static let allCountries = Locale.isoRegionCodes 12 | .compactMap { CallingCountry(isoCode: $0) } 13 | .sorted { $0.name < $1.name } 14 | static let indexOfCurrentCountry = Locale.current.regionCode.flatMap { isoCode in 15 | allCountries.firstIndex { $0.isoCode == isoCode }.map { Int($0) } 16 | } 17 | 18 | init?(isoCode: String) { 19 | self.isoCode = isoCode 20 | self.callingCode = CNPhoneNumber.dialingCode(forISOCountryCode: isoCode) 21 | guard !callingCode.isEmpty else { return nil } 22 | } 23 | 24 | let isoCode, callingCode: String 25 | 26 | var name: String { 27 | return Locale.current.localizedString(forRegionCode: isoCode) ?? isoCode 28 | } 29 | 30 | var description: String { 31 | return "\(flagEmoji) \(name) (\(callingCode))" 32 | } 33 | 34 | var flagEmoji: String { 35 | // originally from https://stackoverflow.com/a/30403199 36 | let base : UInt32 = 127397 37 | var s = "" 38 | for v in isoCode.unicodeScalars { 39 | s.unicodeScalars.append(UnicodeScalar(base + v.value)!) 40 | } 41 | return String(s) 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /PhoneInternationaliser/ContactsManager.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ContactsManager.swift 3 | // PhoneInternationaliser 4 | // 5 | // Created by Ayden Panhuyzen on 16/04/2022. 6 | // 7 | 8 | import Foundation 9 | import Contacts 10 | 11 | class ContactsManager { 12 | private let store = CNContactStore() 13 | 14 | func requestAccess(completionHandler: @escaping (Bool, Error?) -> Void) { 15 | return store.requestAccess(for: .contacts, completionHandler: completionHandler) 16 | } 17 | 18 | var authorizationStatus: CNAuthorizationStatus { 19 | return CNContactStore.authorizationStatus(for: .contacts) 20 | } 21 | 22 | var canRead: Bool { 23 | return authorizationStatus == .authorized 24 | } 25 | 26 | func enumerateContacts(_ block: @escaping (CNContact) -> Void) throws { 27 | let keysToFetch = [CNContactFormatter.descriptorForRequiredKeys(for: .fullName), CNContactPhoneNumbersKey as CNKeyDescriptor] 28 | let request = CNContactFetchRequest(keysToFetch: keysToFetch) 29 | return try store.enumerateContacts(with: request) { contact, _ in block(contact) } 30 | } 31 | 32 | func update(contact: CNMutableContact) throws { 33 | let request = CNSaveRequest() 34 | request.update(contact) 35 | try store.execute(request) 36 | } 37 | } 38 | 39 | -------------------------------------------------------------------------------- /PhoneInternationaliser/Extensions or Categories/CNPhoneNumber+Classification.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CNPhoneNumber+IsLocal.swift 3 | // PhoneInternationaliser 4 | // 5 | // Created by Ayden Panhuyzen on 16/04/2022. 6 | // 7 | 8 | import Foundation 9 | import Contacts 10 | 11 | extension CNPhoneNumber { 12 | var isSpecialPhoneNumber: Bool { 13 | let digitsWithoutDialingCode = digitsRemovingDialingCode() 14 | return digitsWithoutDialingCode.hasPrefix("#") || digitsWithoutDialingCode.hasPrefix("*") || digitsWithoutDialingCode.count < 7 15 | } 16 | 17 | func isLocalPhoneNumber(inRegionWithDialingCode dialingCode: String) -> Bool { 18 | let dialingCodeWithoutPlus = dialingCode.replacingOccurrences(of: "+", with: "") 19 | let digitsWithoutDialingCode = digitsRemovingDialingCode() 20 | return !isSpecialPhoneNumber && !digitsWithoutDialingCode.hasPrefix(dialingCodeWithoutPlus) && digits == digitsWithoutDialingCode 21 | } 22 | 23 | func convertedToInternationalPhoneNumber(inRegionWithISOCode regionISOCode: String) -> CNPhoneNumber? { 24 | let dialingCode = CNPhoneNumber.dialingCode(forISOCountryCode: regionISOCode) 25 | guard !dialingCode.isEmpty, isLocalPhoneNumber(inRegionWithDialingCode: dialingCode) else { return nil } 26 | return CNPhoneNumber(digits: dialingCode + digits, countryCode: regionISOCode) 27 | } 28 | } 29 | 30 | -------------------------------------------------------------------------------- /PhoneInternationaliser/Extensions or Categories/CNPhoneNumber+ThingsThatArePrivateForSomeReason.h: -------------------------------------------------------------------------------- 1 | // 2 | // CNPhoneNumber+ThingsThatArePrivateForSomeReason.h 3 | // PhoneInternationaliser 4 | // 5 | // Created by Ayden Panhuyzen on 2022-04-16. 6 | // 7 | 8 | #import 9 | #import 10 | 11 | NS_ASSUME_NONNULL_BEGIN 12 | 13 | @interface CNPhoneNumber (ThingsThatArePrivateForSomeReason) 14 | @property (nonatomic, readonly, copy) NSString *countryCode; 15 | @property (nonatomic, readonly, copy) NSString *digits; 16 | + (instancetype)phoneNumberWithDigits:(NSString *)digits countryCode:(NSString *)countryCode; 17 | + (NSString *)dialingCodeForISOCountryCode:(NSString *)countryCode; 18 | - (NSString *)digitsRemovingDialingCode; 19 | - (NSString *)formattedStringValue; 20 | @end 21 | 22 | NS_ASSUME_NONNULL_END 23 | -------------------------------------------------------------------------------- /PhoneInternationaliser/Logging/LogReceivingTextView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LogReceivingTextView.swift 3 | // PhoneInternationaliser 4 | // 5 | // Created by Ayden Panhuyzen on 2022-04-16. 6 | // 7 | 8 | import AppKit 9 | import os.log 10 | 11 | class LogReceivingTextView: NSTextView, UserLogReceiving { 12 | private let timeFormatter = { () -> DateFormatter in 13 | let f = DateFormatter() 14 | f.timeStyle = .medium 15 | f.dateStyle = .none 16 | return f 17 | }() 18 | 19 | func didLog(message: String, level: OSLogType) { 20 | let line = "\(timeFormatter.string(from: Date())) [\(level.title)]: \(message)" 21 | DispatchQueue.main.async { 22 | self.textStorage?.append(.init(string: "\(line)\n", attributes: [.font: NSFont.monospacedSystemFont(ofSize: 12, weight: .regular), .foregroundColor: level.colour])) 23 | } 24 | } 25 | } 26 | 27 | private extension OSLogType { 28 | var title: String { 29 | switch self { 30 | case .fault: return "FAULT" 31 | case .error: return "ERROR" 32 | case .debug: return "DEBUG" 33 | case .info: return "INFO" 34 | default: return "LOG" 35 | } 36 | } 37 | 38 | var colour: NSColor { 39 | switch self { 40 | case .fault, .error: return .systemRed 41 | case .info: return .systemBlue.withSystemEffect(.deepPressed) 42 | default: return .textColor 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /PhoneInternationaliser/Logging/UserLogger.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Logger.swift 3 | // PhoneInternationaliser 4 | // 5 | // Created by Ayden Panhuyzen on 2022-04-16. 6 | // 7 | 8 | import Foundation 9 | import os.log 10 | 11 | /** 12 | A logger that logs to system console while also publishing its messages to receivers, allowing them to be used in the UI, for example. 13 | - warning: Not thread safe. 14 | */ 15 | public class UserLogger { 16 | public static let shared = UserLogger() 17 | private let logger = OSLog(subsystem: Bundle.main.bundleIdentifier!, category: "UserFacing") 18 | 19 | private init() {} 20 | 21 | @inlinable public func log(_ message: String) { 22 | _log(message: message, level: .default) 23 | } 24 | 25 | @inlinable public func info(_ message: String) { 26 | _log(message: message, level: .info) 27 | } 28 | 29 | @inlinable public func debug(_ message: String) { 30 | _log(message: message, level: .debug) 31 | } 32 | 33 | @inlinable public func error(_ message: String) { 34 | _log(message: message, level: .error) 35 | } 36 | 37 | @inlinable public func fault(_ message: String) { 38 | _log(message: message, level: .fault) 39 | } 40 | 41 | @usableFromInline internal func _log(message: String, level: OSLogType) { 42 | // rip OSLog's formatting 43 | os_log(level, log: logger, "%{public}@", message) 44 | 45 | receivers.forEach { $0.didLog(message: message, level: level) } 46 | } 47 | 48 | // MARK: - Receivers 49 | 50 | private var _receivers = NSHashTable.weakObjects() // ugh 51 | private var receivers: [UserLogReceiving] { 52 | return _receivers.allObjects as! [UserLogReceiving] 53 | } 54 | 55 | func register(receiver: UserLogReceiving) { 56 | _receivers.add(receiver) 57 | } 58 | 59 | func deregister(receiver: UserLogReceiving) { 60 | _receivers.remove(receiver) 61 | } 62 | } 63 | 64 | protocol UserLogReceiving: AnyObject { 65 | func didLog(message: String, level: OSLogType) 66 | } 67 | 68 | // MARK: - Global Functions (to make it even easier to use) 69 | 70 | @inlinable func userLog(_ message: String) { 71 | UserLogger.shared.log(message) 72 | } 73 | 74 | @inlinable func userInfo(_ message: String) { 75 | UserLogger.shared.info(message) 76 | } 77 | 78 | @inlinable func userDebug(_ message: String) { 79 | UserLogger.shared.debug(message) 80 | } 81 | 82 | @inlinable func userError(_ message: String) { 83 | UserLogger.shared.error(message) 84 | } 85 | 86 | @inlinable func userFault(_ message: String) { 87 | UserLogger.shared.fault(message) 88 | } 89 | -------------------------------------------------------------------------------- /PhoneInternationaliser/PhoneInternationaliser-Bridging-Header.h: -------------------------------------------------------------------------------- 1 | // 2 | // Use this file to import your target's public headers that you would like to expose to Swift. 3 | // 4 | 5 | #import "CNPhoneNumber+ThingsThatArePrivateForSomeReason.h" 6 | -------------------------------------------------------------------------------- /PhoneInternationaliser/PhoneInternationaliser.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.app-sandbox 6 | 7 | com.apple.security.files.user-selected.read-only 8 | 9 | com.apple.security.personal-information.addressbook 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /PhoneInternationaliser/ViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewController.swift 3 | // PhoneInternationaliser 4 | // 5 | // Created by Ayden Panhuyzen on 2022-04-16. 6 | // 7 | 8 | import AppKit 9 | import Contacts 10 | 11 | class ViewController: NSViewController { 12 | @IBOutlet weak var countryPicker: NSPopUpButton! 13 | @IBOutlet var logArea: LogReceivingTextView! 14 | @IBOutlet weak var performButton: NSButton! 15 | let contactsManager = ContactsManager() 16 | 17 | override func viewDidLoad() { 18 | super.viewDidLoad() 19 | 20 | // Register log area to receive future log messages 21 | UserLogger.shared.register(receiver: logArea) 22 | userLog("Welcome!") 23 | loadCountries() 24 | checkContactsAccess() 25 | } 26 | 27 | @IBAction func performButtonClicked(_ sender: NSButton) { 28 | guard let selectedCountry = countryPicker.selectedItem?.representedObject as? CallingCountry else { return } 29 | 30 | if !contactsManager.canRead { 31 | contactsManager.requestAccess { (success, error) in 32 | if !success { 33 | userError("We didn't get contacts access. Error: \(error?.localizedDescription ?? "none")") 34 | } 35 | DispatchQueue.main.async { 36 | self.checkContactsAccess() 37 | } 38 | } 39 | return 40 | } 41 | 42 | sender.isEnabled = false 43 | userLog("Starting…") 44 | 45 | try! contactsManager.enumerateContacts { contact in 46 | var didChangePhoneNumbers = false 47 | 48 | let newPhoneNumbers = contact.phoneNumbers.map { (phoneNumberValue) -> CNLabeledValue in 49 | // Check if local, and convert to an international number 50 | guard phoneNumberValue.value.isLocalPhoneNumber(inRegionWithDialingCode: selectedCountry.callingCode), 51 | let intlPhoneNumber = phoneNumberValue.value.convertedToInternationalPhoneNumber(inRegionWithISOCode: selectedCountry.isoCode) else { return phoneNumberValue } 52 | 53 | let contactName = CNContactFormatter.string(from: contact, style: .fullName) ?? contact.identifier 54 | let label = phoneNumberValue.label.map { type(of: phoneNumberValue).localizedString(forLabel: $0) } ?? "unlabelled" 55 | 56 | userLog("Converting \(contactName)'s local \(label) phone number to international: \(phoneNumberValue.value.formattedStringValue()) -> \(intlPhoneNumber.formattedStringValue())") 57 | 58 | didChangePhoneNumbers = true 59 | return phoneNumberValue.settingValue(intlPhoneNumber) 60 | } 61 | 62 | if didChangePhoneNumbers { 63 | let mutableContact = contact.mutableCopy() as! CNMutableContact 64 | mutableContact.phoneNumbers = newPhoneNumbers 65 | do { 66 | try self.contactsManager.update(contact: mutableContact) 67 | } catch let error { 68 | userError("Couldn't update above contact: \(error.localizedDescription)") 69 | } 70 | } 71 | } 72 | 73 | sender.isEnabled = true 74 | } 75 | 76 | private func loadCountries() { 77 | for country in CallingCountry.allCountries { 78 | let item = countryPicker.menu?.addItem(withTitle: country.description, action: nil, keyEquivalent: "") 79 | 80 | // To make sure keyboard nav still works despite the flags in the dropdown text, we prefix it with the country name again at 0.01pt font size (so its effectively invisible) 81 | let attributedTitle = NSMutableAttributedString(string: country.name + country.description) 82 | attributedTitle.addAttribute(.font, value: NSFont.systemFont(ofSize: 0.01), range: NSRange(location: 0, length: country.name.count)) 83 | item?.attributedTitle = attributedTitle 84 | 85 | item?.representedObject = country 86 | } 87 | 88 | // Select the user's current country 89 | if let currentIndex = CallingCountry.indexOfCurrentCountry { 90 | countryPicker.selectItem(at: currentIndex) 91 | } 92 | } 93 | 94 | private func checkContactsAccess() { 95 | switch contactsManager.authorizationStatus { 96 | case .notDetermined: 97 | performButton.title = "Grant Access" 98 | performButton.isEnabled = true 99 | userInfo("Contacts access needed. Click Grant Access to give permission.") 100 | case .denied, .restricted: 101 | performButton.isEnabled = false 102 | performButton.title = "Cannot Proceed" 103 | userError("Either you or some restriction (Parental Controls, MDM, etc.) denied contacts access.") 104 | case .authorized: 105 | userInfo("Nice! We have contacts access.") 106 | performButton.title = "Update Contacts" 107 | performButton.isEnabled = true 108 | @unknown default: break 109 | } 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PhoneInternationaliser 2 | 3 | ![Screenshot of UI: showing result of operation converting locally-formatted phone numbers to internationally-formatted Canadian phone numbers](Assets/Screenshot.png) 4 | 5 | Make sure all of your Contacts' phone numbers are internationally formatted. 6 | 7 | ## Why? 8 | 9 | I'm on vacation to Europe; it's great. Unfortunately, even in Europe I still have to deal with Canadian carriers and their duopoly, meaning they can do cool things like charge [$15/day for roaming](https://www.rogers.com/customer/support/article/roam-like-home-and-roaming-support). When I got to ~$250, I decided it was time to get a prepaid SIM card instead. Unfortunately, this broke a good portion of my contacts from back home. 10 | 11 | ![A picture of a conversation in the iOS messages app with a phone number without an associated contact showing the message 'you are a broken person'](Assets/BrokenPerson.jpg) 12 | 13 | Every country has its own dialing convention for how phone numbers should be formatted (such as `(647) 123-4567` in [most of North America](https://en.wikipedia.org/wiki/North_American_Numbering_Plan) or `(0)7434 123456` in the U.K.), and a dialing prefix code for calling from other countries (e.g. most of North America's is +1, the U.K.'s is +44, Mexico's is +52, etc.). You don't typically put the international dialing code before numbers unless you have to; you're more likely to use the local convention you're used to. It turns out that when you enter a phone number to the Contacts app by hand on iOS, it stores exactly what you typed – nothing more. 14 | 15 | ![A Canadian number in the Contacts app, with a Canadian SIM Card inserted](Assets/Before.jpg) 16 | ![The same Canadian number in the Contacts app, with a British SIM Card inserted. It's broken.](Assets/Broken.jpg) 17 | 18 | For most users, this will never be a problem. However, iOS derives the prefix to add to these local numbers from the SIM card. So when I inserted a British SIM card into my phone, it started assuming that numbers I entered in Canada, such as `6471234567` should be prefixed with +44, rather than +1. Since `+446471234567` isn't a real number, a huge portion of my contacts broke. (Interestingly, iOS stores the country code if you use the 'Add new contact' functionality in the Messages app, so only about half of my contacts broke: the ones that were entered by hand). I got tired of trying to figure out who was who, so I made this app instead. 19 | 20 | It'll go through all of your contacts, figure out which ones don't have any explicit country associated with them, and prepend that country so that it works no matter your SIM card's location. `6471234567` will become `+16471234567`, so iOS doesn't think it's `+446471234567` anymore. 21 | 22 | ![The same Canadian number in the Contacts app, now prefixed with the proper country code, working no matter the SIM Card.](Assets/Fixed.jpg) 23 | 24 | Apple could fix this by implicitly storing the current country code when a new phone number is entered to the contacts app. If I enter a number without a country code while using a Canadian SIM, that's a Canadian number. It should store this alongside the number in case I switch regions in the future. Until then, this app will have to do. 25 | 26 | ## Getting started 27 | 28 | 1. Build the Xcode project, running the app 29 | 2. Select your country 30 | 3. Follow the in-app instructions --------------------------------------------------------------------------------