Mock UIViewController defined in Storyboard - ios

I have a UIViewController-class instantiated via Storyboard that contains a constant property. For testing, I want to replace/mock whatever the value of the view controller.
I can actually do this by subclassing and defining a new constant and by overriding the methods that use it. However, I do not know how to instantiate the ViewController, since it's not in the storyboard.
It's important that all views and all other functionality of the original ViewController is still present, of course. How to go about it?

If i understand you need to access a property from another ViewController outside your Storyboard without presenting it. Since you're using swift, all you need to do is instantiate the class itself i believe. For example if the ViewController that is not in the storyboard has a class named "SecondController", and the variable inside second controller is called "stringVar" then all you need to do is this:
var secondVC = SecondController()
secondVC.stringVar = "new string value"
Example:
//SecondVC
import UIKit
class SecondViewController: UIViewController {
var something:String! = "String Value";
}
//Main VC
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
var secondVC = SecondViewController()
secondVC.something = "Another String Value"
println(secondVC.something)
}
}

Related

Create a UIViewController concrete class from its type

I wish to create a concrete class from UIViewController type, something like this
func create(with type : UIViewController.Type)->UIViewController{
return type.init(coder: NSCoder())!
}
Apparently, UIViewController's designated initializer is only init(coder : NSCoder). And, when I try to pass in NSCoder() (as shown in the above case), the app crashes.
Anyone knows a better solution in creating a UIViewController concrete class from its type? Or am I pass in the wrong NSCoder in this case?
Code completion does not show this option, but this compiles and runs without a problem:
func create(with type : UIViewController.Type) -> UIViewController {
return type.init()
}
If you just want to create the view controller programmatically or from a XIB then just use the base constructor.
let viewController = MyViewController()
If you have a XIB with the same file name as the class then it will load it automatically.
If you want to load it from a storyboard then you need to define an identifier for the view controller in the storyboard and then call:
let storyboard = UIStoryboard(name: "storyboardName", bundle: nil)
storyboard.instantiateViewController(withIdentifier: "myViewControllerIdentifier")
EDIT
If you want a special create function, you can actually create an extension for UIViewController like this:
extension UIViewController {
static func create() -> Self {
return self.init()
}
}
Then you can call let myViewController = MyViewController.create(). However, unless you want to do something special in that create function it's a bit unnecessary.

Swift/iOS: IBOutlet nil after loading view controller

I'm building an app (in XCode 8.2.1) where some objects are displayed on a 2D board, and when the user taps one of these objects some info should be displayed about it as a styled modal info box. My design is to have the info written in a separate view controller, which I would display when needed.
I've designed a basic stub for the second view controller and added a single label to it in the interface builder. Then I've ctrl-linked this label to my custom VC class:
class InfoViewController: UIViewController {
#IBOutlet weak var info: UILabel!
override func viewDidLoad() {
super.viewDidLoad()
}
func displayInfo() {
info.attributedText = NSAttributedString(string: "abc")
}
}
However, when I test my app and tap the object, the info field is nil even in the viewDidLoad() method of my custom VC class. The way I'm displaying my VC is as follows:
let infoViewController = InfoViewController()
infoViewController.modalPresentationStyle = .overCurrentContext
self.present(infoViewController, animated: true, completion: nil)
infoViewController.displayInfo()
(Note: In the end I will have only one single instance of InfoViewController but this is just for testing. I don't expect having a global instance would make any difference?)
As I said, be it inside the viewDidLoad() method or in the displayInfo() method, info is always nil, such that setting its attributedString attribute crashes the app. Thinking the present method might be called asynchronously, I've tried calling displayInfo() from inside viewDidLoad(), but that didn't make any difference.
Can anyone tell my what I've forgotten that would allow my IBOutlet from being properly initialized properly?
Thanks!
David
The problem is the reference to InfoViewController(), which instantiates the view controller independent of any storyboard scene. You want to use instantiateViewController:
let infoViewController = storyboard?.instantiateViewController(withIdentifier: "Info") as! InfoViewController
infoViewController.modalPresentationStyle = .overCurrentContext
present(infoViewController, animated: true) {
infoViewController.displayInfo()
}
A couple of notes:
This assumes that (a) you've given the scene in the storyboard a "storyboard id"; (b) you've set the base class for that scene to InfoViewController.
Note, I called displayInfo in the completion handler of present because you probably don't want that called until the scene has been presented and the outlets have been hooked up.
Alternatively, you can update non-outlet properties of the InfoViewController immediately after instantiating it and then have its viewDidLoad take those properties and update the outlets, e.g.:
class InfoViewController: UIViewController {
var info: String!
#IBOutlet weak var infoLabel: UILabel!
override func viewDidLoad() {
super.viewDidLoad()
infoLabel.attributedText = NSAttributedString(string: info)
}
}
Note, I changed the #IBOutlet name to be infoLabel and added the String property called info. That tends to be the convention, that outlets bear some suffix indicating the type of control, and model objects, like the String property, are without the suffix. (You'll just want to make sure you remove that old outlet in the connections inspector in IB so that you don't have problems with these property name changes.)
Anyway, you can then do:
let infoViewController = storyboard?.instantiateViewController(withIdentifier: "Info") as! InfoViewController
infoViewController.info = "abc"
infoViewController.modalPresentationStyle = .overCurrentContext
present(infoViewController, animated: true, completion: nil)
The key point is don't try to update outlets of the scene immediately after instantiating it, but make sure that this is deferred until after viewDidLoad was called.
I Replaced
let vc = CCDetailViewController()
With
let vc = self.storyboard?.instantiateViewController(withIdentifier: "CCDetailViewController")
Finally
self.present(vc!, animated: true, completion: nil)
Now It Works...
In my case, I had created new view controller for the same class. Which had ended up with two view controllers in storyboard, but referring to the same class. After deleting old view controller, everything worked fine.
Hope it helps to someone.

Access tab controller views?

I started an app with a single view controller. I've since added a tab controller view and created 4 views that are setup as tabs, and that all seems ok.
The only viewcontroller.swift file I have is the original one. I'm not sure how to access the view controllers for each of the individual tabs.
Should I just use the one viewcontroller.swift for all my code and link the controls in each tab back to it?
To access the other view controllers, create classes of your UIViewController, like so. I'd recommend you put each class in a separate Swift file in your project, but it's not necessary.
class SecondViewController: UIViewController {
...
}
class ThirdViewController: UIViewController {
...
}
class FourthViewController: UIViewController {
...
}
Then assign them in the Identity Inspector by clicking each Storyboard UIViewController:
I make tabViewController just like this. Make my own viewcontrollers. First, init my own viewcontrollers. Then, push them into tabViewController.viewControllers
class TabbarController: UITabBarController {
override func viewDidLoad(){
super.viewDidLoad();
let first = YourFirstViewController();
let second = YourSecondViewController();
let thrid = YourThirdViewController();
self.viewControllers = [first, second, third];
}
}

Access var from rootViewController in CollectionViewCell in Swift

I would like to access a variable from my rootViewController from within a different viewController (it‘s a CollectionViewCell).
window!.rootViewController = ViewController()
I declare the var like so:
import UIKit
class ViewController: UIViewController {
var testString : String = "Test";
override func viewDidLoad() {
[…]
And try to access it this way:
import UIKit
class MainCollectionViewCell: UICollectionViewCell {
override init(frame: CGRect) {
[…]
super.init(frame: frame)
let mainView = self.window!.rootViewController
var testStringFromMainView = mainView.test
[…]
But all I keep getting is:
Type of expression is ambiguous without more context
Strange thing is, when I try for example
mainView.view.backgroundColor = UIColor.redColor()
it works.
I can’t figure out what I am doing wrong. Any help is appreciated!
You must conditionally cast the rootViewController. Your current code only knows that it is a UIViewController, but in order for it to use your variable, it needs to know that it is an instance of your subclassed view controller, ViewController.
Replacing your MainCollectionViewCell.init with this should fix the problem:
if let cvc = self.window!.rootViewController as? ViewController {
var testStringFromMainView = cvc.test
}
Please note that due to the conditional unwrapping, which is much safer than forced unwrapping, this code will not be executed if the rootViewController is not an instance of class ViewController. In other words, you need to look into global variables if your app will have multiple view controllers.
It doesn't work because rootViewController is a type of UIViewController and doesn't have a test property. Anyway, that doesn't matter as you shouldn't be trying to do what you're trying to do - it isn't appropriate for the cell to be trying to navigate up to the root view controller. Anything you need in the cell should be passed (see dependency injection) from the root view controller 'down' to through the view controllers to the cell. In this way your code is logical and the dependencies are obvious. What you're trying to do is hide a dependency in the bottom of your view hierarchy. You can make it work (with a cast), but you shouldn't.

Opening a view from a custom class Swift IOS

I have this problem:
Im having a custom class named CoredataAction
In this class I do all my CoreData actions and it is not an UIViewController.
How can I open a view from my CoreDataAction class?
I have tryied opening a storyboard but did didn't work! It got me an bad_access error
One solution would be to add a variable in your CoredataAction class that holds the initial ViewController, just make sure you set that variable when you initialise your CoredataAction class.
CoredataAction
class CoredataAction {
var parentViewController:UIViewController!
func presentNewViewController() {
let newViewController = parentViewController.storyboard?.instantiateViewControllerWithIdentifier("YOUR STORYBOARD ID") as UIViewController
parentViewController.presentViewController(newViewController, animated: true, completion: nil)
}
}
ViewController
func initCustomClass() {
var coreData = CoredataAction()
coreData.parentViewController = self
}
Another option is to use a protocol to delegate the view presentation back to the ViewController class itself. It would be a very similar setup to the above, it just means the view presentation logic can be kept out of your CoredataAction customClass - Let me know if you would like an example of this.

Resources