Tommaso Piazza bio photo

Tommaso Piazza

Coder, Photographer, Climber, Sailor, Krav Maga Practitioner and Mongol Rally Veteran

Twitter Facebook LinkedIn Github Stackoverflow

Unless you are implementing your own network layer from scratch, Alamofire is the Swift network library of choice on iOS/OSX.

In a recent project I had to make a gzipped request to a service. Unfortunately this is not supported out of the box by Alamofire, so I started digging in the code of the library to see how I could achieve my goal in the nicest way possible.

Overview

ParmeterEncoding

Alamofire has a neat enum called ParameterEncoding. This emum lets you specify how the parameters passed to Alamofire.request(...) will be encoded in the body of the generate NSURLRequest.

Not only that but, in some cases (like .JSON) it will also add the appropriate Content-Type header the HTTP request. Pretty cool right?

Let’s take a look at the ParameterEncoding.

enum ParameterEncoding {
    case URL
    case URLEncodedInURL
    case JSON
    case PropertyList(format: NSPropertyListFormat, options: NSPropertyListWriteOptions)
    case Custom((URLRequestConvertible, [String: AnyObject]?) -> (NSMutableURLRequest, NSError?))

    func encode(request: NSURLRequest, parameters: [String: AnyObject]?) -> (NSURLRequest, NSError?)
    { ... }
}

As you can see there is a variety of encodings that are supported out of the box and the Custom encoding let’s you specify your own. This seemed exactly what I needed.

The Custom constructor takes a function that takes two arguments (URLRequestConvertible, parameters: [String: AnyObject]?) and returns a tuple (NSURLRequest, NSError?). The type of the function is suspiciously similar to the type of the encode function specified further down in the enum.

    func encode(request: NSURLRequest, parameters: [String: AnyObject]?) -> (NSURLRequest, NSError?)

This is no coincidence. In fact URLRequestConvertible in the constructor of Custom is a protocol that specifies that the first parameter is something that will produce a NSRULRequest.

Whatever function you pass to the Custom construct will become the encode(..) function for your custom encoding. The encode(...) function will be called my Alamofire’s Manager class when constructing the request.

Great! So since I wanted to GZIP my JSON encoded parameters I could have just done the following using 1024jp/NSData-GZIP to gzip my request.

let gzipEncoding = ParameterEncoding.Custom { request, parameters in

    let jsonEncoding = Alamofire.ParameterEncoding.JSON
    let (jsonEncodedRequest, _) = jsonEncoding.encode(request, parameters: parameters)
    let mutableRequest = jsonEncodedRequest.mutableCopy() as! NSMutableURLRequest

    var gzipEncodingError: NSError? = nil

    do {
        let gzippedData = try mutableRequest.HTTPBody?.gzippedData()
        mutableRequest.HTTPBody = gzippedData

        if mutableRequest.HTTPBody != nil {
            mutableRequest.setValue("gzip", forHTTPHeaderField: "Content-Encoding")
        }
    } catch {
        gzipEncodingError = error as NSError
    }

    return (mutableRequest, gzipEncodingError)
}

This solution works, but what if I wanted to gzip some other encoding? I would have had to make a custom encoding for every other encoding I wanted to gzip, including other custom ones.

Could there be a more general solution? This is Swift so you can bet there is.

##ParamterEncoding Extension

The result I wanted to achieve was to just be able to create any encoding (.JSON, PropertyList, Custom) and gzip it with no extra effort. Something like this

Alamofire.request(.POST, someURLString, parameters:["data":["verboseData":"veryVerbose"]], encoding:.JSON.gzipped)

If you take a look at the type of Alamofire.request you will notice that the last parameter has type ParameterEncoding. This in turn would mean that the .gzipped property I wanted would have to have the same type. Let’s start with that.

extension ParameterEncoding {

    var gzipped:ParameterEncoding {

        return gzip(self)
    }
}

The computer property gzipped will call another function to gzip … itself! Why? Because we want to take advantage of the fact that each encoding already knows how to encode it’s own parameters. We just want to gzip the body of the request.

extension ParameterEncoding {

    var gzipped:ParameterEncoding {

        return gzip(self)
    }

    private func gzip(encoding:ParameterEncoding) -> ParameterEncoding {
        // think of gzipEncoding as a function that first encodes then gzips
        // this is function composition
        let gzipEncoding = self.gzipOrError  encoding.encode

        return ParameterEncoding.Custom(gzipEncoding)
    }
}

gzip(encoding:ParameterEncoding) -> ParameterEncoding is a function that given a parameter encoding ( self ) will return another encoding. It will return our custom one with the gzipped body and an appropriate “Content-Encoding” header.

The only thing left to do now is to write the last function gzipOrError which has the same type as the return type of encode(...), meaning (NSURLRequest, NSError?), and returns something compatible with the type of Custom, meaning (URLRequestConvertible, parameters: [String: AnyObject]?)

extension ParameterEncoding {

    var gzipped:ParameterEncoding {

        return gzip(self)
    }

    private func gzip(encoding:ParameterEncoding) -> ParameterEncoding {
        // think of gzipEncoding as a function that first encodes then gzips
        // this is function composition
        let gzipEncoding = self.gzipOrError  encoding.encode

        return ParameterEncoding.Custom(gzipEncoding)
    }

    private func gzipOrError(request:NSURLRequest, error:NSError?) -> (NSMutableURLRequest, NSError?) {

        let mutableRequest = request.mutableCopy() as! NSMutableURLRequest

        if error != nil {
            return (mutableRequest, error)
        }

        var gzipEncodingError: NSError? = nil

        do {
            let gzippedData = try mutableRequest.HTTPBody?.gzippedData()
            mutableRequest.HTTPBody = gzippedData

            if mutableRequest.HTTPBody != nil {
                mutableRequest.setValue("gzip", forHTTPHeaderField: "Content-Encoding")
            }
        } catch {
            gzipEncodingError = error as NSError
        }

        return (mutableRequest, gzipEncodingError)
    }
}

Show me the code

Hopefully I have explained how this works without to much confusion. Grab the final version from this Gist.