Skip to content

Advanced Mapping

Alexis Bridoux edited this page Jan 25, 2021 · 5 revisions

Default values

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

Unique field

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)

RawRepresentable

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.

Fallback

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)

Codable with "Binary Data"

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)

CodableConvertible with "Binary Data"

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)

Custom CodableConvertible

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.

Fallback

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.

Subscribing to conversion errors

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)
        }
}