Mark A. Donohoe
Mark A. Donohoe

Reputation: 30408

Can you both find a first instance of a type in a collection, returning that instance as that concrete type?

This code bothers me. Below, I'm trying to find the first instance of a specific type of ViewController in a NavigationController's stack. Simple. But when I've found it, I have to then cast it to the type I just looked for, which seems redundant to me.

func popToFirstViewController<T:UIViewController>(ofType type:T.Type, animated:Bool) -> T? {

    guard let foundViewController = viewControllers.first(where: { $0 is T }) as? T else {
        return nil
    }

    self.popToViewController(foundViewController, animated:animated)

    return foundViewController
}

Only thing I can think of is this...

func popToFirstViewController<T:UIViewController>(ofType type:T.Type, animated:Bool) -> T? {

    guard let foundViewController = viewControllers.flatMap({ $0 as? T }).first() else {
        return nil
    }

    self.popToViewController(foundViewController, animated:animated)

    return foundViewController
}

...but I've repeatedly found using flatMap like this tends to confuse people reading the code, and, as correctly pointed out in the comments below, iterates over the entire collection whereas first doesn't do that.

So is there another way to solve this issue?

Upvotes: 1

Views: 64

Answers (2)

vacawama
vacawama

Reputation: 154613

You can use case patterns to select the viewControllers of the type you are interested in and pop and return the first one you find:

extension UINavigationController {
    func popToFirstViewController<T:UIViewController>(ofType type:T.Type, animated:Bool) -> T? {

        for case let vc as T in viewControllers {
            self.popToViewController(vc, animated: animated)
            return vc
        }

        return nil
    }
}

Example:

Use a button in OrangeViewController to return to GreenViewController earlier in the stack:

@IBAction func popToGreen(_ sender: UIButton) {
     let greenVC = self.navigationController?.popToFirstViewController(
           ofType: GreenViewController.self,
         animated: true
     )

     // Modify a property in GreenViewController that
     // will be moved into a label in viewWillAppear      
     greenVC?.labelText = "Returned here from Orange"
}

popToLastViewController(ofType:animated:)

You might also want a function to pop to the most recent viewController of a type. That is easily achieved with a simple modification (adding .reversed()):

func popToLastViewController<T:UIViewController>(ofType type:T.Type, animated: Bool) -> T? {

    for case let vc as T in viewControllers.reversed() {
        self.popToViewController(vc, animated: animated)
        return vc
    }

    return nil
}

Upvotes: 1

Tim
Tim

Reputation: 60130

I'm in favor of combining flatMap and lazy to get the behavior of conditionally casting to T, stripping out mismatches, and not enumerating the whole array:

func popToFirstViewController<T:UIViewController>(ofType type:T.Type, animated:Bool) -> T? {
    guard let foundViewController = viewControllers.lazy.flatMap({ $0 as? T }).first {
        return nil
    }

    self.popToViewController(foundViewController, animated:animated)
    return foundViewController
}

As for "confusing people that read the code:" flatMap is fairly idiomatic Swift, and will be less ambiguous with the upcoming rename to compactMap. If readers in your environment really have trouble, you could always write a small helper (generic or not) that performs the same work under a clearer name.

Upvotes: 1

Related Questions