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:
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.