When building a mobile application, we might have the need to configure certain properties after the app has been built and released. A common approach is to have the app request this from a server, who then returns the current values, for example in JSON format.
While you could create and edit these JSON files by hand, then write the matching code to decode them, I propose the following: Define our configuration in code, then use the same data structures for encoding and decoding.
When talking about iOS apps written in Swift, this could mean defining the configuration as a Codable
struct and using JSONEncoder/Decoder
.
You can find an example implementation of this approach here, following I will cover why and how to use it.
Benefits
The main benefit of this approach is that we take away the human involvement in generating and editing the configuration file, which avoids accidentally malformed files.
We can write unit tests to make sure our encoded feeds can be decoded, and if we establish some sort of versioning, we can even check if an updated configuration feed is still decodable with a previously shipped version of the configuration.
Example
Here’s an example of how we might define a configuration in Swift:
public struct Configuration: Codable {
let baseURL: String
let homeEndpoint: String
}
let config = Configuration(baseURL: "https://api.myapp.com", homeEndpoint: "/home")
In this case, the configuration is used to configure API endpoints, which may need to be exchanged later.
If we bundle this in a Swift package, we can both import it in our app and provide a command-line application to generate JSON feeds. The same configuration instance used to encode the feed can also acts as a “default” configuration for the app, in case no remote configuration has been loaded yet.
Automatic feed generation
If we store our configuration Swift package under version control, we can use a continuous delivery system to generate feeds every time we update the configuration code. In my example, I use GitHub Actions to generate new feeds and upload them to an AWS S3 bucket. The actions run on every tagged version of the configuration code.
Versioned configurations
In my example implementation, the idea is to match the configuration version to the app version.
So for my app release 1.0.0
I tag the config repository with tag 1.0.0
, which the GitHub action uploads as config-1.0.0.json
.
If I later need to update a feed for an app version which has already been released, I tag the version prefixed with the app version, e.g. 1.0.0-updated
. This lets us check out the released version of the configuration (1.0.0
) and run unit tests to see if the new feed can still be decoded successfully by the released app. If that is not the case, the action will fail and the feed won’t be published.
Is this a good idea?
I recently tried this approach while working on tastytunes. It seems to work well so far, but as of writing this, I have not released multiple app versions yet, so my experience is limited. I plan on updating this post if I discover more findings.
During development, it’s a bit cumbersome to remove and re-add tags for the current config version and Xcode will not fetch updates for the same tag unless you reset the package cache. For now, I have resorted to point the package to the main
branch during development and change to a tag before releasing.
This can be forgotten before a release though, so I’m still looking for ways to optimise my versioning.
How can I try it?
The sources to my proof-of-concept implementation can be found on GitHub and it’s configured as “template repository”, so you could use it as basis for your own version of this. The repository includes an example configuration Swift package, a CLI to generate JSON feeds and GitHub Actions setup to automatically generate, test and publish configuration feeds.
If you have questions or ideas to improve this, feel free to let me know! Thanks for reading and have a great day 🙂