how to unit test UIApplication extension - ios

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.

Related

Find a uiviewcontroller from uinavigationcontroller

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

What is the proper way to get root view controller in iOS 13?

class TopViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
//Code Block 1
let controller = getTopController()
print(controller)// Prints out MyTestProject.TopViewController
//Code Block 2
let controller2 = getRootController()
print(controller2)//Prints out nil , because keywindow is also nil upto this point.
//Code Block 3
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 0.5) {
let controller2 = self.getRootController()
print(controller2)// Prints out MyTestProject.TopViewController
}
}
func getTopController() -> UIViewController? {
guard let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene,
let sceneDelegate = windowScene.delegate as? SceneDelegate else {
return nil
}
return sceneDelegate.window?.rootViewController
}
func getRootController() -> UIViewController? {
let keyWindow = UIApplication.shared.windows.filter {$0.isKeyWindow}.first
let topController = keyWindow?.rootViewController
return topController
}
}
Since iOS 13 there is two approach to get current active / top view controller of the app.
here:
getTopController() and getRootController() shows both of the approaches.
As commented in codes besides print() results are different though.
In Code Block 2:
getRootController can't find the window yet so it prints out nil. Why is this happening?
Also, which is the full proof method of getting reference to top controller in iOS 13, I am confused now?
The problem is that when your view controller viewDidLoad window.makeKey() has not been called yet.
A possible workaround is to get the first window in the windows array if a key window is not available.
func getRootController() -> UIViewController? {
let keyWindow = UIApplication.shared.windows.first(where: { $0.isKeyWindow }) ?? UIApplication.shared.windows.first
let topController = keyWindow?.rootViewController
return topController
}
Please note that this will solve your problem but you should postpone any operation that involve using a key window until it is such.
According to the documentation of UIView, the window property is nil if the view has not yet been added to a window which is the case when viewDidLoad is called.
Try to access that in viewDidAppear
override func viewDidAppear(_ animated: Bool) {
let controller2 = self.view.window.rootViewController
}

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