Rethinking Routers in Swift using Protocol Oriented Programming — Part 2

4 minute read

Introduction

In this blog post, we will be adding the capability to add nested routers on top of part 1 of Rethinking Routes in Swift using Protocol Oriented Programming. If you have not read it (Add link here), please check it out before reading this post.

Nested Routes

Let’s add a few more properties to the Routable Protocol and a few methods inside of RoutesProtocol.swift that will allow us to do nested routes.

// RoutesProtocol.swift
...

protocol Routable {
    typealias Parameters = [String : Any]
    var route: String {get set}
    var urlParams: String! {get set} // Just add this new variable
    init()
}

extension Routable {

    init(_ _arg: String = "") {
        self.init()
        urlParams = _arg
    }

    /// Allows a route to become a nested route
    func nestedRoute(args: String, child: RequestConverterProtocol) -> RequestConverter {

        return RequestConverter(
            method: child.method,
            route: "\(self.route)/\(args)/\(child.route)",
            parameters: child.parameters
        )
    }

    /// Generate the URL string for generated nested routes
    func nestedRouteURL(parent: Routable, child: Routable) -> String {
        let nestedRoute = "\(parent.route)/\(parent.urlParams!)/" + child.route
        return nestedRoute
    }
}

...

Now let’s update our Router.swift file by adding our new variable urlParams to our already existing routes.

// Routes.swift

...

struct User: Readable, Creatable, Deletable, Updatable {
    var route: String = "users"
    var urlParams: String!
}

struct Status: Readable, Creatable, Deletable {
    var route: String = "statuses"
    var urlParams: String!
}
    
...

Since we’re working with reusable components, our nested routes can look a few different ways. I’m going to keep everything separated first, then I’m going to show you the final implementation. Feel free to make your own style.

Inside of Router.swift create an extension for our User route:

extension Router.User {

    func getStatus(params: String) -> RequestConverterProtocol {
        return nestedRoute(args: urlParams, child: Router.Status.get(params: params))
    }
}

Finally, if we move over to our viewDidLoad method inside of ViewController.swift, here’s the updated code:

...

Alamofire.request(Router.User("2").getStatus(params: "2")).validate().responseJSON { response in
    switch response.result {
    case .success:
        debugPrint(response) // https://private-85a46-routable.apiary-mock.com/users/2/statuses/2
    case .failure(let error):
        print(error)
    }
}

...

You should expect the following:

Now that’s pretty awesome! That makes our Router’s look so much cleaner!

Let’s resync, here’s the git command if you need to catch up:

git checkout chapter_3_finishing

So let’s wrap up this post with the final implementation of our Router. Instead of implementing getStatus(_:) for every route that contains statuses, we’re better off making them into protocols. Oh and remember, our fake API doesn’t allow us to .update statuses, so we don’t implement updateStatus(_:).

// Router.swift, Final Implementation...
...

protocol hasStatuses {}
extension hasStatuses where Self: Routable {
    func status(params: String) -> Router.Status {
        var child = Router.Status(params)
        child.route = nestedRouteURL(parent: self, child: child)
        return child
    }

    func getStatus(params: String) -> RequestConverterProtocol {
        return nestedRoute(args: urlParams, child: Router.Status.get(params: params))
    }

    func createStatus(parameters: Parameters) -> RequestConverterProtocol {
        return nestedRoute(args: urlParams, child: Router.Status.create(parameters: parameters))
    }

    func deleteStatus(params: String) -> RequestConverterProtocol {
        return nestedRoute(args: urlParams, child: Router.Status.delete(params: params))
    }
}

...

Now in the User route we simply have to conform to the hasStatuses protocol and now our User route can always get statuses no matter how nested it is:

// Router.swift, Final Implementation...
...

    struct User: Readable, Creatable, Updateable, 
        hasStatuses {
        var route: String = "users"
        var urlParams: String!
    }
    
...

Now we can run the following commands and expect that they will work:

let getUserStatus = Router.User("initFabian").getStatus(params: "2")
let createUserStatus = Router.User("initFabian").createStatus(parameters: ["title":"fabians post"])
let deleteUserStatus = Router.User("initFabian").deleteStatus(params: "2")

By turning our nested routes into protocols, we can be very expressive by saying “This route has a child route.” We can really get crazy… check this out.

//Router.swift, Final Implementation file
struct Router: URLRouter {
    static var basePath: String {
        return "https://private-85a46-routable.apiary-mock.com/"
    }

    struct User: Readable, Creatable, Updateable,
        hasStatuses, hasPictures, hasPosts { // <- Here
        var route: String = "users"
        var urlParams: String!
    }

    struct Status: Readable, Creatable, Deletable,
        hasComments {
        var route: String = "statuses"
        var urlParams: String!
    }

    struct Picture: Readable, Creatable, Deletable, Updateable,
        hasComments {
        var route: String = "pictures"
        var urlParams: String!
    }

    struct Comment: Readable, Creatable, Deletable, Updateable,
        hasUsers {
        var route: String = "comments"
        var urlParams: String!
    }

    struct Post: Readable, Creatable, Deletable, Updateable,
        hasComments {
        var route: String = "posts"
        var urlParams: String!
    }
}

...

Here’s the gist for the entire Router.swift file. It contains all the protocols such as hasComments, hasUsers, etc.

With this update, let’s move to our viewDidLoad(_:) and make a few changes. By the way, these routes don’t exist on our API, I just wanted to show how we can build out these routes dynamically, so don’t be surprised if the API returns a 404 for some of these endpoints.

//ViewController.swift

class ViewController: UIViewController {

    override func viewDidLoad() {
        
        ...

        _ = Router.Post("54").getComment(params: "43")                                  // posts/54/comments/43
        _ = Router.User("initFabian").getStatus(params: "2")                            // users/initFabian/statuses/2
        _ = Router.User("initFabian").picture(params: "2").getComment(params: "3")      // users/initFabian/pictures/2/comments/3


        let crazyNested = Router.User("initFabian")
            .status(params: "3")
            .comment(params: "32352")
            .getUser(params: "fakeUser")

        print(crazyNested)
        // users/initFabian/statuses/3/comments/32352/users/fakeUser

        let extremelyNested = Router.User("initFabian")
            .picture(params: "3")
            .comment(params: "507f191e810c19729de860ea")
            .user(params: "43")
            .status(params: "firstStatus")
            .comment(params: "27")
            .user(params: "63")
            .post(params: "4d6e5acebcd1b3fac9000002")
            .comment(params: "82")
            .user(params: "21")
            .getStatus(params: "1")

        print(extremelyNested)
        // users/initFabian/pictures/3/comments/507f191e810c19729de860ea/users/43/statuses/firstStatus/comments/27/users/63/posts/4d6e5acebcd1b3fac9000002/comments/82/users/21/statuses/1
         ...
    }
}

Conclusion

The purpose of this post was to contain a file that can not only communicate what our API paths are, but to also tell us the relationship between our models. I know we could take this one step further and tie in our models directly into these routes, but that’s beyond this blog post. Well, there you go! I hope you found this post useful and learned something from it. I definitely had a lot of fun building this and look forward to implementing it into already-existing projects. If you have any questions please reach out to me via twitter @initFabian.

Happy Hacking!

Tags:

Categories:

Updated: