Introduction
We all know why dependencies are bad, and writing software dependant on other software is not necessarily good. It probably reduces complexity for the time being, and helps you write code faster, but I wouldn’t say it’s exactly the way things are to be done, from my experience.
Writing software independent of 3rd party libraries, helps build robust software that won’t go out of date with time. I’ve used networking libraries in the past, some really excellent ones (cough Alamofire cough) to abstract and make things easier while building the networking layer. But of late, I’m trying to write code with zero to no-dependencies. Especially, when working with the iOS Platform. The iOS platform comes baked in with most, if not all, things that you need to build out your app.
Networking is something most apps need to deal with these days, the apps on your phone have to communicate with the internet one way or the other, process requests/responses, and display information. When that’s being done all over the place in your code, it’s nightmare.
Recently, I came across a Swift talk by Florian Kugler and Chris Eidhof of objc.io where they talk about writing the Networking Layer for an app, and how best to do that. In this post, I intend to explain their process with some fundamental Swift concepts, and how to keep concerns separate and why it’s good.
The Architecture: Model, Resource, Webservice
Architecting a networking layer can be a task, a difficult one at that, especially without direction. Once you know how to do something, doing something isn’t hard. Let’s see one way to architect your networking layer that you can consider as a direction to write your own.
There are three concerns that need to be addressed when dealing with Networking:
- Send a request to a server for a resource
- Get a response back
- Parse responses into native swift types
We’ll see how to address each of this with Swift.
- Send a request to a server for a resource
- Get a response back
Usually when you’re sending a request over to a server, it’s for a resource of some kind. This could be a post, an article, a movie, or anything. Let’s call this a resource from now.
We could handle this by having a class do that for us, say a Webservice class that could load
a resource, that returns a response.
- Parse responses into native swift types
Usually the responses we get back from server these days is in JSON. So, the task at hand is to parse JSON into native swift types. There are many ways to do this, but let’s see how to handle it so its nicer and cleaner and in a way you don’t have to deal with it each and every time you need to request for a response.
Let’s assume we have the following milestones:
Milestone One: Get Binary Data
Milestone Two: Data —> JSON —> Native Types
Milestone Three: Clean up
Webservice — Class that handles requests
We need a way to send requests to a server for a resource, don’t we? We’ll call that class Webservice
. This class could have a method load
that could be responsible to load
a resource
. We’ll see how to build this out in a while.
Resource
We also need a way to tell the Webservice class to load a resource
, we can accomplish that by having a Resource
object that encapsulates the request URL
and a way to parse the response.
Model — Native Swift Objects with Value Types
Finally, you need native types of the data that you receive from the server to be able to use it across the app. We’ll use structs for each model to address that.
Code
Example:
Let’s assume we have json data being served from a server that looks like this:
1[2 {3 "title": "Pulp Fiction",4 "actors": ["Samuel L Jackson", "John Travolta"]5 },6 {7 "title": "Ocean's Eleven",8 "actors": ["George Clooney", "Brad Pitt", "Matt Damon"]9 },10 {11 "title": "Goodfellas",12 "actors": ["Robert De Niro", "Ray Liotta"]13 },14 {15 "title": "I Am Sam",16 "actors": ["Sean Penn"]17 }18]
For the sake of this example, we’ll put this data in a file called movies.json
, and try to build a networking layer in Swift that parses this data into native types.
Resource
The Resource
is a struct that encapsulates the url
and a way to parse, a parse function, that helps parse the data from the URL. For example, In our case we could have a MoviesResource
that could retrieve movies.
We could model Resource like —>
1struct Resource<A> {2 let url: URL3 let parse: (Data) -> A?4}
- URL: A
url
property that denotes the URL of the resource parse
function: A function that takes someData
and returns an object of the associated resource.
Let’s write an initialiser for a Resource
in an extension like —>
1extension Resource {2 init?(url: URL, parse: @escaping (Any) -> A?) {3 self.url = url4 self.parse = parse5 }6}
Here, along with the URL, we’re just passing a closure to the parse parameter and assigning it to the resource’s parse property.
Now, a movies resource could be initialised like —>
1let url = URL(string: "http://localhost:8080/movies.json")!2let moviesResource = Resource<Data>(url: url) { data in3 return data4}
Webservice
Now that we’ve seen how we could create a Resource type for our data, we need a way for us to fetch the data from the server.
Luckily the URLSession
class offers a way for us to do this easily without having to use any third party libraries, we’ll wrap this up in a function within a Webservice
class so we could use it.
We could write this like —>
1final class Webservice {2 func load<T>(resource: Resource<T>, completion: @escaping (Any) -> ()) {3 URLSession.shared.dataTask(with: resource.url) { (data, _ \_) in4 completion(data)5 }.resume()6 }7}
We’re simply using a shared URLSession object to call the following method
1URLSession.shared.dataTask(with: URL, completionHandler: (Data?, URLResponse?, Error?) -> Void)
with the resource’s URL parameter and call the completion with data, this will just call the closure with whatever data it retrieves at this point.
We could now use the Webservice by creating an instance of it like —>
1let sharedWS = Webservice()2sharedWS.load(resource: moviesResource) { (result) in3 print(result)4}
This should return whatever the size of the data it retrieved was, when you run it a playground, you get something like this —>
1Optional(324 bytes)
Now, this means we have a service trying to communicate with the server and returning response, but the response is in binary and not useful for us.
🏁 We have now reached Milestone One.
We’ll see how to get meaningful data and parse it to native types so we can use it.
Models
Looking at the JSON data, we figure that we need a Movie
type to represent the movies, and possibly an Actor
type with a name property that can hold their names.
We could model this with native types like —>
1// Actor2struct Actor {3 let name: String4}56// Movie7struct Movie {8 let title: String9 let actors: [Actor]10}
We now need a way to model JSON data with native types, we could use a type alias for a dictionary like —>
1typealias JSONDictionary = [String: AnyObject]
We can now write an initialiser for a Movie
like this —>
1extension Movie {2 init?(dictionary: JSONDictionary) {3 guard let title = dictionary["title"] as? String,4 let actors = dictionary["actors"] as? [String] else { return nil }5 self.title = title6 self.actors = actors.flatMap(Actor.init)7 }8}
What’s happening here is pretty straightforward —>
The guard
statement pulls out our fields title
and actors
from the dictionary, which is of type JSONDictionary
and assigns it to the respective properties. We also flatMap
the actors
string and pass each string to the Actor
’s init to instantiate actor objects within the Movie.
Now that we have a way to create Movie
objects, let’s rewrite our moviesResource
to return them —>
1let moviesResource = Resource<[Movie]>(url: url) { data in2 let json = try? JSONSerialization.jsonObject(with: data, options: [])3 guard let dictionaries = json as? [JSONDictionary] else { return nil }4 return dictionaries.flatMap(Movie.init)5}
We do the following here:
- Instantiate
moviesResource
as aResource
that returns array ofMovie
objects - Use
JSONSerialization
class to convert our data (Data
) object into jsonJSON
- Typecast
json
into an array ofJSONDictioanry
objects flatMap
over thedictionaries
array to instantiate aMovie
with each block of json
We now have a process setup that can parse the data into our required objects, but we still haven’t updated our load
method to make use of the parse
method that we’ve modified above.
This can look like —>
1final class Webservice {2 func load<T>(resource: Resource<T>, completion: @escaping (Any) -> ()) {3 URLSession.shared.dataTask(with: resource.url) { (data, \_, \_) in4 completion(data.flatMap(resource.parse))5 }.resume()6 }7}
We modify our load
method for a resource type T
inside which we simply try to flatMap
over the stream of data that gets loaded as Data
, thanks to URSession
, and pass the resource’s parse
method to it so the parse functionality we’ve written is useful, and send it to the completion handler.
Now, let’s see what data we’re able to load with our web service —>
1sharedWS.load(resource: moviesResource) { (result) in2 print(result)3 }
If you run this in a playground, you should see something like this —>
1Optional(2 [__lldb_expr_55.Movie(3 title: "Pulp Fiction",4 actors: [5 __lldb_expr_55.Actor(name: "Samuel L Jackson"), __lldb_expr_55.Actor(name: "John Travolta")]),6 __lldb_expr_55.Movie(title: "Ocean\'s Eleven", actors: [__lldb_expr_55.Actor(name: "George Clooney"),7 __lldb_expr_55.Actor(name: "Brad Pitt"), __lldb_expr_55.Actor(name: "Matt Damon")]),8 __lldb_expr_55.Movie(title: "Goodfellas", actors: [__lldb_expr_55.Actor(name: "Robert Di Niro")]),9 __lldb_expr_55.Movie(title: "I Am Sam", actors: [__lldb_expr_55.Actor(name: "Sean Penn")10 ])11 ])
Which means good news. You’re now able to get native objects from the json now.
We could refactor the load method to give us something nicer —>
1sharedWS.load(resource: moviesResource) { (result) in2 guard let movies = result as? [Movie] else { return }3 for movie in movies {4 print("\nMovie: \(movie.title)")5 for actor in movie.actors {6 print("Actor: \(actor.name)")7 }8 }9}
You’ll see this prettified output —>
1Movie: Pulp Fiction2Actor: Samuel L Jackson3Actor: John Travolta45Movie: Ocean's Eleven6Actor: George Clooney7Actor: Brad Pitt8Actor: Matt Damon910Movie: Goodfellas11Actor: Robert Di Niro1213Movie: I Am Sam14Actor: Sean Penn
🏁 We have now reached Milestone Two.
Refactor
Now that we’ve seen how to get to what we need, we could get to cleaning up the code a bit, and refactoring it to make it much nicer.
We could start by moving our moviesResource
into the Movie
struct to make it more concise using an all
property —>
1struct Movie {2 let title: String3 let actors: [Actor]45 static let all = Resource<[Movie]>(url: url) { data in6 let json = try? JSONSerialization.jsonObject(with: data, options: [])7 guard let dictionaries = json as? [JSONDictionary] else { return nil }8 return dictionaries.flatMap(Movie.init)9 }10}
We could also simplify this further by abstracting away the JSON
decoding part to the Resource
type by creating an extension —>
1extension Resource {2 init(url: URL, parseJSON: @escaping (Any) -> A?) {3 self.url = url4 self.parse = { data in5 let json = try? JSONSerialization.jsonObject(with: data, options: [])6 return json.flatMap(parseJSON)7 }8 }9}
Here, we convert data
object into JSON and flatMap
over it to recursively parse the closure to parse the json data, now our Resource
could become simpler —>
1struct Movie {2 let title: String3 let actors: [Actor]45 static let all = Resource<[Movie]>(url: url) { json in6 guard let dictionaries = json as? [JSONDictionary] else { return nil }7 return dictionaries.flatMap(Movie.init)8 }9}
🏁 We have now reached our final milestone, Milestone Three.
Now, we have code that’s cleaner, concise and keeps concerns separate. This pattern can be followed with whatever Resource that you want to work with.
You can download the files we worked with incase you want to try it our yourself:
So, that was about how to write a Networking layer without any third-party libraries. Feel free to leave any feedback or questions!