ValueTransformer in Core Data explained: Storing absolute URLs

ValueTransformers in Core Data are a powerful way of transforming values before they get inserted into the database and before they get read. They’re set up in an abstract class which handles the value transformations from one representation to another.

They’re often used as a way to store values that aren’t supported by default. Core Data allows you to save types like strings and integers but storing a CGRect or CGPoint requires the usage of a ValueTransformer. Once you get to know how they work you’ll find out that you can use value transformers for other cases too like storing an absolute URL as a relative one.

In some cases this is fine but it also makes it a simple mistake to store absolute URLs which can become a very big problem in the future.

What is a ValueTransformer?

It’s all in the name: it transforms a value. More precisely, it’s transforming a value upon reading from- and writing to the Core Data database.

A ValueTransformer can take any type as input and return a value of a different type. An example is the transformation of a UIColor to NSData on insertion and converting it back from NSData to UIColor once it’s read.

How do I use value transformers in Core Data?

A ValueTransformer is an abstract class and requires to override and implement a few of its methods based on the transformation you try to achieve. Once a value transformer is defined it should be registered so that it’s available for your Core Data Model. Finally, you can define a property as transformable and assign your custom transformer in the model representation.

Creating a ValueTransformer

In this example, we’re going to create a transformer for converting a UIColor into NSData which we can store in our database.

We start by creating a new class called UIColorValueTransformer that inherits from ValueTransformer:

/// A value transformer which transforms `UIColor` instances into data using `NSSecureCoding`.
@objc(UIColorValueTransformer)
public final class UIColorValueTransformer: ValueTransformer {
    // .. Implementation goes here
}

Core Data can basically been seen as a technology that wraps C function calls into an easier to use object-oriented framework to manage a SQL database. It’s not (yet) built in Swift and, therefore, requires us to interact with Objective-C. The @objc attribute tells Swift to make our value transformer available in Objective-C.

The next step is to set up our transformation logic which we can do by overriding some of the methods that are available in the abstract ValueTransformer class. We start by defining the transformed value class and by marking our transformer as reversible.

override public class func transformedValueClass() -> AnyClass {
    return UIColor.self
}

override public class func allowsReverseTransformation() -> Bool {
    return true
}

The transformedValueClass needs to return the type for a “read”, a forward transformation. In our case, we like to have a UIColor type upon reading a value from our persistent container.

By defining our transformer as reversible we basically tell Core Data that our transformer is able to handle both insertions and reads. In other cases, you might only want to transform a value upon insertion to make sure that the value conforms to certain requirements. For example, storing a name with an uppercase first letter. In our case, however, we want to transform in both ways.

Transforming the values

Up next is implementing the actual transformation methods: one for reading from the database and one for writing to the database.

    override public func transformedValue(_ value: Any?) -> Any? {
        guard let color = value as? UIColor else { return nil }
        
        do {
            let data = try NSKeyedArchiver.archivedData(withRootObject: color, requiringSecureCoding: true)
            return data
        } catch {
            assertionFailure("Failed to transform `UIColor` to `Data`")
            return nil
        }
    }
    
    override public func reverseTransformedValue(_ value: Any?) -> Any? {
        guard let data = value as? NSData else { return nil }
        
        do {
            let color = try NSKeyedUnarchiver.unarchivedObject(ofClass: UIColor.self, from: data as Data)
            return color
        } catch {
            assertionFailure("Failed to transform `Data` to `UIColor`")
            return nil
        }
    }

We make use of the NSKeyedArchiver class that allows us to read and store data using secure encoding which is required since iOS 11 for improved security. We help ourselves during debugging by throwing an assertion if the transformation fails. Assertion failures are only triggered in debug builds which are great in our case!

Registering the transformer

The implementation of our value transformer is mostly done. We only need a way to register it and make it available for our Core Data model.

extension UIColorValueTransformer {
    /// The name of the transformer. This is the name used to register the transformer using `ValueTransformer.setValueTrandformer(_"forName:)`.
    static let name = NSValueTransformerName(rawValue: String(describing: UIColorValueTransformer.self))

    /// Registers the value transformer with `ValueTransformer`.
    public static func register() {
        let transformer = UIColorValueTransformer()
        ValueTransformer.setValueTransformer(transformer, forName: name)
    }
}

We make use of a strongly typed name instead of defining our name as a static string. This prevents future mistakes if we decide to rename our transformer.

The static register() method allows us to register the transformer as follows:

UIColorValueTransformer.register()

Make sure to do this before you set up your persistent container as you will otherwise run into errors like:

No NSValueTransformer with class name ‘UIColorValueTransformer’ was found for attribute ‘color’ on entity ‘user’

Setting up a property as transformable by connecting the value transformer

Finally, you need to connect your value transformer to a property in your database. You do this by defining a property as transformable:

Defining a property as transformable so that it can use a ValueTransformer.
Defining a property as transformable so that it can use a ValueTransformer.

You can connect the value transformer in the Data Model Inspector:

Connecting the value transformer inside the Core Data Model.
Connecting the value transformer inside the Core Data Model.

This is all you need to make use of your own value transformer. In this case, to store a UIColor into a Core Data persistent container.

Using a value transformer to store absolute URLs as relative URLs

Now that you know how a value transformer works it’s time to inspire you for other ways of implementing them.

Storing URLs in Core Data

A common scenario in Core Data is storing URLs. iOS 11 introduced the URIAttributeType which makes it tempting to store URLs directly into Core Data. In some cases this is fine but it also makes it a simple mistake to store absolute URLs which can become a very big problem in the future.

URLs often point to directories like an App Group folder or the Documents directory. Some of those URLs are paths that can potentially change as described in the documentation:

Always use the URL returned by this method to locate the group directory rather than manually constructing a URL with an explicit path. The exact location of the directory in the file system might change in future releases of macOS, but this method will always return the correct URL.

You might not always read the documentation carefully enough to prevent all cases which is why I’d like to always take the safest route. This means that I’m storing all my URLs relatively in Core Data.

Why should you use a value transformer to store absolute URLs as relative URLs?

You might wonder why you should use a value transformer to store URLs as relative URLs. The reason is very simple: to prevent code like this:

@NSManaged private var urlPath: String

public var url: URL? {
    NSURL(fileURLWithPath: urlPath, relativeTo: RabbitInfo.shared.appGroupURL).absoluteURL
}

It’s much nicer to define our property like this:

@NSManaged public var url: URL?

Using a value transformer to transform an absolute URL into a relative URL

In this example, we’re storing a URL that’s pointing to a file in the App Group container. It fetches the app group URL from our Config which uses the FileManager to retrieve the right path:

FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: "YOUR_APP_GROUP_NAME")

The final implementation looks as follows:

/// A value transformer which transforms relative URLs by using the App Group URL.
/// Storing absolute URLs in Core Data is discouraged as URLs might change in the future.
/// Example of those URLs are the App Group URL and Documents Directory URL.
@objc(AppGroupRelativeURLTransformer)
public final class AppGroupRelativeURLTransformer: ValueTransformer {

    /// - Returns: An relative URL by removing the App Group URL if it exists in the URL.
    override public func transformedValue(_ value: Any?) -> Any? {
        guard var url = value as? NSURL else { return nil }

        do {
            if let appGroupURLString = Config.appGroupURL?.absoluteString, var URLString = url.absoluteString, let appGroupURLRange = URLString.range(of: appGroupURLString) {
                URLString.removeSubrange(appGroupURLRange)

                url = NSURL(string: URLString) ?? url
            }

            return try NSKeyedArchiver.archivedData(withRootObject: url, requiringSecureCoding: true)
        } catch {
            assertionFailure("Failed to transform a `NSURL` to a relative version")
            return nil
        }
    }

    override public func reverseTransformedValue(_ value: Any?) -> Any? {
        do {
            guard let data = value as? NSData, !data.isEmpty, let url = try NSKeyedUnarchiver.unarchivedObject(ofClass: NSURL.self, from: data as Data) else { return nil }

            if url.scheme == nil, url.host == nil, let urlString = url.absoluteString {
                return NSURL(fileURLWithPath: urlString, relativeTo: Config.appGroupURL).absoluteURL
            } else {
                return url
            }
        } catch {
            assertionFailure("Failed to transform `Data` to `NSURL`")
            return nil
        }
    }

    override public class func transformedValueClass() -> AnyClass {
        return NSURL.self
    }

    override public class func allowsReverseTransformation() -> Bool {
        return true
    }
}

extension AppGroupRelativeURLTransformer {

    /// The name of the transformer. This is the name used to register the transformer using `ValueTransformer.setValueTrandformer(_"forName:)`.
    static let name = NSValueTransformerName(rawValue: String(describing: AppGroupRelativeURLTransformer.self))

    /// Registers the value transformer with `ValueTransformer`.
    public static func register() {
        let transformer = AppGroupRelativeURLTransformer()
        ValueTransformer.setValueTransformer(transformer, forName: name)
    }
}

As you can see, this value transformer looks much like our earlier defined UIColor transformer. Upon inserting a new URL, the relative path is extracted and stored. Once the value is read, we append the current URL pointing to our App Group container. This way, we’re sure to always reference the correct path and we’ll never run into missing files.

So when should I use the URL attribute type?

Only use the URL attribute type if you are sure that a path will not change in the future. Obviously, you can also use it for storing web URLs for which a transformer will not solve any future changes.

Conclusion

ValueTransformers in Core Data allow you to transform values from any type into any other type. Storing a UIColor, CGRect or other types can become possible by transforming them into NSData before insertion. Value transformers can also be used in other cases like transforming a URL from absolute to relative.

If you like to improve your Swift knowledge, even more, check out the Swift category page. Feel free to contact me or tweet to me on Twitter if you have any additional tips or feedback.

Thanks!