CoatySwift Design Rationale

This document aims to explain some of the rationale behind our decisions when building CoatySwift, the problems we faced, and how we decided to solve them. Several sections include coding examples and provide background explanations on why things had and have to be done in a certain way to make CoatySwift do exactly what you want it to do.

The Pleasure and Pain of Static Languages

We decided to use the Codable protocol for all object encoding and decoding tasks. The Codable protocol is part of the Swift standard library and is recommended to be used for object conversion from and to JSON.

Swift is a statically typed language, which makes easy interaction with JSON objects non-trivial.

By implementing Codable we avoid third-party dependencies such as SwiftyJSON. SwiftyJSON, for example, could only provide a limited form of type safety in our context, and the user would be left to manually parse objects and create them ‘on the go’. Codable, on the other hand, allows a standardized interaction with the object parsing API and enables the application programmer to extend the predefined object types by overwriting the encode(to:) / init(from:) methods and calling their base implementations (see HelloWorldTask). Relying on traditional JSON parsing frameworks would require to reimplement this functionality entirely for each custom object. Working with untyped JSON objects in Swift is another way to use JSON data. This approach casts the data to [String: Any] dictionaries and eliminates the power of compiler based type checks what makes this approach inferior to our implementation.

NOTE: Codable is relatively new and under active development. For future releases we hope for better support of heterogenous data structures to simplify the decoding of custom Coaty objects (see rationale on CommunicationManager/ObjectFamily).

Codable Subclassing with Generics

Consider the following: Animal is a super class of the objects Cat and Dog. We define our generic type T when decoding / encoding to be of Type Animal. When decoding / encoding an object holding this T type, Swift is not able to decode it into its subclasses, such as Cat or Dog in a straightforward way. Instead, it always decodes / encodes the object as a type of Animal, ultimately not letting us access subclass-specific fields and decoding / encoding them properly.

SOLUTION As proposed in this post by Kewin Dannerfjord Remeczki, we trick the compiler by adding a ClassWrapper, an object that wraps around the decodable class (and its subclasses), which we can then map to the correct type.

The importance of ObjectFamily

As previously mentioned, Swift is a statically typed language and thus does not provide an easy option of creating dynamic types on the go. This limits us greatly when decoding or encoding objects in the CoatySwift framework, because the application programmer can introduce completely new types that need to be sent over the wire.

We have tried several different options, but so far, have found only one way to integrate application-time objects into CoatySwift while considering the constraints given by Codable as well as Swift itself. We therefore require a mapping between custom objects and their corresponding class implementations. This file is what we call a custom implementation of the ObjectFamily, which gives us the possibility to encode and decode new objects which have been introduced by the application programmer. Below you can see an example for a custom ObjectFamily. You can also find it in the CoatySwift developer guide.

NOTE: We assume that your custom ExampleObject.swift is a subclass of the built-in CoatyObject.swift. You can find this code similarly in the CoatySwift developer guide and in the CoatySwift Example folder.

enum ExampleObjectFamily: String, ObjectFamily {

    /// This is an exemplary objectType for your custom CoatyObject.
    case exampleObject = "io.coaty.hello-coaty.example-object"

    /// Define the mapping between objectType and your custom CoatyObject class type.
    /// For every objectType enum case you need a corresponding Swift class matching.
    func getType() -> AnyObject.Type {
        switch self {
        case .exampleObject:
            return ExampleObject.self
        }
    }
}

This information is also explicitly needed when bootstrapping CoatySwift applications and resolving controllers, as we need information about this object family to properly decode and encode objects being published or received in the CommunicationManager. An example for this can be found in the CoatySwift Example folder:

_ = Container.resolve(components: components,
                      configuration: configuration,
                      objectFamily: ExampleObjectFamily.self)

Here, the ExampleObjectFamily refers to custom implementations needed for the application to run, such as the ExampleObject.

Decoding and Encoding - A Complex Process

Using the information gathered from the ObjectFamily subclass given by the application programmer, we are able to provide type safety even with custom objects. We have adjusted our decoding and encoding mechanisms to work in the following way:

  1. Encode / Decode via application-specific Object Family

    First, our implementation tries to match an object against one of the classes provided by the application programmers in order to encode / decode it. If this fails, go to 2.

  2. Fallback Solution

    If none of the custom objects matched the object we want to encode / decode, check whether it’s a built-in CoatySwift type. You can find all generic CoatyObjects in CoatySwift/Classes/Common/CoatyObjectFamily.swift. If even this fails, a coding error will be thrown, as there is no type information given to us that can help us to infer the actual type of the object.

NOTE: If you need to encode / decode a property value of a custom Coaty object type that can be any valid JSON data, and you don’t know the JSON structure in advance, declare the property type as AnyCodable. Using this type, you can encode or decode mixed-type values in dictionaries and other collections that require Encodable or Decodable conformance.