Artificial Intelligence

Leveraging Structs and Generics in the Networking Layer with Swift 4

Swift Talk #1 and #8 introduces an approach (hereinafter al


Filed under:

Swift Talk #1 and #8 introduces an approach (hereinafter alternative approach) to using structs and generics to build a networking layer in Foundation.

The original code was written in Swift 2.2. Here, I will make an update for Swift 4, incorporating the use of newly available JSON parsing features. I will also show how it could be extended to fetch images.

The Swift Talk Approach

Let's say you have a simple JSON response:

{
    "name": "Metal Toad"
}

You can represent this response in a Swift struct:

struct Company: Decodable {
    let name: String
}

With the alternative approach, making a network call is as easy as:

let resource = Resource<Company>(method: .get, url: <URL>)
Networking().load(resource: resource) { response in 
    guard let name = response?.name else { return }
    print(name)  // "Metal Toad"
}

First, we create a resource object that is type specific to the Company struct specifying the HTTP method and url.

Second, we initialize our Networking class and call load with the resource as the parameter.

Just like magic we receive a response of type Company that was specified when constructing our resource object.

One of the benefits to this approach is a guarantee that the response will be of type Company. Also, the response object is provided in a completion closure next to the place where the method is called, thereby improving locality of reasoning.

JSON Decoding before Swift 4

Network responses return static byte buffers encapsulated by the Data struct (or NSData if using reference semantics). Let's discuss how we convert the byte buffer into our Company struct.

Before Swift 4, we used JSONSerialization.jsonObject to convert Data into an object of type [String: AnyObject].

//static byte buffer from a network request
let data: Data
let json: [String: AnyObject] = try? JSONSerialization.jsonObject(with: data, options: [])

Next, we need a function to look at values in the JSON object and mapping them to the response object. It checks whether the JSON has the expected keys, and whether values conform to their expected types in the response object.

extension Company {
    func parseJSON(dictionary: [String: AnyObject]) {
        guard let id = dictionary["name"] as? String else { return nil }
        self.name = name
    }
}

Enter Swift 4's JSONDecoder

Swift 4 introduces a new interface for JSON decoding. We no longer need a parseJSON function to process [String: AnyObject]. Instead, we can directly go from Data to our target object, in this case Company.

Before we had to define the parse function of the Resource struct on a case by case basis, specifying the steps needed to transform Data into the response object.

struct Resource<A> {
    let url: URL
    let parse: (Data) -> A?
}

With Swift 4, we no longer need to define a parse function on a case by case basis. Instead, we can statically define it generically over any return type that conforms to Decodable in our Resource object.

Let's take a look at JSONDecoder().decode(_:from:):

func decode<T>(_ type: T.Type, from data: Data) throws -> T where T : Decodable

This method takes Data as an argument and is generic over the return type, as long as the return type conforms to the protocol Decodable.

struct Resource<A> where A: Decodable {
    let url: URL
    let parse: (Data) -> A? = { data in return try? JSONDecoder().decode(A.self, from: data) }
}

Therefore, the use of this method is perfect for integrating into the Resource object of the alternative approach, since it is also generic over the return type. Another benefit is a check that the model we are asking for comforms to Decodable, otherwise it will fail at compile time.

Image Resource

The alternative approach can also be extended to support the fetching of image resources. Like JSON resources above, image data comes back as static byte buffers in Data. To make it useful we simply need to use UIImage.init?(data: Data). Therefore we define a new ImageResource object.

struct ImageResource {
    let url: URL
    let method: HttpMethod<Data>
    let parse: (Data) -> UIImage?
}
 
extension ImageResource {
    init(imageUrl: URL) {
        self.url = imageUrl
        self.method = .get
        self.parse = { data in return UIImage(data: data) }
    }
}

ImageResource now holds everything we need to fetch and parse data into a useful instance of UIImage.

Playground

Please see this gist for a Swift playground where we make networking calls to fetch a JSON resource, to post a request with a JSON body, and fetch a simple image resource.

Conclusion

Swift 4's new JSON processing features enhances the alternative approach by removing the requirement of specifying a custom JSON parsing method for each custom type, as long as it conforms to Decodable as required by JSONDecoder. Furthermore, the alternative approach is flexible enough to be extended to other types of network calls, like the fetching of images.

This is my own personal exploration into this alternative approach. Suggestions for improvements, especially in how this approach can be further optimized, is welcomed.

Similar posts

Get notified on new marketing insights

Be the first to know about new B2B SaaS Marketing insights to build or refine your marketing function with the tools and knowledge of today’s industry.