TestsTested | ✓ |
LangLanguage | SwiftSwift |
License | MIT |
ReleasedLast Release | Jun 2016 |
SPMSupports SPM | ✗ |
Maintained by Sam Williams.
MagneticFields is a library for adding fields to your model objects. It’ll give you:
MagneticFields is available through CocoaPods. To install it, add the following line to your Podfile:
pod "MagneticFields"
To run the example project, clone the repo, and run pod install
from the Example directory first.
class Person {
let age = Field<Int>()
}
person.age.value = 10
The basic field type is Field
, a generic class whose type parameter is its value type. For multivalued fields, there is ArrayField
, which wraps another field that describes the single-valued type.
let tag = Field<String>(name: "Tag")
let tags = ArrayField(Field<String>(), name: "Tag")
The inner field is responsible for validations, transformations, etc.. The ArrayField
owns top-level attributes like name
, key
, etc. – but for convenience, it will copy them from the inner field at initialization.
The unary prefix operator *
is provided to wrap a Field
in an ArrayField
. So you can also write the above declaration like this:
let tags = *Field<String>(name: "Tag")
Simple closure validations:
let age = Field<Int>().require { $0 > 0 }
Rules can be chained, too, implying an AND. Order is not important.
let age = Field<Int>().require { $0 > 0 }.require { $0 % 2 == 0 }
By default, nil
values will be considered valid. To change that for a given rule, pass allowNil: false
to require
.
To validate a field value, either call field.valid
(returning a Bool
) or field.validate()
, which returns a ValidationState
enum:
public enum ValidationState:Equatable {
case Unknown
case Invalid([String])
case Valid
}
The associated value of the .Invalid
case is a list of error messages (e.g., ["must be greater than 0", "is required"]
).
Fields will automatically have the following timestamps:
updatedAt
: the last time any value was setchangedAt
: the last time a new value was set (compared using ==
)This library includes the Observer
and Observable
protocols for generic, type-safe change observation. Fields implement both protocols.
An Observable
can have any number of registered Observer
objects. The -->
operator is a shortcut for the addObserver
method (<--
works the same, only with its arguments swapped). Observation events are triggered once when the observer is added, and after that whenever a field value is set.
An observer can be added if it implements the Observer
protocol, which has a valueChanged(observable, value: value)
method.
field --> observer
Or, a closure can be provided. In place of an observer object, an owner
is used only to identify each closure; each owner can only have one associated closure.
field --> owner { value in
print(value)
}
We can still register a closure even if no observer is given. This is effectively registering the closure with a null observer. There can only be one of these at a time.
age --> { value in
print("Age was changed to \(value)")
}
Since Field
itself implements both Observable
and Observer
, the -->
operator can be used to create a link between two field values.
sourceField --> destinationField
This will set the value of destinationField
to that of sourceField
immediately, and again whenever sourceField
’s value changes.
The <-->
operator is a shortcut for <--
followed by -->
(and can only be used between two Fields).
field1 <--> field2
Since <--
is called first, both fields will initially have the value of field2
.
Fields and Observables have a strongly typed value
property, which must match an Observer’s associated type (ValueType
) or the closure’s parameter.
So this will fail to compile:
let name = Field<String>()
let age = Field<Int>()
name --> age
Unregistering observers is done with the removeObserver
method, or the -/->
operator. All observers can be removed with removeAllObservers()
.
It can be useful to distinguish between a value that’s nil because hasn’t been loaded yet (e.g., from an API), and one that is known to be nil. For this, fields provide the state
property, whose values are in the LoadState
enum:
public enum LoadState {
case NotSet
case Set
case Loading
case Error
}
All fields are initially in the .NotSet
state, but automatically become .Set
when their value is set to anything.
The .Loading
state can be useful when the process of loading takes time. You might decide to show a spinner in the UI while making an API request, for example.