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.
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.
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.
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.
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.
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
}
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With