├── .gitignore ├── .swiftpm └── xcode │ └── package.xcworkspace │ └── xcshareddata │ └── IDEWorkspaceChecks.plist ├── Tests └── CodeFieldsTests │ └── CodeFieldsTests.swift ├── Package.swift ├── README.md └── Sources └── CodeFields ├── CodeFields.swift └── AuthCodeCellsView.swift /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.build 3 | /Packages 4 | /*.xcodeproj 5 | xcuserdata/ 6 | DerivedData/ 7 | .swiftpm/config/registries.json 8 | .swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata 9 | .netrc 10 | -------------------------------------------------------------------------------- /.swiftpm/xcode/package.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Tests/CodeFieldsTests/CodeFieldsTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import CodeFields 3 | 4 | final class CodeFieldsTests: XCTestCase { 5 | func testExample() throws { 6 | // This is an example of a functional test case. 7 | // Use XCTAssert and related functions to verify your tests produce the correct 8 | // results. 9 | // XCTAssertEqual(CodeFields().text, "Hello, World!") 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version: 5.7 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | 4 | import PackageDescription 5 | 6 | let package = Package( 7 | name: "CodeFields", 8 | products: [ 9 | // Products define the executables and libraries a package produces, and make them visible to other packages. 10 | .library( 11 | name: "CodeFields", 12 | targets: ["CodeFields"]), 13 | ], 14 | dependencies: [ 15 | // Dependencies declare other packages that this package depends on. 16 | // .package(url: /* package url */, from: "1.0.0"), 17 | ], 18 | targets: [ 19 | // Targets are the basic building blocks of a package. A target can define a module or a test suite. 20 | // Targets can depend on other targets in this package, and on products in packages this package depends on. 21 | .target( 22 | name: "CodeFields", 23 | dependencies: []), 24 | .testTarget( 25 | name: "CodeFieldsTests", 26 | dependencies: ["CodeFields"]), 27 | ] 28 | ) 29 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # CodeFields: UIKit + SwiftUI 2 | 3 | 4 | # Installation 5 | 6 | Use SPM 7 | 8 | File -> Add Packages -> search https://github.com/Nanodroop/CodeFields -> Add Package 9 | 10 | Then import CodeFields in your projects: 11 | _______ 12 | ``` 13 | 14 | import CodeFields 15 | 16 | ... 17 | 18 | AuthCodeCellsView( 19 | text: $code, // text of TextField 20 | showKeyboardNow: true, // show or hide keyboard 21 | length: 6, // number of cells 22 | errorMessage: errorMessage, // shows a text error 23 | isActive: isActive, // activity status 24 | onCommit: { _ in checkCode() }, // your method for check code 25 | onTap: { showKeyboardNow = true } // show or hide keyboard 26 | ) 27 | ... 28 | ``` 29 | 30 | When 31 | ``` 32 | @State var code = "" 33 | @State var showKeyboardNow = false 34 | @State var codeLength = 6 35 | @State var errorMessage = "" 36 | @State var showingAlert = false 37 | @State var isActive = false 38 | ``` 39 | 40 | Also you can use modifiers for settings Custom UI of Texfield: 41 | 42 | ``` 43 | .alert("The right code", isPresented: $showingAlert) { 44 | Button("OK", role: .cancel) { } 45 | } // or NavigationLink 46 | .onChange(of: code) { _ in 47 | withAnimation(.easeInOut(duration: 0.2)) { 48 | clearCodeError() 49 | } 50 | } 51 | .onAppear { 52 | isActive = true 53 | showKeyboardNow = true 54 | } 55 | .onDisappear { 56 | isActive = false 57 | showKeyboardNow = false 58 | } 59 | ``` 60 | 61 | # Different condition: 62 | image 63 | 64 | # It works: 65 | ![CustomCodeField](https://user-images.githubusercontent.com/83034148/200137704-1a5107db-c379-4661-84b5-f8f96c6fd736.gif) 66 | 67 | -------------------------------------------------------------------------------- /Sources/CodeFields/CodeFields.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | @available(macOS 10.15, *) 4 | @available(iOS 13.0, *) 5 | public struct CustomCodeField: UIViewRepresentable { 6 | @Binding public var text: String 7 | public let codeCount: Int 8 | public let isFirstResponder: Bool 9 | public let onCommit: (String) -> Void 10 | 11 | public func makeCoordinator() -> Coordinator { 12 | Coordinator(self) 13 | } 14 | 15 | public func makeUIView(context: Context) -> UITextField { 16 | let textField = UITextField() 17 | textField.delegate = context.coordinator 18 | textField.keyboardType = .numberPad 19 | textField.textContentType = .oneTimeCode 20 | textField.font = .preferredFont(forTextStyle: .largeTitle) 21 | textField.tintColor = .clear 22 | textField.textColor = .clear 23 | 24 | return textField 25 | } 26 | 27 | public func updateUIView(_ uiView: UITextField, context: Context) { 28 | uiView.text = text 29 | if isFirstResponder && !uiView.isFirstResponder { 30 | uiView.becomeFirstResponder() 31 | } 32 | } 33 | 34 | public class Coordinator: NSObject, UITextFieldDelegate { 35 | let parent: CustomCodeField 36 | 37 | public init(_ uiTextField: CustomCodeField) { 38 | parent = uiTextField 39 | } 40 | 41 | public func textFieldDidChangeSelection(_ textField: UITextField) { 42 | let textFieldText = textField.text ?? "" 43 | parent.text = textFieldText 44 | if textFieldText.count == parent.codeCount 45 | && textField.isFirstResponder { 46 | parent.onCommit(textFieldText) 47 | } 48 | } 49 | 50 | public func textField( 51 | _ textField: UITextField, 52 | shouldChangeCharactersIn range: NSRange, 53 | replacementString string: String 54 | ) -> Bool { 55 | if let text = textField.text, text.count >= parent.codeCount, 56 | let char = string.cString(using: .utf8) { 57 | return strcmp(char, "\\b") == -92 58 | } else { 59 | return true 60 | } 61 | } 62 | } 63 | } 64 | 65 | -------------------------------------------------------------------------------- /Sources/CodeFields/AuthCodeCellsView.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | @available(iOS 13.0, *) 4 | @available(macOS 10.15, *) 5 | public struct AuthCodeCellsView: View { 6 | @Binding public var code: String 7 | public let showKeyboardNow: Bool 8 | public let cornerRadius: CGFloat = 5 9 | public let height: CGFloat = 50 10 | public let length: Int 11 | public let errorMessage: String 12 | public let isActive: Bool 13 | public let onCommit: (String) -> Void 14 | public let onTapGesture: () -> Void 15 | 16 | public init( 17 | text: Binding, 18 | showKeyboardNow: Bool, 19 | length: Int, 20 | errorMessage: String = "", 21 | isActive: Bool = true, 22 | onCommit: @escaping (String) -> Void, 23 | onTap: @escaping () -> Void 24 | ) { 25 | self._code = text 26 | self.showKeyboardNow = showKeyboardNow 27 | self.length = length 28 | self.errorMessage = errorMessage 29 | self.isActive = isActive 30 | self.onCommit = onCommit 31 | self.onTapGesture = onTap 32 | } 33 | 34 | public var body: some View { 35 | ZStack(alignment: .leading) { 36 | VStack(alignment: .leading, spacing: 8) { 37 | HStack(spacing: 8) { 38 | ForEach(0.. some View { 69 | Text(text) 70 | .foregroundColor(Color.black) 71 | .font(.system(.headline)) 72 | .frame(width: 50, height: height) 73 | .background( 74 | RoundedRectangle(cornerRadius: cornerRadius).stroke( 75 | hasError ? Color(.red).opacity(0.8) : Color(.blue).opacity(0.8), 76 | lineWidth: isActive ? 1 : .zero 77 | ) 78 | .background(backgroundColor) 79 | ) 80 | .clipShape(RoundedRectangle(cornerRadius: cornerRadius)) 81 | } 82 | 83 | var backgroundColor: Color { 84 | if isActive { 85 | return hasError ? .init(.red).opacity(0.2) : .init(.blue).opacity(0.2) 86 | } else { 87 | return .init(.gray).opacity(0.2) 88 | } 89 | } 90 | 91 | func codeLetter(with index: Int) -> String { 92 | if code.count > index { 93 | let start = code.startIndex 94 | let current = code.index(start, offsetBy: index) 95 | return .init(code[current]) 96 | } else { 97 | return "" 98 | } 99 | } 100 | } 101 | 102 | 103 | 104 | 105 | --------------------------------------------------------------------------------