Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Optional binding succeeds if it shouldn't

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.

like image 652
Martin R Avatar asked Sep 14 '14 21:09

Martin R


People also ask

What is optional binding in Swift?

Optional Binding is the process of checking if an Optional variable has a value and if it has then storing the value in another variable. “Optionals in Swift are variables that can store both an object or a nil value.” I will be writing more about Optionals, Optional Binding, Optional Chaining, etc. in the next post, I will update the link here.

What is optional binding in Python?

Optional binding is used to find out if an optional contains a value and it is considered a safe way to unwrap optional. With this, we extract the value and store it in a new variable. In this tutorial, you’ll learn what is optional binding.

What is optional chaining in JavaScript?

Optional chaining allows us to call properties, methods, and subscripts on an optional that might be nil. If any of the chained values return nil, the return value will be nil. It does not store the value on the left into a variable. The following code gives an example of optional chaining using a fictitious car object.


1 Answers

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
    }
}
like image 85
vacawama Avatar answered Oct 09 '22 11:10

vacawama