Find a uiviewcontroller from uinavigationcontroller - ios

I am trying to figure out a generic way to find a uiviewcontroller which is auto type casted. Currently, I do have this.
extension UINavigationController {
func contoller(ofType type:AnyClass) -> UIViewController? {
for controller in self.viewControllers {
if controller.isKind(of: type) {
return controller
}
}
return nil
}
}
Calling will be like:
if let controller = self.navigationController?.contoller(ofType: MyController.self) as? MyController{}
That's how I am able to get controller object and I need to type cast it also.
I am trying to figure out a way to do this like as:
if let controller:MyController = self.navigationController?.contoller(ofType: MyController.self){}
So that I will not need to do any type casting.
For this, I may need to do some changes in UINavigationController extension function.
Need some suggestion for this.

Use Generics:
extension UINavigationController {
func controller<T: UIViewController>(ofType _: T.Type) -> UIViewController? {
for controller in viewControllers where controller is T {
return controller
}
return nil
}
}
EDIT 1: I would return also the VC as type we requested.
And recommend to use first in the name, cause there might be more than one. And you might want to introduce lastController(as:) in the future
extension UINavigationController {
func firstController<T: UIViewController>(as _: T.Type) -> T? {
for case let controller as T in viewControllers {
return controller
}
return nil
}
}
Then usage could be:
let nav = UINavigationController()
nav.viewControllers = [UIViewController(), UITabBarController()]
let tabVC = nav.firstController(as: UITabBarController.self)
EDIT 2: You can shorten the extension body:
extension UINavigationController {
func firstController<T: UIViewController>(as _: T.Type) -> T? {
viewControllers.first(where: { $0 is T }) as? T
}
}

Related

how to unit test UIApplication extension

Suppose I use this code that extracts the top most viewController
import UIKit
extension UIApplication {
class func topViewController(_ base: UIViewController? = UIApplication.shared.keyWindow?.rootViewController) -> UIViewController? {
if let navigationController = base as? UINavigationController, navigationController.viewControllers.count > 0 {
return topViewController(navigationController.visibleViewController)
}
if let tabBarController = base as? UITabBarController {
if let selected = tabBarController.selectedViewController {
return topViewController(selected)
}
}
if let presentedViewController = base?.presentedViewController {
return topViewController(presentedViewController)
}
return base
}
}
How do I facilitate unit testing of this code? I would need to use an instance of UIApplication.shared. Any tips would be appreciated.
If instead this was an extension to UIViewController, you could omit the parameter (base) altogether.
The call would then change to
let top = UIApplication.shared.keyWindow?.rootViewController.topViewController()
In order to unit test this, we can simply create a ViewController and perform our tests.

What is the Use of Returning the Same function itself in Swift

Well I see some syntax in the following function which returns the topMostViewController. This function is defined in AppDelegate
func topViewController(controller: UIViewController? = UIApplication.shared.keyWindow?.rootViewController) -> UIViewController? {
if let navigationController = controller as? UINavigationController {
//***A topViewController which is Returning itself
//***This is where I got Confusion
return topViewController(controller: navigationController.visibleViewController)
} else if let tabController = controller as? UITabBarController {
if let selected = tabController.selectedViewController {
return topViewController(controller: selected)
}
} else if let presented = controller?.presentedViewController {
return topViewController(controller: presented)
}
return controller
}
And it's used as
if (self.topViewController() as? SomeViewController) != nil {
if orientation.isPortrait {
return .portrait
} else {
return .landscape
}
}
I understood that the code is trying to set orientation based on the currently visible View Controller but I don't understand what is the necessity of returning the same function itself in topViewController. Also I see some syntax like
extension UIApplication {
/// The top most view controller
static var topMostViewController: UIViewController? {
return UIApplication.shared.keyWindow?.rootViewController?.visibleViewController
}
}
extension UIViewController {
/// The visible view controller from a given view controller
var visibleViewController: UIViewController? {
if let navigationController = self as? UINavigationController {
// *** Here it's returning Same variable i.e visibleViewController
// *** a function could call itself recursively. But how can a Variable calls itself recursively?
return navigationController.topViewController?.visibleViewController
} else if let tabBarController = self as? UITabBarController {
return tabBarController.selectedViewController?.visibleViewController
} else if let presentedViewController = presentedViewController {
return presentedViewController.visibleViewController
} else {
return self
}
}
}
Edited
That is called recursion. There is a condition in the recursion that cause t end the cycle :
Not in the navigationController, because it has another visible controller
Not in the tabBarController, because it has another visible controller
Not presenting another controller, because the presented one is visible
if one of these appears -> we go down one level and call this function again until none of these true.
It is a recursive function. The topViewController function calls itself to find the top most controller which is visible. The function will exit when controller?.presentedViewController returns nil (Which means that the value held by controller is the top most visible controller). You can also achieve the same without a recursive function as mentioned here: How to find topmost view controller on iOS, but it looks much more cleaner than the looping implementation.

How to compare UIViewController in Swift 3?

I am trying to compare to UIViewController in Swift 3 but there is some error
extension UINavigationController
{
func myPopToViewController(viewController:UIViewController, animated:Bool) -> UIViewController? {
var arrViewControllers:[UIViewController] = []
arrViewControllers = self.viewControllers
for vc:UIViewController in arrViewControllers {
if(vc.isKind(of: viewController) ) // This Line gives me error
{
return (self.navigationController?.popToViewController(vc, animated: animated)?.last)!
}
}
return nil
}
}
/Users/varunnaharia/Documents/Projects/appname/appname/Public/UINavigationController+Extra.swift:18:30: Cannot convert value of type 'UIViewController' to expected argument type 'AnyClass' (aka 'AnyObject.Type')
and if try to use
if(vc is viewController)
It gives
/Users/varunnaharia/Documents/Projects/appname/appname/Public/UINavigationController+Extra.swift:18:22: Use of undeclared type 'viewController'
I am calling it through this
self.navigationController?.popOrPopToViewController(viewController: MyUIViewController(), animated: false)
for viewsController in arrViewControllers
{
if(viewsController.isKind(of: YourControllerClassName.self)){
}
}
Swift 4
Hope it will work for you
extension UINavigationController {
func myPopToViewController(viewController:UIViewController, animated:Bool) {
var arrViewControllers:[UIViewController] = []
arrViewControllers = self.viewControllers
for vc:UIViewController in arrViewControllers {
if(vc.isKind(of: viewController.classForCoder)){
(self.popToViewController(vc, animated: animated))
}
}
}
}
In swift, we use is instead of isKind(of:).
is is used to check the type of the object.
So you can use,
if(vc is UIViewController)
But I think here you are trying to match the 2 references of UIViewController.
So, you need to use === instead of is. This operator is used to match 2 references of same type.
if(vc === viewController)
If you want to compare to a particular view controller you have to compare their refererences.
Try this...
if(vc === viewController) )
{
return (self.navigationController?.popToViewController(vc, animated: animated)?.last)!
}
i just modify the answer of Mr. #BangOperator for move to particular View controller.
extension UINavigationController {
func popTo(controllerToPop:UIViewController) {
//1. get all View Controllers from Navigation Controller
let controllersArray = self.viewControllers
//2. check whether that view controller is exist in the Navigation Controller
let objContain: Bool = controllersArray.contains(where: { $0 == controllerToPop })
//3. if true then move to that particular controller
if objContain {
self.popToViewController(controllerToPop, animated: true)
}
}
}

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

UIViewController extension to instantiate from storyboard

I'm trying to write a little extension in Swift to handle instantiation of a UIViewController from a storyboard.
My idea is the following: Since UIStoryboard's method instantiateViewControllerWithIdentifier needs an identifier to instantiate a given storyboard's view controller, why don't assign every view controller in my storyboard an identifier equal to its exact class name (i.e a UserDetailViewController would have an identifier of "UserDetailViewController"), and, create a class method on UIViewController that would:
accept a UIStoryboard instance as a unique parameter
get the current class name as a string
call instantiateViewControllerWithIdentifier on the storyboard instance with the class name as a parameter
get the newly created UIViewController instance, and return it
So, instead of (which repeats the class name as a string, not very nice)
let vc = self.storyboard?.instantiateViewControllerWithIdentifier("UserDetailViewController") as UserDetailViewController
it would be:
let vc = UserDetailViewController.instantiateFromStoryboard(self.storyboard!)
I used to do it in Objective-C with the following category:
+ (instancetype)instantiateFromStoryboard:(UIStoryboard *)storyboard
{
return [storyboard instantiateViewControllerWithIdentifier:NSStringFromClass([self class])];
}
But I'm completely stuck with the Swift version. I hope is that there is some kind of way to do it.
I tried the following:
extension UIViewController {
class func instantiateFromStoryboard(storyboard: UIStoryboard) -> Self {
return storyboard.instantiateViewControllerWithIdentifier(NSStringFromClass(Self))
}
}
Returning Self instead of AnyObject allows the type inference to work. Otherwise, I would have to cast every single return of this method, which is annoying, but maybe you have a better solution?
This gives me the error: Use of unresolved identifier 'Self'
The NSStringFromClass part seems to be the problem.
What do you think?
Is there any way to return Self from class functions?
How would you get this working without the need to cast the return value every time? (i.e keeping -> Self as return value)
How about writing an extension to UIStoryboard instead of UIViewController?
extension UIStoryboard {
func instantiateVC<T: UIViewController>() -> T? {
// get a class name and demangle for classes in Swift
if let name = NSStringFromClass(T.self)?.componentsSeparatedByString(".").last {
return instantiateViewControllerWithIdentifier(name) as? T
}
return nil
}
}
Even adopting this approach, cost of an use side is low as well.
let vc: UserDetailViewController? = aStoryboard.instantiateVC()
Thanks to MartinR and his answer, I know the answer:
UPDATE: rewritten with a protocol.
Instantiable
protocol StringConvertible {
var rawValue: String {get}
}
protocol Instantiable: class {
static var storyboardName: StringConvertible {get}
}
extension Instantiable {
static func instantiateFromStoryboard() -> Self {
return instantiateFromStoryboardHelper()
}
private static func instantiateFromStoryboardHelper<T>() -> T {
let identifier = String(describing: self)
let storyboard = UIStoryboard(name: storyboardName.rawValue, bundle: nil)
return storyboard.instantiateViewController(withIdentifier: identifier) as! T
}
}
//MARK: -
extension String: StringConvertible { // allow string as storyboard name
var rawValue: String {
return self
}
}
StoryboardName
enum StoryboardName: String, StringConvertible {
case main = "Main"
//...
}
Usage:
class MyViewController: UIViewController, Instantiable {
static var storyboardName: StringConvertible {
return StoryboardName.main //Or you can use string value "Main"
}
}
let viewController = MyController.instantiateFromStoryboard()
you can create UIViewController Instance like this:
Create enum with all your storyboard name.
enum AppStoryboard: String {
case main = "Main"
case profile = "Profile"
}
Then, here is the extension for instantiate UIViewController
extension UIViewController {
class func instantiate<T: UIViewController>(appStoryboard: AppStoryboard) -> T {
let storyboard = UIStoryboard(name: appStoryboard.rawValue, bundle: nil)
let identifier = String(describing: self)
return storyboard.instantiateViewController(withIdentifier: identifier) as! T
}
}
Usage:
let profileVC: ProfileVC = ProfileVC.instantiate(appStoryboard: .profile)
self.navigationController?.pushViewController(profileVC,animated:true)
We are porting our objective c project to swift. We have split the project into modules. Modules have their own storyboards. We have extended your(even our's as well) problem's solution to one more level by avoiding explicit storyboard names.
// Add you modules here. Make sure rawValues refer to a stroyboard file name.
enum StoryModule : String {
case SomeModule
case AnotherModule = "AnotherModulesStoryBoardName"
// and so on...
}
extension UIStoryboard {
class func instantiateController<T>(forModule module : StoryModule) -> T {
let storyboard = UIStoryboard.init(name: module.rawValue, bundle: nil);
let name = String(T).componentsSeparatedByString(".").last
return storyboard.instantiateViewControllerWithIdentifier(name!) as! T
}
}
// Some controller whose UI is in a stroyboard named "SomeModule.storyboard",
// and whose storyboardID is the class name itself, ie "MyViewController"
class MyViewController : UIViewController {
// Controller Code
}
// Usage
class AClass
{
// Here we must alwasy provide explicit type
let viewController : MyViewController = UIStoryboard.instantiateController(forModule: StoryModule.SomeModule)
}
Two things:
Class constructors in Objective-C are convenience initializers in Swift. Use convenience init rather than class func.
NSStringFromClass(Self) with NSStringFromClass(self.type).
Here is a modern Swift example, based on #findall's solution:
extension UIStoryboard {
func instantiate<T>() -> T {
return instantiateViewController(withIdentifier: String(describing: T.self)) as! T
}
static let main = UIStoryboard(name: "Main", bundle: nil)
}
Usage:
let userDetailViewController = UIStoryboard.main.instantiate() as UserDetailViewController
I think it is ok to fail when trying to instantiate a view controller from a storyboard as this kind of problem should be detected soon.
Or, you can do so
func instantiateViewControllerWithIdentifier<T>(_ identifier: T.Type) -> T {
let identifier = String(describing: identifier)
return instantiateViewController(withIdentifier: identifier) as! T
}
Use protocol in UIViewController to reach your thoughts
let vc = YourViewController.instantiate(from: .StoryboardName)
You can see the use of my link :D
https://github.com/JavanC/StoryboardDesignable
You can add this extension :-
extension UIStoryboard{
func instantiateViewController<T:UIViewController>(type: T.Type) -> T? {
var fullName: String = NSStringFromClass(T.self)
if let range = fullName.range(of:".", options:.backwards, range:nil, locale: nil){
fullName = fullName.substring(from: range.upperBound)
}
return self.instantiateViewController(withIdentifier:fullName) as? T
}
}
And can instantiate view controller like this :-
self.storyboard?.instantiateViewController(type: VC.self)!
In complement for the version of #ChikabuZ, here mine that takes into account which bundle the storyboard is in (for example, if your storyboads are in another bundle than your app). I added also a small func if you want to use xib instead of storyboad.
extension UIViewController {
static func instantiate<TController: UIViewController>(_ storyboardName: String) -> TController {
return instantiateFromStoryboardHelper(storyboardName)
}
static func instantiate<TController: UIViewController>(_ storyboardName: String, identifier: String) -> TController {
return instantiateFromStoryboardHelper(storyboardName, identifier: identifier)
}
fileprivate static func instantiateFromStoryboardHelper<T: UIViewController>(_ name: String, identifier: String? = nil) -> T {
let storyboard = UIStoryboard(name: name, bundle: Bundle(for: self))
return storyboard.instantiateViewController(withIdentifier: identifier ?? String(describing: self)) as! T
}
static func instantiate<TController: UIViewController>(xibName: String? = nil) -> TController {
return TController(nibName: xibName ?? String(describing: self), bundle: Bundle(for: self))
}
}
I had a similar thought and settled on using the extension below. It still uses the normal instantiation process, but removes reliance on stringly typed Storyboard and View Controller names:
let myVC = UIStoryboard(.main).instantiate(MyViewController.self)
The return type above is pre-cast to MyViewController, not the standard UIViewController.
extension UIStoryboard {
enum Name: String {
case main = "Main"
case launch = "LaunchScreen"
case other = "Other"
}
convenience init(_ name: Name, bundle: Bundle? = nil) {
self.init(name: name.rawValue, bundle: bundle)
}
func instantiate<T: UIViewController>(_ type: T.Type) -> T {
instantiateViewController(withIdentifier: String(describing: type)) as! T
}
}
Note that you must ensure that each VC's Storyboard Identifier exactly matches its class name! Failure to do so will result in the exception:
Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: 'Storyboard (<UIStoryboard: 0x6000035c04e0>) doesn't contain a view controller with identifier 'MyViewController''

Resources