-
Notifications
You must be signed in to change notification settings - Fork 2
Advanced Mapping
It's possible to declare default values that should be used if the stored value is nil
. Doing so transform the output type as non optional.
let name = Field(\.name, default: "John") // output: String
let avatar = Field(\.avatar, as: UIImage.self, default: .placeholder) // output: UIImage
This declaration ensures the unicity of the entity attribute in the entity table. Each time an assign(:to:)
function is called, a fetch will be performed to make sure the value is not already used.
let name = UniqueField(\.name)
It's possible to store an enum with its raw type in Core Data. The requirement is to have the RawValue
to be storable in Core Data: Int16
, Int32
, Int64
, String
. To use this feature, the entity attribute has to be declared with the raw type of the enum.
enum ModelType: String {
case duck, dog
}
// ...
@NSManaged var type: String?
// ...
let type = Field(\.type, as: ModelType.self)
It might be possible in a future release to fetch the entity or model on this field in a nice way.
If the stored raw type cannot be converted to the RawRepresentable
type, it's possible to declare a fallback value. If not such value is specified and the stored value is erroneous, the program will exit with a preconditionFailure.
let type = Field(\.type, as: ModelType.self, fallback: .dog)
You can specify a Codable
object when storing an attribute as "Binary Data". To do so, set the value type of the attribute as "Binary Data", then declare the type with the field. The library will automatically try to encode and decode the value to store it as binary data.
struct Controller: Codable {
var color: String
var accuracy: Double
}
//...
let field = Field(\.controller, as: Controller.self)
Some Foundation
classes are hardly made Codable
, like the NSObject
subclasses. If it's possible to store them as Transformable
and to specify a transformer name in the model editor or even to write one, the library offers another way to do so. Which we think is clearer and easier to use.
As for Codable
object, the attribute has to be set to "Binary Data". Then, it's needed to make the NSObject
conform to CodableConvertible
.
If no customisations is needed on the way the NSObject
is stored, you can simply declare the conformance of the NSObject
to CodableConvertible
. For instance with a UIColor
.
extension UIColor: CodableConvertible {}
// ...
let color = Field(\.color, as: UIColor.self)
If needed, it's possible to specify how to store the NSObject
. For example to store a UIColor
, you first have to declare an object that will act as the Codable
version of the NSObject
:
struct CodableColor: CodableConvertibleModel {
var red: CGFloat
var green: CGFloat
var blue: CGFloat
public var converted: UIColor { UIColor(red: red, green: green, blue: blue) }
}
Then you can wire up with the NSObject
:
extension UIColor: CodableConvertible {
public var codableModel: CodableColor {
guard let components = cgColor.components, components.count >= 3 else {
preconditionFailure("UIColor with invalid components")
}
return CodableColor(red: red, green: green, blue: blue)
}
}
You can finally set the color attribute as "BinaryData" and declare it.
let color = Field(\.color, as: UIColor.self)
We find this approach especially useful when working with a remote API that will send JSON or any other data structures that Codable
can work with. When that's the case, a CodableConvertibleModel
can easily be reused when communicating with the API.
Similarly to the RawRepresentable
fields, it's possible to specify a fallback value to use in case of the conversion from the raw value to the RawRepresentable
type fails. If no such value is specified, the program will exit with a preconditionFailure.
let avatar = Field(\.avatar, as: UIImage.self, fallback: .avatarPlaceholder)
let color = Field(\.color, as: UIColor.self, fallback: .green)
Exiting with a preconditionFailure is an implementation design choice. We consider than storing a value which cannot be converted to a desired type should be resolved in the development stage, and is not relevant for the end-user.
When the conversion of a field can go wrong - for instance a RawRepresentable
or Codable
conversion - it's possible to subscribe to an error publisher to take relevant actions. This will not prevent a precondition failure if no fallback value is provided, but let you know if the conversion went wrong. Thus, it could be really useful when using fallback values.
All the the fields have a conversionErrorPublisher: AnyPublisher<ConversionError, Never>
that you can can subscribe to to be notified when the conversion for this field has failed.
For example, to subscribe to the color field from above:
color.conversionErrorPublisher
.sink { print("Conversion error of \($0.attributeLabel): \($0.errorDescription ?? "No description")")
.store(in: &subscriptions)
It's possible to print all the conversion error for the fields. To do so, it's simpler to declare a new protocol with only the conversion error publisher requirement and use it with reflection (to cast without dealing with an associatedType
)
protocol ConversionErrorObservableField {
var conversionErrorPublisher: AnyPublisher<ConversionError, Never> { get }
}
extension FieldInterface: ConversionErrorObservableField {}
extension UniqueFieldInterface: ConversionErrorObservableField {}
Then
func setupConversionErrorSubscriptions() {
Mirror(reflecting: self).children
.compactMap { $0.value as? ConversionErrorObservableField }
.forEach { observableField in
observableField
.conversionErrorPublisher
.sink { print("Conversion error of \($0.attributeLabel): \($0.errorDescription ?? "No description")") }
.store(in: &subscriptions)
}
}
Wiki valid for version 1.0.0