Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Identifying a subclass with Swift Generics works with custom class but not with UITapGestureRecognizer

There's something I wanted to do in swift, but I couldn't figure out how to achieve it, that is to remove gesture recognisers given a Class Type, here's my code (and example), i'm using swift 2.0 in Xcode 7 beta 5:

I have 3 classes that inherits from UITapGestureRecognizer

class GestureONE: UIGestureRecognizer { /*...*/ }
class GestureTWO: UIGestureRecognizer { /*...*/ }
class GestureTHREE: UIGestureRecognizer { /*...*/ }

Add them to a view

var gesture1 =     GestureONE()
var gesture11 =    GestureONE()
var gesture2 =     GestureTWO()
var gesture22 =    GestureTWO()
var gesture222 =   GestureTWO()
var gesture3 =     GestureTHREE()

var myView = UIView()
myView.addGestureRecognizer(gesture1)
myView.addGestureRecognizer(gesture11)
myView.addGestureRecognizer(gesture2)
myView.addGestureRecognizer(gesture22)
myView.addGestureRecognizer(gesture222)
myView.addGestureRecognizer(gesture3)

I print the object:

print(myView.gestureRecognizers!)
// playground prints "[<__lldb_expr_224.TapONE: 0x7fab52c20b40; baseClass = UITapGestureRecognizer; state = Possible; view = <UIView 0x7fab52d259c0>>, <__lldb_expr_224.TapONE: 0x7fab52d21250; baseClass = UITapGestureRecognizer; state = Possible; view = <UIView 0x7fab52d259c0>>, <__lldb_expr_224.TapTWO: 0x7fab52d24a60; baseClass = UITapGestureRecognizer; state = Possible; view = <UIView 0x7fab52d259c0>>, <__lldb_expr_224.TapTWO: 0x7fab52c21130; baseClass = UITapGestureRecognizer; state = Possible; view = <UIView 0x7fab52d259c0>>, <__lldb_expr_224.TapTWO: 0x7fab52e13260; baseClass = UITapGestureRecognizer; state = Possible; view = <UIView 0x7fab52d259c0>>, <__lldb_expr_224.TapTHREE: 0x7fab52c21410; baseClass = UITapGestureRecognizer; state = Possible; view = <UIView 0x7fab52d259c0>>]"

Have this extension I made with a generic function

extension UIView {
    func removeGestureRecognizers<T: UIGestureRecognizer>(type: T.Type) {
        if let gestures = self.gestureRecognizers {
            for gesture in gestures {
                if gesture is T {
                    removeGestureRecognizer(gesture)
                }
            }
        }
    }
}

Then I use it

myView.gestureRecognizers?.count // Prints 6
myView.removeGestureRecognizers(GestureTWO)
myView.gestureRecognizers?.count // Prints 0

Is removing all of the gestures D:

And here's an experiment with custom classes

//** TEST WITH ANIMALS*//
class Animal { /*...*/ }

class Dog: Animal { /*...*/ }
class Cat: Animal { /*...*/ }
class Hipo: Animal { /*...*/ }

class Zoo {
    var animals = [Animal]()
}

var zoo = Zoo()

var dog1 = Dog()
var cat1 = Cat()
var cat2 = Cat()
var cat3 = Cat()
var hipo1 = Hipo()
var hipo2 = Hipo()

zoo.animals.append(dog1)
zoo.animals.append(cat1)
zoo.animals.append(cat2)
zoo.animals.append(cat3)
zoo.animals.append(hipo1)
zoo.animals.append(hipo2)

print(zoo.animals)
//playground prints "[Dog, Cat, Cat, Cat, Hipo, Hipo]"

extension Zoo {
    func removeAnimalType<T: Animal>(type: T.Type) {
        for (index, animal) in animals.enumerate() {
            if animal is T {
                animals.removeAtIndex(index)
            }
        }
    }
}

zoo.animals.count // prints 6
zoo.removeAnimalType(Cat)
zoo.animals.count // prints 3

It's actually removing the classes it should :D

What am I missing with the UIGestureRecognizer's? I ended up with a workaround making a function that has no generics (boring) like this:

extension UIView {
    func removeActionsTapGestureRecognizer() {
        if let gestures = self.gestureRecognizers {
            gestures.map({
                if $0 is ActionsTapGestureRecognizer {
                    self.removeGestureRecognizer($0)
                }
            })
        }
    }
}

This works of course, but still I would like to have a real solution

I appreciate your help!!

Note: First question I ask here

like image 811
dGambit Avatar asked Aug 08 '15 17:08

dGambit


1 Answers

TL;DR:

Use dynamicType to check the runtime type of each gesture recognizer against your type parameter.


Great question. It looks like you're encountering a scenario where the difference between Objective-C's dynamic typing and Swift's static typing becomes clear.

In Swift, SomeType.Type is the metatype type of a type, which essentially allows you to specify a compile-time type parameter. But this might not be the same as the type at runtime.

class BaseClass { ... }
class SubClass: BaseClass { ... }

let object: BaseClass = SubClass()

In the example above, object's compile-time class is BaseClass, but at runtime, it is SubClass. You can check the runtime class with dynamicType:

print(object.dynamicType)
// prints "SubClass"

So why does that matter? As you saw with your Animal test, things behaved as you expected: your method takes an argument whose type is a metatype type of an Animal subclass, and then you only remove animals that conform to that type. The compiler knows that T can be any particular subclass of Animal. But if you specify an Objective-C type (UIGestureRecognizer), the compiler dips its toes into the uncertain world of Objective-C dynamic typing, and things get a little less predictable until runtime.

I must warn you that I'm a bit wooly on the details here... I don't know the specifics of how the compiler/runtime treats generics when mixing the worlds of Swift & Objective-C. Perhaps somebody with some better knowledge of the subject could chip in and elucidate!

As a comparison, let's just quickly try a variation of your method where the compiler can steer a bit further clear of the Objective-C world:

class SwiftGesture: UIGestureRecognizer {}

class GestureONE: SwiftGesture {}
class GestureTWO: SwiftGesture {}
class GestureTHREE: SwiftGesture {}

extension UIView {
    func removeGestureRecognizersOfType<T: SwiftGesture>(type: T.Type) {
        guard let gestureRecognizers = self.gestureRecognizers else { return }
        for case let gesture as T in gestureRecognizers {
            self.removeGestureRecognizer(gesture)
        }
    }
}

myView.removeGestureRecognizers(GestureTWO)

With the above code, only GestureTWO instances will be removed, which is what we want, if only for Swift types. The Swift compiler can look at this generic method declaration without concerning itself with Objective-C types.

Fortunately, as discussed above, Swift is capable of inspecting the runtime type of an object, using dynamicType. With this knowledge, it only takes a minor tweak to make your method work with Objective-C types:

func removeGestureRecognizersOfType<T: UIGestureRecognizer>(type: T.Type) {
    guard let gestureRecognizers = self.gestureRecognizers else { return }
    for case let gesture in gestureRecognizers where gesture.dynamicType == type {
        self.removeGestureRecognizer(gesture)
    }
}

The for loop binds to the gesture variable only gesture recognizers whose runtime type is equal to the passed in metatype type value, so we successfully remove only the specified type of gesture recognizers.

like image 128
Stuart Avatar answered Oct 17 '22 05:10

Stuart