├── .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 |
63 |
64 | # It works:
65 | 
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 |
--------------------------------------------------------------------------------