GitHub

GTCodable – Automate Common Tasks in Swift Using POP, Reflection and the Codable Protocol

About GTCodable

I’ve been always wanted to have a really fast and natural way to save data to file when making iOS projects. Something that wouldn’t require from me to write again and again the same lines of code, even the simplest ones so I can extract some data out of an object and save it persistently to my app’s documents directory. Something that would be easy to remember and use, and why not, something that would give me a variety of saving options?

So, the GTCodable protocol came to life! It’s a combination of three different forces in Swift:

  • POP (Protocol Oriented Programming): It’s a protocol, it’s not a class, it’s not a struct. It’s a protocol with its extension and default function implementation, that allows any other custom class or struct to conform to it and use its features right away.
  • Reflection: The need to come up with a solution that would feel really natural without any weird workarounds led me towards Reflection, which even though is not that powerful in Swift and caused me a few troubles along the way, it made the difference by enabling me to access properties and values of objects in runtime and perform certain actions on them in a fully automatic fashion. For example, without reflection it wouldn’t be possible to convert an object in to a dictionary (see more next) with GTCodable.
  • Encodable & Decodable Protocols: Or in one word, the new Codable protocol in Swift 4, which lets us JSON encode and decode at a blink of the eye. It’s not like the old NSCoding protocol where we had to manually specify the properties to encode and decode. Encodable & Decodable protocols have vital effect on GTCodable.

With GTCodable protocol I’m not reinventing the wheel, I’m just putting together different tools that Swift provides us with and I’m automating certain tasks. In a fast forward mode, here’s what GTCodable has to offer or it’s being capable of:

  • Save an object in JSON format, Plist file, or data file (archive made by NSKeyedArchiver) by calling one method only for each case (I don’t think it could be faster and more natural than that).
  • Save manually JSON data, dictionary, or archive data to custom URLs with custom names.
  • Exclude properties from being encoded or converted.
  • Convert objects to dictionaries (have I said that?).
  • Load data from JSON, Plist, or data file by calling one method only.
  • Initialize objects and populate values to their properties with one function call only.
  • Combine and nest custom types that conform to GTCodable protocol.
  • Use collections and dictionaries and combination of them (dictionaries inside collections, collections inside dictionaries, etc) of objects conforming to GTCodable protocol.
  • Use assistive functions to get String representations of JSON data, values of properties of an object, even the path to the documents directory so you don’t have to remember and type it again any longer.

Of course, like every new game that respects itself, there are some rules to follow, so please keep reading about how to use GTCodable, or at least read the Rules section later in this document.

How to Install

There’s no need to install, just clone or download the repository and drag the GTCodable.swift file into your iOS Swift project.

Check it out on Github

GTCodable Provided Functions At A Glance

Here’s a list of functions provided by the GTCodable protocol. Names are self explanatory, and more details about each function exist in the next parts.

GTCodable protocol contains some additional private functions that perform important work behind the scenes, but there’s no need to be listed here as they are not publicly available.

Using GTCodable

Consider the following struct that conforms to GTCodable protocol:

Let’s initialise an object of type User:

Then, encoding into JSON and saving to file:

Saving a Plist file containing the properties and their values:

Archiving and saving to file:

The outcome:

gtcodable_output
Generated files: JSON data, Plist and Archive

All the above three functions return a Bool value that indicates whether each process was successful or not.

When using any of the functions that were just presented, the struct’s name is used automatically as the file name with the following cases applying:

  • For JSON files, the json extension is appended.
  • For Plist files, the plist extension is appended.
  • For archives, no extension is appended.

Note that a directory called “appdata” is created automatically in the documents directory. It’s the place where GTCodable stores files and loads from.

Besides the above convenient functions to save an object using different formats by calling a single method only, you can also use the following methods to encode or convert and get the results as objects:

The toJson() function returns a JSON encoded object, if encoding is successful. The getTextualRepresentation(fromJson:) is another function provided by the GTCodable protocol, and returns a textual representation of a JSON object as its name suggests. Here’s what it’s printed:

To get an archived object:

However, one of the most interesting cases is that you can get a dictionary containing all properties and their values, which practically means that you can convert an object into a [String: Any] dictionary:

Output:

In addition to the toDictionary() function, you can use another one named toDictionary(fromJson:) if you want to get a dictionary from a JSON object:

GTCodable allows you to do the exact opposite fast as well, meaning to create a JSON object based on a given dictionary. For example:

To manually save a JSON object to a custom URL use the save(json:toURL:) function:

In a similar fashion, you can manually save a dictionary to a custom URL:

Same as above, if you have an archived object you can manually save it in case you don’t want to use the saveArchive() convenient function:

Loading and Initializing With GTCodable Protocol

Going to the opposite direction, here’s how to load data from files that were originally created using any of the saveJSON(), savePlist() or saveArchive() functions.

Loading from from the default JSON file:

From the default Plist file:

And from the default file with archived data:

These three functions return a Bool value indicating whether each action was performed successfully or not.

Except for the above convenient and fast functions, you can also load manually from custom URLs. Here’s how you can load JSON data:

Loading from a Plist file:

Loading archived data:

Lastly, you can populate data from a JSON object, dictionary, or an archive to a GTCodable-conforming object using any of the three “initialize(using…)” functions demonstrated next.

Excluding Properties

You won’t always want to include all properties when encoding an object as JSON or converting to dictionary. Suppose, for example, that we don’t want to include the avatarFile property when saving our JSON encoded data to file. With GTCodable is really easy to ignore properties, as all you need is to declare an array of String objects named mandatorily* excludedProperties, and append the names of the properties you want to be excluded to it*.

In action, and in the User struct, the easiest way to do it is as shown next:

Of course, if more properties had to be ignored, then their names should be appended to that array. The following excludes not only the avatarFile, but the id and username properties as well:

It’s really important to stress this: Properties you want to be excluded from encoding or conversion must be declared as Optionals, like for example the following property:

in the User struct. This is a rule, and please respect it to avoid problems.

As an alternative, the excludedProperties array can be declared in the struct as Optional:

Then, and after the initialisation of a User object, we can add values to it:

Always make sure that the property names you add to the excludedProperties array match to the actual names and there are no typos.

Advanced Usage

The samples demonstrated previously are quite simple, and of course GTCodable’s powerfulness doesn’t stop there. Let’s extend our example by creating the following struct:

Both the AdditionalInfo struct and the Gender enum conform to GTCodable protocol, and this is a requirement for custom types. Notice also that a specific type (Int) is defined for the Gender enum, which is also another requirement.

Updating the User struct now:

And initialising such an object:

By JSON-encoding the user object, the additionalInfo property will be included to the produced JSON data:

The same, for example, will happen if we get a dictionary from the user object:

What if we would want to exclude a property from the AdditionalInfo struct though? How would we exclude the lastName?

To do that, declare the excludedProperties String array in the AdditionalInfo struct, and add the name of the property or properties you want to be ignored:

In the output JSON the last name is missing now:

Alternatively, you can add values to the excludedProperties array while configuring the user object. At first, you need to declare the excludedProperties as an optional in the AdditionalInfo struct:

Then:

Important Tip: Suppose that you have properties to ignore in the AdditionalInfo struct, but not in the User, which contains an object of the AdditionalInfo. Normally, you would expect to declare the excludedProperties array in the AdditionalInfo only. But not! Even if there are not excluded properties in the container class or struct (in our scenario in the User struct) but exist in the contained custom types, then you have to mandatorily declare the excludedProperties array and initialize it in the container structure, without adding any content to it.

That means that even if we wouldn’t have any properties to exclude in the User struct, we would still need to declare the following without adding any values:

A GTCodable-conforming custom type can contain collections or dictionaries with GTCodable-conforming types as well. Consider the following array in the User struct:

Let’s add a couple of buddies to our user object:

Let’s JSON encode and see the results:

Notice that the properties that should be ignored are actually excluded from the final outcome.

Even though all previous examples contains structs only, the exact same principles and techniques apply to classes too!

Other Features

The most important and vital functions of the GTCodable protocol have been already presented and explained. However, GTCodable comes with a few more additional, auxiliary, let’s say “bonus” functions that can make our work a little bit faster.

We have already seen the first one; it’s the getTextualRepresentation(fromJson:) and returns a textual representation of a JSON object. You can use it with any JSON encoded object, not only those you create with GTCodable.

The next one is called getDocDirPath(). It returns the path to the documents directory as a String value. If your custom types conform to GTCodable, then you get that path for free and you don’t have to specify it again by yourself.

The getRaw() function returns the raw value of an enum (or more generally, a RawRepresentable type) as Any? type. If no raw value exists, it just returns nil. If it doesn’t make any sense to you because we can use the rawValue property to get the enum value, you’re right. However, it’d be impossible to get an enum’s value without it while using Reflection.

Lastly, it’s the describeSelf() function. This one returns a String value that lists an object’s properties and their values. Take for example the user object of the User struct:

Here’s what is printed in Xcode console:

The Rules

Please follow the next rules for a proper usage of the GTCodable protocol.

  • Stating the obvious: Classes, structs and enums can conform to GTCodable protocol.
  • When it’s necessary to exclude properties from encoding or conversion, declare a String array in your custom type (class or struct) and name it excludedProperties mandatorily.
  • The names of the properties that should be excluded from encoding or conversion must be added to the excludedProperties array. Watch out for typos.
  • Properties that should be excluded from encoding and conversion must be declared as Optionals. Avoiding doing that will result in failure when trying to load data to an object.
  • Always declare and initialise the excludedProperties array if your class or struct contains custom types that conform to GTCodable protocol and the have their own properties that should be ignored.
  • If any problems are met with properties of custom types inside a class or struct that conform to GTCodable protocol, try to initialise them at the time of declaration if possible.
  • Always set an explicit type to Enums that conform to GTCodable.
  • There is no need to adopt the Encodable, Decodable, or Codable Swift protocols when using GTCodable.
  • Tuples are not supported; avoid tuples. Use arrays or dictionaries instead.

Check it out on Github