11 Oct 2015
Lately I’ve been working on a Swift framework that I’m integrating into an existing app with CocoaPods. The framework relies on an #if DEBUG
macro to run one of two code paths, depending on whether we’re building with the Debug
configuration or something else.
public var baseURL: NSURL {
#if DEBUG
return NSURL(string:"https://coolapp-staging.herokuapp.com")!
#else
return NSURL(string:"https://coolapp.com")!
#endif
}
In order for our code to trigger an #if <something>
macro, we need to set a -D<something>
flag in the other Swift flags
section of our target’s build settings. There’s nothing special about “debug” as it’s used here, we’re just creating a global variable and naming it DEBUG
.
So far so good – we can see that the #if DEBUG
branch runs when we build with our Debug
configuration and the #else
branch runs otherwise. But this changes when we consume our framework in another project. Firstly, CocoaPods doesn’t look at the build settings in our library’s Xcode project at all. The xcconfig files that CocoaPods generates for our framework are entirely independent of the project file in our framework’s own Xcode project. (Thanks to Caleb Davenport at North for pointing this out.) This means that even if we specify a -DDEBUG
flag for the Debug
build configuration in our framework’s build settings, they won’t be there when CocoaPods installs the framework into our app’s workspace.
So let’s set the flag higher up, say in our app target’s build settings. Well it turns out that those flags don’t trickle down to our framework targets at compile time. Any flags you set on the app target only apply to the app target.
OK, different idea – why don’t we make the changes in our podspec instead, using pod_target_xcconfig
? Unfortunately, it doesn’t seem possible to set flags for only our Debug
configuration, which is the whole point. And besides, we don’t want to be beholden to the consumer of our API — what if they’re using a different naming convention for their build configurations?
Fortunately, we can use CocoaPods’s post_install_hooks
to get what we want. As you can see in the docs, each framework target holds an array of build_configurations
representing the xcconfig
files generated for each of our project’s build configurations. Each of these build_configuration
objects then holds a hash of build_settings
representing the structured data inside the xcconfig
file. Using post_install_hooks
we can just write out the relevant flags for the configurations we care about.
post_install do |installer|
installer.pods_project.targets.each do |target|
if target.name == 'CoolFramework'
target.build_configurations.each do |config|
if config.name == 'Debug'
config.build_settings['OTHER_SWIFT_FLAGS'] = '-DDEBUG'
else
config.build_settings['OTHER_SWIFT_FLAGS'] = ''
end
end
end
end
end
Bong bong.
04 Oct 2015
Moya is functional networking library, built on top of Alamofire, that applies the best of Swift’s language and compiler features to your network requests. Its key insight is tying URL request-building logic to an enumerated type, both guaranteeing that you don’t miss anything (switch
statements need to be exhaustive), and allowing you to cleanly factor code out of your main suite of public “call this endpoint” functions. On top of that, there are subspecs supporting both ReactiveCocoa and RxSwift, making it relatively easy to support functional reactive programming in your project.
What’s in the Box?
Target
At its heart, Moya revolves around a single protocol called MoyaTarget
. This is the enumerated type alluded to earlier, and encompasses your base URL and paths, supplied parameters, and HTTP methods. Let’s use the Twitter API for this example, and call our implementation TwitterTarget
.
public enum Target: MoyaTarget {
case .HomeTimeline
case .NewPost(text: String)
var baseURL: NSURL { return NSURL(string: "https://api.twitter.com/1.1/")! }
var path: String {
switch self {
case .HomeTimeline:
return "statuses/user_timeline.json"
case .NewPost:
return "statuses/update.json"
}
}
var method: Moya.Method {
switch self {
case .HomeTimeline:
return .GET
case .NewPost:
return .POST
}
}
var parameters: [String: AnyObject] {
switch self {
case .HomeTimeline:
return [:]
case .NewPost(let text):
return ["status": text]
}
}
var sampleData: NSData { return NSData() } // We just need to return something here to fully implement the protocol
}
So for each of the protocol methods we can switch on self
(enums ftw), using let
bindings to pull out associated values where we need them. The only non-obvious method is sampleData
— it’s related to testing, we’ll come back to it later.
Provider
In order to interact with our TwitterTarget
type we’ll need an instance of MoyaProvider
; its request
method is the gateway between TwitterTarget
and Twitter’s servers. While we’re at it, let’s write a public function to wrap all of this stuff up.
let provider = MoyaProvider<TwitterTarget>()
public func getTimeline() -> (tweets: [Tweet]?, error: ErrorType?) {
provider.request(.HomeTimeline) { [unowned self] (data: NSData?, statusCode: Int?, response: NSURLResponse?, error: ErrorType?) in
guard self.requestSucceeded(statusCode) else {
let error = // some error
return (nil, error)
}
guard let data = data,
json = try? NSJSONSerialization(data: data, options: NSJSONReadingOptions(),
jsonArray = json as? [[String: AnyObject]] else {
let error = // some error
return (nil, error)
}
let tweets = jsonArray.flatMap { Tweet(json: $0) }
return (tweets, nil)
}
}
Using Moya we’ve factored everything but the bare essentials out of our public getTimeline
function. The resulting code is simple, safe, and easy to reason about.
Testing
An awesome feature of Moya is the ability to test our logic with canned API responses with just a couple of small changes to our code. First, let’s implement that sampleData
method for real using the sample responses in Twitter’s API docs:
public enum Target: MoyaTarget {
(...)
var sampleData: NSData {
switch self {
case .HomeTimeline:
let jsonStr = "[{\"coordinates\": null,\"truncated\": false,\"created_at\": \"Tue Aug 28 21:16:23 +0000 2012\",\"favorited\": false,\"id_str\": \"240558470661799936\",\"in_reply_to_user_id_str\": null,\"entities\": {\"urls\": [],\"hashtags\": [],\"user_mentions\": []},\"text\": ...]" // truncated because massive
if let data = jsonStr.dataUsingEncoding(NSUTF8StringEncoding) {
return data
} else {
print("Couldn't serialize string")
return NSData()
}
case .NewPost:
(...)
}
}
}
Now in order to use this in our tests, we’re going to need an instance of MoyaProvider
that we’ve configured for testing:
let testProvider = MoyaProvider<TwitterTarget>(endpointClosure: MoyaProvider.DefaultEndpointMapping, endpointResolver: MoyaProvider.DefaultEndpointResolution, stubBehavior: MoyaProvider.ImmediateStubbingBehaviour)
This is almost the default configuration we get with a new MoyaProvider
, except for the stubBehavior
parameter. We’re using it to tell the provider that we want to use the stubbed responses we specified above, and not actually call out to the network. Now we can write tests like this (using Quick and Nimble, of course):
describe("Getting the user's home timeline") {
it("Should have some tweets") {
getTimeline() { (tweets, error) in
expect(tweets).toNot(beNil())
}
}
}
Where to go from Here?
In order to add custom headers (say, for authentication), you’re going to need to provide a value for the endpointClosure
parameter of MoyaProvider
.
let ourEndpointClosure = { (target: TwitterTarget) -> Endpoint in
let url = target.baseURL.URLByAppendingPathComponent(target.path).absoluteString
let endpoint = Endpoint(URL: url, sampleResponse: .Success(200, {target.sampleData}), method: target.method, parameters: target.parameters)
let headers = headersForTarget(target)
return endpoint.endpointByAddingHTTPHeaderFields(headers)
}
let provider = MoyaProvider<TwitterTarget>(endpointClosure: ourEndpointClosure)
Functional Reactive Programming
If you’d rather use signals than completion closures you can still use everything we’ve done so far, only swapping the base MoyaProvider
out with RxMoyaProvider
(for RxSwift) or ReactiveCocoaMoyaProvider
(for Reactive Cocoa). Now the provider’s request
method returns an Observable
(or Signal
) that you can map, filter, and bind to stuff.
Happy coding!
03 Oct 2015
What is Marginal Futility?
In the world of economics, marginal utility is the amount of benefit provided by some small change — another slice of pizza, an extra minute of leisure time, or that one last beer. With Marginal Futility, my goal is to log the little things I figure out as I try to get better at programming, hopefully giving others a leg up along the way.
The blog is inspired by awesome people like Julia Evans, Natasha the Robot, and Ash Furrow, who advocate writing at all costs. Also, a massive thank you to the Recurse Center, the greatest place on earth.