Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Welcome To Ask or Share your Answers For Others

Categories

0 votes
144 views
in Technique[技术] by (71.8m points)

ios - 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.

See Question&Answers more detail:os

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome To Ask or Share your Answers For Others

1 Reply

0 votes
by (71.8m points)

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
    }
}

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
OGeek|极客中国-欢迎来到极客的世界,一个免费开放的程序员编程交流平台!开放,进步,分享!让技术改变生活,让极客改变未来! Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Click Here to Ask a Question

...