Artsy's API requires something called an XApp token in order to perform requests. The token comes back with an expiry date, after which the token will no longer work. A new token will have to be fetched.

1
2
3
4
{
  "xapp_token": "SOME_TOKEN",
  "expires_in":"2014-09-19T12:22:21.570Z"
}

In our previous iOS apps, tragically written in Objective-C, we have a lot of code that looks like the following. getXappTokenWithCompletion: checks to make sure that there is a valid token. If there is, it invokes the completion block immediately. Otherwise, it fetches a token, sets it in a static variable, and then invokes the completion block.

1
2
3
4
5
6
7
[ArtsyAPI getXappTokenWithCompletion:^(NSString *xappToken, NSDate *expirationDate) {
    [ArtsyAPI getSomething:^(NSDictionary *results) {
       // do something
    } failure:^(NSError *error) {
        // handle herror
    }];
}];

That's kind of ugly. A better approach might be to embed the token-requesting logic within the getSomething: method. But that kind of sucks, since we'd have to reproduce that logic for every network-accessing method. If we have ten methods, that's ten times we need to duplicate that logic.

With our new app (written in Swift), we're using a network abstraction layer we've created called Moya. Moya sits on top of Alamofire and provides an abstraction for API endpoints. Instead of having ten different network-accessing methods, there is only one method to which you pass one of the ten different possible enum values. This means you have compile-time safety in your networking code, but that's not really what we're here to talk about.

Moya has this cool last-minute closure that it invokes to sign requests, so we can sign these requests like this.

1
2
3
4
5
6
7
8
9
10
11
var endpointsClosure = { (target: ArtsyAPI, method: Moya.Method, parameters: [String: AnyObject]) -> Endpoint<ArtsyAPI> in
        let endpoint: Endpoint<ArtsyAPI> = Endpoint<ArtsyAPI>(URL: url(target), sampleResponse: .Success(200, target.sampleData), method: method, parameters: parameters)

        // Sign all non-XApp token requests
        switch target {
        case .XApp:
            return endpoint
        default:
            return endpoint.endpointByAddingHTTPHeaderFields(["X-Xapp-Token": /* global XApp token */])
        }
    }

So that's kind of cool.

Since there is only one method for accessing the API, we can easily inject the token-checking method there. Something like

1
2
3
4
5
6
7
8
9
10
public func XAppRequest(token: ArtsyAPI, completion: MoyaCompletion) {
    if /* token is valid */ {
        moyaProvider.sharedProvider.request(token, completion: completion)
    } else {
      moyaProvider.request(ArtsyAPI.XApp, completion: { (data, statusCode, error) -> () in
          /* store token somewhere */
          moyaProvider.sharedProvider.request(token, completion: completion)
      })
  }
}

That's better, but it's still kind of ugly. We've got duplicated code in there, and we're just kind of abstracting away the callback mess; it still exists, we just don't see if as often.

OK, so what alternative is there? Well, Moya supports a ReactiveCocoa extension that uses signals instead of callback closures. Super-cool. So we can rewrite our XAppRequest function to be the following.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
private func XAppTokenRequest() -> RACSignal {
    let newTokenSignal = moyaProvider.request(ArtsyAPI.XApp).filterSuccessfulStatusCodes().mapJSON().doNext({ (response) -> Void in
        /* store new token globally */
    }).logError().ignoreValues()

    let validTokenSignal = RACSignal.`return`(/* does the token exist and is valid? */)
    return RACSignal.`if`(validTokenSignal, then: RACSignal.empty(), `else`: newTokenSignal)
}

public func XAppRequest(token: ArtsyAPI) -> RACSignal {
    return XAppTokenRequest().then({ () -> RACSignal! in
        return moyaProvider.request(token, method: method, parameters: parameters)
    })
}

Neato. So we have abstracted the "check if there is a valid token and get one if there isn't" into its own private method called XAppTokenRequest. If the token exists and is valid, then the function returns RACSignal.empty(), a signal which completes immediately. Otherwise, we perform a fetch, which completes when the XApp token request is finished.

Then we just need to use then on RACSignal to create a new signal that is generated once the XAppTokenRequest signal completes. Since the then closure is only invoked once the XAppTokenRequest signal completes, the newly created request signal will be generated after the token is set, which is ideal.

All the code above is kind of simplified. That's OK, since it's just a proof of concept. If you want the full code, it's all available on GitHub and the conversation surrounding this change is in a merged pull request.

If you have run into this problem and have a different solution, we'd love to hear from you.

Categories: ios, mobile, networking, open-source


Comments