7

I'm extensively using KVC to build unified interface for the needs of an app. For instance, one of my functions gets an object, which undergoes several checks based solely on dictionary of string keys.

Thus I need a way to check if an object by the key is of collection type.

I expected to have been able to make some protocol check (like IEnumerable in C# to check if it can be enumerated), but it didn't work out:

if let refCollection = kvcEntity.value(forKey: refListLocalKey) as? AnySequence<CKEntity> { ... }

I tried AnyCollection, too.

I know I could iterate all main collection types, by simply typing:

if let a = b as? Set { ...} // (or: if a is Set {...})
if let a = b as? Array { ...}
if let a = b as? Dictionary { ...}

But this doesn't seem proper from inheritance/polymorphism point of view.

Hexfire
  • 5,945
  • 8
  • 32
  • 42
  • have you try this http://stackoverflow.com/questions/39536742/in-swift-how-do-i-check-if-an-object-is-a-collection-of-any-type – john afordis Dec 20 '16 at 06:21
  • Possible duplicate of [Using isKindOfClass with Swift](http://stackoverflow.com/questions/24019707/using-iskindofclass-with-swift) – Anni S Dec 20 '16 at 06:25
  • I used search before posting. As of isKindOfClass, it implies iteration over hard-coded types. I would want a proper protocol check or something along those lines. – Hexfire Dec 20 '16 at 06:28
  • If you knew which possible type it could be? You can just check for them, no need to iterate all collection types. – Anni S Dec 20 '16 at 06:31
  • Yes, it's the workaround I would have to resort to. The issue doesn't block me from implementing what I want. I just want to do it best possible way. Inheritrance is a good thing and there must be a way to work with in cases alike. – Hexfire Dec 20 '16 at 06:37
  • You have tried AnyCollection but not Collection? Collection would be the protocol to try. It's the protocol which is directly adopted by the most common types like Dictionary, Set, etc. I don't think AnyCollection encompasses new native Swift types. – Michael Fourre Dec 20 '16 at 06:41
  • @Hexfire, If you want to check wether it conformsToProtocol you can have a look at http://stackoverflow.com/questions/28124684/swift-check-if-generic-type-conforms-to-protocol I hope this will help you. – Anni S Dec 20 '16 at 06:41
  • 2
    @AnniS That doesn't work with `Collection`. It results in an error. – rmaddy Dec 20 '16 at 06:48
  • 2
    @AnniS, conformsToProtocol comes from objc and is only applicable to NSObject-derived types. Swift collections are not. – Hexfire Dec 20 '16 at 07:42
  • Yeah, 'as' in Swift is equivalent to conformsToProtocol in ObjC(stackoverflow link I added for the same). – Anni S Dec 20 '16 at 08:10

3 Answers3

5

Collection can no longer be used for type-checking, hence Ahmad F's solution would no longer compile.

I did some investigation. Some people advice to bridge to obj-c collections and use isKindOfClass, others try to employ reflection (by using Mirror). Neither is satisfactory.

There's a pretty straight-forward, a bit rough yet efficient way to accomplish the task via splitting object type if our concern is Array, Dictionary or Set (list can be updated):

func isCollection<T>(_ object: T) -> Bool {
    let collectionsTypes = ["Set", "Array", "Dictionary"]
    let typeString = String(describing: type(of: object))

    for type in collectionsTypes {
        if typeString.contains(type) { return true }
    }
    return false
}

Usage:

var set : Set! = Set<String>()
var dictionary : [String:String]! = ["key" : "value"]
var array = ["a", "b"]
var int = 3
isCollection(int) // false
isCollection(set) // true
isCollection(array) // true
isCollection(dictionary) // true

Hard-code is the drawback but it does the job.

Hexfire
  • 5,945
  • 8
  • 32
  • 42
2

NOTE: This solution does NOT work with Swift 5+.

func isCollection<T>(object: T) -> Bool {
    switch object {
    case _ as Collection:
        return true
    default:
        return false
    }
}

Calling:

// COLLECTION TESTING //

let arrayOfInts = [1, 2, 3, 4, 5]
isCollection(object: arrayOfInts) // true

let setOfStrings:Set<String> = ["a", "b", "c"]
isCollection(object: setOfStrings) // true

// [String : String]
let dictionaryOfStrings = ["1": "one", "2": "two", "3": "three"]
isCollection(object: dictionaryOfStrings) // true


// NON-COLLECTION TESTING //

let int = 101
isCollection(object: int) // false

let string = "string" // false

let date = Date()
isCollection(object: date) // false
Ahmad F
  • 30,560
  • 17
  • 97
  • 143
  • 1
    In swift 5: ERROR "Protocol 'Collection' can only be used as a generic constraint because it has Self or associated type requirements" – Luc-Olivier Oct 28 '20 at 13:42
1

You could create another protocol

protocol CountableCollection {
    var count: Int { get }
}

extension Array: CountableCollection where Element: Any {
}

extension Dictionary: CountableCollection where Key == String, Value == Any {
}

Add all the methods that you need from Collection to the new protocol that has been created (I have added just count getter for demonstration).


After this you could simply do

if someVar is CountableCollection {
    print(someVar.count)
}

someVar would be true if it is an Array or Dictionary. You can also make it conform to Set if required.

Kaunteya
  • 3,107
  • 1
  • 35
  • 66
  • This won't work for custom collections defined in 3rd party libraries, or even in own code, e.g. `struct MyAwesomeCollection: Collection` – Cristik Oct 31 '20 at 15:47