Red Davis
Red Writes Here

Red Writes Here

@Validate

Validations in Swift

Red Davis's photo
Red Davis

Published on Oct 27, 2021

3 min read

Subscribe to my newsletter and never miss my upcoming articles

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.

🍕

 
Share this