2

I am working on a Swift project involving Alamofire, ObjectMapper and RealmSwift and ran into a situation where I have to check if an object of a generics type is an array of Realm's Objects.

My question is: How to check if an object of a generics type is an array of a subtype of a type in Swift?

Lengthy version: I made a minimal working example of my problem. Please see my gist here. You can copy it to Xcode Playground and see that the line 31 runs while the line 35 doesn't.

I was able to make it works by changing the line 70 & 71

from

if let array = data as? [Object] {
  add(array)
}

to

if let array = data as? NSArray where array.count > 0 && array[0] is Object {
  add(array as! [Object])
}

but that solution is not even close to perfect because it has to involve Foundation. I would prefer something "pure" Swift.

<script src="https://gist.github.com/T-Pham/44fe5b7c3a669db34d856b54e15f278a.js"></script>

Removed: the short version does not fully represent the actual problem.

Short version:

protocol Protocol {
  init()
}

class Parent {}
final class Child1: Parent, Protocol {}
final class Child2: Parent, Protocol {}

func foo<T: Protocol>(array: [T]) {
  if array is [Parent] { // This won't compile
  }
}

foo([Child1(), Child1()])
Thanh Pham
  • 2,021
  • 21
  • 30
  • 1
    I believe your "short version" above does not show the problem you're actually asking about. In the short version it seems as if we know that the Swift typing system knows, at runtime, that our "generic" is an array with content of a type (generic) that conforms to `Protocol`. Checking whether this wrapped type is a subclass of `Parent` is a simple task (e.g. `{ if let elem = array.first where elem is Parent { return true }; return false }` as body of `foo(...) -> Bool`). Looking closer at your gist, however, you have a `data` object that is of type `AnyObject`, and that may contain single ... – dfrib Jul 21 '16 at 08:37
  • ... object instances or an array of object instances. This is a trickier case (as often when Swift strong typing "collides" with Cocoa objects) than if we know the content to be an array, and one alternative is the one you use: attempted `NSArray` casting. – dfrib Jul 21 '16 at 08:40
  • Agree @dfri. The problem is actually far more complex than the short version. But it serves as a good starting point, no? :P – Thanh Pham Jul 21 '16 at 08:51
  • Changing the short version to "func foo(array: T)" makes it work but still not as expected. – Thanh Pham Jul 21 '16 at 08:53
  • Actually it made me post an answer in vain (which I have now deleted) as I thought it was representative of the problem you had, which I noticed (after looking closer at your gist), that it's really not. Minimal examples are good, as it, in this case, seems kind of different than your actual problem, it might only confuse prospective answerers (as myself) > – dfrib Jul 21 '16 at 08:54
  • Sorry for the confusion @dfri. I edited the question to remove the short version to avoid further confusion. – Thanh Pham Jul 21 '16 at 09:01
  • No worries! Good luck with your issue. – dfrib Jul 21 '16 at 09:04
  • Have you tried this http://stackoverflow.com/questions/28787009/swift-error-with-generic-array – Khundragpan Jul 21 '16 at 10:18

1 Answers1

0

In Swift type system, [Child] and [Parent] aren't equal because Swift's generics doesn't support covariance. You can use overload with generics type constraint instead subtype polymorphism and dynamic type casting.

First, create three overloads of handleResponse() methods same as myRequest〜 methods:

func handleResponse(alamofireReponse: AlamofireReponse<AnyObject, NSError>, completionHandler: (MyResponse<AnyObject> -> Void)) {
    switch alamofireReponse {
    case .Success(let data):
        completionHandler(.Success(data))
    case .Failure(let error):
        completionHandler(.Failure(error))
    }
}

func handleResponse<T: Mappable where T: Object>(alamofireReponse: AlamofireReponse<T, NSError>, completionHandler: (MyResponse<T> -> Void)) {
    switch alamofireReponse {
    case .Success(let data):
        add(data)
        completionHandler(.Success(data))
    case .Failure(let error):
        completionHandler(.Failure(error))
    }
}

func handleResponse<T: Mappable where T: Object>(alamofireReponse: AlamofireReponse<[T], NSError>, completionHandler: (MyResponse<[T]> -> Void)) {
    switch alamofireReponse {
    case .Success(let data):
        add(data)
        completionHandler(.Success(data))
    case .Failure(let error):
        completionHandler(.Failure(error))
    }
}

Then, change type constraints of myRequest〜 methods to satisfy the constraints of each handleResponse methods.

func myRequestObject<T: Mappable where T: Object>(completionHandler: (MyResponse<T> -> Void)) {
    responseObject { alamofireReponse in
        handleResponse(alamofireReponse, completionHandler: completionHandler)
    }
}

func myRequestArray<T: Mappable where T: Object>(completionHandler: (MyResponse<[T]> -> Void)) {
    responseArray { alamofireReponse in
        handleResponse(alamofireReponse, completionHandler: completionHandler)
    }
}

By doing this, handleResponse () method and add () method will be dispatched to the appropriate method at compile time depending on the type of argument. You do not need to check the type dynamically.

The entire code is below, just in case:

import Foundation

// Alamofire
enum AlamofireReponse<T, E> {
    case Success(T)
    case Failure(E)
}

func responseJSON(alamofireReponse: AlamofireReponse<AnyObject, NSError> -> Void) {
    alamofireReponse(.Success([["type": "not_object"], ["type": "not_object"]]))
}

// ObjectMapper
protocol Mappable {
    init()
}

// AlamofireObjectMapper
func responseObject<T: Mappable>(alamofireReponse: AlamofireReponse<T, NSError> -> Void) {
    alamofireReponse(.Success(T()))
}

func responseArray<T: Mappable>(alamofireReponse: AlamofireReponse<[T], NSError> -> Void) {
    alamofireReponse(.Success([T(), T()]))
}

// RealmSwift
class Object {}

func add(object: Object) {
    print("adding single object works") // This line would run
}

func add<T: SequenceType where T.Generator.Element: Object>(objects: T) {
    print("adding multiple objects works")  // This line would not
}

// My code
final class Post: Object, Mappable {}
final class Comment: Object, Mappable {}

enum MyResponse<T> {
    case Success(T)
    case Failure(NSError)
}

func myRequestJSON(completionHandler: (MyResponse<AnyObject> -> Void)) {
    responseJSON { alamofireReponse in
        handleResponse(alamofireReponse, completionHandler: completionHandler)
    }
}

func myRequestObject<T: Mappable where T: Object>(completionHandler: (MyResponse<T> -> Void)) {
    responseObject { alamofireReponse in
        handleResponse(alamofireReponse, completionHandler: completionHandler)
    }
}

func myRequestArray<T: Mappable where T: Object>(completionHandler: (MyResponse<[T]> -> Void)) {
    responseArray { alamofireReponse in
        handleResponse(alamofireReponse, completionHandler: completionHandler)
    }
}

func handleResponse(alamofireReponse: AlamofireReponse<AnyObject, NSError>, completionHandler: (MyResponse<AnyObject> -> Void)) {
    switch alamofireReponse {
    case .Success(let data):
        completionHandler(.Success(data))
    case .Failure(let error):
        completionHandler(.Failure(error))
    }
}

func handleResponse<T: Mappable where T: Object>(alamofireReponse: AlamofireReponse<T, NSError>, completionHandler: (MyResponse<T> -> Void)) {
    switch alamofireReponse {
    case .Success(let data):
        add(data)
        completionHandler(.Success(data))
    case .Failure(let error):
        completionHandler(.Failure(error))
    }
}

func handleResponse<T: Mappable where T: Object>(alamofireReponse: AlamofireReponse<[T], NSError>, completionHandler: (MyResponse<[T]> -> Void)) {
    switch alamofireReponse {
    case .Success(let data):
        add(data)
        completionHandler(.Success(data))
    case .Failure(let error):
        completionHandler(.Failure(error))
    }
}

// Usage
myRequestJSON { response in
}

myRequestObject { (response: MyResponse<Post>) in
}

myRequestArray { (response: MyResponse<[Post]>) in
}
kishikawa katsumi
  • 10,418
  • 1
  • 41
  • 53