Traverse view controller hierarchy in Swift
Asked Answered
A

3

5

I would like to traverse the view controller hierarchy in Swift and find a particular class. Here is the code:

extension UIViewController{

    func traverseAndFindClass<T : UIViewController>() -> UIViewController?{

        var parentController = self.parentViewController as? T?
                                    ^
                                    |
                                    |
        // Error: Could not find a user-defined conversion from type 'UIViewController?' to type 'UIViewController'
        while(parentController != nil){

            parentController = parentController!.parentViewController

        }

        return parentController

    }

}

Now, I know that the parentViewController property returns an optional UIViewController, but I do not know how in the name of God I can make the Generic an optional type. Maybe use a where clause of some kind ?

Afterburning answered 13/9, 2014 at 22:1 Comment(0)
T
6

Your method should return T? instead of UIViewController?, so that the generic type can be inferred from the context. Checking for the wanted class has also to be done inside the loop, not only once before the loop.

This should work:

extension UIViewController {
    
    func traverseAndFindClass<T : UIViewController>() -> T? {
        
        var currentVC = self
        while let parentVC = currentVC.parentViewController {
            if let result = parentVC as? T {
                return result
            }
            currentVC = parentVC
        }
        return nil
    }
}

Example usage:

if let vc = self.traverseAndFindClass() as SpecialViewController? {
    // ....
}

Update: The above method does not work as expected (at least not in the Debug configuration) and I have posted the problem as a separate question: Optional binding succeeds if it shouldn't. One possible workaround (from an answer to that question) seems to be to replace

if let result = parentVC as? T { ...

with

if let result = parentVC as Any as? T { ...

or to remove the type constraint in the method definition:

func traverseAndFindClass<T>() -> T? {

Update 2: The problem has been fixed with Xcode 7, the traverseAndFindClass() method now works correctly.


Swift 4 update:

extension UIViewController {
    
    func traverseAndFindClass<T : UIViewController>() -> T? {
        var currentVC = self
        while let parentVC = currentVC.parent {
            if let result = parentVC as? T {
                return result
            }
            currentVC = parentVC
        }
        return nil
    }
}
Thermomotor answered 13/9, 2014 at 23:30 Comment(6)
Hi, there, thanks, your solution compiles, however, when it's checking against the correct type/class, everything seems to pass the check. I logged shortly before traverseAndFindClass returns with NSLog("comparing \(parentVC) to \(T.description())") and got two classes that are not class siblings in any way.Afterburning
@the_critic: You are right, I can confirm that it does not work, and that seems to be strange problem, perhaps a Swift bug. Since this problem is independent of your original question, I have posted it as a new question, and updated my answer with possible workarounds.Thermomotor
@the_critic: That "optional binding bug" has been fixed in Xcode 7. The traverseAndFindClass() function now works correctly.Thermomotor
Awesome! Thanks for taking the time and adding this comment!Afterburning
Answer updated, currentVC needs typing to UIViewController, otherwise the compiler will assign it type Self which produces a compiler error when attempting to assign parent to current in the last step of the while-loop.Dishcloth
@JohnRogers: Thank you for the feedback. Apart from parentViewController being renamed to parent, the code compiles without problems for my (tested with Xcode 9.2/Swift 4.1)Thermomotor
E
4

One liner solution (using recursion), Swift 4.1+:

extension UIViewController {
  func findParentController<T: UIViewController>() -> T? {
    return self is T ? self as? T : self.parent?.findParentController() as T?
  }
}

Example usage:

if let vc = self.findParentController() as SpecialViewController? {
  // ....
}
Eyler answered 15/3, 2018 at 9:21 Comment(0)
W
1

Instead of while loops, we could use recursion (please note that none of the following code is thoroughly tested):

// Swift 2.3

public extension UIViewController {

    public var topViewController: UIViewController {
        let o = topPresentedViewController
        return o.childViewControllers.last?.topViewController ?? o
    }

    public var topPresentedViewController: UIViewController {
        return presentedViewController?.topPresentedViewController ?? self
    }
}

On the more general issue of traversing the view controller hierarchy, a possible approach is to have two dedicated sequences, so that we can:

for ancestor in vc.ancestors {
    //...
}

or:

for descendant in vc.descendants {
    //...
}

where:

public extension UIViewController {

    public var ancestors: UIViewControllerAncestors {
        return UIViewControllerAncestors(of: self)
    }

    public var descendants: UIViewControllerDescendants {
        return UIViewControllerDescendants(of: self)
    }
}

Implementing ancestor sequence:

public struct UIViewControllerAncestors: GeneratorType, SequenceType {
    private weak var vc: UIViewController?

    public mutating func next() -> UIViewController? {
        guard let vc = vc?.parentViewController ?? vc?.presentingViewController else {
            return nil
        }
        self.vc = vc
        return vc
    }

    public init(of vc: UIViewController) {
        self.vc = vc
    }
}

Implementing descendant sequence:

public struct UIViewControllerDescendants: GeneratorType, SequenceType {
    private weak var root: UIViewController?
    private var index = -1
    private var nextDescendant: (() -> UIViewController?)? // TODO: `Descendants?` when Swift allows recursive type definitions

    public mutating func next() -> UIViewController? {
        if let vc = nextDescendant?() {
            return vc
        }
        guard let root = root else {
            return nil
        }
        while index < root.childViewControllers.endIndex - 1 {
            index += 1
            let vc = root.childViewControllers[index]
            var descendants = vc.descendants
            nextDescendant = { return descendants.next() }
            return vc
        }
        guard let vc = root.presentedViewController where root === vc.presentingViewController else {
            return nil
        }
        self.root = nil
        var descendants = vc.descendants
        nextDescendant = { return descendants.next() }
        return vc
    }

    public init(of vc: UIViewController) {
        root = vc
    }
}
Wincer answered 15/11, 2016 at 11:6 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.