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