Crusty, the antagonistic character from WWDC 2015 Session 408, has been making the rounds on the blogosphere, writing about protocol-oriented programming. For me, protocol extensions in Swift are easily the coolest new feature, because they enable us to add behavior to a set of data easily.
In a post from a few weeks ago called Templating, I describe a flexible networking architecture, that relies heavily on protocols to define and send network requests. It’s been working well in practice, and a few cases popped up where the architecture made it really easy to handle new unexpected request types. I hope to write about those soon.
For this post, I’d like to examine the new protocol extensions, and what they can do for this networking design. Ultimately, what we were doing in the Templating post was adding behavior to a set of data. Since I wrote it first in Objective-C, we did it with decoration.
With Swift 2, however, while we could continue to use decoration to wrap our data with new functionality, we’ve been given a new power with protocol extensions. Protocol extensions let us add concrete methods to a protocol that are dependent on the abstract methods in that protocol. It’s a form of the template method pattern, but one that doesn’t rely on inheritance.
I’m still very new at the Swift stuff, so you’ll have to forgive my sins.
Let’s define a lighter version of the SKRequestTemplate
protocol from the previous post, but in Swift this time. Since we’re adding sendRequest()
directly onto the protocol, it’s no longer a template, but the request itself.
protocol Request {
var baseURL : NSURL? { get }
var method : String { get }
var path : String { get }
var parameters : Dictionary<String, String> { get }
}
The baseURL
property is marked as an optional, becuase -[NSURL URLWithString:]
returns an optional by default. Since there’s no good default for a URL (like the empty string is for strings), we’ll prevent the user of our networking protocol from having to use a scary bang and allow her to return an optional here.
Okay, let’s define our first request. We’ll use GitHub’s Zen endpoint, which just returns a short proverb as a string.
struct ZenRequest : Request {
let baseURL = NSURL(string: "https://api.github.com/")
let path: String = "zen"
}
Uh-oh, the compiler is already complaining. Swift wants to require us to return something for the method and parameters. That would make this request ugly, though, so we won’t be doing that. We could mark each property with the optional
keyword, but then we have to mark the whole protocol as @objc
, and I want to make this as Swifty as possible. (Ash Furrow lays out this problem neatly in his post, Protocols and Swift.)
Fortunately, we’re saved by protocol extensions here. We can give a default implementation for these properties in an extension, and we can leave it out of the individual request structs.
extension Request {
var method : String { return "GET" }
var path : String { return "" }
var parameters : Dictionary<String, String> { return Dictionary() }
}
Now let’s add our sendRequest
function to the RequestTemplate
:
extension Request {
func buildRequest() -> NSURLRequest? {
guard let baseURL = baseURL else { return nil }
guard let URLComponents = NSURLComponents(URL: baseURL, resolvingAgainstBaseURL: true) else { return nil }
URLComponents.path = (URLComponents.path ?? "") + path
guard let URL = URLComponents.URL else { return nil }
let request = NSMutableURLRequest(URL: URL)
request.HTTPMethod = method
return request
}
func sendRequest(success success: (string: String) -> (), failure: (error: ErrorType) -> ()) {
let session = NSURLSession.sharedSession()
guard let request = buildRequest() else { return }
guard let task = session.dataTaskWithRequest(request, completionHandler: { (taskData, taskResponse, taskError) -> Void in
if let taskError = taskError {
failure (error: taskError)
} else if let taskData = taskData {
guard let string = NSString(data: taskData, encoding: NSUTF8StringEncoding) as? String else { return }
success(string: string)
}
}) else { return }
task.resume()
}
}
Wow, look at all those guard let
statements! Exclamation points are for your writing, not your code. We can now create and send our request:
ZenRequest().sendRequest(
success: { string in
print(string)
},
failure: { error in
print(error)
})
This is a really simple version of templated requests (I left out a few complexities, like parameters, headers, JSON parsing, etc), but I certainly like how succinct it is. A GET request with no parameters is only a few lines of code. Supporting different types of requests, such as multi-part requests, means overriding the buildRequest
method, and returning whatever cooked URL request is appropriate.