A long long time ago, in a previous life, I was a Rails developer and always loved the simplicity of the validation API. However, UIKit or SwiftUI contains no such API's (with good reason). Therefore, with every iOS and macOS project I find myself adding some kind of user input validation and every time the result feels awkward. It always feels like an after thought.
So I figured it was time to design an API that felt more natural.
As we are validating a property a property wrapper seemed the sensible place to start...
The easiest way to explain [@Validate](https://github.com/reddavis/Validate)
is to show it.
import SwiftUI
import Validate
struct ContentView: View
{
// #1
@Validate(.presence()) var name: String = ""
var body: some View {
VStack(spacing: 60) {
VStack {
// #2
TextField("Name", text: self.$name)
.textFieldStyle(.roundedBorder)
.padding()
VStack {
// #3
ForEach(self._name.errors.localizedDescriptions, id: \.self) { message in
Text(message)
.foregroundColor(.red)
.font(.footnote)
}
}
}
Button("Save") {
// ...
}
.buttonStyle(.bordered)
}
}
}
There are a few things to highlight.
#1
The @Validate
property wrapper accepts an array of Validation
instances. Validation
is simply a struct that has a throwing closure for validation.
/// A struct for encapsulating validations.
public struct Validation<Value>
{
public typealias Validate = (_ value: Value) throws -> Void
public let validate: Validate
// MARK: Initialization
/// Initialize a new `Validation` instance.
/// - Parameter validate: A `Validation.Validate` closure.
public init(_ validate: @escaping Validate)
{
self.validate = validate
}
}
Validate has several validation's built in such as: .presence()
, .format(_ pattern: String)
, .count(greaterThan minimum: Int)
, .count(lessThan maximum: Int)
and .count(equalTo count: Int)
.
If the built in validations don't meet your requirements, Validation
is public so anyone can create their own validations. This is how .count(greaterThan minimum: Int)
is put together:
public extension Validation
{
typealias CountGreaterThanErrorMessageBuilder = (_ minimum: Int) -> String
/// Validate a collection's count is greater than a minimum amount.
/// - Parameters:
/// - greaterThan: The minimum amount.
/// - message: Closure to build custom error message.
/// - Returns: A Validation instance.
static func count<T: Collection>(
greaterThan minimum: Int,
message: CountGreaterThanErrorMessageBuilder? = nil
) -> Validation<T>
{
.init { value in
guard value.count > minimum else
{
throw ValidationError.buildGreaterThan(
minimum: minimum,
message: message
)
}
}
}
}
#2
A small detail, but important if you use SwiftUI. @Validate
acts similar to @State
in terms that it's projectedValue
is a Binding
. This means you get the same benefits of @State
but with the added feature of validation.
#3
Validate
exposes a var errors: [Error]
property to access all the errors produced from validation along with a handy var isValid: Bool
.
Onwards
If you wanna check Validate out, you can find it over on Github.
🍕