Handling routes from an API in a type-safe way with Swift

I’m working on an application that has to handle some kind of routing directions coming from an API in the following form:

{
    "title": "Tap to go to your profile!",
    "destination": "profile"
}

These items would show in a form of timeline of events, that when tapped, would take you to specific parts of the application.

I was thinking about how I could leverage Swift’s type-safety to make sure the application validates the destinations for each of these items and make a more robust solution for this.

Immediately, enums came to mind. I can easily define an enumeration with raw values:

enum Destination: String {
    case profile
    case info
    case feed
}

But then, I looked closely at the API’s documentation, and noticed that some destinations could come with attached metadata:

{
    "title": "Tap to go to Oscar's profile!",
    "destination": "profile:859928508374784884"
}

As you can probably guess, tapping on the item above should take you to the profile screen for the user with id 859928508374784884. I thought of enums with associated values, but that wouldn’t cut it. Associated values in enumerations prevent me from inflating instances from raw values.

I decided to come up with my alternative for enumerations to handle this specific case so I could build destination instances in a type-safe manner directly from the JSON objects coming from the API.


First, I started with a structure. Since I’m interested in being able to convert a raw string to an instance of my type and viceversa, RawRepresentable is the protocol to adopt.

struct Destination: RawRepresentable {
    private let _identifier: String
    
    var rawValue: String {
        return _identifier
    }
    
    init?(rawValue: String) {
        _identifier = rawValue
    }
}

Now, I need to check whether the raw value passed to the initializer is a valid one. For that, I defined a string set that contained all the possible identifiers for a Destination, and check for that on the failable initializer from RawRepresentable.

struct Destination: RawRepresentable {
    // ...
    
    private static let options: Set<String> = [
        "info",
        "profile",
        "feed",
        "event"
    ]
    
    init?(rawValue: String) {
        guard Destination.options.contains(rawValue) else {
            return nil
        }
        
        _identifier = rawValue
    }
}

The only thing missing right now would be handling the attached metadata for the available destinations.

For that, I added a value property that would hold the destination’s metadata (if any), and a couple more checks to the the initializer:

struct Destination: RawRepresentable {
    // ...
    var value: String?
    
    public var rawValue: String {
        return _identifier + (value != nil ? ":(value!)" : "")
    }
    
    init?(rawValue: String) {
        // Split the raw value
        let ids = rawValue.split(separator: ":")
            
        // Check that the identifier exists
        guard let identifier = ids.first else {
            return nil
        }
            
        // Convert from String.SubSequence
        let id = String(identifier)
        
        // Make sure this identifier is valid
        guard Destination.options.contains(id) else {
            return nil
        }
            
        _identifier = id
        
        // If metadata exists, add that metadata to value
        if ids.count == 2 {
            value = String(ids[1])
        }
    }
}

Now, conform with Codable so that Destination can be parsed from and to raw data:

struct Destination: RawRepresentable {
    // ...
    enum DestinationError: Error {
        case unsupportedDestination
        case wrongFormat
    }
    
    enum CodingKeys: String, CodingKey {
        case _identifier = "destination"
    }
    
    func encode(to encoder: Encoder) throws {
        var container = encoder.container(keyedBy: CodingKeys.self)
        
        try container.encode(_identifier, forKey: ._identifier)
    }
    
    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        let rawValue = try container.decode(String.self, forKey: ._identifier)
        let ids = rawValue.split(separator: ":")
        
        guard let identifier = ids.first else {
            throw DestinationError.wrongFormat
        }
        
        let id = String(identifier)
        
        guard Destination.options.contains(id) else {
            throw DestinationError.unsupportedDestination
        }
        
        _identifier = id
        
        if ids.count == 2 {
            value = String(ids[1])
        }
    }
}

Unfortunately, I coulnd’t find an elegant way to extract the initialization checks to another method (because scope initialization, etc), so the initializer from RawRepresentable has to be basically duplicated on the one from Decodable.

One last step would be making Destination conform with Hashable:

struct Destination: RawRepresentable, Codable, Hashable {
    // ...
    var hashValue: Int {
        return _identifier.hashValue
    }
}

func ==(lhs: Destination, rhs: Destination) -> Bool {
    return lhs.hashValue == rhs.hashValue
}

Note that hashValue takes only into account the identifier for the destination, not it’s attached value. That way, profile:859928508374784884 and a profile are considered equal, even though the first one has attached metadata.

This is super specific in this case, because I only care about the route and the metadata is treated as optional. However, in your own implementation, taking value into consideration for Hashable might make sense.

And that’s it. A quick hack would be to add static properties to Destination with prebuilt values so that they can be treated as enumeration cases:

struct Destination: RawRepresentable {
    static let info = Destination(rawValue: "info")!
    static let profile = Destination(rawValue: "profile")!
    static let feed = Destination(rawValue: "feed")!
    static let event = Destination(rawValue: "event")!
    
    static func profile(value: String) -> Destination? {
        return Destination.rawValue("profile:(value)")
    }
}

Here’s how all that comes together:

let json = ["destination": "profile:859928508374784884"]
let jsonData = try! JSONSerialization.data(withJSONObject: json, options: .prettyPrinted)

let decoder = JSONDecoder()
let parsedDestination = try? decoder.decode(Destination.self, from: jsonData)

let simpleDestination = Destination.profile
let profileDestination = Destination.profile(value: "859928508374784884")!

parsedDestination == profileDestination // #=> true

And of course, you can switch on Destination values as well:

switch parsedDestination! {
case .profile: 
    router.navigateToProfile(parsedDestination)
    
default:
    // Handle other cases.
}

I thought this could make for a nice example on how to leverage Swift’s flexibility to help when the default data structures are not compatible with our use cases.

If you have any comments or idea, or I missed something, feel free to ping me on Twitter @Swanros.

Thanks for reading!

Becoming a Better iOS Developer Through Tooling: My AltConf 2017 talk

The fine folks at Realm have now uploaded the remaining videos from AltConf 2017 — including the presentation that I gave there.

You can watch it a Realm Academy, here.

Presenting in the US for the first time was a really exciting experience for me — and something that I’m going to push myself to do more of. I really loved the rush of being there presenting in a foreign country on a foreign language.

I’m gonna call this one a victory — although I know there’s some stuff in the presentation that could’ve used a little more polishing, I’m quite happy with how everything turned out.

What’s nice about this, is that I can now watch myself with a calm head and take notes of things that I can improve for future conference talks in the US or around the world.

I’d really appreciate any constructive feedback you share with me about this presentation. Hit me up at oscar@swanros.com, or @Swanros on Twitter.

Apple starts rejecting apps with “hot code push” features

This thread from Apple’s Developer Forums is making some waves on the internets today. Apparently, they’re now rejecting apps that include code that can modify your app after it’s released to the store.

Rollout.io seems to be the SDK that’s setting the alarm off, so far.

If you use Rollout.io, or another “push code after you release to the store” service, it’s time to start thinking about changing your strategy. (clears throat I told you).

From Rollout.io‘s FAQ:

Does Rollout comply to Apple’s Guidelines?

Yes. As per Apple’s official guidelines, Rollout.io does NOT alter binaries. Rollout uses JavascriptCore to add logic to your patches. For more details, check out how Rollout is compliant with App Store guidelines.

I wouldn’t build my business on such a fine line.

And also, please don’t act surprised when Apple start rejecting React Native apps down the road.

The moral of the story: stay away from JavaScript.

Swift blog: Why is my Hashable implementation crashing on an iPhone 4s?

Today I found what I thought of as a bug that had me scratching my head for about an hour.

My app ran just perfectly on my iPhone 6S Plus. However, when I sent a build to my designer for him to do proper testing, it always crashed on the same spot.

Ran it on the iPhone 4s simulator and the crash looked like this:

No output on the console nor the debugger. Tried to run instruments to catch the crash, with no luck.

Did a bit of research, and finally got to the official Swift documentation.

Using overflow operators (&+, &-, &*) fixes the issue:

public var hashValue: Int {
    return firstValue &+ secondValue
}

Numbers can overflow in both the positive and negative direction.

Which is exactly what may happen if you run your app on a 32 bit device, as the iPhone 4s and 5.

So, heads up! Make sure you do proper testing!

Why you can’t delete an app from iTunes Connect

I hacked a Swift app in the past few hours. Submitted to beta testers, made some changes, and now I’m ready to send it to the App Store.

However, I decided that I didn’t like the name I used for the beta. So… being me, I not only want to change the actual Bundle name on Info.plist, I also want to change the App ID, Bundle Identifier, etc…

I said to myself: “self, why have that app sitting in iTunes Connect if you’re not going to use it?”

So, I went to iTunes Connect, selected the app I had set up for beta distribution… and started to look for the “Delete” button. Without success.

I removed all my builds. Turned off beta testing. Removed all my beta testers. Signed out. Signed in. Refreshed.

Still no Delete button.

Turns out…

Hi XXX,

I am following up with you about the deletion of your app, “XXX”. Recent changes have been made to the App Delete feature. In order to delete your app from iTunes Connect, you must now have one approved version before the delete button becomes available. For more information on the recent changes, please see the “Deleting an App” section of the iTunes Connect Guide (page 96-97):

Oh, Apple, Apple…

Swift app size

I’ve been writing Swift for the most part of the past 6 months. Mostly playing with it, nothing serious.

But today, I submitted my first Swift app to iTunes Connect (currently in beta review), and was reminded of the fact that Swift apps have embedded libraries.

8.6MB of libraries.

My actual code is less than 6KB.