├── .gitignore ├── .swift-version ├── Example ├── Example.gif ├── Example.xcodeproj │ ├── project.pbxproj │ └── project.xcworkspace │ │ └── contents.xcworkspacedata └── Example │ ├── AppDelegate.swift │ ├── Assets.xcassets │ └── AppIcon.appiconset │ │ └── Contents.json │ ├── Base.lproj │ ├── LaunchScreen.storyboard │ └── Main.storyboard │ ├── Info.plist │ └── ViewController.swift ├── FormattedTextField.podspec ├── FormattedTextField.xcodeproj ├── project.pbxproj ├── project.xcworkspace │ └── contents.xcworkspacedata └── xcshareddata │ └── xcschemes │ └── FormattedTextField.xcscheme ├── FormattedTextField ├── FormattedTextField.h ├── FormattedTextField.swift ├── Info.plist ├── MaskTextFormatter.swift ├── StringExtension.swift ├── TextFormatter.swift └── UITextFieldExtension.swift ├── FormattedTextFieldTests ├── Info.plist └── MaskTextFormatterTests.swift ├── LICENSE └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | # 3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 4 | 5 | ## Build generated 6 | build/ 7 | DerivedData/ 8 | 9 | ## Various settings 10 | *.pbxuser 11 | !default.pbxuser 12 | *.mode1v3 13 | !default.mode1v3 14 | *.mode2v3 15 | !default.mode2v3 16 | *.perspectivev3 17 | !default.perspectivev3 18 | xcuserdata/ 19 | 20 | ## Other 21 | *.moved-aside 22 | *.xcuserstate 23 | 24 | ## Obj-C/Swift specific 25 | *.hmap 26 | *.ipa 27 | *.dSYM.zip 28 | *.dSYM 29 | 30 | ## Playgrounds 31 | timeline.xctimeline 32 | playground.xcworkspace 33 | 34 | # Swift Package Manager 35 | # 36 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 37 | # Packages/ 38 | .build/ 39 | 40 | # CocoaPods 41 | # 42 | # We recommend against adding the Pods directory to your .gitignore. However 43 | # you should judge for yourself, the pros and cons are mentioned at: 44 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 45 | # 46 | # Pods/ 47 | 48 | # Carthage 49 | # 50 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 51 | # Carthage/Checkouts 52 | 53 | Carthage/Build 54 | 55 | # fastlane 56 | # 57 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the 58 | # screenshots whenever they are needed. 59 | # For more information about the recommended setup visit: 60 | # https://github.com/fastlane/fastlane/blob/master/fastlane/docs/Gitignore.md 61 | 62 | fastlane/report.xml 63 | fastlane/Preview.html 64 | fastlane/screenshots 65 | fastlane/test_output 66 | -------------------------------------------------------------------------------- /.swift-version: -------------------------------------------------------------------------------- 1 | 5.0 2 | -------------------------------------------------------------------------------- /Example/Example.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/seaburg/FormattedTextField/0c07399f37a366f08c04d93c1b5e7ef0f31afe81/Example/Example.gif -------------------------------------------------------------------------------- /Example/Example.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 46; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 8F0F533B1DC1524A00791173 /* FormattedTextField.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8F0F53371DC1524A00791173 /* FormattedTextField.swift */; }; 11 | 8F0F533E1DC1524A00791173 /* UITextFieldExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8F0F533A1DC1524A00791173 /* UITextFieldExtension.swift */; }; 12 | 8F2C0C791DD7C3F700F06656 /* MaskTextFormatter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8F2C0C771DD7C3F700F06656 /* MaskTextFormatter.swift */; }; 13 | 8F2C0C7A1DD7C3F700F06656 /* TextFormatter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8F2C0C781DD7C3F700F06656 /* TextFormatter.swift */; }; 14 | 8F93C22B1DC15913007A4101 /* StringExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8F93C22A1DC15913007A4101 /* StringExtension.swift */; }; 15 | 8FD40B571DC1366F0015DB1D /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8FD40B561DC1366F0015DB1D /* AppDelegate.swift */; }; 16 | 8FD40B591DC1366F0015DB1D /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8FD40B581DC1366F0015DB1D /* ViewController.swift */; }; 17 | 8FD40B5C1DC1366F0015DB1D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 8FD40B5A1DC1366F0015DB1D /* Main.storyboard */; }; 18 | 8FD40B5E1DC1366F0015DB1D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 8FD40B5D1DC1366F0015DB1D /* Assets.xcassets */; }; 19 | 8FD40B611DC1366F0015DB1D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 8FD40B5F1DC1366F0015DB1D /* LaunchScreen.storyboard */; }; 20 | /* End PBXBuildFile section */ 21 | 22 | /* Begin PBXFileReference section */ 23 | 8F0F53371DC1524A00791173 /* FormattedTextField.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FormattedTextField.swift; sourceTree = ""; }; 24 | 8F0F533A1DC1524A00791173 /* UITextFieldExtension.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UITextFieldExtension.swift; sourceTree = ""; }; 25 | 8F2C0C771DD7C3F700F06656 /* MaskTextFormatter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MaskTextFormatter.swift; sourceTree = ""; }; 26 | 8F2C0C781DD7C3F700F06656 /* TextFormatter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TextFormatter.swift; sourceTree = ""; }; 27 | 8F93C22A1DC15913007A4101 /* StringExtension.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StringExtension.swift; sourceTree = ""; }; 28 | 8FD40B531DC1366F0015DB1D /* Example.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Example.app; sourceTree = BUILT_PRODUCTS_DIR; }; 29 | 8FD40B561DC1366F0015DB1D /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 30 | 8FD40B581DC1366F0015DB1D /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; }; 31 | 8FD40B5B1DC1366F0015DB1D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 32 | 8FD40B5D1DC1366F0015DB1D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 33 | 8FD40B601DC1366F0015DB1D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 34 | 8FD40B621DC1366F0015DB1D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 35 | /* End PBXFileReference section */ 36 | 37 | /* Begin PBXFrameworksBuildPhase section */ 38 | 8FD40B501DC1366F0015DB1D /* Frameworks */ = { 39 | isa = PBXFrameworksBuildPhase; 40 | buildActionMask = 2147483647; 41 | files = ( 42 | ); 43 | runOnlyForDeploymentPostprocessing = 0; 44 | }; 45 | /* End PBXFrameworksBuildPhase section */ 46 | 47 | /* Begin PBXGroup section */ 48 | 8F0F53351DC1524A00791173 /* FormattedTextField */ = { 49 | isa = PBXGroup; 50 | children = ( 51 | 8F2C0C771DD7C3F700F06656 /* MaskTextFormatter.swift */, 52 | 8F2C0C781DD7C3F700F06656 /* TextFormatter.swift */, 53 | 8F93C22A1DC15913007A4101 /* StringExtension.swift */, 54 | 8F0F53371DC1524A00791173 /* FormattedTextField.swift */, 55 | 8F0F533A1DC1524A00791173 /* UITextFieldExtension.swift */, 56 | ); 57 | name = FormattedTextField; 58 | path = ../../FormattedTextField; 59 | sourceTree = ""; 60 | }; 61 | 8FD40B4A1DC1366F0015DB1D = { 62 | isa = PBXGroup; 63 | children = ( 64 | 8FD40B551DC1366F0015DB1D /* Example */, 65 | 8FD40B541DC1366F0015DB1D /* Products */, 66 | ); 67 | sourceTree = ""; 68 | }; 69 | 8FD40B541DC1366F0015DB1D /* Products */ = { 70 | isa = PBXGroup; 71 | children = ( 72 | 8FD40B531DC1366F0015DB1D /* Example.app */, 73 | ); 74 | name = Products; 75 | sourceTree = ""; 76 | }; 77 | 8FD40B551DC1366F0015DB1D /* Example */ = { 78 | isa = PBXGroup; 79 | children = ( 80 | 8F0F53351DC1524A00791173 /* FormattedTextField */, 81 | 8FD40B561DC1366F0015DB1D /* AppDelegate.swift */, 82 | 8FD40B581DC1366F0015DB1D /* ViewController.swift */, 83 | 8FD40B5A1DC1366F0015DB1D /* Main.storyboard */, 84 | 8FD40B5D1DC1366F0015DB1D /* Assets.xcassets */, 85 | 8FD40B5F1DC1366F0015DB1D /* LaunchScreen.storyboard */, 86 | 8FD40B621DC1366F0015DB1D /* Info.plist */, 87 | ); 88 | path = Example; 89 | sourceTree = ""; 90 | }; 91 | /* End PBXGroup section */ 92 | 93 | /* Begin PBXNativeTarget section */ 94 | 8FD40B521DC1366F0015DB1D /* Example */ = { 95 | isa = PBXNativeTarget; 96 | buildConfigurationList = 8FD40B651DC1366F0015DB1D /* Build configuration list for PBXNativeTarget "Example" */; 97 | buildPhases = ( 98 | 8FD40B4F1DC1366F0015DB1D /* Sources */, 99 | 8FD40B501DC1366F0015DB1D /* Frameworks */, 100 | 8FD40B511DC1366F0015DB1D /* Resources */, 101 | ); 102 | buildRules = ( 103 | ); 104 | dependencies = ( 105 | ); 106 | name = Example; 107 | productName = Example; 108 | productReference = 8FD40B531DC1366F0015DB1D /* Example.app */; 109 | productType = "com.apple.product-type.application"; 110 | }; 111 | /* End PBXNativeTarget section */ 112 | 113 | /* Begin PBXProject section */ 114 | 8FD40B4B1DC1366F0015DB1D /* Project object */ = { 115 | isa = PBXProject; 116 | attributes = { 117 | LastSwiftUpdateCheck = 0800; 118 | LastUpgradeCheck = 0800; 119 | ORGANIZATIONNAME = "Evgeniy Yurtaev"; 120 | TargetAttributes = { 121 | 8FD40B521DC1366F0015DB1D = { 122 | CreatedOnToolsVersion = 8.0; 123 | ProvisioningStyle = Automatic; 124 | }; 125 | }; 126 | }; 127 | buildConfigurationList = 8FD40B4E1DC1366F0015DB1D /* Build configuration list for PBXProject "Example" */; 128 | compatibilityVersion = "Xcode 3.2"; 129 | developmentRegion = English; 130 | hasScannedForEncodings = 0; 131 | knownRegions = ( 132 | English, 133 | en, 134 | Base, 135 | ); 136 | mainGroup = 8FD40B4A1DC1366F0015DB1D; 137 | productRefGroup = 8FD40B541DC1366F0015DB1D /* Products */; 138 | projectDirPath = ""; 139 | projectRoot = ""; 140 | targets = ( 141 | 8FD40B521DC1366F0015DB1D /* Example */, 142 | ); 143 | }; 144 | /* End PBXProject section */ 145 | 146 | /* Begin PBXResourcesBuildPhase section */ 147 | 8FD40B511DC1366F0015DB1D /* Resources */ = { 148 | isa = PBXResourcesBuildPhase; 149 | buildActionMask = 2147483647; 150 | files = ( 151 | 8FD40B611DC1366F0015DB1D /* LaunchScreen.storyboard in Resources */, 152 | 8FD40B5E1DC1366F0015DB1D /* Assets.xcassets in Resources */, 153 | 8FD40B5C1DC1366F0015DB1D /* Main.storyboard in Resources */, 154 | ); 155 | runOnlyForDeploymentPostprocessing = 0; 156 | }; 157 | /* End PBXResourcesBuildPhase section */ 158 | 159 | /* Begin PBXSourcesBuildPhase section */ 160 | 8FD40B4F1DC1366F0015DB1D /* Sources */ = { 161 | isa = PBXSourcesBuildPhase; 162 | buildActionMask = 2147483647; 163 | files = ( 164 | 8F0F533E1DC1524A00791173 /* UITextFieldExtension.swift in Sources */, 165 | 8F0F533B1DC1524A00791173 /* FormattedTextField.swift in Sources */, 166 | 8F93C22B1DC15913007A4101 /* StringExtension.swift in Sources */, 167 | 8F2C0C791DD7C3F700F06656 /* MaskTextFormatter.swift in Sources */, 168 | 8FD40B591DC1366F0015DB1D /* ViewController.swift in Sources */, 169 | 8F2C0C7A1DD7C3F700F06656 /* TextFormatter.swift in Sources */, 170 | 8FD40B571DC1366F0015DB1D /* AppDelegate.swift in Sources */, 171 | ); 172 | runOnlyForDeploymentPostprocessing = 0; 173 | }; 174 | /* End PBXSourcesBuildPhase section */ 175 | 176 | /* Begin PBXVariantGroup section */ 177 | 8FD40B5A1DC1366F0015DB1D /* Main.storyboard */ = { 178 | isa = PBXVariantGroup; 179 | children = ( 180 | 8FD40B5B1DC1366F0015DB1D /* Base */, 181 | ); 182 | name = Main.storyboard; 183 | sourceTree = ""; 184 | }; 185 | 8FD40B5F1DC1366F0015DB1D /* LaunchScreen.storyboard */ = { 186 | isa = PBXVariantGroup; 187 | children = ( 188 | 8FD40B601DC1366F0015DB1D /* Base */, 189 | ); 190 | name = LaunchScreen.storyboard; 191 | sourceTree = ""; 192 | }; 193 | /* End PBXVariantGroup section */ 194 | 195 | /* Begin XCBuildConfiguration section */ 196 | 8FD40B631DC1366F0015DB1D /* Debug */ = { 197 | isa = XCBuildConfiguration; 198 | buildSettings = { 199 | ALWAYS_SEARCH_USER_PATHS = NO; 200 | CLANG_ANALYZER_NONNULL = YES; 201 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 202 | CLANG_CXX_LIBRARY = "libc++"; 203 | CLANG_ENABLE_MODULES = YES; 204 | CLANG_ENABLE_OBJC_ARC = YES; 205 | CLANG_WARN_BOOL_CONVERSION = YES; 206 | CLANG_WARN_CONSTANT_CONVERSION = YES; 207 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 208 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 209 | CLANG_WARN_EMPTY_BODY = YES; 210 | CLANG_WARN_ENUM_CONVERSION = YES; 211 | CLANG_WARN_INFINITE_RECURSION = YES; 212 | CLANG_WARN_INT_CONVERSION = YES; 213 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 214 | CLANG_WARN_SUSPICIOUS_MOVES = YES; 215 | CLANG_WARN_UNREACHABLE_CODE = YES; 216 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 217 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 218 | COPY_PHASE_STRIP = NO; 219 | DEBUG_INFORMATION_FORMAT = dwarf; 220 | ENABLE_STRICT_OBJC_MSGSEND = YES; 221 | ENABLE_TESTABILITY = YES; 222 | GCC_C_LANGUAGE_STANDARD = gnu99; 223 | GCC_DYNAMIC_NO_PIC = NO; 224 | GCC_NO_COMMON_BLOCKS = YES; 225 | GCC_OPTIMIZATION_LEVEL = 0; 226 | GCC_PREPROCESSOR_DEFINITIONS = ( 227 | "DEBUG=1", 228 | "$(inherited)", 229 | ); 230 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 231 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 232 | GCC_WARN_UNDECLARED_SELECTOR = YES; 233 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 234 | GCC_WARN_UNUSED_FUNCTION = YES; 235 | GCC_WARN_UNUSED_VARIABLE = YES; 236 | IPHONEOS_DEPLOYMENT_TARGET = 10.0; 237 | MTL_ENABLE_DEBUG_INFO = YES; 238 | ONLY_ACTIVE_ARCH = YES; 239 | SDKROOT = iphoneos; 240 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 241 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 242 | TARGETED_DEVICE_FAMILY = "1,2"; 243 | }; 244 | name = Debug; 245 | }; 246 | 8FD40B641DC1366F0015DB1D /* Release */ = { 247 | isa = XCBuildConfiguration; 248 | buildSettings = { 249 | ALWAYS_SEARCH_USER_PATHS = NO; 250 | CLANG_ANALYZER_NONNULL = YES; 251 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 252 | CLANG_CXX_LIBRARY = "libc++"; 253 | CLANG_ENABLE_MODULES = YES; 254 | CLANG_ENABLE_OBJC_ARC = YES; 255 | CLANG_WARN_BOOL_CONVERSION = YES; 256 | CLANG_WARN_CONSTANT_CONVERSION = YES; 257 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 258 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 259 | CLANG_WARN_EMPTY_BODY = YES; 260 | CLANG_WARN_ENUM_CONVERSION = YES; 261 | CLANG_WARN_INFINITE_RECURSION = YES; 262 | CLANG_WARN_INT_CONVERSION = YES; 263 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 264 | CLANG_WARN_SUSPICIOUS_MOVES = YES; 265 | CLANG_WARN_UNREACHABLE_CODE = YES; 266 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 267 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 268 | COPY_PHASE_STRIP = NO; 269 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 270 | ENABLE_NS_ASSERTIONS = NO; 271 | ENABLE_STRICT_OBJC_MSGSEND = YES; 272 | GCC_C_LANGUAGE_STANDARD = gnu99; 273 | GCC_NO_COMMON_BLOCKS = YES; 274 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 275 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 276 | GCC_WARN_UNDECLARED_SELECTOR = YES; 277 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 278 | GCC_WARN_UNUSED_FUNCTION = YES; 279 | GCC_WARN_UNUSED_VARIABLE = YES; 280 | IPHONEOS_DEPLOYMENT_TARGET = 10.0; 281 | MTL_ENABLE_DEBUG_INFO = NO; 282 | SDKROOT = iphoneos; 283 | SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; 284 | TARGETED_DEVICE_FAMILY = "1,2"; 285 | VALIDATE_PRODUCT = YES; 286 | }; 287 | name = Release; 288 | }; 289 | 8FD40B661DC1366F0015DB1D /* Debug */ = { 290 | isa = XCBuildConfiguration; 291 | buildSettings = { 292 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 293 | INFOPLIST_FILE = Example/Info.plist; 294 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 295 | PRODUCT_BUNDLE_IDENTIFIER = com.ey.Example; 296 | PRODUCT_NAME = "$(TARGET_NAME)"; 297 | SWIFT_VERSION = 5.0; 298 | }; 299 | name = Debug; 300 | }; 301 | 8FD40B671DC1366F0015DB1D /* Release */ = { 302 | isa = XCBuildConfiguration; 303 | buildSettings = { 304 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 305 | INFOPLIST_FILE = Example/Info.plist; 306 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 307 | PRODUCT_BUNDLE_IDENTIFIER = com.ey.Example; 308 | PRODUCT_NAME = "$(TARGET_NAME)"; 309 | SWIFT_VERSION = 5.0; 310 | }; 311 | name = Release; 312 | }; 313 | /* End XCBuildConfiguration section */ 314 | 315 | /* Begin XCConfigurationList section */ 316 | 8FD40B4E1DC1366F0015DB1D /* Build configuration list for PBXProject "Example" */ = { 317 | isa = XCConfigurationList; 318 | buildConfigurations = ( 319 | 8FD40B631DC1366F0015DB1D /* Debug */, 320 | 8FD40B641DC1366F0015DB1D /* Release */, 321 | ); 322 | defaultConfigurationIsVisible = 0; 323 | defaultConfigurationName = Release; 324 | }; 325 | 8FD40B651DC1366F0015DB1D /* Build configuration list for PBXNativeTarget "Example" */ = { 326 | isa = XCConfigurationList; 327 | buildConfigurations = ( 328 | 8FD40B661DC1366F0015DB1D /* Debug */, 329 | 8FD40B671DC1366F0015DB1D /* Release */, 330 | ); 331 | defaultConfigurationIsVisible = 0; 332 | defaultConfigurationName = Release; 333 | }; 334 | /* End XCConfigurationList section */ 335 | }; 336 | rootObject = 8FD40B4B1DC1366F0015DB1D /* Project object */; 337 | } 338 | -------------------------------------------------------------------------------- /Example/Example.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Example/Example/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // Example 4 | // 5 | // Created by Evgeniy Yurtaev on 26/10/2016. 6 | // Copyright © 2016 Evgeniy Yurtaev. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | @UIApplicationMain 12 | class AppDelegate: UIResponder, UIApplicationDelegate { 13 | 14 | var window: UIWindow? 15 | 16 | 17 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { 18 | // Override point for customization after application launch. 19 | return true 20 | } 21 | 22 | func applicationWillResignActive(_ application: UIApplication) { 23 | // 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. 24 | // Use this method to pause ongoing tasks, disable timers, and invalidate graphics rendering callbacks. Games should use this method to pause the game. 25 | } 26 | 27 | func applicationDidEnterBackground(_ application: UIApplication) { 28 | // 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. 29 | // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits. 30 | } 31 | 32 | func applicationWillEnterForeground(_ application: UIApplication) { 33 | // 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. 34 | } 35 | 36 | func applicationDidBecomeActive(_ application: UIApplication) { 37 | // 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. 38 | } 39 | 40 | func applicationWillTerminate(_ application: UIApplication) { 41 | // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:. 42 | } 43 | 44 | 45 | } 46 | 47 | -------------------------------------------------------------------------------- /Example/Example/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "size" : "20x20", 6 | "scale" : "2x" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "size" : "20x20", 11 | "scale" : "3x" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "size" : "29x29", 16 | "scale" : "2x" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "size" : "29x29", 21 | "scale" : "3x" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "size" : "40x40", 26 | "scale" : "2x" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "size" : "40x40", 31 | "scale" : "3x" 32 | }, 33 | { 34 | "idiom" : "iphone", 35 | "size" : "60x60", 36 | "scale" : "2x" 37 | }, 38 | { 39 | "idiom" : "iphone", 40 | "size" : "60x60", 41 | "scale" : "3x" 42 | }, 43 | { 44 | "idiom" : "ipad", 45 | "size" : "20x20", 46 | "scale" : "1x" 47 | }, 48 | { 49 | "idiom" : "ipad", 50 | "size" : "20x20", 51 | "scale" : "2x" 52 | }, 53 | { 54 | "idiom" : "ipad", 55 | "size" : "29x29", 56 | "scale" : "1x" 57 | }, 58 | { 59 | "idiom" : "ipad", 60 | "size" : "29x29", 61 | "scale" : "2x" 62 | }, 63 | { 64 | "idiom" : "ipad", 65 | "size" : "40x40", 66 | "scale" : "1x" 67 | }, 68 | { 69 | "idiom" : "ipad", 70 | "size" : "40x40", 71 | "scale" : "2x" 72 | }, 73 | { 74 | "idiom" : "ipad", 75 | "size" : "76x76", 76 | "scale" : "1x" 77 | }, 78 | { 79 | "idiom" : "ipad", 80 | "size" : "76x76", 81 | "scale" : "2x" 82 | }, 83 | { 84 | "idiom" : "ipad", 85 | "size" : "83.5x83.5", 86 | "scale" : "2x" 87 | }, 88 | { 89 | "idiom" : "ios-marketing", 90 | "size" : "1024x1024", 91 | "scale" : "1x" 92 | } 93 | ], 94 | "info" : { 95 | "version" : 1, 96 | "author" : "xcode" 97 | } 98 | } -------------------------------------------------------------------------------- /Example/Example/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 | -------------------------------------------------------------------------------- /Example/Example/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 | -------------------------------------------------------------------------------- /Example/Example/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 | CFBundleVersion 20 | 1 21 | LSRequiresIPhoneOS 22 | 23 | UILaunchStoryboardName 24 | LaunchScreen 25 | UIMainStoryboardFile 26 | Main 27 | UIRequiredDeviceCapabilities 28 | 29 | armv7 30 | 31 | UISupportedInterfaceOrientations 32 | 33 | UIInterfaceOrientationPortrait 34 | UIInterfaceOrientationLandscapeLeft 35 | UIInterfaceOrientationLandscapeRight 36 | 37 | UISupportedInterfaceOrientations~ipad 38 | 39 | UIInterfaceOrientationPortrait 40 | UIInterfaceOrientationPortraitUpsideDown 41 | UIInterfaceOrientationLandscapeLeft 42 | UIInterfaceOrientationLandscapeRight 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /Example/Example/ViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewController.swift 3 | // Example 4 | // 5 | // Created by Evgeniy Yurtaev on 26/10/2016. 6 | // Copyright © 2016 Evgeniy Yurtaev. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class ViewController: UIViewController, FormattedTextFieldDelegate { 12 | 13 | @IBOutlet private var textField: FormattedTextField! 14 | 15 | override func viewDidLoad() { 16 | super.viewDidLoad() 17 | textField.textAlignment = .center 18 | textField.placeholderMode = .always 19 | updateTextFieldMask() 20 | } 21 | 22 | // MARK: - Actions 23 | 24 | @IBAction private func textFieldTextChanged(_ textField: FormattedTextField) { 25 | updateTextFieldMask() 26 | } 27 | 28 | // MARK: - FormattedTextFieldDelegate 29 | 30 | func textField(_ textField: UITextField, shouldChangeUnformattedText text: String, in range: NSRange, replacementString: String) -> Bool { 31 | return (replacementString.isEmpty || Int(replacementString) != nil) 32 | } 33 | 34 | // MARK: - Private 35 | 36 | private func updateTextFieldMask() { 37 | let textMask = mask(forPhoneNumber: textField.unformattedText ?? "") ?? "+_ ___ ___ ________" 38 | let formatter = textField.textFormatter as? MaskTextFormatter 39 | if formatter?.mask != textMask { 40 | textField.textFormatter = MaskTextFormatter(mask: textMask, maskSymbol: "_") 41 | } 42 | 43 | let placeholderStartIndex = textMask.index(textMask.startIndex, offsetBy: (textField.text?.count ?? 0)) 44 | textField.placeholder = String(textMask[placeholderStartIndex...]) 45 | } 46 | 47 | private func mask(forPhoneNumber phoneNumber: String) -> String? { 48 | let masks: [(format: String, mask: String)] = [ 49 | ("1", "+_ (___) ___ ____"), 50 | ("7", "+_ (___) ___ ____"), 51 | ("44", "+__ (___) ____ ____"), 52 | ("49", "+__ (_____) ___-____"), 53 | ("54", "+__ (___) ___ ____"), 54 | ("86", "+__ (___) ___ ____"), 55 | ("358", "+___ _ ___ ___"), 56 | ] 57 | return masks.first { (mask, _) -> Bool in 58 | phoneNumber.hasPrefix(mask) 59 | }?.mask 60 | } 61 | } 62 | 63 | -------------------------------------------------------------------------------- /FormattedTextField.podspec: -------------------------------------------------------------------------------- 1 | Pod::Spec.new do |spec| 2 | spec.name = 'FormattedTextField' 3 | spec.version = '0.5' 4 | spec.summary = 'iOS formatted text field which supports symbols with variable-width encoding' 5 | spec.homepage = 'https://github.com/seaburg/FormattedTextField' 6 | spec.license = { :type => "MIT", :file => "LICENSE" } 7 | spec.author = { "Evgeniy Yurtaev" => "evgeniyyurt@gmail.com" } 8 | spec.source = { :git => 'https://github.com/seaburg/FormattedTextField.git', :tag => '0.5' } 9 | spec.source_files = 'FormattedTextField/*.swift' 10 | spec.platform = :ios, "9.0" 11 | spec.requires_arc = true 12 | end 13 | -------------------------------------------------------------------------------- /FormattedTextField.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 46; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 8F9320221DD775890097A8AF /* FormattedTextField.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 8FFF61571DBC074D0033E1FC /* FormattedTextField.framework */; }; 11 | 8F9320291DD7759D0097A8AF /* TextFormatter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8F9320281DD7759D0097A8AF /* TextFormatter.swift */; }; 12 | 8F93202B1DD776C30097A8AF /* MaskTextFormatter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8F93202A1DD776C30097A8AF /* MaskTextFormatter.swift */; }; 13 | 8FBA8C5E1DD77C2A00B12B0A /* MaskTextFormatterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8F93201F1DD775890097A8AF /* MaskTextFormatterTests.swift */; }; 14 | 8FD40B471DBEB3840015DB1D /* UITextFieldExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8FD40B461DBEB3840015DB1D /* UITextFieldExtension.swift */; }; 15 | 8FFF615C1DBC074E0033E1FC /* FormattedTextField.h in Headers */ = {isa = PBXBuildFile; fileRef = 8FFF615A1DBC074E0033E1FC /* FormattedTextField.h */; settings = {ATTRIBUTES = (Public, ); }; }; 16 | 8FFF61641DBC07C70033E1FC /* FormattedTextField.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8FFF61621DBC07C70033E1FC /* FormattedTextField.swift */; }; 17 | 8FFF61651DBC07C70033E1FC /* StringExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8FFF61631DBC07C70033E1FC /* StringExtension.swift */; }; 18 | /* End PBXBuildFile section */ 19 | 20 | /* Begin PBXContainerItemProxy section */ 21 | 8F9320231DD775890097A8AF /* PBXContainerItemProxy */ = { 22 | isa = PBXContainerItemProxy; 23 | containerPortal = 8FFF614E1DBC074D0033E1FC /* Project object */; 24 | proxyType = 1; 25 | remoteGlobalIDString = 8FFF61561DBC074D0033E1FC; 26 | remoteInfo = FormattedTextField; 27 | }; 28 | /* End PBXContainerItemProxy section */ 29 | 30 | /* Begin PBXFileReference section */ 31 | 8F93201D1DD775890097A8AF /* FormattedTextFieldTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = FormattedTextFieldTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 32 | 8F93201F1DD775890097A8AF /* MaskTextFormatterTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MaskTextFormatterTests.swift; sourceTree = ""; }; 33 | 8F9320211DD775890097A8AF /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 34 | 8F9320281DD7759D0097A8AF /* TextFormatter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TextFormatter.swift; sourceTree = ""; }; 35 | 8F93202A1DD776C30097A8AF /* MaskTextFormatter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MaskTextFormatter.swift; sourceTree = ""; }; 36 | 8FD40B461DBEB3840015DB1D /* UITextFieldExtension.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UITextFieldExtension.swift; sourceTree = ""; }; 37 | 8FFF61571DBC074D0033E1FC /* FormattedTextField.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = FormattedTextField.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 38 | 8FFF615A1DBC074E0033E1FC /* FormattedTextField.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = FormattedTextField.h; sourceTree = ""; }; 39 | 8FFF615B1DBC074E0033E1FC /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 40 | 8FFF61621DBC07C70033E1FC /* FormattedTextField.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FormattedTextField.swift; sourceTree = ""; }; 41 | 8FFF61631DBC07C70033E1FC /* StringExtension.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StringExtension.swift; sourceTree = ""; }; 42 | /* End PBXFileReference section */ 43 | 44 | /* Begin PBXFrameworksBuildPhase section */ 45 | 8F93201A1DD775890097A8AF /* Frameworks */ = { 46 | isa = PBXFrameworksBuildPhase; 47 | buildActionMask = 2147483647; 48 | files = ( 49 | 8F9320221DD775890097A8AF /* FormattedTextField.framework in Frameworks */, 50 | ); 51 | runOnlyForDeploymentPostprocessing = 0; 52 | }; 53 | 8FFF61531DBC074D0033E1FC /* Frameworks */ = { 54 | isa = PBXFrameworksBuildPhase; 55 | buildActionMask = 2147483647; 56 | files = ( 57 | ); 58 | runOnlyForDeploymentPostprocessing = 0; 59 | }; 60 | /* End PBXFrameworksBuildPhase section */ 61 | 62 | /* Begin PBXGroup section */ 63 | 8F93201E1DD775890097A8AF /* FormattedTextFieldTests */ = { 64 | isa = PBXGroup; 65 | children = ( 66 | 8F93201F1DD775890097A8AF /* MaskTextFormatterTests.swift */, 67 | 8F9320211DD775890097A8AF /* Info.plist */, 68 | ); 69 | path = FormattedTextFieldTests; 70 | sourceTree = ""; 71 | }; 72 | 8FFF614D1DBC074D0033E1FC = { 73 | isa = PBXGroup; 74 | children = ( 75 | 8FFF61591DBC074D0033E1FC /* FormattedTextField */, 76 | 8F93201E1DD775890097A8AF /* FormattedTextFieldTests */, 77 | 8FFF61581DBC074D0033E1FC /* Products */, 78 | ); 79 | sourceTree = ""; 80 | }; 81 | 8FFF61581DBC074D0033E1FC /* Products */ = { 82 | isa = PBXGroup; 83 | children = ( 84 | 8FFF61571DBC074D0033E1FC /* FormattedTextField.framework */, 85 | 8F93201D1DD775890097A8AF /* FormattedTextFieldTests.xctest */, 86 | ); 87 | name = Products; 88 | sourceTree = ""; 89 | }; 90 | 8FFF61591DBC074D0033E1FC /* FormattedTextField */ = { 91 | isa = PBXGroup; 92 | children = ( 93 | 8FFF61621DBC07C70033E1FC /* FormattedTextField.swift */, 94 | 8FFF61631DBC07C70033E1FC /* StringExtension.swift */, 95 | 8FD40B461DBEB3840015DB1D /* UITextFieldExtension.swift */, 96 | 8FFF615A1DBC074E0033E1FC /* FormattedTextField.h */, 97 | 8FFF615B1DBC074E0033E1FC /* Info.plist */, 98 | 8F9320281DD7759D0097A8AF /* TextFormatter.swift */, 99 | 8F93202A1DD776C30097A8AF /* MaskTextFormatter.swift */, 100 | ); 101 | path = FormattedTextField; 102 | sourceTree = ""; 103 | }; 104 | /* End PBXGroup section */ 105 | 106 | /* Begin PBXHeadersBuildPhase section */ 107 | 8FFF61541DBC074D0033E1FC /* Headers */ = { 108 | isa = PBXHeadersBuildPhase; 109 | buildActionMask = 2147483647; 110 | files = ( 111 | 8FFF615C1DBC074E0033E1FC /* FormattedTextField.h in Headers */, 112 | ); 113 | runOnlyForDeploymentPostprocessing = 0; 114 | }; 115 | /* End PBXHeadersBuildPhase section */ 116 | 117 | /* Begin PBXNativeTarget section */ 118 | 8F93201C1DD775890097A8AF /* FormattedTextFieldTests */ = { 119 | isa = PBXNativeTarget; 120 | buildConfigurationList = 8F9320271DD775890097A8AF /* Build configuration list for PBXNativeTarget "FormattedTextFieldTests" */; 121 | buildPhases = ( 122 | 8F9320191DD775890097A8AF /* Sources */, 123 | 8F93201A1DD775890097A8AF /* Frameworks */, 124 | 8F93201B1DD775890097A8AF /* Resources */, 125 | ); 126 | buildRules = ( 127 | ); 128 | dependencies = ( 129 | 8F9320241DD775890097A8AF /* PBXTargetDependency */, 130 | ); 131 | name = FormattedTextFieldTests; 132 | productName = FormattedTextFieldTests; 133 | productReference = 8F93201D1DD775890097A8AF /* FormattedTextFieldTests.xctest */; 134 | productType = "com.apple.product-type.bundle.unit-test"; 135 | }; 136 | 8FFF61561DBC074D0033E1FC /* FormattedTextField */ = { 137 | isa = PBXNativeTarget; 138 | buildConfigurationList = 8FFF615F1DBC074E0033E1FC /* Build configuration list for PBXNativeTarget "FormattedTextField" */; 139 | buildPhases = ( 140 | 8FFF61521DBC074D0033E1FC /* Sources */, 141 | 8FFF61531DBC074D0033E1FC /* Frameworks */, 142 | 8FFF61541DBC074D0033E1FC /* Headers */, 143 | 8FFF61551DBC074D0033E1FC /* Resources */, 144 | ); 145 | buildRules = ( 146 | ); 147 | dependencies = ( 148 | ); 149 | name = FormattedTextField; 150 | productName = FormattedTextField; 151 | productReference = 8FFF61571DBC074D0033E1FC /* FormattedTextField.framework */; 152 | productType = "com.apple.product-type.framework"; 153 | }; 154 | /* End PBXNativeTarget section */ 155 | 156 | /* Begin PBXProject section */ 157 | 8FFF614E1DBC074D0033E1FC /* Project object */ = { 158 | isa = PBXProject; 159 | attributes = { 160 | LastSwiftUpdateCheck = 0800; 161 | LastUpgradeCheck = 1020; 162 | ORGANIZATIONNAME = "Evgeniy Yurtaev"; 163 | TargetAttributes = { 164 | 8F93201C1DD775890097A8AF = { 165 | CreatedOnToolsVersion = 8.0; 166 | ProvisioningStyle = Automatic; 167 | }; 168 | 8FFF61561DBC074D0033E1FC = { 169 | CreatedOnToolsVersion = 8.0; 170 | LastSwiftMigration = 1020; 171 | ProvisioningStyle = Automatic; 172 | }; 173 | }; 174 | }; 175 | buildConfigurationList = 8FFF61511DBC074D0033E1FC /* Build configuration list for PBXProject "FormattedTextField" */; 176 | compatibilityVersion = "Xcode 3.2"; 177 | developmentRegion = en; 178 | hasScannedForEncodings = 0; 179 | knownRegions = ( 180 | en, 181 | Base, 182 | ); 183 | mainGroup = 8FFF614D1DBC074D0033E1FC; 184 | productRefGroup = 8FFF61581DBC074D0033E1FC /* Products */; 185 | projectDirPath = ""; 186 | projectRoot = ""; 187 | targets = ( 188 | 8FFF61561DBC074D0033E1FC /* FormattedTextField */, 189 | 8F93201C1DD775890097A8AF /* FormattedTextFieldTests */, 190 | ); 191 | }; 192 | /* End PBXProject section */ 193 | 194 | /* Begin PBXResourcesBuildPhase section */ 195 | 8F93201B1DD775890097A8AF /* Resources */ = { 196 | isa = PBXResourcesBuildPhase; 197 | buildActionMask = 2147483647; 198 | files = ( 199 | ); 200 | runOnlyForDeploymentPostprocessing = 0; 201 | }; 202 | 8FFF61551DBC074D0033E1FC /* Resources */ = { 203 | isa = PBXResourcesBuildPhase; 204 | buildActionMask = 2147483647; 205 | files = ( 206 | ); 207 | runOnlyForDeploymentPostprocessing = 0; 208 | }; 209 | /* End PBXResourcesBuildPhase section */ 210 | 211 | /* Begin PBXSourcesBuildPhase section */ 212 | 8F9320191DD775890097A8AF /* Sources */ = { 213 | isa = PBXSourcesBuildPhase; 214 | buildActionMask = 2147483647; 215 | files = ( 216 | 8FBA8C5E1DD77C2A00B12B0A /* MaskTextFormatterTests.swift in Sources */, 217 | ); 218 | runOnlyForDeploymentPostprocessing = 0; 219 | }; 220 | 8FFF61521DBC074D0033E1FC /* Sources */ = { 221 | isa = PBXSourcesBuildPhase; 222 | buildActionMask = 2147483647; 223 | files = ( 224 | 8FFF61651DBC07C70033E1FC /* StringExtension.swift in Sources */, 225 | 8FD40B471DBEB3840015DB1D /* UITextFieldExtension.swift in Sources */, 226 | 8FFF61641DBC07C70033E1FC /* FormattedTextField.swift in Sources */, 227 | 8F93202B1DD776C30097A8AF /* MaskTextFormatter.swift in Sources */, 228 | 8F9320291DD7759D0097A8AF /* TextFormatter.swift in Sources */, 229 | ); 230 | runOnlyForDeploymentPostprocessing = 0; 231 | }; 232 | /* End PBXSourcesBuildPhase section */ 233 | 234 | /* Begin PBXTargetDependency section */ 235 | 8F9320241DD775890097A8AF /* PBXTargetDependency */ = { 236 | isa = PBXTargetDependency; 237 | target = 8FFF61561DBC074D0033E1FC /* FormattedTextField */; 238 | targetProxy = 8F9320231DD775890097A8AF /* PBXContainerItemProxy */; 239 | }; 240 | /* End PBXTargetDependency section */ 241 | 242 | /* Begin XCBuildConfiguration section */ 243 | 8F9320251DD775890097A8AF /* Debug */ = { 244 | isa = XCBuildConfiguration; 245 | buildSettings = { 246 | INFOPLIST_FILE = FormattedTextFieldTests/Info.plist; 247 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; 248 | PRODUCT_BUNDLE_IDENTIFIER = com.ey.FormattedTextFieldTests; 249 | PRODUCT_NAME = "$(TARGET_NAME)"; 250 | SWIFT_VERSION = 4.2; 251 | }; 252 | name = Debug; 253 | }; 254 | 8F9320261DD775890097A8AF /* Release */ = { 255 | isa = XCBuildConfiguration; 256 | buildSettings = { 257 | INFOPLIST_FILE = FormattedTextFieldTests/Info.plist; 258 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; 259 | PRODUCT_BUNDLE_IDENTIFIER = com.ey.FormattedTextFieldTests; 260 | PRODUCT_NAME = "$(TARGET_NAME)"; 261 | SWIFT_VERSION = 4.2; 262 | }; 263 | name = Release; 264 | }; 265 | 8FFF615D1DBC074E0033E1FC /* Debug */ = { 266 | isa = XCBuildConfiguration; 267 | buildSettings = { 268 | ALWAYS_SEARCH_USER_PATHS = NO; 269 | CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; 270 | CLANG_ANALYZER_NONNULL = YES; 271 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 272 | CLANG_CXX_LIBRARY = "libc++"; 273 | CLANG_ENABLE_MODULES = YES; 274 | CLANG_ENABLE_OBJC_ARC = YES; 275 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 276 | CLANG_WARN_BOOL_CONVERSION = YES; 277 | CLANG_WARN_COMMA = YES; 278 | CLANG_WARN_CONSTANT_CONVERSION = YES; 279 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 280 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 281 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 282 | CLANG_WARN_EMPTY_BODY = YES; 283 | CLANG_WARN_ENUM_CONVERSION = YES; 284 | CLANG_WARN_INFINITE_RECURSION = YES; 285 | CLANG_WARN_INT_CONVERSION = YES; 286 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 287 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 288 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 289 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 290 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 291 | CLANG_WARN_STRICT_PROTOTYPES = YES; 292 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 293 | CLANG_WARN_SUSPICIOUS_MOVES = YES; 294 | CLANG_WARN_UNREACHABLE_CODE = YES; 295 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 296 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 297 | COPY_PHASE_STRIP = NO; 298 | CURRENT_PROJECT_VERSION = 1; 299 | DEBUG_INFORMATION_FORMAT = dwarf; 300 | ENABLE_STRICT_OBJC_MSGSEND = YES; 301 | ENABLE_TESTABILITY = YES; 302 | GCC_C_LANGUAGE_STANDARD = gnu99; 303 | GCC_DYNAMIC_NO_PIC = NO; 304 | GCC_NO_COMMON_BLOCKS = YES; 305 | GCC_OPTIMIZATION_LEVEL = 0; 306 | GCC_PREPROCESSOR_DEFINITIONS = ( 307 | "DEBUG=1", 308 | "$(inherited)", 309 | ); 310 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 311 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 312 | GCC_WARN_UNDECLARED_SELECTOR = YES; 313 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 314 | GCC_WARN_UNUSED_FUNCTION = YES; 315 | GCC_WARN_UNUSED_VARIABLE = YES; 316 | IPHONEOS_DEPLOYMENT_TARGET = 10.0; 317 | MTL_ENABLE_DEBUG_INFO = YES; 318 | ONLY_ACTIVE_ARCH = YES; 319 | SDKROOT = iphoneos; 320 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 321 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 322 | TARGETED_DEVICE_FAMILY = "1,2"; 323 | VERSIONING_SYSTEM = "apple-generic"; 324 | VERSION_INFO_PREFIX = ""; 325 | }; 326 | name = Debug; 327 | }; 328 | 8FFF615E1DBC074E0033E1FC /* Release */ = { 329 | isa = XCBuildConfiguration; 330 | buildSettings = { 331 | ALWAYS_SEARCH_USER_PATHS = NO; 332 | CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; 333 | CLANG_ANALYZER_NONNULL = YES; 334 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 335 | CLANG_CXX_LIBRARY = "libc++"; 336 | CLANG_ENABLE_MODULES = YES; 337 | CLANG_ENABLE_OBJC_ARC = YES; 338 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 339 | CLANG_WARN_BOOL_CONVERSION = YES; 340 | CLANG_WARN_COMMA = YES; 341 | CLANG_WARN_CONSTANT_CONVERSION = YES; 342 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 343 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 344 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 345 | CLANG_WARN_EMPTY_BODY = YES; 346 | CLANG_WARN_ENUM_CONVERSION = YES; 347 | CLANG_WARN_INFINITE_RECURSION = YES; 348 | CLANG_WARN_INT_CONVERSION = YES; 349 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 350 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 351 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 352 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 353 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 354 | CLANG_WARN_STRICT_PROTOTYPES = YES; 355 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 356 | CLANG_WARN_SUSPICIOUS_MOVES = YES; 357 | CLANG_WARN_UNREACHABLE_CODE = YES; 358 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 359 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 360 | COPY_PHASE_STRIP = NO; 361 | CURRENT_PROJECT_VERSION = 1; 362 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 363 | ENABLE_NS_ASSERTIONS = NO; 364 | ENABLE_STRICT_OBJC_MSGSEND = YES; 365 | GCC_C_LANGUAGE_STANDARD = gnu99; 366 | GCC_NO_COMMON_BLOCKS = YES; 367 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 368 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 369 | GCC_WARN_UNDECLARED_SELECTOR = YES; 370 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 371 | GCC_WARN_UNUSED_FUNCTION = YES; 372 | GCC_WARN_UNUSED_VARIABLE = YES; 373 | IPHONEOS_DEPLOYMENT_TARGET = 10.0; 374 | MTL_ENABLE_DEBUG_INFO = NO; 375 | SDKROOT = iphoneos; 376 | SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; 377 | TARGETED_DEVICE_FAMILY = "1,2"; 378 | VALIDATE_PRODUCT = YES; 379 | VERSIONING_SYSTEM = "apple-generic"; 380 | VERSION_INFO_PREFIX = ""; 381 | }; 382 | name = Release; 383 | }; 384 | 8FFF61601DBC074E0033E1FC /* Debug */ = { 385 | isa = XCBuildConfiguration; 386 | buildSettings = { 387 | CLANG_ENABLE_MODULES = YES; 388 | CODE_SIGN_IDENTITY = ""; 389 | DEFINES_MODULE = YES; 390 | DYLIB_COMPATIBILITY_VERSION = 1; 391 | DYLIB_CURRENT_VERSION = 1; 392 | DYLIB_INSTALL_NAME_BASE = "@rpath"; 393 | INFOPLIST_FILE = FormattedTextField/Info.plist; 394 | INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; 395 | IPHONEOS_DEPLOYMENT_TARGET = 9.0; 396 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; 397 | PRODUCT_BUNDLE_IDENTIFIER = com.ey.FormattedTextField; 398 | PRODUCT_NAME = "$(TARGET_NAME)"; 399 | SKIP_INSTALL = YES; 400 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 401 | SWIFT_VERSION = 5.0; 402 | }; 403 | name = Debug; 404 | }; 405 | 8FFF61611DBC074E0033E1FC /* Release */ = { 406 | isa = XCBuildConfiguration; 407 | buildSettings = { 408 | CLANG_ENABLE_MODULES = YES; 409 | CODE_SIGN_IDENTITY = ""; 410 | DEFINES_MODULE = YES; 411 | DYLIB_COMPATIBILITY_VERSION = 1; 412 | DYLIB_CURRENT_VERSION = 1; 413 | DYLIB_INSTALL_NAME_BASE = "@rpath"; 414 | INFOPLIST_FILE = FormattedTextField/Info.plist; 415 | INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; 416 | IPHONEOS_DEPLOYMENT_TARGET = 9.0; 417 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; 418 | PRODUCT_BUNDLE_IDENTIFIER = com.ey.FormattedTextField; 419 | PRODUCT_NAME = "$(TARGET_NAME)"; 420 | SKIP_INSTALL = YES; 421 | SWIFT_VERSION = 5.0; 422 | }; 423 | name = Release; 424 | }; 425 | /* End XCBuildConfiguration section */ 426 | 427 | /* Begin XCConfigurationList section */ 428 | 8F9320271DD775890097A8AF /* Build configuration list for PBXNativeTarget "FormattedTextFieldTests" */ = { 429 | isa = XCConfigurationList; 430 | buildConfigurations = ( 431 | 8F9320251DD775890097A8AF /* Debug */, 432 | 8F9320261DD775890097A8AF /* Release */, 433 | ); 434 | defaultConfigurationIsVisible = 0; 435 | defaultConfigurationName = Release; 436 | }; 437 | 8FFF61511DBC074D0033E1FC /* Build configuration list for PBXProject "FormattedTextField" */ = { 438 | isa = XCConfigurationList; 439 | buildConfigurations = ( 440 | 8FFF615D1DBC074E0033E1FC /* Debug */, 441 | 8FFF615E1DBC074E0033E1FC /* Release */, 442 | ); 443 | defaultConfigurationIsVisible = 0; 444 | defaultConfigurationName = Release; 445 | }; 446 | 8FFF615F1DBC074E0033E1FC /* Build configuration list for PBXNativeTarget "FormattedTextField" */ = { 447 | isa = XCConfigurationList; 448 | buildConfigurations = ( 449 | 8FFF61601DBC074E0033E1FC /* Debug */, 450 | 8FFF61611DBC074E0033E1FC /* Release */, 451 | ); 452 | defaultConfigurationIsVisible = 0; 453 | defaultConfigurationName = Release; 454 | }; 455 | /* End XCConfigurationList section */ 456 | }; 457 | rootObject = 8FFF614E1DBC074D0033E1FC /* Project object */; 458 | } 459 | -------------------------------------------------------------------------------- /FormattedTextField.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /FormattedTextField.xcodeproj/xcshareddata/xcschemes/FormattedTextField.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 34 | 35 | 45 | 46 | 52 | 53 | 54 | 55 | 56 | 57 | 63 | 64 | 70 | 71 | 72 | 73 | 75 | 76 | 79 | 80 | 81 | -------------------------------------------------------------------------------- /FormattedTextField/FormattedTextField.h: -------------------------------------------------------------------------------- 1 | // 2 | // FormattedTextField.h 3 | // FormattedTextField 4 | // 5 | // Created by Evgeniy Yurtaev on 22/10/2016. 6 | // Copyright © 2016 Evgeniy Yurtaev. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | //! Project version number for FormattedTextField. 12 | FOUNDATION_EXPORT double FormattedTextFieldVersionNumber; 13 | 14 | //! Project version string for FormattedTextField. 15 | FOUNDATION_EXPORT const unsigned char FormattedTextFieldVersionString[]; 16 | 17 | // In this header, you should import all the public headers of your framework using statements like #import 18 | 19 | 20 | -------------------------------------------------------------------------------- /FormattedTextField/FormattedTextField.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FormattedTextField.swift 3 | // FormattedTextField 4 | // 5 | // Created by Evgeniy Yurtaev on 16/10/2016. 6 | // 7 | // 8 | 9 | import UIKit 10 | 11 | @objc public protocol FormattedTextFieldDelegate: UITextFieldDelegate { 12 | @objc optional func textField(_ textField: UITextField, shouldChangeUnformattedText text: String, in range: NSRange, replacementString: String) -> Bool 13 | } 14 | 15 | open class FormattedTextField: UITextField { 16 | public typealias Delegate = FormattedTextFieldDelegate 17 | 18 | public enum PlaceholderMode { 19 | case whileEmpty 20 | case always 21 | } 22 | 23 | deinit { 24 | removeTarget(self, action: #selector(self.textViewEditingChanged(_:)), for: .editingChanged) 25 | } 26 | 27 | public override init(frame: CGRect) { 28 | super.init(frame: frame) 29 | commonInitFormattedTextField() 30 | } 31 | 32 | public required init?(coder aDecoder: NSCoder) { 33 | super.init(coder: aDecoder) 34 | commonInitFormattedTextField() 35 | } 36 | 37 | private func commonInitFormattedTextField() { 38 | delegateProxy.shouldChangeHandler = { [unowned self] (range, string) in 39 | return self.shouldChangeCharacters(in: range, replacementString: string) 40 | } 41 | delegateProxy.shouldClearHandler = { [unowned self] in 42 | return self.shouldClear() 43 | } 44 | delegateProxy.delegate = super.delegate 45 | super.delegate = delegateProxy 46 | 47 | if let unformattedText = unformattedText { 48 | if let formatter = textFormatter { 49 | text = formatter.formattedText(from: unformattedText) 50 | } else { 51 | text = unformattedText 52 | } 53 | } 54 | 55 | placeholderLabel.font = font 56 | placeholderLabel.textColor = UIColor(white: 170/255.0, alpha: 0.5) 57 | if let attributedPlaceholder = super.attributedPlaceholder { 58 | placeholderLabel.attributedText = attributedPlaceholder 59 | } else if let placeholder = super.placeholder { 60 | placeholderLabel.text = placeholder 61 | } 62 | addSubview(placeholderLabel) 63 | 64 | addTarget(self, action: #selector(self.textViewEditingChanged(_:)), for: .editingChanged) 65 | 66 | if #available(iOS 11, *) { 67 | if smartInsertDeleteType != .no { 68 | print("[FormattedTextField] warning: smartInsertDeleteType is unsupported"); 69 | } 70 | } 71 | } 72 | 73 | open var textFormatter: TextFromatter? { 74 | didSet(oldFormatter) { 75 | let text = (self.text ?? "") 76 | let selectedRange = selectedCharactersRange ?? text.startIndex.. CGRect { 193 | return super.editingRect(forBounds: bounds).inset(by: textRectInset) 194 | } 195 | 196 | open override func textRect(forBounds bounds: CGRect) -> CGRect { 197 | return super.textRect(forBounds: bounds).inset(by: textRectInset) 198 | } 199 | 200 | // MARK: - Private 201 | 202 | private var textRectInset: UIEdgeInsets { 203 | return isPlaceholderVisible ? UIEdgeInsets(top: 0, left: 0, bottom: 0, right: placeholderLabelWidth) : .zero 204 | } 205 | 206 | @objc private func textViewEditingChanged(_ sender: AnyObject?) { 207 | layoutPlaceholder() 208 | } 209 | 210 | private func layoutPlaceholder() { 211 | placeholderLabel.frame = placeholderFrame 212 | } 213 | 214 | private var placeholderFrame: CGRect { 215 | if !isPlaceholderVisible { 216 | return .zero 217 | } 218 | 219 | let textRect = isEditing ? editingRect(forBounds: bounds) : self.textRect(forBounds: bounds) 220 | 221 | var placeholderLabelFrame = textRect 222 | placeholderLabelFrame.size.width = placeholderLabelWidth 223 | 224 | switch textAlignment { 225 | case .center: 226 | placeholderLabelFrame.origin.x = textRect.midX + enteredTextWidth * 0.5 227 | case .left, .justified: 228 | fallthrough 229 | case .natural where UIView.userInterfaceLayoutDirection(for: semanticContentAttribute) == .leftToRight: 230 | placeholderLabelFrame.origin.x += enteredTextWidth 231 | case .right: 232 | placeholderLabelFrame.origin.x = textRect.maxX 233 | default: 234 | // TODO: Add support for right-to-left direction 235 | placeholderLabelFrame = .zero 236 | } 237 | return placeholderLabelFrame 238 | } 239 | 240 | private var isPlaceholderVisible: Bool { 241 | if placeholder?.isEmpty ?? true { 242 | return false 243 | } 244 | 245 | // Hides placeholder before text field adds scrolling text 246 | var isVisible = (placeholderAndTextRect.width - enteredTextWidth - placeholderHiddingGap >= placeholderLabelWidth) 247 | if isVisible { 248 | switch placeholderMode { 249 | case .always: 250 | isVisible = true 251 | case .whileEmpty: 252 | isVisible = unformattedText?.isEmpty ?? true 253 | } 254 | } 255 | return isVisible 256 | } 257 | 258 | private var placeholderAndTextRect: CGRect { 259 | return isEditing ? super.editingRect(forBounds: bounds) : super.textRect(forBounds: bounds) 260 | } 261 | 262 | // UITextFields adds scrolling before entered text fills all available width 263 | private var placeholderHiddingGap: CGFloat = 10 264 | 265 | private var enteredTextWidth: CGFloat { 266 | guard let text = self.text else { 267 | return 0 268 | } 269 | var attributes: [NSAttributedString.Key: Any]? = nil 270 | if let placeholderFont = font { 271 | attributes = [ .font: placeholderFont] 272 | } 273 | return (text as NSString).size(withAttributes: attributes).width 274 | } 275 | 276 | private var placeholderLabelWidth: CGFloat { 277 | return placeholderLabel.sizeThatFits(CGSize(width: CGFloat.infinity, height: CGFloat.infinity)).width 278 | } 279 | 280 | private let placeholderLabel: UILabel = UILabel() 281 | private let delegateProxy: TextFieldDelegateProxy = TextFieldDelegateProxy() 282 | 283 | private func shouldChangeCharacters(in range: NSRange, replacementString string: String) -> Bool { 284 | if let shouldChange = delegateProxy.delegate?.textField?(self, shouldChangeCharactersIn: range, replacementString: string) { 285 | if !shouldChange { 286 | return false 287 | } 288 | } 289 | let text = self.text ?? "" 290 | guard let charactersRange = text.range(fromUtf16NsRange: range) else { 291 | return false 292 | } 293 | 294 | let unformattedText: String 295 | var unformattedRange: Range 296 | if let formatter = textFormatter { 297 | (unformattedText, unformattedRange) = formatter.unformattedText(from: text, range: charactersRange) 298 | } else { 299 | unformattedText = text 300 | unformattedRange = charactersRange 301 | } 302 | 303 | let isBackspace = (string.isEmpty && unformattedRange.isEmpty) 304 | if isBackspace && unformattedRange.lowerBound != unformattedText.startIndex { 305 | unformattedRange = unformattedText.index(before: unformattedRange.lowerBound).. 324 | if let formatter = textFormatter { 325 | (formattedText, formattedRange) = formatter.formattedText(from: newUnformattedText, range: cursorPosition.. Bool { 339 | if let shouldClear = delegateProxy.delegate?.textFieldShouldClear?(self), !shouldClear { 340 | return false 341 | } 342 | unformattedText = nil 343 | sendActions(for: .editingChanged) 344 | 345 | return false 346 | } 347 | } 348 | 349 | // MARK: - TextFieldDelegateProxy 350 | 351 | private class TextFieldDelegateProxy: NSObject, UITextFieldDelegate { 352 | weak var delegate: UITextFieldDelegate? 353 | 354 | var shouldChangeHandler: ((NSRange, String) -> Bool)? 355 | var shouldClearHandler: (() -> Bool)? 356 | 357 | func textFieldShouldBeginEditing(_ textField: UITextField) -> Bool { 358 | return delegate?.textFieldShouldBeginEditing?(textField) ?? true 359 | } 360 | 361 | func textFieldDidBeginEditing(_ textField: UITextField) { 362 | delegate?.textFieldDidBeginEditing?(textField) 363 | } 364 | 365 | func textFieldShouldEndEditing(_ textField: UITextField) -> Bool { 366 | return delegate?.textFieldShouldEndEditing?(textField) ?? true 367 | } 368 | 369 | func textFieldDidEndEditing(_ textField: UITextField) { 370 | delegate?.textFieldDidEndEditing?(textField) 371 | } 372 | 373 | @available(iOS 10.0, *) 374 | func textFieldDidEndEditing(_ textField: UITextField, reason: UITextField.DidEndEditingReason) { 375 | delegate?.textFieldDidEndEditing?(textField, reason: reason) 376 | } 377 | 378 | func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool { 379 | return shouldChangeHandler?(range, string) ?? true 380 | } 381 | 382 | func textFieldShouldClear(_ textField: UITextField) -> Bool { 383 | return shouldClearHandler?() ?? true 384 | } 385 | 386 | public func textFieldShouldReturn(_ textField: UITextField) -> Bool { 387 | return delegate?.textFieldShouldReturn?(textField) ?? true 388 | } 389 | } 390 | -------------------------------------------------------------------------------- /FormattedTextField/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 | 0.4 19 | CFBundleVersion 20 | $(CURRENT_PROJECT_VERSION) 21 | NSPrincipalClass 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /FormattedTextField/MaskTextFormatter.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MaskTextFormatter.swift 3 | // FormattedTextField 4 | // 5 | // Created by Evgeniy Yurtaev on 12/11/2016. 6 | // Copyright © 2016 Evgeniy Yurtaev. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | public class MaskTextFormatter: TextFromatter { 12 | public let mask: String 13 | public let maskSymbol: Character 14 | 15 | public init(mask: String, maskSymbol: Character = "×") { 16 | self.mask = mask 17 | self.maskSymbol = maskSymbol 18 | } 19 | 20 | public func formattedText(from text: String, range: Range) -> (text: String, range: Range) { 21 | let unformattedRange = NSMakeRange( 22 | text.distance(from: text.startIndex, to: range.lowerBound), 23 | text.distance(from: range.lowerBound, to: range.upperBound) 24 | ) 25 | 26 | guard let prefixEndIndex = mask.range(of: String(maskSymbol))?.lowerBound else { 27 | return (mask, mask.startIndex..= text.count { 42 | break 43 | } 44 | let textCharacter = text[text.index(text.startIndex, offsetBy: index)] 45 | formattedText.append(textCharacter) 46 | index += 1 47 | } else { 48 | if index == NSMaxRange(unformattedRange) { 49 | formattedRange.length += 1 50 | } 51 | formattedText.append(maskCharacter) 52 | } 53 | } 54 | 55 | let lowerBound = formattedText.index(formattedText.startIndex, offsetBy: formattedRange.location) 56 | let upperBound = formattedText.index(lowerBound, offsetBy: formattedRange.length) 57 | 58 | return (String(formattedText), lowerBound..) -> (text: String, range: Range) { 62 | let formattedRange = NSMakeRange( 63 | text.distance(from: text.startIndex, to: range.lowerBound), 64 | text.distance(from: range.lowerBound, to: range.upperBound) 65 | ) 66 | 67 | var unformattedText = String() 68 | var unformattedRange = NSMakeRange(0, 0) 69 | for i in 0..<(min(mask.count, text.count)) { 70 | let index = mask.index(mask.startIndex, offsetBy: i) 71 | let maskCharacter = mask[index] 72 | if maskCharacter != maskSymbol { 73 | continue; 74 | } 75 | 76 | let textCharacter = text[text.index(text.startIndex, offsetBy: i)] 77 | unformattedText.append(textCharacter) 78 | 79 | if i < formattedRange.location { 80 | unformattedRange.location += 1 81 | } else if i < NSMaxRange(formattedRange) { 82 | unformattedRange.length += 1 83 | } 84 | } 85 | 86 | let lowerBound = unformattedText.index(unformattedText.startIndex, offsetBy: unformattedRange.location) 87 | let upperBound = unformattedText.index(lowerBound, offsetBy: unformattedRange.length) 88 | 89 | return (unformattedText, lowerBound.. Range? { 13 | guard let lowerBound = index(startIndex, offsetBy: nsrange.location, limitedBy: endIndex) else { 14 | return nil 15 | } 16 | guard let upperBound = index(lowerBound, offsetBy: nsrange.length, limitedBy: endIndex) else { 17 | return nil 18 | } 19 | 20 | return lowerBound..) -> NSRange { 24 | let location = distance(from: startIndex, to: range.lowerBound) 25 | let length = distance(from: range.lowerBound, to: range.upperBound) 26 | 27 | return NSMakeRange(location, length) 28 | } 29 | 30 | func range(fromUtf16NsRange nsrange: NSRange) -> Range? { 31 | guard let utf16LowerBound = utf16.index(utf16.startIndex, offsetBy: nsrange.location, limitedBy: utf16.endIndex) else { 32 | return nil 33 | } 34 | guard let utf16UpperBound = utf16.index(utf16LowerBound, offsetBy: nsrange.length, limitedBy: utf16.endIndex) else { 35 | return nil 36 | } 37 | 38 | guard let lowerBound = String.Index(utf16LowerBound, within: self) else { 39 | return range(fromUtf16NsRange: NSMakeRange(nsrange.location + 1, nsrange.length)) 40 | } 41 | guard let upperBound = String.Index(utf16UpperBound, within: self) else { 42 | return range(fromUtf16NsRange: NSMakeRange(nsrange.location, nsrange.length + 1)) 43 | } 44 | 45 | return lowerBound..) -> NSRange? { 49 | guard let utf16LowerBound = range.lowerBound.samePosition(in: utf16) else { 50 | return nil; 51 | } 52 | guard let utf16UpperBound = range.upperBound.samePosition(in: utf16) else { 53 | return nil; 54 | } 55 | let location = utf16.distance(from: utf16.startIndex, to: utf16LowerBound) 56 | let length = utf16.distance(from: utf16LowerBound, to: utf16UpperBound) 57 | 58 | return NSMakeRange(location, length) 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /FormattedTextField/TextFormatter.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TextFormatter.swift 3 | // FormattedTextField 4 | // 5 | // Created by Evgeniy Yurtaev on 12/11/2016. 6 | // Copyright © 2016 Evgeniy Yurtaev. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | public protocol TextFromatter { 12 | func formattedText(from text: String, range: Range) -> (text: String, range: Range) 13 | func unformattedText(from text: String, range: Range) -> (text: String, range: Range) 14 | } 15 | 16 | public extension TextFromatter { 17 | func formattedText(from text: String) -> String { 18 | return formattedText(from: text, range: text.startIndex.. String { 22 | return unformattedText(from: text, range: text.startIndex..? { 13 | get { 14 | guard let selectedTextRange = selectedTextRange else { 15 | return nil 16 | } 17 | guard let text = text else { 18 | return nil 19 | } 20 | let location = offset(from: beginningOfDocument, to: selectedTextRange.start) 21 | let length = offset(from: selectedTextRange.start, to: selectedTextRange.end) 22 | let range = text.range(fromUtf16NsRange: NSMakeRange(location, length)) 23 | 24 | return range 25 | } 26 | set(value) { 27 | guard let value = value else { 28 | selectedTextRange = nil 29 | return 30 | } 31 | guard let text = text else { 32 | return 33 | } 34 | guard let utf16Range = text.utf16Nsrange(fromRange: value) else { 35 | selectedTextRange = nil 36 | return 37 | } 38 | 39 | let from = position(from: beginningOfDocument, offset: utf16Range.location)! 40 | let to = position(from: from, offset: utf16Range.length)! 41 | selectedTextRange = textRange(from: from, to: to) 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /FormattedTextFieldTests/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 | CFBundleVersion 20 | 1 21 | 22 | 23 | -------------------------------------------------------------------------------- /FormattedTextFieldTests/MaskTextFormatterTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FormattedTextFieldTests.swift 3 | // FormattedTextFieldTests 4 | // 5 | // Created by Evgeniy Yurtaev on 12/11/2016. 6 | // Copyright © 2016 Evgeniy Yurtaev. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | @testable import FormattedTextField 11 | 12 | class FormattedTextFieldTests: XCTestCase { 13 | 14 | func testSimpleStringFormatting() { 15 | let textFormatter = MaskTextFormatter(mask: "××× ×××") 16 | let string = "123456" 17 | let unformattedRange = string.startIndex..