Optional binding succeeds if it shouldn't
Asked Answered
S

1

10

This is what I posted as a possible solution to Traverse view controller hierarchy in Swift (slightly modified):

extension UIViewController {

    func traverseAndFindClass<T where T : UIViewController>(T.Type) -> T? {
        var currentVC = self
        while let parentVC = currentVC.parentViewController {
            println("comparing \(parentVC) to \(T.description())")
            if let result = parentVC as? T { // (XXX)
                return result
            }
            currentVC = parentVC
        }
        return nil
    }
}

The method should traverse up the parent view controller hierarchy and return the first instance of the given class, or nil if none is found.

But it does not work, and I cannot figure out why. The optional binding marked with (XXX) always succeeds, so that the first parent view controller is returned even if it is not an instance of T.

This can easily be reproduced: Create a project from the "iOS Master-Detail Application" template in Xcode 6 GM, and add the following code to viewDidLoad() of the MasterViewController class:

if let vc = self.traverseAndFindClass(UICollectionViewController.self) {
    println("found: \(vc)")
} else {
    println("not found")
}

self is a MasterViewController (a subclass of UITableViewController), and its parent view controller is a UINavigationController. There is no UICollectionViewController in the parent view controllers hierarchy, so I would expect that the method returns nil and the output is "not found".

But this is what happens:

comparing <UINavigationController: 0x7fbc00c4de10> to UICollectionViewController
found: <UINavigationController: 0x7fbc00c4de10>

This is obviously wrong, because UINavigationController is not a subclass of UICollectionViewController. Perhaps I made some stupid error, but I could not find it.


In order to isolate the problem, I also tried to reproduce it with my own class hierarchy, independent of UIKit:

class BaseClass : NSObject {
    var parentViewController : BaseClass?
}

class FirstSubClass : BaseClass { }

class SecondSubClass : BaseClass { }

extension BaseClass {

    func traverseAndFindClass<T where T : BaseClass>(T.Type) -> T? {
        var currentVC = self
        while let parentVC = currentVC.parentViewController {
            println("comparing \(parentVC) to \(T.description())")
            if let result = parentVC as? T { // (XXX)
                return result
            }
            currentVC = parentVC
        }
        return nil
    }
}

let base = BaseClass()
base.parentViewController = FirstSubClass()

if let result = base.traverseAndFindClass(SecondSubClass.self) {
    println("found: \(result)")
} else {
    println("not found")
}

And guess what? Now it works as expected! The output is

comparing <MyApp.FirstSubClass: 0x7fff38f78c40> to MyApp.SecondSubClass
not found

UPDATE:

  • Removing the type constraint in the generic method

    func traverseAndFindClass<T>(T.Type) -> T?
    

    as suggested by @POB in a comment makes it work as expected.

  • Replacing the optional binding by a "two-step binding"

    if let result = parentVC as Any as? T { // (XXX)
    

    as suggested by @vacawama in his answer also makes it work as expected.

  • Changing the build configuration from "Debug" to "Release" also makes the method work as expected. (I have tested this only in the iOS Simulator so far.)

The last point could indicate that this is a Swift compiler or runtime bug. And I still cannot see why the problem occurs with subclasses of UIViewController, but not with subclasses of my BaseClass. Therefore I will keep the question open for a while before accepting an answer.


UPDATE 2: This has been fixed as of Xcode 7.

With the final Xcode 7 release the problem does not occur anymore. The optional binding if let result = parentVC as? T in the traverseAndFindClass() method now works (and fails) as expected, both in Release and Debug configuration.

Stimulant answered 14/9, 2014 at 21:7 Comment(5)
If you try func traverseAndFindClass<T>(T.Type) -> T? {/*...*/} instead of func traverseAndFindClass<T where T : UIViewController>(T.Type) -> T? {/*...*/}, it will print "Not found" for if let vc = self.traverseAndFindClass(UICollectionViewController.self). However, if let vc = self.traverseAndFindClass(UIViewController.self) and if let vc = self.traverseAndFindClass(UINavigationController.self) will both print "Found: <vc>".Annabellannabella
@POB: That does indeed work (as does the solution suggested in vacawama's answer), but I still fail to understand why.Stimulant
Could be something obscure, have you tried asking on dev forums?Pruett
@nkukushkin: "Cross-posted" at devforums.apple.com/thread/245561.Stimulant
Re: In order to isolate the problem. To replicate, the code in the extension should be func traverseAndFindClass<T where T : **NSObject**>(T.Type) -> T? {Upheave
P
3

If you try to conditionally cast an object of type UINavigationController to a UICollectionViewController in a Playground:

var nc = UINavigationController()

if let vc = nc as? UICollectionViewController {
    println("Yes")
} else {
    println("No")
}

You get this error:

Playground execution failed: :33:16: error: 'UICollectionViewController' is not a subtype of 'UINavigationController' if let vc = nc as? UICollectionViewController {

but if instead you do:

var nc = UINavigationController()

if let vc = (nc as Any) as? UICollectionViewController {
    println("Yes")
} else {
    println("No")
}

it prints "No".

So I suggest trying:

extension UIViewController {

    func traverseAndFindClass<T where T : UIViewController>(T.Type) -> T? {
        var currentVC = self
        while let parentVC = currentVC.parentViewController {
            println("comparing \(parentVC) to \(T.description())")
            if let result = (parentVC as Any) as? T { // (XXX)
                return result
            }
            currentVC = parentVC
        }
        return nil
    }
}
Picnic answered 15/9, 2014 at 2:13 Comment(2)
That does indeed work as expected (as does the solution suggested by @POB in a comment), but I still fail to understand why. And where is the difference to my second BaseClass/FirstSubClass/SecondSubClass example? Btw. if let result = (parentVC as AnyObject) as? T does not work.Stimulant
The same problem still occurs with the latest Xcode 6.3.1, and you workaround also still works. Seems that there is no better solution!Stimulant

© 2022 - 2024 — McMap. All rights reserved.