Skip to content

Commit

Permalink
Initial code commit
Browse files Browse the repository at this point in the history
  • Loading branch information
sanzaru committed Oct 15, 2024
1 parent 878c361 commit dc4935b
Show file tree
Hide file tree
Showing 8 changed files with 440 additions and 0 deletions.
22 changes: 22 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
.build
.index-build
DerivedData
/.previous-build
xcuserdata
.DS_Store
*~
\#*
.\#*
.*.sw[nop]
*.xcscmblueprint
/default.profraw
*.xcodeproj
Utilities/Docker/*.tar.gz
.swiftpm
Package.resolved
/build
*.pyc
.docc-build
.vscode
Utilities/InstalledSwiftPMConfiguration/config.json
.devcontainer
23 changes: 23 additions & 0 deletions .swiftlint.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
included:
- Sources

disabled_rules:
- line_length
- identifier_name
- type_name
- file_length
- type_body_length
- function_body_length


nesting:
type_level: 4


custom_rules:
trojan_source:
regex: "[\u202A\u202B\u202D\u202E\u2066\u2067\u2068\u202C\u2069]"
severity: error
message: "Source should not contain characters that may be used in reordering attacks. https://trojansource.codes/trojan-source.pdf"

reporter: "xcode"
26 changes: 26 additions & 0 deletions Package.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
// swift-tools-version: 6.0

import PackageDescription

let package = Package(
name: "Formify",
platforms: [.iOS(.v16), .macOS(.v13), .tvOS(.v16), .watchOS(.v9)],
products: [
.library(
name: "Formify",
targets: ["Formify"]),
],
dependencies: [
.package(url: "https://github.com/SimplyDanny/SwiftLintPlugins", from: "0.57.0")
],
targets: [
.target(
name: "Formify",
plugins: [.plugin(name: "SwiftLintBuildToolPlugin", package: "SwiftLintPlugins")]
),
.testTarget(
name: "FormifyTests",
dependencies: ["Formify"]
),
]
)
185 changes: 185 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,185 @@
# 📃 Formify - Swift library for fast and easy form and input validation

Formify is a Swift library designed for easy form and input validation. With Formify, you can easily validate all your
TextField or TextEditor elements without needing to add any special changes or modifiers.

### Key facts:

* **👌 Ease of use:** It is very easy to implement Formify into your views and validate inputs - even in existing ones.
* **🏎️ Speed:** With no special magic or any ObservableObjects and subscribers, the library is very fast
* **📐 Size:** Very small footprint
* **🚀 Performance:** Minimal performance impact

## Requirements

* XCode 16
* Swift 6
* iOS 16.0 / macOS 13.0 / tvOS 16.0 / watchOS 9.0

## Installation

### Swift Package Manager

Add the following to the Package.swift of your Swift package:

```
dependencies: [
.package(url: "https://github.com/sanzaru/formify.git", from: "0.0.1")
]
```

### XCode

Add the following package to your project:

https://github.com/sanzaru/formify.git

## Usage

Formify uses `FormifyField` objects with `FormifyOperator` operators for input management and validation.
All `FormifyField` objects come with a `value` attribute of type String. This value can be used as a binding inside a
`TextField` or `TextEditor` view.

You can check the validity by simply checking the `isValid` attribute or the `errors` array of the field.

The simplest form of validation would be to declare a state variable inside a view, use the `value` of the
`FormifyField` inside a TextField, and check the `isValid` attribute:

```swift
...

@State private var formField = FormifyField(operators: [.required, .pattern(/[A-Za-z ]+/)])

...

TextField("", text: $formField.value)
.textFieldStyle(.plain)

...

Button { } label: {
Text("Submit")
}
.disabled(!formField.isValid)

...

```

> [!NOTE]
> You can also pass the `FormifyField` as a `@Binding` into views or use the object inside an `@ObservableObject` as a
`@Published` variable. See the example for more information.


## Operators

| Name | Description | Example |
| --- | --- | --- |
| .required | If set, the field becomes required and cannot be left empty. | ```.required``` |
| .minLength(Int) | If set, the value must be longer than the provided length. | ```.minLength(10)``` |
| .maxLength(Int) | If set, the value must be shorter than the provided length. | ```.maxLength(10)``` |
| .pattern(RegEx) | If set, the value must match the provided regular expression. | ```.pattern(/[a-zA-Z]/)``` |


## Example

The following example shows a simple view with a form containing three fields: a name, an email address, and a custom
value. The name and email fields are required and validated against specific patterns. The name field also has minimum
and maximum length validation, while the custom field is only required without additional validation:

```swift
import SwiftUI
import Formify

struct ContentView: View {
struct FormFields {
var name = FormifyField(operators: [.required, .minLength(10), .maxLength(20), .pattern(/[A-Za-z ]+/)])
var email = FormifyField("[email protected]", operators: [.pattern(/[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/)])
var custom = FormifyField("Preset value", operators: [.required])

var isValid: Bool {
name.isValid && email.isValid && custom.isValid
}
}
@State private var formFields = FormFields()

var body: some View {
NavigationStack {
Form {
// Name text field
VStack(alignment: .leading) {
Text("Name*")
.foregroundColor(.teal)

TextField("John Doe", text: $formFields.name.value)
.textFieldStyle(.plain)
.modifier(FormValidationErrorWrapperModifier(formField: $formFields.name))
}

// Email text field
VStack(alignment: .leading) {
Text("Email*")
.foregroundColor(.teal)

TextField("[email protected]", text: $formFields.email.value)
.textFieldStyle(.plain)
.modifier(FormValidationErrorWrapperModifier(formField: $formFields.email))
}

// Custom text field
VStack(alignment: .leading) {
Text("Custom")
.foregroundColor(.teal)

TextField("Some value", text: $formFields.custom.value)
.textFieldStyle(.plain)
.modifier(FormValidationErrorWrapperModifier(formField: $formFields.custom))
}
}
.navigationTitle("Example Form")
.toolbar {
ToolbarItem(placement: .topBarTrailing) {
Button { print("Submit") } label: {
Text("Submit")
}
.disabled(!formFields.isValid)
}
}
}
}
}
```

Additionally, the following example shows a simple ViewModifier that wraps all errors and displays a message underneath
the TextField:

```swift
import SwiftUI
import Formify

struct FormValidationErrorWrapperModifier: ViewModifier {
@Binding var formField: FormifyField

func body(content: Content) -> some View {
VStack(alignment: .leading) {
content

let errors = formField.errors
if !errors.isEmpty, formField.isTouched {
ForEach(errors, id: \.self) { error in
Group {
switch error {
case .pattern: Text("Invalid pattern")
case .required: Text("Required")
case .minLength(let length): Text("Min length \(length) / \(formField.minLength ?? 0)")
case .maxLength(let length): Text("Max length \(length) / \(formField.maxLength ?? 0)")
}
}
.font(.caption)
.foregroundColor(.red)
}
}
}
}
}
```
15 changes: 15 additions & 0 deletions Sources/Formify/FormifyError.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
//
// FormifyError.swift
// Formify
//
// This file is part of the Formify Swift library: https://github.com/sanzaru/formify
// Created by Martin Albrecht on 15.10.24.
// Licensed under Apache License v2.0
//

public enum FormifyError: Error, Hashable {
case required
case pattern
case minLength(Int)
case maxLength(Int)
}
84 changes: 84 additions & 0 deletions Sources/Formify/FormifyField.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
//
// FormifyField.swift
//
// This file is part of the Formify Swift library: https://github.com/sanzaru/formify
// Created by Martin Albrecht on 15.10.24.
// Licensed under Apache License v2.0
//

import Foundation

public struct FormifyField {
public var value: String {
didSet {
if value != oldValue {
if !value.isEmpty {
isTouched = true
}

validate()
}
}
}

public private(set) var errors = [FormifyError]()
public private(set) var isRequired = true
public private(set) var isTouched = false
public private(set) var minLength: Int?
public private(set) var maxLength: Int?
public private(set) var pattern: Regex<Substring>?

public var isValid: Bool {
errors.isEmpty
}

public init(_ initialValue: String = "", operators: [FormifyOperator] = []) {
self.value = initialValue

operators.forEach {
switch $0 {
case .required:
isRequired = true
case .minLength(let length):
minLength = length
case .maxLength(let length):
maxLength = length
case .pattern(let regex):
pattern = regex
}
}
}
}

// MARK: Validation

extension FormifyField {
private mutating func validate() {
errors = []

if isRequired && value.isEmpty {
errors.append(.required)
return
}

if let minLength, value.count < minLength {
errors.append(.minLength(value.count))
}

if let maxLength, value.count > maxLength {
errors.append(.maxLength(value.count))
}

if let pattern, (try? pattern.wholeMatch(in: value)) == nil {
errors.append(.pattern)
}
}
}

// MARK: Collection

public extension Collection where Element == FormifyField {
var isValid: Bool {
filter({ $0.errors.isEmpty }).isEmpty
}
}
15 changes: 15 additions & 0 deletions Sources/Formify/FormifyOperator.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
//
// FormifyOperator.swift
// Formify
//
// This file is part of the Formify Swift library: https://github.com/sanzaru/formify
// Created by Martin Albrecht on 15.10.24.
// Licensed under Apache License v2.0
//

public enum FormifyOperator {
case required
case pattern(Regex<Substring>)
case minLength(Int)
case maxLength(Int)
}
Loading

0 comments on commit dc4935b

Please sign in to comment.