├── .gitignore ├── .travis.yml ├── CHANGELOG.md ├── Example ├── Podfile ├── Resources │ ├── Images.xcassets │ │ ├── AppIcon.appiconset │ │ │ └── Contents.json │ │ ├── Contents.json │ │ └── sampleOTP.imageset │ │ │ ├── Contents.json │ │ │ └── sampleOTP.png │ └── sampleOtpGif.gif ├── SampleOTP.xcodeproj │ ├── project.pbxproj │ └── xcshareddata │ │ └── xcschemes │ │ └── SampleOTP-Example.xcscheme └── SampleOTP │ ├── AppDelegate.swift │ ├── Base.lproj │ ├── LaunchScreen.xib │ └── Main.storyboard │ ├── ContentView.swift │ ├── Info.plist │ └── ViewController.swift ├── LICENSE ├── Package.swift ├── README.md ├── SampleOTP.podspec ├── Sources └── SampleOTP │ ├── Contracts.swift │ ├── SampleOTPAnimationHandler.swift │ ├── SampleOTPStyleHandler.swift │ ├── SampleOTPTextField.swift │ ├── SampleOTPView.swift │ ├── SampleOTPViewUIModel.swift │ └── SampleOTPViewWrapper.swift ├── Tests ├── SampleOTPTests │ ├── SampleOTPAnimationHandlerTests.swift │ ├── SampleOTPStyleHandlerTests.swift │ ├── SampleOTPTestPlan.xctestplan │ ├── SampleOTPTextFieldTests.swift │ ├── SampleOTPViewTests.swift │ └── SampleOTPViewUIModelTests.swift └── Supporting Files │ └── Info.plist └── _Pods.xcodeproj /.gitignore: -------------------------------------------------------------------------------- 1 | # macOS 2 | .DS_Store 3 | 4 | # Xcode 5 | build/ 6 | *.pbxuser 7 | !default.pbxuser 8 | *.mode1v3 9 | !default.mode1v3 10 | *.mode2v3 11 | !default.mode2v3 12 | *.perspectivev3 13 | !default.perspectivev3 14 | xcuserdata/ 15 | *.xccheckout 16 | *.moved-aside 17 | DerivedData 18 | *.hmap 19 | *.ipa 20 | 21 | # Bundler 22 | .bundle 23 | 24 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 25 | # Carthage/Checkouts 26 | 27 | Carthage/Build 28 | 29 | # We recommend against adding the Pods directory to your .gitignore. However 30 | # you should judge for yourself, the pros and cons are mentioned at: 31 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-ignore-the-pods-directory-in-source-control 32 | # 33 | # Note: if you ignore the Pods directory, make sure to uncomment 34 | # `pod install` in .travis.yml 35 | # 36 | # Pods/ 37 | 38 | # CocoaPods 39 | Pods/ 40 | Podfile.lock 41 | 42 | # Build artifacts 43 | *.xcworkspace 44 | *.xcuserdata 45 | DerivedData/ 46 | build/ 47 | 48 | # Other 49 | *.lock 50 | 51 | # CocoaPods Workspace 52 | *.xcworkspace/ 53 | 54 | # Local Examples 55 | SampleOTP/Assets/ 56 | SampleOTP/Classes/ 57 | 58 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | # references: 2 | # * https://www.objc.io/issues/6-build-tools/travis-ci/ 3 | # * https://github.com/supermarin/xcpretty#usage 4 | 5 | osx_image: xcode7.3 6 | language: objective-c 7 | # cache: cocoapods 8 | # podfile: Example/Podfile 9 | # before_install: 10 | # - gem install cocoapods # Since Travis is not always on latest version 11 | # - pod install --project-directory=Example 12 | script: 13 | - set -o pipefail && xcodebuild test -enableCodeCoverage YES -workspace Example/SampleOTP.xcworkspace -scheme SampleOTP-Example -sdk iphonesimulator9.3 ONLY_ACTIVE_ARCH=NO | xcpretty 14 | - pod lib lint 15 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # 📦 Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | ## [0.2.6] - 2025-05-05 6 | 7 | ### ✨ Added 8 | - ✅ SwiftUI Support via `SampleOTPViewWrapper` 9 | - ✅ Swift Package Manager (SPM) support for modern iOS integration 10 | - ✅ Full unit test coverage for core components 11 | 12 | ### 🐛 Fixed 13 | - 🧩 Mismatch between iOS deployment targets (SPM, podspec, Xcode) resolved 14 | 15 | ### 🔄 Changed 16 | - 🧾 Improved README with SwiftUI usage example 17 | -------------------------------------------------------------------------------- /Example/Podfile: -------------------------------------------------------------------------------- 1 | use_frameworks! 2 | 3 | platform :ios, '12.0' 4 | 5 | target 'SampleOTP_Example' do 6 | pod 'SampleOTP', :path => '../' 7 | 8 | target 'SampleOTP_Tests' do 9 | inherit! :search_paths 10 | 11 | 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /Example/Resources/Images.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" : "ios-marketing", 45 | "size" : "1024x1024", 46 | "scale" : "1x" 47 | } 48 | ], 49 | "info" : { 50 | "version" : 1, 51 | "author" : "xcode" 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /Example/Resources/Images.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Example/Resources/Images.xcassets/sampleOTP.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "sampleOTP.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Example/Resources/Images.xcassets/sampleOTP.imageset/sampleOTP.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BolaGamal/SampleOTP/d4e3a374b54fa2b55664a4b214f3f392fdc88bf0/Example/Resources/Images.xcassets/sampleOTP.imageset/sampleOTP.png -------------------------------------------------------------------------------- /Example/Resources/sampleOtpGif.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BolaGamal/SampleOTP/d4e3a374b54fa2b55664a4b214f3f392fdc88bf0/Example/Resources/sampleOtpGif.gif -------------------------------------------------------------------------------- /Example/SampleOTP.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 54; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 002C63D8326BFF041CAC6C87 /* Pods_SampleOTP_Tests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 28AC4841EA680FB0CD41C7CB /* Pods_SampleOTP_Tests.framework */; }; 11 | 04051ECB2DC99D6F00451D97 /* SampleOTPTextFieldTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04051EC42DC99D5A00451D97 /* SampleOTPTextFieldTests.swift */; }; 12 | 04051ECC2DC99D6F00451D97 /* SampleOTPAnimationHandlerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04051EC22DC99D5A00451D97 /* SampleOTPAnimationHandlerTests.swift */; }; 13 | 04051ECD2DC99D6F00451D97 /* SampleOTPStyleHandlerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04051EC32DC99D5A00451D97 /* SampleOTPStyleHandlerTests.swift */; }; 14 | 04051ECE2DC99D6F00451D97 /* SampleOTPViewUIModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04051EC62DC99D5A00451D97 /* SampleOTPViewUIModelTests.swift */; }; 15 | 04051ED02DC99D6F00451D97 /* SampleOTPViewTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04051EC52DC99D5A00451D97 /* SampleOTPViewTests.swift */; }; 16 | 041262042DC990FD00737FFD /* SampleOTPView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 041261FF2DC990E300737FFD /* SampleOTPView.swift */; }; 17 | 041262052DC990FD00737FFD /* SampleOTPStyleHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 041261FD2DC990E300737FFD /* SampleOTPStyleHandler.swift */; }; 18 | 041262062DC990FD00737FFD /* SampleOTPViewWrapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 041262012DC990E300737FFD /* SampleOTPViewWrapper.swift */; }; 19 | 041262082DC990FD00737FFD /* SampleOTPViewUIModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 041262002DC990E300737FFD /* SampleOTPViewUIModel.swift */; }; 20 | 041262092DC990FD00737FFD /* SampleOTPTextField.swift in Sources */ = {isa = PBXBuildFile; fileRef = 041261FE2DC990E300737FFD /* SampleOTPTextField.swift */; }; 21 | 0412620A2DC990FD00737FFD /* SampleOTPAnimationHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 041261FC2DC990E300737FFD /* SampleOTPAnimationHandler.swift */; }; 22 | 0465460B2DCC26F7001741E8 /* SampleOTPTextField.swift in Sources */ = {isa = PBXBuildFile; fileRef = 041261FE2DC990E300737FFD /* SampleOTPTextField.swift */; }; 23 | 0465460C2DCC26F7001741E8 /* SampleOTPStyleHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 041261FD2DC990E300737FFD /* SampleOTPStyleHandler.swift */; }; 24 | 0465460D2DCC26F7001741E8 /* SampleOTPAnimationHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 041261FC2DC990E300737FFD /* SampleOTPAnimationHandler.swift */; }; 25 | 0465460E2DCC26F7001741E8 /* SampleOTPViewUIModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 041262002DC990E300737FFD /* SampleOTPViewUIModel.swift */; }; 26 | 0465460F2DCC26F7001741E8 /* SampleOTPViewWrapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 041262012DC990E300737FFD /* SampleOTPViewWrapper.swift */; }; 27 | 046546102DCC2AAC001741E8 /* SampleOTPView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 041261FF2DC990E300737FFD /* SampleOTPView.swift */; }; 28 | 046546122DCE2682001741E8 /* Contracts.swift in Sources */ = {isa = PBXBuildFile; fileRef = 046546112DCE2682001741E8 /* Contracts.swift */; }; 29 | 046546132DCE2682001741E8 /* Contracts.swift in Sources */ = {isa = PBXBuildFile; fileRef = 046546112DCE2682001741E8 /* Contracts.swift */; }; 30 | 0489AC962DCE61F80009C461 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0489AC8D2DCE61F80009C461 /* AppDelegate.swift */; }; 31 | 0489AC972DCE61F80009C461 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0489AC8E2DCE61F80009C461 /* ContentView.swift */; }; 32 | 0489AC982DCE61F80009C461 /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0489AC942DCE61F80009C461 /* ViewController.swift */; }; 33 | 0489AC992DCE61F80009C461 /* LaunchScreen.xib in Resources */ = {isa = PBXBuildFile; fileRef = 0489AC912DCE61F80009C461 /* LaunchScreen.xib */; }; 34 | 0489AC9B2DCE61F80009C461 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 0489AC932DCE61F80009C461 /* Main.storyboard */; }; 35 | F72565F408477227F75B3006 /* Pods_SampleOTP_Example.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 8954C0C0B5D01B6B467E85AF /* Pods_SampleOTP_Example.framework */; }; 36 | /* End PBXBuildFile section */ 37 | 38 | /* Begin PBXContainerItemProxy section */ 39 | 607FACE61AFB9204008FA782 /* PBXContainerItemProxy */ = { 40 | isa = PBXContainerItemProxy; 41 | containerPortal = 607FACC81AFB9204008FA782 /* Project object */; 42 | proxyType = 1; 43 | remoteGlobalIDString = 607FACCF1AFB9204008FA782; 44 | remoteInfo = SampleOTP; 45 | }; 46 | /* End PBXContainerItemProxy section */ 47 | 48 | /* Begin PBXFileReference section */ 49 | 01AE13C3F8803B3DC1350A6B /* SampleOTP.podspec */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text; name = SampleOTP.podspec; path = ../SampleOTP.podspec; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.ruby; }; 50 | 04051EC22DC99D5A00451D97 /* SampleOTPAnimationHandlerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SampleOTPAnimationHandlerTests.swift; sourceTree = ""; }; 51 | 04051EC32DC99D5A00451D97 /* SampleOTPStyleHandlerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SampleOTPStyleHandlerTests.swift; sourceTree = ""; }; 52 | 04051EC42DC99D5A00451D97 /* SampleOTPTextFieldTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SampleOTPTextFieldTests.swift; sourceTree = ""; }; 53 | 04051EC52DC99D5A00451D97 /* SampleOTPViewTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SampleOTPViewTests.swift; sourceTree = ""; }; 54 | 04051EC62DC99D5A00451D97 /* SampleOTPViewUIModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SampleOTPViewUIModelTests.swift; sourceTree = ""; }; 55 | 041261FC2DC990E300737FFD /* SampleOTPAnimationHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SampleOTPAnimationHandler.swift; sourceTree = ""; }; 56 | 041261FD2DC990E300737FFD /* SampleOTPStyleHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SampleOTPStyleHandler.swift; sourceTree = ""; }; 57 | 041261FE2DC990E300737FFD /* SampleOTPTextField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SampleOTPTextField.swift; sourceTree = ""; }; 58 | 041261FF2DC990E300737FFD /* SampleOTPView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SampleOTPView.swift; sourceTree = ""; }; 59 | 041262002DC990E300737FFD /* SampleOTPViewUIModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SampleOTPViewUIModel.swift; sourceTree = ""; }; 60 | 041262012DC990E300737FFD /* SampleOTPViewWrapper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SampleOTPViewWrapper.swift; sourceTree = ""; }; 61 | 0425B8172DCA538E0024D2C5 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 62 | 046546112DCE2682001741E8 /* Contracts.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Contracts.swift; sourceTree = ""; }; 63 | 046B55B02DCC1E7C009742AD /* SampleOTPTestPlan.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; path = SampleOTPTestPlan.xctestplan; sourceTree = ""; }; 64 | 047DCDC72DCF61A0006AC25D /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Images.xcassets; sourceTree = ""; }; 65 | 047DCDC82DCF61A0006AC25D /* sampleOtpGif.gif */ = {isa = PBXFileReference; lastKnownFileType = image.gif; path = sampleOtpGif.gif; sourceTree = ""; }; 66 | 0489AC8D2DCE61F80009C461 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 67 | 0489AC8E2DCE61F80009C461 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; 68 | 0489AC8F2DCE61F80009C461 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 69 | 0489AC902DCE61F80009C461 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/LaunchScreen.xib; sourceTree = ""; }; 70 | 0489AC922DCE61F80009C461 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 71 | 0489AC942DCE61F80009C461 /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; }; 72 | 04CF507D2DCA905100D5EBBD /* Package.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = Package.swift; path = ../Package.swift; sourceTree = SOURCE_ROOT; }; 73 | 103778E1A9F2A78279120E52 /* Pods-SampleOTP_Example.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-SampleOTP_Example.debug.xcconfig"; path = "Target Support Files/Pods-SampleOTP_Example/Pods-SampleOTP_Example.debug.xcconfig"; sourceTree = ""; }; 74 | 1A111F879D13CB04C24CC697 /* Pods-SampleOTP_Example.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-SampleOTP_Example.release.xcconfig"; path = "Target Support Files/Pods-SampleOTP_Example/Pods-SampleOTP_Example.release.xcconfig"; sourceTree = ""; }; 75 | 28AC4841EA680FB0CD41C7CB /* Pods_SampleOTP_Tests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_SampleOTP_Tests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 76 | 607FACD01AFB9204008FA782 /* SampleOTP_Example.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = SampleOTP_Example.app; sourceTree = BUILT_PRODUCTS_DIR; }; 77 | 607FACE51AFB9204008FA782 /* SampleOTP_Tests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = SampleOTP_Tests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 78 | 625B4CAD85FA61CD74101E8F /* README.md */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = net.daringfireball.markdown; name = README.md; path = ../README.md; sourceTree = ""; }; 79 | 83D40776495930CA6C589484 /* LICENSE */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text; name = LICENSE; path = ../LICENSE; sourceTree = ""; }; 80 | 8954C0C0B5D01B6B467E85AF /* Pods_SampleOTP_Example.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_SampleOTP_Example.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 81 | 94E1B135F19D8CF97EA9A08F /* Pods-SampleOTP_Tests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-SampleOTP_Tests.debug.xcconfig"; path = "Target Support Files/Pods-SampleOTP_Tests/Pods-SampleOTP_Tests.debug.xcconfig"; sourceTree = ""; }; 82 | 9AE40BABF5BA91EBE7151059 /* Pods-SampleOTP_Tests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-SampleOTP_Tests.release.xcconfig"; path = "Target Support Files/Pods-SampleOTP_Tests/Pods-SampleOTP_Tests.release.xcconfig"; sourceTree = ""; }; 83 | /* End PBXFileReference section */ 84 | 85 | /* Begin PBXFrameworksBuildPhase section */ 86 | 607FACCD1AFB9204008FA782 /* Frameworks */ = { 87 | isa = PBXFrameworksBuildPhase; 88 | buildActionMask = 2147483647; 89 | files = ( 90 | F72565F408477227F75B3006 /* Pods_SampleOTP_Example.framework in Frameworks */, 91 | ); 92 | runOnlyForDeploymentPostprocessing = 0; 93 | }; 94 | 607FACE21AFB9204008FA782 /* Frameworks */ = { 95 | isa = PBXFrameworksBuildPhase; 96 | buildActionMask = 2147483647; 97 | files = ( 98 | 002C63D8326BFF041CAC6C87 /* Pods_SampleOTP_Tests.framework in Frameworks */, 99 | ); 100 | runOnlyForDeploymentPostprocessing = 0; 101 | }; 102 | /* End PBXFrameworksBuildPhase section */ 103 | 104 | /* Begin PBXGroup section */ 105 | 04051EC72DC99D5A00451D97 /* SampleOTPTests */ = { 106 | isa = PBXGroup; 107 | children = ( 108 | 046B55B02DCC1E7C009742AD /* SampleOTPTestPlan.xctestplan */, 109 | 04051EC22DC99D5A00451D97 /* SampleOTPAnimationHandlerTests.swift */, 110 | 04051EC32DC99D5A00451D97 /* SampleOTPStyleHandlerTests.swift */, 111 | 04051EC42DC99D5A00451D97 /* SampleOTPTextFieldTests.swift */, 112 | 04051EC52DC99D5A00451D97 /* SampleOTPViewTests.swift */, 113 | 04051EC62DC99D5A00451D97 /* SampleOTPViewUIModelTests.swift */, 114 | ); 115 | path = SampleOTPTests; 116 | sourceTree = ""; 117 | }; 118 | 04051ECA2DC99D5A00451D97 /* Tests */ = { 119 | isa = PBXGroup; 120 | children = ( 121 | 0425B8152DCA53200024D2C5 /* Supporting Files */, 122 | 04051EC72DC99D5A00451D97 /* SampleOTPTests */, 123 | ); 124 | name = Tests; 125 | path = ../Tests; 126 | sourceTree = SOURCE_ROOT; 127 | }; 128 | 041262022DC990E300737FFD /* SampleOTP */ = { 129 | isa = PBXGroup; 130 | children = ( 131 | 046546112DCE2682001741E8 /* Contracts.swift */, 132 | 041261FC2DC990E300737FFD /* SampleOTPAnimationHandler.swift */, 133 | 041261FD2DC990E300737FFD /* SampleOTPStyleHandler.swift */, 134 | 041261FE2DC990E300737FFD /* SampleOTPTextField.swift */, 135 | 041261FF2DC990E300737FFD /* SampleOTPView.swift */, 136 | 041262002DC990E300737FFD /* SampleOTPViewUIModel.swift */, 137 | 041262012DC990E300737FFD /* SampleOTPViewWrapper.swift */, 138 | ); 139 | path = SampleOTP; 140 | sourceTree = ""; 141 | }; 142 | 041262032DC990E300737FFD /* Sources */ = { 143 | isa = PBXGroup; 144 | children = ( 145 | 041262022DC990E300737FFD /* SampleOTP */, 146 | ); 147 | name = Sources; 148 | path = ../Sources; 149 | sourceTree = SOURCE_ROOT; 150 | }; 151 | 0425B8152DCA53200024D2C5 /* Supporting Files */ = { 152 | isa = PBXGroup; 153 | children = ( 154 | 0425B8172DCA538E0024D2C5 /* Info.plist */, 155 | ); 156 | path = "Supporting Files"; 157 | sourceTree = ""; 158 | }; 159 | 047DCDC92DCF61A0006AC25D /* Resources */ = { 160 | isa = PBXGroup; 161 | children = ( 162 | 047DCDC72DCF61A0006AC25D /* Images.xcassets */, 163 | 047DCDC82DCF61A0006AC25D /* sampleOtpGif.gif */, 164 | ); 165 | path = Resources; 166 | sourceTree = ""; 167 | }; 168 | 0489AC8C2DCE61640009C461 /* Example */ = { 169 | isa = PBXGroup; 170 | children = ( 171 | 047DCDC92DCF61A0006AC25D /* Resources */, 172 | 0489AC952DCE61F80009C461 /* SampleOTP */, 173 | ); 174 | name = Example; 175 | sourceTree = SOURCE_ROOT; 176 | }; 177 | 0489AC952DCE61F80009C461 /* SampleOTP */ = { 178 | isa = PBXGroup; 179 | children = ( 180 | 0489AC8D2DCE61F80009C461 /* AppDelegate.swift */, 181 | 0489AC8E2DCE61F80009C461 /* ContentView.swift */, 182 | 0489AC8F2DCE61F80009C461 /* Info.plist */, 183 | 0489AC912DCE61F80009C461 /* LaunchScreen.xib */, 184 | 0489AC932DCE61F80009C461 /* Main.storyboard */, 185 | 0489AC942DCE61F80009C461 /* ViewController.swift */, 186 | ); 187 | path = SampleOTP; 188 | sourceTree = ""; 189 | }; 190 | 607FACC71AFB9204008FA782 = { 191 | isa = PBXGroup; 192 | children = ( 193 | 0489AC8C2DCE61640009C461 /* Example */, 194 | 041262032DC990E300737FFD /* Sources */, 195 | 607FACF51AFB993E008FA782 /* Podspec Metadata */, 196 | 04051ECA2DC99D5A00451D97 /* Tests */, 197 | 607FACD11AFB9204008FA782 /* Products */, 198 | A757BFAA6E256000D514EFDA /* Pods */, 199 | C96C752615DDA74AEBDF3285 /* Frameworks */, 200 | ); 201 | sourceTree = ""; 202 | }; 203 | 607FACD11AFB9204008FA782 /* Products */ = { 204 | isa = PBXGroup; 205 | children = ( 206 | 607FACD01AFB9204008FA782 /* SampleOTP_Example.app */, 207 | 607FACE51AFB9204008FA782 /* SampleOTP_Tests.xctest */, 208 | ); 209 | name = Products; 210 | sourceTree = ""; 211 | }; 212 | 607FACF51AFB993E008FA782 /* Podspec Metadata */ = { 213 | isa = PBXGroup; 214 | children = ( 215 | 04CF507D2DCA905100D5EBBD /* Package.swift */, 216 | 01AE13C3F8803B3DC1350A6B /* SampleOTP.podspec */, 217 | 625B4CAD85FA61CD74101E8F /* README.md */, 218 | 83D40776495930CA6C589484 /* LICENSE */, 219 | ); 220 | name = "Podspec Metadata"; 221 | sourceTree = ""; 222 | }; 223 | A757BFAA6E256000D514EFDA /* Pods */ = { 224 | isa = PBXGroup; 225 | children = ( 226 | 103778E1A9F2A78279120E52 /* Pods-SampleOTP_Example.debug.xcconfig */, 227 | 1A111F879D13CB04C24CC697 /* Pods-SampleOTP_Example.release.xcconfig */, 228 | 94E1B135F19D8CF97EA9A08F /* Pods-SampleOTP_Tests.debug.xcconfig */, 229 | 9AE40BABF5BA91EBE7151059 /* Pods-SampleOTP_Tests.release.xcconfig */, 230 | ); 231 | path = Pods; 232 | sourceTree = ""; 233 | }; 234 | C96C752615DDA74AEBDF3285 /* Frameworks */ = { 235 | isa = PBXGroup; 236 | children = ( 237 | 8954C0C0B5D01B6B467E85AF /* Pods_SampleOTP_Example.framework */, 238 | 28AC4841EA680FB0CD41C7CB /* Pods_SampleOTP_Tests.framework */, 239 | ); 240 | name = Frameworks; 241 | sourceTree = ""; 242 | }; 243 | /* End PBXGroup section */ 244 | 245 | /* Begin PBXNativeTarget section */ 246 | 607FACCF1AFB9204008FA782 /* SampleOTP_Example */ = { 247 | isa = PBXNativeTarget; 248 | buildConfigurationList = 607FACEF1AFB9204008FA782 /* Build configuration list for PBXNativeTarget "SampleOTP_Example" */; 249 | buildPhases = ( 250 | 05B37802A2606AB75E940A7E /* [CP] Check Pods Manifest.lock */, 251 | 607FACCC1AFB9204008FA782 /* Sources */, 252 | 607FACCD1AFB9204008FA782 /* Frameworks */, 253 | 607FACCE1AFB9204008FA782 /* Resources */, 254 | BCF4A51D07D2AB42C3499391 /* [CP] Embed Pods Frameworks */, 255 | ); 256 | buildRules = ( 257 | ); 258 | dependencies = ( 259 | ); 260 | name = SampleOTP_Example; 261 | productName = SampleOTP; 262 | productReference = 607FACD01AFB9204008FA782 /* SampleOTP_Example.app */; 263 | productType = "com.apple.product-type.application"; 264 | }; 265 | 607FACE41AFB9204008FA782 /* SampleOTP_Tests */ = { 266 | isa = PBXNativeTarget; 267 | buildConfigurationList = 607FACF21AFB9204008FA782 /* Build configuration list for PBXNativeTarget "SampleOTP_Tests" */; 268 | buildPhases = ( 269 | A60150D16D51B841CABA6010 /* [CP] Check Pods Manifest.lock */, 270 | 607FACE11AFB9204008FA782 /* Sources */, 271 | 607FACE21AFB9204008FA782 /* Frameworks */, 272 | 607FACE31AFB9204008FA782 /* Resources */, 273 | ); 274 | buildRules = ( 275 | ); 276 | dependencies = ( 277 | 607FACE71AFB9204008FA782 /* PBXTargetDependency */, 278 | ); 279 | name = SampleOTP_Tests; 280 | productName = Tests; 281 | productReference = 607FACE51AFB9204008FA782 /* SampleOTP_Tests.xctest */; 282 | productType = "com.apple.product-type.bundle.unit-test"; 283 | }; 284 | /* End PBXNativeTarget section */ 285 | 286 | /* Begin PBXProject section */ 287 | 607FACC81AFB9204008FA782 /* Project object */ = { 288 | isa = PBXProject; 289 | attributes = { 290 | BuildIndependentTargetsInParallel = YES; 291 | LastSwiftUpdateCheck = 0830; 292 | LastUpgradeCheck = 1630; 293 | ORGANIZATIONNAME = CocoaPods; 294 | TargetAttributes = { 295 | 607FACCF1AFB9204008FA782 = { 296 | CreatedOnToolsVersion = 6.3.1; 297 | LastSwiftMigration = 0900; 298 | }; 299 | 607FACE41AFB9204008FA782 = { 300 | CreatedOnToolsVersion = 6.3.1; 301 | LastSwiftMigration = 0900; 302 | TestTargetID = 607FACCF1AFB9204008FA782; 303 | }; 304 | }; 305 | }; 306 | buildConfigurationList = 607FACCB1AFB9204008FA782 /* Build configuration list for PBXProject "SampleOTP" */; 307 | compatibilityVersion = "Xcode 3.2"; 308 | developmentRegion = en; 309 | hasScannedForEncodings = 0; 310 | knownRegions = ( 311 | en, 312 | Base, 313 | ); 314 | mainGroup = 607FACC71AFB9204008FA782; 315 | productRefGroup = 607FACD11AFB9204008FA782 /* Products */; 316 | projectDirPath = ""; 317 | projectRoot = ""; 318 | targets = ( 319 | 607FACCF1AFB9204008FA782 /* SampleOTP_Example */, 320 | 607FACE41AFB9204008FA782 /* SampleOTP_Tests */, 321 | ); 322 | }; 323 | /* End PBXProject section */ 324 | 325 | /* Begin PBXResourcesBuildPhase section */ 326 | 607FACCE1AFB9204008FA782 /* Resources */ = { 327 | isa = PBXResourcesBuildPhase; 328 | buildActionMask = 2147483647; 329 | files = ( 330 | 0489AC992DCE61F80009C461 /* LaunchScreen.xib in Resources */, 331 | 0489AC9B2DCE61F80009C461 /* Main.storyboard in Resources */, 332 | ); 333 | runOnlyForDeploymentPostprocessing = 0; 334 | }; 335 | 607FACE31AFB9204008FA782 /* Resources */ = { 336 | isa = PBXResourcesBuildPhase; 337 | buildActionMask = 2147483647; 338 | files = ( 339 | ); 340 | runOnlyForDeploymentPostprocessing = 0; 341 | }; 342 | /* End PBXResourcesBuildPhase section */ 343 | 344 | /* Begin PBXShellScriptBuildPhase section */ 345 | 05B37802A2606AB75E940A7E /* [CP] Check Pods Manifest.lock */ = { 346 | isa = PBXShellScriptBuildPhase; 347 | buildActionMask = 2147483647; 348 | files = ( 349 | ); 350 | inputFileListPaths = ( 351 | ); 352 | inputPaths = ( 353 | "${PODS_PODFILE_DIR_PATH}/Podfile.lock", 354 | "${PODS_ROOT}/Manifest.lock", 355 | ); 356 | name = "[CP] Check Pods Manifest.lock"; 357 | outputFileListPaths = ( 358 | ); 359 | outputPaths = ( 360 | "$(DERIVED_FILE_DIR)/Pods-SampleOTP_Example-checkManifestLockResult.txt", 361 | ); 362 | runOnlyForDeploymentPostprocessing = 0; 363 | shellPath = /bin/sh; 364 | shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; 365 | showEnvVarsInLog = 0; 366 | }; 367 | A60150D16D51B841CABA6010 /* [CP] Check Pods Manifest.lock */ = { 368 | isa = PBXShellScriptBuildPhase; 369 | buildActionMask = 2147483647; 370 | files = ( 371 | ); 372 | inputFileListPaths = ( 373 | ); 374 | inputPaths = ( 375 | "${PODS_PODFILE_DIR_PATH}/Podfile.lock", 376 | "${PODS_ROOT}/Manifest.lock", 377 | ); 378 | name = "[CP] Check Pods Manifest.lock"; 379 | outputFileListPaths = ( 380 | ); 381 | outputPaths = ( 382 | "$(DERIVED_FILE_DIR)/Pods-SampleOTP_Tests-checkManifestLockResult.txt", 383 | ); 384 | runOnlyForDeploymentPostprocessing = 0; 385 | shellPath = /bin/sh; 386 | shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; 387 | showEnvVarsInLog = 0; 388 | }; 389 | BCF4A51D07D2AB42C3499391 /* [CP] Embed Pods Frameworks */ = { 390 | isa = PBXShellScriptBuildPhase; 391 | buildActionMask = 2147483647; 392 | files = ( 393 | ); 394 | inputPaths = ( 395 | "${PODS_ROOT}/Target Support Files/Pods-SampleOTP_Example/Pods-SampleOTP_Example-frameworks.sh", 396 | "${BUILT_PRODUCTS_DIR}/SampleOTP/SampleOTP.framework", 397 | ); 398 | name = "[CP] Embed Pods Frameworks"; 399 | outputPaths = ( 400 | "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/SampleOTP.framework", 401 | ); 402 | runOnlyForDeploymentPostprocessing = 0; 403 | shellPath = /bin/sh; 404 | shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-SampleOTP_Example/Pods-SampleOTP_Example-frameworks.sh\"\n"; 405 | showEnvVarsInLog = 0; 406 | }; 407 | /* End PBXShellScriptBuildPhase section */ 408 | 409 | /* Begin PBXSourcesBuildPhase section */ 410 | 607FACCC1AFB9204008FA782 /* Sources */ = { 411 | isa = PBXSourcesBuildPhase; 412 | buildActionMask = 2147483647; 413 | files = ( 414 | 0489AC962DCE61F80009C461 /* AppDelegate.swift in Sources */, 415 | 0489AC972DCE61F80009C461 /* ContentView.swift in Sources */, 416 | 0489AC982DCE61F80009C461 /* ViewController.swift in Sources */, 417 | 046546132DCE2682001741E8 /* Contracts.swift in Sources */, 418 | 041262042DC990FD00737FFD /* SampleOTPView.swift in Sources */, 419 | 041262052DC990FD00737FFD /* SampleOTPStyleHandler.swift in Sources */, 420 | 041262062DC990FD00737FFD /* SampleOTPViewWrapper.swift in Sources */, 421 | 041262082DC990FD00737FFD /* SampleOTPViewUIModel.swift in Sources */, 422 | 041262092DC990FD00737FFD /* SampleOTPTextField.swift in Sources */, 423 | 0412620A2DC990FD00737FFD /* SampleOTPAnimationHandler.swift in Sources */, 424 | ); 425 | runOnlyForDeploymentPostprocessing = 0; 426 | }; 427 | 607FACE11AFB9204008FA782 /* Sources */ = { 428 | isa = PBXSourcesBuildPhase; 429 | buildActionMask = 2147483647; 430 | files = ( 431 | 04051ECB2DC99D6F00451D97 /* SampleOTPTextFieldTests.swift in Sources */, 432 | 0465460B2DCC26F7001741E8 /* SampleOTPTextField.swift in Sources */, 433 | 0465460C2DCC26F7001741E8 /* SampleOTPStyleHandler.swift in Sources */, 434 | 046546122DCE2682001741E8 /* Contracts.swift in Sources */, 435 | 0465460D2DCC26F7001741E8 /* SampleOTPAnimationHandler.swift in Sources */, 436 | 0465460E2DCC26F7001741E8 /* SampleOTPViewUIModel.swift in Sources */, 437 | 046546102DCC2AAC001741E8 /* SampleOTPView.swift in Sources */, 438 | 0465460F2DCC26F7001741E8 /* SampleOTPViewWrapper.swift in Sources */, 439 | 04051ECC2DC99D6F00451D97 /* SampleOTPAnimationHandlerTests.swift in Sources */, 440 | 04051ECD2DC99D6F00451D97 /* SampleOTPStyleHandlerTests.swift in Sources */, 441 | 04051ECE2DC99D6F00451D97 /* SampleOTPViewUIModelTests.swift in Sources */, 442 | 04051ED02DC99D6F00451D97 /* SampleOTPViewTests.swift in Sources */, 443 | ); 444 | runOnlyForDeploymentPostprocessing = 0; 445 | }; 446 | /* End PBXSourcesBuildPhase section */ 447 | 448 | /* Begin PBXTargetDependency section */ 449 | 607FACE71AFB9204008FA782 /* PBXTargetDependency */ = { 450 | isa = PBXTargetDependency; 451 | target = 607FACCF1AFB9204008FA782 /* SampleOTP_Example */; 452 | targetProxy = 607FACE61AFB9204008FA782 /* PBXContainerItemProxy */; 453 | }; 454 | /* End PBXTargetDependency section */ 455 | 456 | /* Begin PBXVariantGroup section */ 457 | 0489AC912DCE61F80009C461 /* LaunchScreen.xib */ = { 458 | isa = PBXVariantGroup; 459 | children = ( 460 | 0489AC902DCE61F80009C461 /* Base */, 461 | ); 462 | name = LaunchScreen.xib; 463 | sourceTree = ""; 464 | }; 465 | 0489AC932DCE61F80009C461 /* Main.storyboard */ = { 466 | isa = PBXVariantGroup; 467 | children = ( 468 | 0489AC922DCE61F80009C461 /* Base */, 469 | ); 470 | name = Main.storyboard; 471 | sourceTree = ""; 472 | }; 473 | /* End PBXVariantGroup section */ 474 | 475 | /* Begin XCBuildConfiguration section */ 476 | 607FACED1AFB9204008FA782 /* Debug */ = { 477 | isa = XCBuildConfiguration; 478 | buildSettings = { 479 | ALWAYS_SEARCH_USER_PATHS = NO; 480 | CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; 481 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 482 | CLANG_CXX_LIBRARY = "libc++"; 483 | CLANG_ENABLE_MODULES = YES; 484 | CLANG_ENABLE_OBJC_ARC = YES; 485 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 486 | CLANG_WARN_BOOL_CONVERSION = YES; 487 | CLANG_WARN_COMMA = YES; 488 | CLANG_WARN_CONSTANT_CONVERSION = YES; 489 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 490 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 491 | CLANG_WARN_EMPTY_BODY = YES; 492 | CLANG_WARN_ENUM_CONVERSION = YES; 493 | CLANG_WARN_INFINITE_RECURSION = YES; 494 | CLANG_WARN_INT_CONVERSION = YES; 495 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 496 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 497 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 498 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 499 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 500 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 501 | CLANG_WARN_STRICT_PROTOTYPES = YES; 502 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 503 | CLANG_WARN_UNREACHABLE_CODE = YES; 504 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 505 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 506 | COPY_PHASE_STRIP = NO; 507 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 508 | ENABLE_STRICT_OBJC_MSGSEND = YES; 509 | ENABLE_TESTABILITY = YES; 510 | ENABLE_USER_SCRIPT_SANDBOXING = NO; 511 | GCC_C_LANGUAGE_STANDARD = gnu99; 512 | GCC_DYNAMIC_NO_PIC = NO; 513 | GCC_NO_COMMON_BLOCKS = YES; 514 | GCC_OPTIMIZATION_LEVEL = 0; 515 | GCC_PREPROCESSOR_DEFINITIONS = ( 516 | "DEBUG=1", 517 | "$(inherited)", 518 | ); 519 | GCC_SYMBOLS_PRIVATE_EXTERN = NO; 520 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 521 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 522 | GCC_WARN_UNDECLARED_SELECTOR = YES; 523 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 524 | GCC_WARN_UNUSED_FUNCTION = YES; 525 | GCC_WARN_UNUSED_VARIABLE = YES; 526 | IPHONEOS_DEPLOYMENT_TARGET = 12.0; 527 | MTL_ENABLE_DEBUG_INFO = YES; 528 | ONLY_ACTIVE_ARCH = YES; 529 | SDKROOT = iphoneos; 530 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 531 | }; 532 | name = Debug; 533 | }; 534 | 607FACEE1AFB9204008FA782 /* Release */ = { 535 | isa = XCBuildConfiguration; 536 | buildSettings = { 537 | ALWAYS_SEARCH_USER_PATHS = NO; 538 | CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; 539 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 540 | CLANG_CXX_LIBRARY = "libc++"; 541 | CLANG_ENABLE_MODULES = YES; 542 | CLANG_ENABLE_OBJC_ARC = YES; 543 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 544 | CLANG_WARN_BOOL_CONVERSION = YES; 545 | CLANG_WARN_COMMA = YES; 546 | CLANG_WARN_CONSTANT_CONVERSION = YES; 547 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 548 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 549 | CLANG_WARN_EMPTY_BODY = YES; 550 | CLANG_WARN_ENUM_CONVERSION = YES; 551 | CLANG_WARN_INFINITE_RECURSION = YES; 552 | CLANG_WARN_INT_CONVERSION = YES; 553 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 554 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 555 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 556 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 557 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 558 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 559 | CLANG_WARN_STRICT_PROTOTYPES = YES; 560 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 561 | CLANG_WARN_UNREACHABLE_CODE = YES; 562 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 563 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 564 | COPY_PHASE_STRIP = NO; 565 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 566 | ENABLE_NS_ASSERTIONS = NO; 567 | ENABLE_STRICT_OBJC_MSGSEND = YES; 568 | ENABLE_USER_SCRIPT_SANDBOXING = NO; 569 | GCC_C_LANGUAGE_STANDARD = gnu99; 570 | GCC_NO_COMMON_BLOCKS = YES; 571 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 572 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 573 | GCC_WARN_UNDECLARED_SELECTOR = YES; 574 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 575 | GCC_WARN_UNUSED_FUNCTION = YES; 576 | GCC_WARN_UNUSED_VARIABLE = YES; 577 | IPHONEOS_DEPLOYMENT_TARGET = 12.0; 578 | MTL_ENABLE_DEBUG_INFO = NO; 579 | SDKROOT = iphoneos; 580 | SWIFT_COMPILATION_MODE = wholemodule; 581 | SWIFT_OPTIMIZATION_LEVEL = "-O"; 582 | VALIDATE_PRODUCT = YES; 583 | }; 584 | name = Release; 585 | }; 586 | 607FACF01AFB9204008FA782 /* Debug */ = { 587 | isa = XCBuildConfiguration; 588 | baseConfigurationReference = 103778E1A9F2A78279120E52 /* Pods-SampleOTP_Example.debug.xcconfig */; 589 | buildSettings = { 590 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 591 | INFOPLIST_FILE = SampleOTP/Info.plist; 592 | IPHONEOS_DEPLOYMENT_TARGET = 12.0; 593 | LD_RUNPATH_SEARCH_PATHS = ( 594 | "$(inherited)", 595 | "@executable_path/Frameworks", 596 | ); 597 | MODULE_NAME = ExampleApp; 598 | PRODUCT_BUNDLE_IDENTIFIER = "org.cocoapods.demo.$(PRODUCT_NAME:rfc1034identifier)"; 599 | PRODUCT_NAME = "$(TARGET_NAME)"; 600 | SWIFT_SWIFT3_OBJC_INFERENCE = Default; 601 | SWIFT_VERSION = 4.0; 602 | }; 603 | name = Debug; 604 | }; 605 | 607FACF11AFB9204008FA782 /* Release */ = { 606 | isa = XCBuildConfiguration; 607 | baseConfigurationReference = 1A111F879D13CB04C24CC697 /* Pods-SampleOTP_Example.release.xcconfig */; 608 | buildSettings = { 609 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 610 | INFOPLIST_FILE = SampleOTP/Info.plist; 611 | IPHONEOS_DEPLOYMENT_TARGET = 12.0; 612 | LD_RUNPATH_SEARCH_PATHS = ( 613 | "$(inherited)", 614 | "@executable_path/Frameworks", 615 | ); 616 | MODULE_NAME = ExampleApp; 617 | PRODUCT_BUNDLE_IDENTIFIER = "org.cocoapods.demo.$(PRODUCT_NAME:rfc1034identifier)"; 618 | PRODUCT_NAME = "$(TARGET_NAME)"; 619 | SWIFT_SWIFT3_OBJC_INFERENCE = Default; 620 | SWIFT_VERSION = 4.0; 621 | }; 622 | name = Release; 623 | }; 624 | 607FACF31AFB9204008FA782 /* Debug */ = { 625 | isa = XCBuildConfiguration; 626 | baseConfigurationReference = 94E1B135F19D8CF97EA9A08F /* Pods-SampleOTP_Tests.debug.xcconfig */; 627 | buildSettings = { 628 | CODE_SIGN_IDENTITY = "Apple Development"; 629 | CODE_SIGN_STYLE = Automatic; 630 | DEVELOPMENT_TEAM = ""; 631 | FRAMEWORK_SEARCH_PATHS = ( 632 | "$(PLATFORM_DIR)/Developer/Library/Frameworks", 633 | "$(inherited)", 634 | ); 635 | GCC_PREPROCESSOR_DEFINITIONS = ( 636 | "DEBUG=1", 637 | "$(inherited)", 638 | ); 639 | INFOPLIST_FILE = "/Users/mac/Desktop/podz/SampleOTP/Tests/Supporting Files/Info.plist"; 640 | IPHONEOS_DEPLOYMENT_TARGET = 12.0; 641 | LD_RUNPATH_SEARCH_PATHS = ( 642 | "$(inherited)", 643 | "@executable_path/Frameworks", 644 | "@loader_path/Frameworks", 645 | ); 646 | PRODUCT_BUNDLE_IDENTIFIER = "org.cocoapods.$(PRODUCT_NAME:rfc1034identifier)"; 647 | PRODUCT_NAME = "$(TARGET_NAME)"; 648 | PROVISIONING_PROFILE_SPECIFIER = ""; 649 | SWIFT_SWIFT3_OBJC_INFERENCE = Default; 650 | SWIFT_VERSION = 4.0; 651 | TEST_HOST = "$(BUILT_PRODUCTS_DIR)/SampleOTP_Example.app/SampleOTP_Example"; 652 | }; 653 | name = Debug; 654 | }; 655 | 607FACF41AFB9204008FA782 /* Release */ = { 656 | isa = XCBuildConfiguration; 657 | baseConfigurationReference = 9AE40BABF5BA91EBE7151059 /* Pods-SampleOTP_Tests.release.xcconfig */; 658 | buildSettings = { 659 | CODE_SIGN_IDENTITY = "Apple Development"; 660 | CODE_SIGN_STYLE = Automatic; 661 | DEVELOPMENT_TEAM = ""; 662 | FRAMEWORK_SEARCH_PATHS = ( 663 | "$(PLATFORM_DIR)/Developer/Library/Frameworks", 664 | "$(inherited)", 665 | ); 666 | INFOPLIST_FILE = "/Users/mac/Desktop/podz/SampleOTP/Tests/Supporting Files/Info.plist"; 667 | IPHONEOS_DEPLOYMENT_TARGET = 12.0; 668 | LD_RUNPATH_SEARCH_PATHS = ( 669 | "$(inherited)", 670 | "@executable_path/Frameworks", 671 | "@loader_path/Frameworks", 672 | ); 673 | PRODUCT_BUNDLE_IDENTIFIER = "org.cocoapods.$(PRODUCT_NAME:rfc1034identifier)"; 674 | PRODUCT_NAME = "$(TARGET_NAME)"; 675 | PROVISIONING_PROFILE_SPECIFIER = ""; 676 | SWIFT_SWIFT3_OBJC_INFERENCE = Default; 677 | SWIFT_VERSION = 4.0; 678 | TEST_HOST = "$(BUILT_PRODUCTS_DIR)/SampleOTP_Example.app/SampleOTP_Example"; 679 | }; 680 | name = Release; 681 | }; 682 | /* End XCBuildConfiguration section */ 683 | 684 | /* Begin XCConfigurationList section */ 685 | 607FACCB1AFB9204008FA782 /* Build configuration list for PBXProject "SampleOTP" */ = { 686 | isa = XCConfigurationList; 687 | buildConfigurations = ( 688 | 607FACED1AFB9204008FA782 /* Debug */, 689 | 607FACEE1AFB9204008FA782 /* Release */, 690 | ); 691 | defaultConfigurationIsVisible = 0; 692 | defaultConfigurationName = Release; 693 | }; 694 | 607FACEF1AFB9204008FA782 /* Build configuration list for PBXNativeTarget "SampleOTP_Example" */ = { 695 | isa = XCConfigurationList; 696 | buildConfigurations = ( 697 | 607FACF01AFB9204008FA782 /* Debug */, 698 | 607FACF11AFB9204008FA782 /* Release */, 699 | ); 700 | defaultConfigurationIsVisible = 0; 701 | defaultConfigurationName = Release; 702 | }; 703 | 607FACF21AFB9204008FA782 /* Build configuration list for PBXNativeTarget "SampleOTP_Tests" */ = { 704 | isa = XCConfigurationList; 705 | buildConfigurations = ( 706 | 607FACF31AFB9204008FA782 /* Debug */, 707 | 607FACF41AFB9204008FA782 /* Release */, 708 | ); 709 | defaultConfigurationIsVisible = 0; 710 | defaultConfigurationName = Release; 711 | }; 712 | /* End XCConfigurationList section */ 713 | }; 714 | rootObject = 607FACC81AFB9204008FA782 /* Project object */; 715 | } 716 | -------------------------------------------------------------------------------- /Example/SampleOTP.xcodeproj/xcshareddata/xcschemes/SampleOTP-Example.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 9 | 10 | 16 | 22 | 23 | 24 | 30 | 36 | 37 | 38 | 39 | 40 | 45 | 46 | 52 | 53 | 54 | 55 | 58 | 59 | 60 | 61 | 63 | 69 | 70 | 71 | 72 | 73 | 83 | 85 | 91 | 92 | 93 | 94 | 100 | 102 | 108 | 109 | 110 | 111 | 113 | 114 | 117 | 118 | 119 | -------------------------------------------------------------------------------- /Example/SampleOTP/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // SampleOTP 4 | // 5 | // Created by BolaGamal on 12/26/2024. 6 | // Copyright (c) 2024 BolaGamal. 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: [UIApplicationLaunchOptionsKey: 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 throttle down OpenGL ES frame rates. 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 inactive 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/SampleOTP/Base.lproj/LaunchScreen.xib: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 25 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | -------------------------------------------------------------------------------- /Example/SampleOTP/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 | 35 | 36 | 37 | 38 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 167 | 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 | -------------------------------------------------------------------------------- /Example/SampleOTP/ContentView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ContentView.swift 3 | // SampleOTP 4 | // 5 | // Created by Bola Gamal on 5/5/25. 6 | // Copyright © 2025 CocoaPods. All rights reserved. 7 | // 8 | 9 | 10 | import SwiftUI 11 | import SampleOTP 12 | 13 | 14 | @available(iOS 13.0, *) 15 | struct ContentView: View { 16 | var body: some View { 17 | VStack(spacing: 20) { 18 | Text("Enter OTP") 19 | .font(.headline) 20 | .bold() 21 | 22 | SampleOTPViewWrapper( 23 | model: SampleOTPViewUIModel( 24 | length: 6, 25 | space: 12, 26 | font: .systemFont(ofSize: 20, weight: .medium), 27 | textColor: .black, 28 | tintColor: .systemBlue, 29 | isSecureTextEntry: false, 30 | placeholder: "-", 31 | placeholderFont: .italicSystemFont(ofSize: 20), 32 | placeholderColor: .lightGray, 33 | borderWidth: 1.5, 34 | borderColor: UIColor.gray.cgColor, 35 | fieldCornerRadius: 8, 36 | fieldBackgroundColor: .white, 37 | animationOTP: .pulse, 38 | typingOTPStyle: .active, 39 | activeTextColor: .blue, 40 | activeBorderColor: UIColor.systemGreen.cgColor, 41 | activeBorderWidth: 2, 42 | activeFieldCornerRadius: 10, 43 | activeFieldBackgroundColor: .yellow.withAlphaComponent(0.2) 44 | ), 45 | onCompletion: { code in 46 | print("Completed: \(code)") 47 | } 48 | ) 49 | .frame(height: 60) 50 | .padding() 51 | .background(Color.gray.opacity(0.1)) 52 | .cornerRadius(12) 53 | .shadow(radius: 3) 54 | } 55 | .padding() 56 | } 57 | } 58 | 59 | //MARK: - Preview 60 | //@available(iOS 13.0, *) 61 | //#Preview { 62 | // ContentView() 63 | //} 64 | -------------------------------------------------------------------------------- /Example/SampleOTP/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | APPL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1 23 | LSRequiresIPhoneOS 24 | 25 | UILaunchStoryboardName 26 | LaunchScreen 27 | UIMainStoryboardFile 28 | Main 29 | UIRequiredDeviceCapabilities 30 | 31 | armv7 32 | 33 | UISupportedInterfaceOrientations 34 | 35 | UIInterfaceOrientationPortrait 36 | UIInterfaceOrientationLandscapeLeft 37 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /Example/SampleOTP/ViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewController.swift 3 | // SampleOTP 4 | // 5 | // Created by BolaGamal on 12/26/2024. 6 | // Copyright (c) 2024 BolaGamal. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import SampleOTP 11 | 12 | class ViewController: UIViewController { 13 | //MARK: - Outlets 14 | @IBOutlet private weak var basicOTP: SampleOTPView! 15 | @IBOutlet private weak var focusedOTP: SampleOTPView! 16 | @IBOutlet private weak var activeOTP: SampleOTPView! 17 | @IBOutlet private weak var animationOTP1: SampleOTPView! 18 | @IBOutlet private weak var animationOTP2: SampleOTPView! 19 | @IBOutlet private weak var animationOTP3: SampleOTPView! 20 | 21 | override func viewDidLoad() { 22 | super.viewDidLoad() 23 | testBasicOTP() 24 | testActiveOTP() 25 | testFocusedOTP() 26 | testPulseAnimationOTP() 27 | testBounceAnimationOTP() 28 | testFadeInAnimationOTP() 29 | } 30 | 31 | override func didReceiveMemoryWarning() { 32 | super.didReceiveMemoryWarning() 33 | // Dispose of any resources that can be recreated. 34 | } 35 | 36 | //MARK: - Actions 37 | @IBAction func clearAction(_ sender: Any) { 38 | animationOTP3.clearFields() 39 | } 40 | 41 | @IBAction func validateAction(_ sender: Any) { 42 | animationOTP3.shakeView() 43 | } 44 | } 45 | 46 | //MARK: - Demo Testing 47 | extension ViewController { 48 | 49 | private func testBasicOTP() { 50 | basicOTP.configure(with: SampleOTPViewUIModel(fieldBackgroundColor: .white)) 51 | } 52 | 53 | private func testFocusedOTP() { 54 | focusedOTP.configure( 55 | with: SampleOTPViewUIModel( 56 | borderWidth: 1.5, 57 | fieldBackgroundColor: .white, 58 | typingOTPStyle: .focused, 59 | activeBorderColor: UIColor.red.cgColor) 60 | ) 61 | } 62 | 63 | private func testActiveOTP() { 64 | activeOTP.configure( 65 | with: SampleOTPViewUIModel( 66 | fieldBackgroundColor: .white, 67 | typingOTPStyle: .active, 68 | activeTextColor: .white, 69 | activeFieldBackgroundColor: .purple) 70 | ) 71 | } 72 | 73 | private func testBounceAnimationOTP() { 74 | animationOTP1.configure( 75 | with: SampleOTPViewUIModel( 76 | fieldBackgroundColor: .white, 77 | animationOTP: .bounce, 78 | typingOTPStyle: .active, 79 | activeTextColor: .white, 80 | activeFieldBackgroundColor: .purple) 81 | ) 82 | } 83 | 84 | private func testFadeInAnimationOTP() { 85 | animationOTP2.configure( 86 | with: SampleOTPViewUIModel( 87 | fieldBackgroundColor: .white, 88 | animationOTP: .fadeIn, 89 | typingOTPStyle: .active, 90 | activeTextColor: .white, 91 | activeFieldBackgroundColor: .purple) 92 | ) 93 | } 94 | 95 | private func testPulseAnimationOTP() { 96 | animationOTP3.configure( 97 | with: SampleOTPViewUIModel( 98 | fieldBackgroundColor: .white, 99 | animationOTP: .pulse, 100 | typingOTPStyle: .active, 101 | activeTextColor: .white, 102 | activeFieldBackgroundColor: .purple) 103 | ) 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2024 BolaGamal 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | 2 | // swift-tools-version:5.5 3 | import PackageDescription 4 | 5 | let package = Package( 6 | name: "SampleOTP", 7 | platforms: [ 8 | .iOS(.v12) 9 | ], 10 | products: [ 11 | .library( 12 | name: "SampleOTP", 13 | targets: ["SampleOTP"] 14 | ) 15 | ], 16 | dependencies: [], 17 | targets: [ 18 | .target( 19 | name: "SampleOTP", 20 | dependencies: [], 21 | path: "Sources/SampleOTP" 22 | ), 23 | .testTarget( 24 | name: "SampleOTPTests", 25 | dependencies: ["SampleOTP"], 26 | path: "Tests/SampleOTPTests" 27 | ) 28 | ] 29 | ) 30 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 4 | 5 | 6 | # SampleOTP 7 | 8 | [![Build Status](https://api.travis-ci.com/BolaGamal/SampleOTP.svg?branch=master)](https://app.travis-ci.com/github/BolaGamal/SampleOTP) 9 | [![SwiftPM compatible](https://img.shields.io/badge/SPM-compatible-brightgreen)](https://swift.org/package-manager/) 10 | [![CocoaPods](https://img.shields.io/cocoapods/v/SampleOTP.svg)](https://cocoapods.org/pods/SampleOTP) 11 | [![License](https://img.shields.io/cocoapods/l/SampleOTP.svg?style=flat)](https://github.com/BolaGamal/SampleOTP/blob/master/LICENSE) 12 | [![Platform](https://img.shields.io/cocoapods/p/SampleOTP.svg?style=flat)](https://cocoapods.org/pods/SampleOTP) 13 | 14 | ## 📌 Overview 15 | 16 | `SampleOTP` is a Swift library designed to simplify the implementation of One-Time Password (OTP) input fields in iOS applications. It provides a **customizable** and **user-friendly** UI model that enhances the user experience during OTP entry. 17 | 18 | ![sampleOtpGif](https://github.com/BolaGamal/SampleOTP/blob/master/Example/Resources/sampleOtpGif.gif) 19 | 20 | --- 21 | 22 | ## ✨ Features 23 | 24 | SampleOTP gives you full control over the OTP input experience, combining simplicity with deep customization: 25 | 26 | - ✅ **OTP Length** – Define how many digits the OTP should have. 27 | - 🔐 **Secure Input Mode** – Mask OTP characters for sensitive inputs. 28 | - 🎨 **Custom Appearance** – Adjust spacing, text color, font size, background color. 29 | - 🎭 **Placeholder Customization** – Use any symbol or character. 30 | - ✨ **Animation Styles** – Pulse, fadeIn, and bounce feedback animations. 31 | - 🛠️ **Borders & Corners** – Customize border width, color, and corner radius (both normal and active states). 32 | - 🎯 **Dynamic Typing Styles** – Automatically change styling when field is active or focused. 33 | - ☑️ **Validation Support** – Easily check OTP completeness and validity. 34 | - 📱 **SwiftUI Compatible** – Use with SwiftUI via a convenient wrapper. 35 | - 🔁 **Supports both Swift Package Manager and CocoaPods** 36 | 37 | --- 38 | 39 | ## 📦 Installation 40 | 41 | ### 🔸 Swift Package Manager (SPM) 42 | 43 | 1. Open **Xcode > File > Add Packages** 44 | 2. Enter the URL: 45 | 46 | ``` 47 | https://github.com/BolaGamal/SampleOTP.git 48 | ``` 49 | 50 | 3. Choose the version and add the package. 51 | 52 | --- 53 | 54 | ### 🔸 CocoaPods 55 | 56 | If you prefer [CocoaPods](https://cocoapods.org/pods/SampleOTP): 57 | 58 | Add the following line to your `Podfile`: 59 | 60 | ```ruby 61 | pod 'SampleOTP' 62 | ``` 63 | 64 | Then run: 65 | 66 | ```bash 67 | pod install 68 | ``` 69 | 70 | --- 71 | 72 | ## 🛠️ Usage 73 | 74 | ### 1️⃣ **Import the Framework** 75 | ```swift 76 | import SampleOTPView 77 | ``` 78 | 79 | --- 80 | 81 | ### 2️⃣ **Adding `SampleOTPView` to Your ViewController** 82 | 83 | #### 📌 **Using Storyboard:** 84 | 1. Drag a `UIView` onto your storyboard. 85 | 2. Set its class to `SampleOTPView`. 86 | 3. Connect it as an `IBOutlet`: 87 | 88 | ```swift 89 | @IBOutlet weak var sampleOTP: SampleOTPView! 90 | ``` 91 | 92 | #### 📌 **Programmatic Approach:** 93 | ```swift 94 | let sampleOTP = SampleOTPView() 95 | ``` 96 | 97 | --- 98 | 99 | ### 3️⃣ **Configuring `SampleOTPViewUIModel`** 100 | 101 | To fully customize the OTP input field, use `SampleOTPViewUIModel`: 102 | 103 | ```swift 104 | let otpViewModel = SampleOTPViewUIModel( 105 | length: 6, // Number of OTP digits 106 | space: 10, // Spacing between OTP fields 107 | layoutDirection: .unspecified, // RTL (Right-to-Left) Support 108 | font: .systemFont(ofSize: 18), // Font for OTP input 109 | textColor: .black, // Color of entered text 110 | tintColor: .black, // Cursor color 111 | isSecureTextEntry: true, // Mask OTP for security 112 | placeholder: "*", // Placeholder character 113 | placeholderColor: .gray, // Color of placeholder 114 | borderWidth: 2, // Default border width 115 | borderColor: .gray, // Default border color 116 | fieldCornerRadius: 8, // Default corner radius 117 | fieldBackgroundColor: .white, // Default field background 118 | 119 | // 🎬 Animation & Typing Style 120 | animationOTP: .pulse, // OTP field animation style 121 | typingOTPStyle: .active // Style when typing OTP 122 | 123 | // 🟢 Active (Focused) State 124 | activeTextColor: .blue, // Text color when active 125 | activeBorderColor: .green, // Border color when active 126 | activeBorderWidth: 3, // Border width when active 127 | activeFieldCornerRadius: 10, // Corner radius when active 128 | activeFieldBackgroundColor: .lightGray, // Background color when active 129 | ) 130 | 131 | // Apply the configuration 132 | sampleOTP.configure(with: otpViewModel) 133 | ``` 134 | --- 135 | 136 | ## 🧩 SwiftUI Support 137 | 138 | From iOS 13.0+, you can use `SampleOTPViewWrapper` in SwiftUI like this: 139 | 140 | ```swift 141 | import SwiftUI 142 | import SampleOTP 143 | 144 | @available(iOS 13.0, *) 145 | struct ContentView: View { 146 | var body: some View { 147 | SampleOTPViewWrapper( 148 | model: SampleOTPViewUIModel( 149 | length: 6, 150 | space: 10, 151 | font: .systemFont(ofSize: 18), 152 | textColor: .black, 153 | tintColor: .systemBlue, 154 | isSecureTextEntry: false, 155 | placeholder: "-", 156 | placeholderFont: .italicSystemFont(ofSize: 16), 157 | placeholderColor: .gray, 158 | borderWidth: 1.5, 159 | borderColor: .gray, 160 | fieldCornerRadius: 8, 161 | fieldBackgroundColor: .white, 162 | animationOTP: .pulse, 163 | typingOTPStyle: .active, 164 | activeTextColor: .blue, 165 | activeBorderColor: .systemGreen, 166 | activeBorderWidth: 2, 167 | activeFieldCornerRadius: 10, 168 | activeFieldBackgroundColor: .yellow.withAlphaComponent(0.2) 169 | ), 170 | onCompletion: { code in 171 | print("Completed: \(code)") 172 | } 173 | ) 174 | .frame(height: 60) 175 | .padding() 176 | } 177 | } 178 | ``` 179 | 180 | --- 181 | 182 | ## 📌 Example Project 183 | 184 | To run the example project, clone the repo and execute: 185 | 186 | ```sh 187 | pod install 188 | ``` 189 | inside the `Example` directory. 190 | 191 | --- 192 | 193 | ## 💡 Compatibility 194 | 195 | - ✅ iOS 12.0+ for UIKit 196 | - ✅ iOS 13.0+ for SwiftUI 197 | - ✅ Supports both **CocoaPods** & **Swift Package Manager (SPM)** 198 | 199 | --- 200 | 201 | ## 👤 Author 202 | 203 | Pola Gamal 204 | 📧 bola.gamal222@gmail.com 205 | 206 | --- 207 | 208 | ## 📜 License 209 | 210 | `SampleOTP` is released under the MIT License. See [LICENSE](https://github.com/BolaGamal/SampleOTP/blob/master/LICENSE) for details. 211 | -------------------------------------------------------------------------------- /SampleOTP.podspec: -------------------------------------------------------------------------------- 1 | # 2 | # Be sure to run `pod lib lint SampleOTP.podspec' to ensure this is a 3 | # valid spec before submitting. 4 | # 5 | # Any lines starting with a # are optional, but their use is encouraged 6 | # To learn more about a Podspec see https://guides.cocoapods.org/syntax/podspec.html 7 | # 8 | 9 | Pod::Spec.new do |s| 10 | s.name = 'SampleOTP' 11 | s.version = '0.3.5' 12 | s.summary = 'A customizable OTP input field for iOS.' 13 | s.description = <<-DESC 14 | SampleOTP simplifies the implementation of one-time password (OTP) input fields in iOS apps. 15 | It offers a flexible, secure, and user-friendly UI model with full support for UIKit and SwiftUI. 16 | DESC 17 | s.homepage = 'https://github.com/BolaGamal/SampleOTP' 18 | s.license = { :type => 'MIT', :file => 'LICENSE' } 19 | s.author = { 'BolaGamal' => 'bola.gamal222@gmail.com' } 20 | s.source = { :git => 'https://github.com/BolaGamal/SampleOTP.git', :tag => s.version.to_s } 21 | s.social_media_url = 'https://www.linkedin.com/in/pola-gamal-53438322a/' 22 | s.source_files = 'Sources/SampleOTP/**/*.{swift}' 23 | s.swift_version = '5.5' 24 | s.ios.deployment_target = '12.0' 25 | s.pod_target_xcconfig = { 'OTHER_SWIFT_FLAGS' => '-DCOCOAPODS' } 26 | end 27 | -------------------------------------------------------------------------------- /Sources/SampleOTP/Contracts.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Contract.swift 3 | // SampleOTP 4 | // 5 | // Created by Bola Gamal on 12/27/24. 6 | // 7 | 8 | //MARK: - OTP TextField Delegate 9 | protocol SampleOTPTextFieldDelegate: AnyObject { 10 | func textFieldDidEnterBackspace(_ textField: SampleOTPTextField) 11 | } 12 | 13 | 14 | // MARK: - Style Handler Protocol 15 | protocol SampleOTPStyleHandlerProtocol { 16 | var getTypingStyle: SampleOTPViewUIModel.TypingOTPStyle? { get } 17 | func applyStyle(to textField: SampleOTPTextField, isFocused: Bool) 18 | } 19 | 20 | 21 | // MARK: - Animation Handler Protocol 22 | protocol SampleOTPAnimationHandlerProtocol { 23 | func applyAnimation(to textField: SampleOTPTextField) 24 | } 25 | -------------------------------------------------------------------------------- /Sources/SampleOTP/SampleOTPAnimationHandler.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SampleOTPAnimationHandler.swift 3 | // SampleOTP 4 | // 5 | // Created by Bola Gamal on 12/27/24. 6 | // 7 | 8 | import UIKit 9 | 10 | 11 | // MARK: - Animation Handler 12 | struct SampleOTPAnimationHandler: SampleOTPAnimationHandlerProtocol { 13 | private let uiModel: SampleOTPViewUIModel 14 | 15 | init(uiModel: SampleOTPViewUIModel) { 16 | self.uiModel = uiModel 17 | } 18 | 19 | func applyAnimation(to textField: SampleOTPTextField) { 20 | guard let animationType = uiModel.animationOTP else { return } 21 | switch animationType { 22 | case .pulse: 23 | UIView.animate(withDuration: 0.2, animations: { 24 | textField.transform = CGAffineTransform(scaleX: 1.1, y: 1.1) 25 | }) { _ in 26 | UIView.animate(withDuration: 0.2) { 27 | textField.transform = .identity 28 | } 29 | } 30 | case .fadeIn: 31 | textField.alpha = 0.5 32 | UIView.animate(withDuration: 0.3) { 33 | textField.alpha = 1.0 34 | } 35 | case .bounce: 36 | UIView.animate(withDuration: 0.2, animations: { 37 | textField.transform = CGAffineTransform(translationX: 0, y: -5) 38 | }) { _ in 39 | UIView.animate(withDuration: 0.2) { 40 | textField.transform = .identity 41 | } 42 | } 43 | } 44 | } 45 | } 46 | 47 | -------------------------------------------------------------------------------- /Sources/SampleOTP/SampleOTPStyleHandler.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SampleOTPStyleHandler.swift 3 | // SampleOTP 4 | // 5 | // Created by Bola Gamal on 12/27/24. 6 | // 7 | 8 | import UIKit 9 | 10 | 11 | // MARK: - Style Handler 12 | struct SampleOTPStyleHandler: SampleOTPStyleHandlerProtocol { 13 | private let uiModel: SampleOTPViewUIModel 14 | 15 | init(uiModel: SampleOTPViewUIModel) { 16 | self.uiModel = uiModel 17 | } 18 | 19 | var getTypingStyle: SampleOTPViewUIModel.TypingOTPStyle? { 20 | uiModel.typingOTPStyle 21 | } 22 | 23 | func applyStyle(to textField: SampleOTPTextField, isFocused: Bool) { 24 | guard let typingStyle = uiModel.typingOTPStyle else { return } 25 | var isActive: Bool = false 26 | switch typingStyle { 27 | case .active: 28 | isActive = !(textField.text?.isEmpty ?? true) 29 | case .focused: 30 | isActive = isFocused 31 | } 32 | textField.textColor = isActive ? uiModel.activeTextColor : uiModel.textColor 33 | textField.layer.borderColor = isActive ? uiModel.activeBorderColor : uiModel.borderColor 34 | textField.layer.borderWidth = isActive ? uiModel.activeBorderWidth : uiModel.borderWidth 35 | textField.backgroundColor = isActive ? uiModel.activeFieldBackgroundColor : uiModel.fieldBackgroundColor 36 | textField.layer.cornerRadius = isActive ? uiModel.activeFieldCornerRadius : uiModel.fieldCornerRadius 37 | } 38 | } 39 | 40 | //MARK: - Helper Methods 41 | extension SampleOTPStyleHandler { 42 | 43 | static func getTextFieldBorderColor(model: SampleOTPViewUIModel, isActive: Bool) -> CGColor { 44 | return isActive ? model.activeBorderColor : model.borderColor 45 | } 46 | 47 | static func getTextFieldBackgroundColor(model: SampleOTPViewUIModel, isActive: Bool) -> UIColor { 48 | return isActive ? model.activeFieldBackgroundColor : model.fieldBackgroundColor 49 | } 50 | 51 | static func getTextFieldCornerRadius(model: SampleOTPViewUIModel, isActive: Bool) -> CGFloat { 52 | return isActive ? model.activeFieldCornerRadius : model.fieldCornerRadius 53 | } 54 | 55 | static func getTextFieldBorderWidth(model: SampleOTPViewUIModel, isActive: Bool) -> CGFloat { 56 | return isActive ? model.activeBorderWidth : model.borderWidth 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /Sources/SampleOTP/SampleOTPTextField.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SampleOTPTextField.swift 3 | // SampleOTP 4 | // 5 | // Created by Bola Gamal on 12/27/24. 6 | // 7 | 8 | import UIKit 9 | 10 | 11 | //MARK: - OTP TextField 12 | class SampleOTPTextField: UITextField { 13 | weak var otpTextFieldDelegate: SampleOTPTextFieldDelegate? 14 | 15 | override init(frame: CGRect) { 16 | super.init(frame: frame) 17 | } 18 | 19 | convenience init(uiModel: SampleOTPViewUIModel) { 20 | self.init(frame: .zero) 21 | setup(with: uiModel) 22 | } 23 | 24 | required init?(coder: NSCoder) { 25 | fatalError("init(coder:) has not been implemented") 26 | } 27 | 28 | override func deleteBackward() { 29 | otpTextFieldDelegate?.textFieldDidEnterBackspace(self) 30 | super.deleteBackward() 31 | } 32 | 33 | override func canPerformAction(_ action: Selector, withSender sender: Any?) -> Bool { 34 | return action == #selector(UIResponderStandardEditActions.paste(_:)) 35 | } 36 | 37 | func setup(with uiModel: SampleOTPViewUIModel) { 38 | self.font = uiModel.font 39 | self.textColor = uiModel.textColor 40 | self.placeholder = uiModel.placeholder 41 | self.keyboardType = uiModel.keyboardType 42 | self.textAlignment = uiModel.textAlignment 43 | self.isSecureTextEntry = uiModel.isSecureTextEntry 44 | self.backgroundColor = uiModel.fieldBackgroundColor 45 | self.layer.borderWidth = uiModel.borderWidth 46 | self.layer.borderColor = uiModel.borderColor 47 | self.layer.cornerRadius = uiModel.fieldCornerRadius 48 | self.setAttributedPlaceholder(uiModel) 49 | if let tintColor = uiModel.tintColor { self.tintColor = tintColor } 50 | } 51 | } 52 | 53 | //MARK: - Helper Methods 54 | extension SampleOTPTextField { 55 | 56 | func setAttributedPlaceholder(_ uiModel: SampleOTPViewUIModel) { 57 | guard uiModel.placeholderColor != nil || 58 | uiModel.placeholderFont != nil else { return } 59 | 60 | var attributes: [NSAttributedString.Key: Any] = [:] 61 | 62 | if let placeholderFont = uiModel.placeholderFont { 63 | attributes[.font] = placeholderFont 64 | } 65 | 66 | if let placeholderColor = uiModel.placeholderColor { 67 | attributes[.foregroundColor] = placeholderColor 68 | } 69 | 70 | attributedPlaceholder = NSAttributedString(string: placeholder ?? "", attributes: attributes) 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /Sources/SampleOTP/SampleOTPView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SampleOTPView.swift 3 | // SampleOTP 4 | // 5 | // Created by Bola Gamal on 12/27/24. 6 | // 7 | 8 | import UIKit 9 | 10 | @IBDesignable 11 | @objc public class SampleOTPView: UIView { 12 | // MARK: - Private Properties 13 | private let stackView = UIStackView() 14 | private(set) var debounceTimer: Timer? 15 | private(set) var styleHandler: SampleOTPStyleHandlerProtocol? 16 | private(set) var animationHandler: SampleOTPAnimationHandlerProtocol? 17 | private(set) var textFields: [SampleOTPTextField] = [] 18 | private(set) var model: SampleOTPViewUIModel? 19 | 20 | public var didFinishEnteringOTP: ((String) -> Void)? 21 | 22 | // MARK: - Init 23 | @objc public override init(frame: CGRect) { 24 | super.init(frame: frame) 25 | setupStackView() 26 | } 27 | 28 | @objc public required init?(coder: NSCoder) { 29 | super.init(coder: coder) 30 | setupStackView() 31 | } 32 | 33 | deinit { 34 | debounceTimer?.invalidate() 35 | } 36 | 37 | public func configure(with uiModel: SampleOTPViewUIModel) { 38 | self.model = uiModel 39 | self.styleHandler = SampleOTPStyleHandler(uiModel: uiModel) 40 | self.animationHandler = SampleOTPAnimationHandler(uiModel: uiModel) 41 | stackView.spacing = uiModel.space 42 | stackView.semanticContentAttribute = uiModel.layoutDirection 43 | stackView.subviews.forEach { $0.removeFromSuperview() } 44 | textFields.removeAll() 45 | 46 | for _ in 0.. 0 { 119 | let previousField = textFields[previousFieldIndex - 1] 120 | previousField.text = "" 121 | previousField.becomeFirstResponder() 122 | } 123 | } 124 | } 125 | 126 | // MARK: - Validation 127 | extension SampleOTPView { 128 | public func isValidOTP() -> Bool { 129 | let otp = getOTP() 130 | return otp.count == textFields.count && otp.allSatisfy { $0.isNumber } 131 | } 132 | } 133 | 134 | //MARK: - Public Functions 135 | extension SampleOTPView { 136 | public func clearFields() { 137 | textFields.forEach { 138 | $0.text = "" 139 | styleHandler?.applyStyle(to: $0, isFocused: false) 140 | } 141 | textFields.first?.becomeFirstResponder() 142 | } 143 | 144 | public func getOTP() -> String { 145 | return textFields.compactMap { $0.text }.joined() 146 | } 147 | 148 | public func setEditable(_ isEditable: Bool) { 149 | textFields.forEach { $0.isUserInteractionEnabled = isEditable } 150 | } 151 | 152 | public func shakeView(duration: TimeInterval = 0.5, intensity: CGFloat = 10) { 153 | let animation = CAKeyframeAnimation(keyPath: "transform.translation.x") 154 | animation.duration = duration 155 | animation.values = [-intensity, intensity, -intensity * 0.8, intensity * 0.8, -intensity * 0.5, intensity * 0.5, 0] 156 | layer.add(animation, forKey: "shake") 157 | } 158 | 159 | public func setOTP(_ otp: String) { 160 | guard otp.count <= textFields.count else { return } 161 | 162 | for (index, char) in otp.enumerated() { 163 | textFields[index].text = String(char) 164 | styleHandler?.applyStyle(to: textFields[index], isFocused: false) 165 | } 166 | if otp.count == textFields.count { 167 | didFinishEnteringOTP?(getOTP()) 168 | } 169 | } 170 | } 171 | 172 | //MARK: - UITextFieldDelegate 173 | extension SampleOTPView: UITextFieldDelegate { 174 | 175 | public func textFieldDidBeginEditing(_ textField: UITextField) { 176 | guard let textField = textField as? SampleOTPTextField else { return } 177 | styleHandler?.applyStyle(to: textField, isFocused: true) 178 | } 179 | 180 | public func textFieldDidEndEditing(_ textField: UITextField) { 181 | guard let textField = textField as? SampleOTPTextField else { return } 182 | styleHandler?.applyStyle(to: textField, isFocused: false) 183 | } 184 | 185 | public func textFieldShouldReturn(_ textField: UITextField) -> Bool { 186 | textField.resignFirstResponder() 187 | return true 188 | } 189 | 190 | public func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool { 191 | guard let text = textField.text, let textField = textField as? SampleOTPTextField else { return false } 192 | 193 | replaceFieldTextIfNeeded(for: textField, string: string) 194 | 195 | if isPasteOperation(string) { 196 | handlePaste(string) 197 | return false 198 | } 199 | if isStartWithSpace(currentText: text, newCharacter: string) { 200 | return false 201 | } 202 | return shouldAllowCharacterInput(currentText: text, newCharacter: string) 203 | } 204 | 205 | private func replaceFieldTextIfNeeded(for textField: SampleOTPTextField, string: String) { 206 | guard let text = textField.text, text.count == 1, string.count == 1, text != string else { return } 207 | textField.text = string 208 | moveFocusToNextField(for: textField) 209 | debounceCompletionCheck() 210 | } 211 | 212 | private func isPasteOperation(_ string: String) -> Bool { 213 | return string.count == textFields.count 214 | } 215 | 216 | private func handlePaste(_ string: String) { 217 | let filteredString = string.filter { $0.isNumber } 218 | DispatchQueue.main.async { 219 | self.setOTP(filteredString) 220 | if let lastField = self.textFields.last(where: { $0.text?.isEmpty ?? false }) { 221 | lastField.becomeFirstResponder() 222 | } else { 223 | ///Dismiss if all fields are filled 224 | self.endEditing(true) 225 | } 226 | } 227 | } 228 | 229 | private func isStartWithSpace(currentText: String, newCharacter: String) -> Bool { 230 | return currentText.isEmpty && newCharacter == " " 231 | } 232 | ///Handle single character input 233 | private func shouldAllowCharacterInput(currentText: String, newCharacter: String) -> Bool { 234 | return currentText.count + newCharacter.count <= 1 235 | } 236 | } 237 | 238 | //MARK: - SampleOTPTextFieldDelegate 239 | extension SampleOTPView: SampleOTPTextFieldDelegate { 240 | 241 | func textFieldDidEnterBackspace(_ textField: SampleOTPTextField) { 242 | let text = textField.text 243 | textField.text = "" 244 | if text?.isEmpty ?? true { moveFocusToPreviousField(for: textField) } 245 | changeStyleForActiveField(for: textField) 246 | } 247 | 248 | private func changeStyleForActiveField(for textField: SampleOTPTextField) { 249 | guard let typingStyle = styleHandler?.getTypingStyle, typingStyle == .active else { return } 250 | self.styleHandler?.applyStyle(to: textField, isFocused: false) 251 | } 252 | } 253 | -------------------------------------------------------------------------------- /Sources/SampleOTP/SampleOTPViewUIModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SampleOTPViewUIModel.swift 3 | // SampleOTP 4 | // 5 | // Created by Bola Gamal on 12/27/24. 6 | // 7 | 8 | import UIKit 9 | 10 | 11 | //MARK: - UIModel 12 | public struct SampleOTPViewUIModel: Equatable { 13 | //MARK: Enums 14 | public enum AnimationOTP { 15 | case pulse, fadeIn, bounce 16 | } 17 | 18 | public enum TypingOTPStyle { 19 | case active, focused 20 | } 21 | 22 | //MARK: Properties 23 | public let length: Int 24 | public let space: CGFloat 25 | public let layoutDirection: UISemanticContentAttribute 26 | 27 | //textField attribute 28 | public let font: UIFont 29 | public let textColor: UIColor 30 | public var tintColor: UIColor? 31 | public let isSecureTextEntry: Bool 32 | public let placeholder: String 33 | public var placeholderFont: UIFont? 34 | public var placeholderColor: UIColor? 35 | public let borderWidth: CGFloat 36 | public let borderColor: CGColor 37 | public let fieldCornerRadius: CGFloat 38 | public let fieldBackgroundColor: UIColor 39 | public let keyboardType: UIKeyboardType 40 | public let textAlignment: NSTextAlignment 41 | 42 | //animation 43 | public let animationOTP: AnimationOTP? 44 | 45 | //active style 46 | public let typingOTPStyle: TypingOTPStyle? 47 | public let activeTextColor: UIColor 48 | public let activeBorderColor: CGColor 49 | public let activeBorderWidth: CGFloat 50 | public let activeFieldCornerRadius: CGFloat 51 | public let activeFieldBackgroundColor: UIColor 52 | 53 | public init( 54 | length: Int = 5, 55 | space: CGFloat = 10, 56 | layoutDirection: UISemanticContentAttribute = .forceLeftToRight, 57 | font: UIFont = UIFont.systemFont(ofSize: 18), 58 | textColor: UIColor = .black, 59 | tintColor: UIColor? = nil, 60 | isSecureTextEntry: Bool = false, 61 | placeholder: String = "", 62 | placeholderFont: UIFont? = nil, 63 | placeholderColor: UIColor? = nil, 64 | borderWidth: CGFloat = 0, 65 | borderColor: CGColor = UIColor.clear.cgColor, 66 | fieldCornerRadius: CGFloat = 6, 67 | fieldBackgroundColor: UIColor = .clear, 68 | keyboardType: UIKeyboardType = .asciiCapableNumberPad, 69 | textAlignment: NSTextAlignment = .center, 70 | animationOTP: AnimationOTP? = nil, 71 | typingOTPStyle: TypingOTPStyle? = nil, 72 | activeTextColor: UIColor? = nil, 73 | activeBorderColor: CGColor? = nil, 74 | activeBorderWidth: CGFloat? = nil, 75 | activeFieldCornerRadius: CGFloat? = nil, 76 | activeFieldBackgroundColor: UIColor? = nil) 77 | { 78 | self.length = length 79 | self.space = space 80 | self.layoutDirection = layoutDirection 81 | self.font = font 82 | self.textColor = textColor 83 | self.tintColor = tintColor 84 | self.isSecureTextEntry = isSecureTextEntry 85 | self.placeholder = placeholder 86 | self.placeholderFont = placeholderFont 87 | self.placeholderColor = placeholderColor 88 | self.borderWidth = borderWidth 89 | self.borderColor = borderColor 90 | self.fieldCornerRadius = fieldCornerRadius 91 | self.fieldBackgroundColor = fieldBackgroundColor 92 | self.keyboardType = keyboardType 93 | self.textAlignment = textAlignment 94 | self.animationOTP = animationOTP 95 | self.typingOTPStyle = typingOTPStyle 96 | self.activeTextColor = activeTextColor ?? self.textColor 97 | self.activeBorderColor = activeBorderColor ?? self.borderColor 98 | self.activeBorderWidth = activeBorderWidth ?? self.borderWidth 99 | self.activeFieldCornerRadius = activeFieldCornerRadius ?? self.fieldCornerRadius 100 | self.activeFieldBackgroundColor = activeFieldBackgroundColor ?? self.fieldBackgroundColor 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /Sources/SampleOTP/SampleOTPViewWrapper.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SampleOTPViewWrapper.swift 3 | // SampleOTP 4 | // 5 | // Created by Bola Gamal on 5/5/25. 6 | // Copyright © 2025 CocoaPods. All rights reserved. 7 | // 8 | 9 | 10 | import UIKit 11 | import SwiftUI 12 | 13 | public struct SampleOTPViewWrapper: UIViewRepresentable { 14 | public var model: SampleOTPViewUIModel 15 | public var onCompletion: ((String) -> Void)? 16 | 17 | public init(model: SampleOTPViewUIModel, onCompletion: ((String) -> Void)? = nil) { 18 | self.model = model 19 | self.onCompletion = onCompletion 20 | } 21 | 22 | public func makeUIView(context: Context) -> SampleOTPView { 23 | let otpView = SampleOTPView() 24 | otpView.configure(with: model) 25 | otpView.didFinishEnteringOTP = onCompletion 26 | return otpView 27 | } 28 | 29 | public func updateUIView(_ uiView: SampleOTPView, context: Context) { 30 | if uiView.model != model { 31 | uiView.configure(with: model) 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /Tests/SampleOTPTests/SampleOTPAnimationHandlerTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import SampleOTP 3 | 4 | final class SampleOTPAnimationHandlerTests: XCTestCase { 5 | 6 | ///Expect no crash — nothing to assert explicitly 7 | func testApplyAnimationWithNilTypeDoesNotCrash() { 8 | let model = SampleOTPViewUIModel(animationOTP: nil) 9 | let animationHandler = SampleOTPAnimationHandler(uiModel: model) 10 | let textField = SampleOTPTextField() 11 | 12 | animationHandler.applyAnimation(to: textField) 13 | } 14 | 15 | ///We can’t test animation visually, but we ensure it runs without crash 16 | func testApplyAnimationWithPulse() { 17 | let model = SampleOTPViewUIModel(animationOTP: .pulse) 18 | let animationHandler = SampleOTPAnimationHandler(uiModel: model) 19 | let textField = SampleOTPTextField() 20 | 21 | animationHandler.applyAnimation(to: textField) 22 | } 23 | 24 | func testApplyAnimationWithFadeIn() { 25 | let model = SampleOTPViewUIModel(animationOTP: .fadeIn) 26 | let animationHandler = SampleOTPAnimationHandler(uiModel: model) 27 | let textField = SampleOTPTextField() 28 | 29 | animationHandler.applyAnimation(to: textField) 30 | } 31 | 32 | func testApplyAnimationWithBounce() { 33 | let model = SampleOTPViewUIModel(animationOTP: .bounce) 34 | let animationHandler = SampleOTPAnimationHandler(uiModel: model) 35 | let textField = SampleOTPTextField() 36 | 37 | animationHandler.applyAnimation(to: textField) 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /Tests/SampleOTPTests/SampleOTPStyleHandlerTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import SampleOTP 3 | 4 | final class SampleOTPStyleHandlerTests: XCTestCase { 5 | 6 | func testGetTextFieldBorderColorWhenActiveAndInactive() { 7 | let activeColor = UIColor.green.cgColor 8 | let inactiveColor = UIColor.gray.cgColor 9 | let model = SampleOTPViewUIModel( 10 | borderColor: inactiveColor, 11 | activeBorderColor: activeColor 12 | ) 13 | 14 | let activeBorderColor = SampleOTPStyleHandler.getTextFieldBorderColor(model: model, isActive: true) 15 | let inactiveBorderColor = SampleOTPStyleHandler.getTextFieldBorderColor(model: model, isActive: false) 16 | 17 | XCTAssertEqual(activeBorderColor, activeColor) 18 | XCTAssertEqual(inactiveBorderColor, inactiveColor) 19 | } 20 | 21 | func testGetTextFieldBackgroundColor() { 22 | let activeBG = UIColor.lightGray 23 | let inactiveBG = UIColor.white 24 | let model = SampleOTPViewUIModel( 25 | fieldBackgroundColor: inactiveBG, 26 | activeFieldBackgroundColor: activeBG 27 | ) 28 | 29 | let activeBackground = SampleOTPStyleHandler.getTextFieldBackgroundColor(model: model, isActive: true) 30 | let inactiveBackground = SampleOTPStyleHandler.getTextFieldBackgroundColor(model: model, isActive: false) 31 | 32 | XCTAssertEqual(activeBackground, activeBG) 33 | XCTAssertEqual(inactiveBackground, inactiveBG) 34 | } 35 | 36 | func testGetTextFieldCornerRadius() { 37 | let defaultRadius: CGFloat = 4 38 | let activeRadius: CGFloat = 10 39 | 40 | let model = SampleOTPViewUIModel( 41 | fieldCornerRadius: defaultRadius, 42 | activeFieldCornerRadius: activeRadius 43 | ) 44 | 45 | let active = SampleOTPStyleHandler.getTextFieldCornerRadius(model: model, isActive: true) 46 | let inactive = SampleOTPStyleHandler.getTextFieldCornerRadius(model: model, isActive: false) 47 | 48 | XCTAssertEqual(active, activeRadius) 49 | XCTAssertEqual(inactive, defaultRadius) 50 | } 51 | 52 | func testGetTextFieldBorderWidth() { 53 | let defaultWidth: CGFloat = 1 54 | let activeWidth: CGFloat = 3 55 | 56 | let model = SampleOTPViewUIModel( 57 | borderWidth: defaultWidth, 58 | activeBorderWidth: activeWidth 59 | ) 60 | 61 | let active = SampleOTPStyleHandler.getTextFieldBorderWidth(model: model, isActive: true) 62 | let inactive = SampleOTPStyleHandler.getTextFieldBorderWidth(model: model, isActive: false) 63 | 64 | XCTAssertEqual(active, activeWidth) 65 | XCTAssertEqual(inactive, defaultWidth) 66 | } 67 | 68 | } -------------------------------------------------------------------------------- /Tests/SampleOTPTests/SampleOTPTestPlan.xctestplan: -------------------------------------------------------------------------------- 1 | { 2 | "configurations" : [ 3 | { 4 | "id" : "D219E046-0B03-437D-BC7D-22F0AF98B360", 5 | "name" : "Configuration 1", 6 | "options" : { 7 | 8 | } 9 | } 10 | ], 11 | "defaultOptions" : { 12 | "testTimeoutsEnabled" : true 13 | }, 14 | "testTargets" : [ 15 | { 16 | "target" : { 17 | "containerPath" : "container:SampleOTP.xcodeproj", 18 | "identifier" : "607FACE41AFB9204008FA782", 19 | "name" : "SampleOTP_Tests" 20 | } 21 | } 22 | ], 23 | "version" : 1 24 | } 25 | -------------------------------------------------------------------------------- /Tests/SampleOTPTests/SampleOTPTextFieldTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import SampleOTP 3 | 4 | class MockOTPDelegate: SampleOTPTextFieldDelegate { 5 | 6 | var didCallBackspace = false 7 | 8 | func textFieldDidEnterBackspace(_ textField: SampleOTPTextField) { 9 | didCallBackspace = true 10 | } 11 | } 12 | 13 | final class SampleOTPTextFieldTests: XCTestCase { 14 | 15 | func testDeleteBackwardCallsDelegate() { 16 | let mockDelegate = MockOTPDelegate() 17 | let textField = SampleOTPTextField() 18 | textField.otpTextFieldDelegate = mockDelegate 19 | 20 | // Simulate deletion 21 | textField.deleteBackward() 22 | 23 | XCTAssertTrue(mockDelegate.didCallBackspace) 24 | } 25 | 26 | func testCanPerformActionPasteAllowedOnly() { 27 | let textField = SampleOTPTextField() 28 | let pasteSelector = #selector(UIResponderStandardEditActions.paste(_:)) 29 | let copySelector = #selector(UIResponderStandardEditActions.copy(_:)) 30 | 31 | XCTAssertTrue(textField.canPerformAction(pasteSelector, withSender: nil)) 32 | XCTAssertFalse(textField.canPerformAction(copySelector, withSender: nil)) 33 | } 34 | 35 | func testSetAttributedPlaceholderWithFontAndColor() { 36 | let textField = SampleOTPTextField() 37 | let font = UIFont.systemFont(ofSize: 18) 38 | let color = UIColor.red 39 | 40 | let model = SampleOTPViewUIModel( 41 | placeholder: "0", 42 | placeholderFont: font, 43 | placeholderColor: color 44 | ) 45 | 46 | textField.placeholder = model.placeholder 47 | textField.setAttributedPlaceholder(model) 48 | 49 | let attributes = textField.attributedPlaceholder?.attributes(at: 0, effectiveRange: nil) 50 | XCTAssertEqual(attributes?[.font] as? UIFont, font) 51 | XCTAssertEqual(attributes?[.foregroundColor] as? UIColor, color) 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /Tests/SampleOTPTests/SampleOTPViewTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import SampleOTP 3 | 4 | final class SampleOTPViewTests: XCTestCase { 5 | 6 | ///accessing via reflection to validate count 7 | func testConfigureCreatesCorrectNumberOfFields() { 8 | let otpView = SampleOTPView() 9 | let model = SampleOTPViewUIModel(length: 4) 10 | otpView.configure(with: model) 11 | 12 | let fields = otpView.textFields 13 | XCTAssertEqual(fields.count, 4) 14 | } 15 | 16 | func testClearFieldsResetsTextAndFocus() { 17 | let otpView = SampleOTPView() 18 | let model = SampleOTPViewUIModel(length: 3) 19 | otpView.configure(with: model) 20 | otpView.setOTP("123") 21 | otpView.clearFields() 22 | 23 | let fields = otpView.textFields 24 | XCTAssertEqual(fields.compactMap { $0.text }.joined(), "") 25 | } 26 | 27 | func testGetOTPReturnsCorrectCode() { 28 | let otpView = SampleOTPView() 29 | let model = SampleOTPViewUIModel(length: 3) 30 | otpView.configure(with: model) 31 | otpView.setOTP("789") 32 | XCTAssertEqual(otpView.getOTP(), "789") 33 | } 34 | 35 | func testSetOTPTriggersCompletionIfFull() { 36 | let expectation = XCTestExpectation(description: "OTP Completion Called") 37 | let otpView = SampleOTPView() 38 | let model = SampleOTPViewUIModel(length: 4) 39 | otpView.configure(with: model) 40 | otpView.didFinishEnteringOTP = { code in 41 | XCTAssertEqual(code, "2468") 42 | expectation.fulfill() 43 | } 44 | otpView.setOTP("2468") 45 | wait(for: [expectation], timeout: 1.0) 46 | } 47 | 48 | func testIsValidOTP() { 49 | let otpView = SampleOTPView() 50 | let model = SampleOTPViewUIModel(length: 3) 51 | otpView.configure(with: model) 52 | otpView.setOTP("111") 53 | XCTAssertTrue(otpView.isValidOTP()) 54 | 55 | otpView.setOTP("1a3") 56 | XCTAssertFalse(otpView.isValidOTP()) 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /Tests/SampleOTPTests/SampleOTPViewUIModelTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import SampleOTP 3 | 4 | final class SampleOTPViewUIModelTests: XCTestCase { 5 | 6 | func testDefaultModelProperties() { 7 | let model = SampleOTPViewUIModel( 8 | length: 6, 9 | space: 8, 10 | font: .boldSystemFont(ofSize: 14), 11 | textColor: .black, 12 | isSecureTextEntry: true, 13 | placeholder: "-", 14 | borderWidth: 1, 15 | borderColor: UIColor.gray.cgColor, 16 | fieldCornerRadius: 6, 17 | fieldBackgroundColor: .white, 18 | animationOTP: .none, 19 | typingOTPStyle: .active, 20 | activeTextColor: .blue, 21 | activeBorderColor: UIColor.green.cgColor, 22 | activeBorderWidth: 2, 23 | activeFieldCornerRadius: 8, 24 | activeFieldBackgroundColor: .lightGray 25 | ) 26 | 27 | XCTAssertEqual(model.length, 6) 28 | XCTAssertEqual(model.space, 8) 29 | XCTAssertEqual(model.placeholder, "-") 30 | XCTAssertEqual(model.borderWidth, 1) 31 | XCTAssertTrue(model.isSecureTextEntry) 32 | } 33 | 34 | func testFallbacksFromMainStyleProperties() { 35 | let model = SampleOTPViewUIModel( 36 | textColor: .red, 37 | borderColor: UIColor.blue.cgColor 38 | ) 39 | 40 | XCTAssertEqual(model.activeTextColor, .red) 41 | XCTAssertEqual(model.activeBorderColor, UIColor.blue.cgColor) 42 | XCTAssertEqual(model.activeBorderWidth, model.borderWidth) 43 | XCTAssertEqual(model.activeFieldCornerRadius, model.fieldCornerRadius) 44 | XCTAssertEqual(model.activeFieldBackgroundColor, model.fieldBackgroundColor) 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /Tests/Supporting Files/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | BNDL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1 23 | 24 | 25 | -------------------------------------------------------------------------------- /_Pods.xcodeproj: -------------------------------------------------------------------------------- 1 | Example/Pods/Pods.xcodeproj --------------------------------------------------------------------------------