Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

support interoperability with objective-c #21

Closed
angelodipaolo opened this issue Feb 1, 2016 · 1 comment
Closed

support interoperability with objective-c #21

angelodipaolo opened this issue Feb 1, 2016 · 1 comment
Assignees
Milestone

Comments

@angelodipaolo
Copy link
Contributor

The public API that ELWebService exposes does not bridge over to Objective-C which means networking calls cannot be made directly from Objective-C code.

The main issue is that ServiceTaskResult cannot be represented in Obj-C because it is defined as an enumeration with associated values. The associated values are used to encapsulate the data that gets passed through the response handler chain.

An example of using ServiceTaskResult to pass values through response handler chains:

service
    .GET("/brewers")
    .responseJSON { json in
      if let models: [Brewer] = JSONDecoder<Brewer>.decode(json)  {
          // pass encoded value via ServiceTaskResult
          return .Value(models)
      } else {
        // any value conforming to ErrorType
        return .Failure(JSONDecoderError.FailedToDecodeBrewer) 
      }
    }
    .updateUI { value in
        if let brewers = value as? [Brewer] {
            // update some UI with brewer models
        }
    }
    .resume()
@angelodipaolo angelodipaolo added this to the v3.0.0 milestone Feb 1, 2016
@angelodipaolo angelodipaolo self-assigned this Feb 1, 2016
@angelodipaolo
Copy link
Contributor Author

Proposed Solutions

  1. Change ServiceTaskResult from an Enumeration to a Class.
  2. Overload ServiceTask's response handler API with Obj-C friendly methods
  3. Add specially named methods to ServiceTask's response handler API (not overloads)

1. Change ServiceTaskResult from an Enumeration to a Class.

ServiceTaskResult becomes a class that provides two initializers, one for errors and another for the actual value payload.

@objc public final class ServiceTaskResult: NSObject {
    private(set) var value: AnyObject?
    private(set) var error: NSError?

    @objc public init(value: AnyObject) {
        self.value = value
    }

    @objc public init(error: NSError) {
        self.error = error
    }
}

Old concise syntax:

.responseJSON { json in
      if let models: [Brewer] = JSONDecoder<Brewer>.decode(json)  {
          // pass encoded value via ServiceTaskResult
          return .Value(models)
      } else {
        // any value conforming to ErrorType
        return .Failure(JSONDecoderError.FailedToDecodeBrewer) 
      }
    }

New crufty syntax:

.responseJSON { json in
      if let models: [Brewer] = JSONDecoder<Brewer>.decode(json)  {
          // pass encoded value via ServiceTaskResult
          return ServiceTaskResult(value: models)
      } else {
        // need to return NSError instead of ErrorType
        return NSError(domain: "foo", code: 500, userInfo: nil)
      }
    }

pros

  • Makes ServiceTask's API callable from Obj-C

cons

  • Conflates "Result" API with separate types for both Objective-C and Swift
  • Lose the use of ErrorType when calling from Swift because NSError is required to expose the ServiceTaskResult class to Obj-C.
  • Lose the brevity and expressiveness of using enum cases. return .Value(models) now becomes return ServiceTaskResult(value: models)

2. Overload ServiceTask's response handler API with Obj-C friendly methods

Keep ServiceTaskResult as an enum but add a ObjCHandlerResult class that encapsulates the result/error data and can be initialized from Obj-C.

@objc public final class ObjCHandlerResult: NSObject {
    private(set) var value: AnyObject?
    private(set) var error: NSError?

    @objc public init(value: AnyObject) {
        self.value = value
    }

    @objc public init(error: NSError) {
        self.error = error
    }
}

Overload the response handler methods to accept a closure which returns a ObjCHandlerResult value.

extension ServiceTask {
    // existing method that cannot be represented in Obj-C
    public func response(handler: ResponseProcessingHandler) -> Self {
        dispatch_async(handlerQueue) {
            if let taskResult = self.taskResult {
                switch taskResult {
                case .Failure(_): return // bail out to avoid next handler from running
                case .Value(_): break
                case .Empty: break
                }
            }

            self.taskResult = handler(self.responseData, self.urlResponse)
        }

        return self
    }

    // overload with Obj-C friendly closure return type
    @objc public func response(handler: (NSData?, NSURLResponse?) -> ObjCHandlerResult?) -> Self {
        let processResponse: ResponseProcessingHandler = { data, response in
            let result = handler(data, response)

            if let error = result?.error {
                return .Failure(error)
            } else if let value = result?.value {
                return .Value(value)
            } else {
                return .Empty
            }
        }

        return response(processResponse)
    }
}

Example Objective-C usage:

ServiceTask *task = [service GET:@"/brewers"];

[task responseJSON:^HandlerResult *(id json) {
    NSArray *models = [self decodeBrewersFromJSON:json];

    if (models == nil) {
        NSError *error
        return [[HandlerResult alloc] initWithError:error];
    } else {
        return [[HandlerResult alloc] initWithValue:models];
    }

}];

[task updateUI:^(id value) {
    if ([value isKindOfClass:[NSArray class]]) {
         // update some UI with brewer models
    }
}];

[task resume];

Example Swift usage:

service
    .GET("/brewers")
    .responseJSON { json in
      if let models: [Brewer] = JSONDecoder<Brewer>.decode(json)  {
          // pass encoded value via ServiceTaskResult
          return .Value(models)
      } else {
        // any value conforming to ErrorType
        return .Failure(JSONDecoderError.FailedToDecodeBrewer) 
      }
    }
    .updateUI { value in
        if let brewers = value as? [Brewer] {
            // update some UI with brewer models
        }
    }
    .resume()

pros

  • Makes ServiceTask's API callable from Obj-C
  • Retains the brevity and expressiveness of using ServiceTaskResult enum cases in Swift

cons

  • Conflates "Result" API with separate types for both Objective-C and Swift
  • Causes ambiguity when calling response handler methods which can only be resolved by explicitly declaring the type of the handler closure. This breaks the chaining syntax you get with trailing closures.

Currently you can use trailing closure syntax to chain ServiceTask methods.

extension ServiceTask {
    func responseAsBrews(handler: ([Brew]) -> Void) -> Self {
        return
            // chaining with trailing closures 
            responseJSON { json in
                if let json = self.jsonObject(json, forKey: "brews"),
                    let jsonArray = json as? [AnyObject],
                    let decodedArray = ModelDecoder<Brew>.decodeArray(jsonArray) {
                        return .Value(decodedArray)
                } else {
                    return .Failure(ServiceTaskDecodeError.FailedToDecodeJSONArray)
                }
            }
            .updateUI { value in
                if let brews = value as? [Brew] {
                    handler(brews)
                }
            }
    }
}

The overloaded methods cause ambiguity and break trailing closure chaining which means the closures must be explicitly typed. This is not necessarily a con and can encourage more reusable code since the closure cannot be defined inline when calling the response handler methods. The example below uses a computed property to define the handler closure which can be reused in other response handlers.

extension ServiceTask {
    // explicitly typed closure
    // declaring a property
    var parseBrewModelsHandler: JSONHandler {
        return { json in
            if let json = self.jsonObject(json, forKey: "brews"),
                let jsonArray = json as? [AnyObject],
                let decodedArray = ModelDecoder<Brew>.decodeArray(jsonArray) {
                    return .Value(decodedArray)
            } else {
                return .Failure(ServiceTaskDecodeError.FailedToDecodeJSONArray)
            }
        }
    }

    func responseAsBrews(handler: ([Brew]) -> Void) -> Self {
        return
            responseJSON(parseBrewModelsHandler)
            .updateUI { value in
                if let brews = value as? [Brew] {
                    handler(brews)
                }
            }
    }
}

3. Add specially named methods to ServiceTask's response handler API (not overloads)

Add specially named methods that are designed only to be called from Obj-C. Same approach as #2 except it avoids the ambiguity that breaks the closure type inference.

extension ServiceTask {
    // existing method that cannot be represented in Obj-C
    public func response(handler: ResponseProcessingHandler) -> <<error type>>

    // specially-named obj-c friendly method
    @objc public func responseObjC(handler: (NSData?, NSURLResponse?) -> ObjCHandlerResult?) -> Self
}

pros

  • Makes ServiceTask's API callable from Obj-C
  • Avoids the ambiguity introduced by #2 which breaks trailing closure chaining

cons

  • Conflates the ServiceTask response handler API by having separate methods for adding response handlers with different names.
  • Conflates "Result" API with separate types for both Objective-C and Swift

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant