Instantiating UIViewController in a test - ios

I'm working in Swift 2, and would like to test functions within my view controller. I've made a dependency injection-like service which looks like this:
extension UIViewController: {
func getDbService() -> IDbService {
let context = (UIApplication.sharedApplication().delegate as! AppDelegate).managedObjectContext
return DbService(context: context)
}
}
With this, I can set AppDelegate's context as a mocked one for test purposes. However, the problem arises when I try to instantiate a view controller. Here's the code:
class LoginViewController: UIViewController {
var token: String?
override func viewDidLoad() {
super.viewDidLoad()
let dbService = getDbService()
self.token = dbService.getToken()
//....do stuff with token
}
}
I instantiate the test like so:
class LoginViewControllerTests: XCTestCase {
func testTokenExists() {
let mockContext = MockContextUtils.getMockContext()
let appDelegate = UIApplication.sharedApplication().delegate as! AppDelegate
appDelegate.managedObjectContext = mockContext
let sut = LoginViewController()
let _ = sut.view // Apparently this renders the view; I set a breakpoint, viewDidLoad is called
XCTAssertNotNil(sut.token) // FAILS, BECAUSE APPDELEGATE CALLS APP DATABASE, AND NOT MOCK.
}
}
The reason this simple test fails is because LoginViewController has no idea what app delegate is. Is there a way to introduce that in the initialization phase?

So it appears that initializing the view controller with its default controller does not grab the app delegate which we set up in the test. Instead, I've initialized the view controller through the storyboard via so:
let storyboard = UIStoryboard(name: "Main", bundle: NSBundle.mainBundle())
let sut = storyboard.instantiateInitialViewController() as? LoginViewController
XCTAssertNotNil(sut)
XCTAssertNotNil(sut.view)
XCTAssertNotNil(sut.token)
which solves the problem.

Related

Framework Delegates in swift

Hi I created a simple framework with a delegate to return back a value to my main app.
Steps I did:
Created a framework in Xcode
Created a protocol as public
Imported my framework in main app project and integrated my protocol successfully
But not able to grab the value from framework
In framework
public protocol MyDataSendingDelegateProtocol: NSObject {
func sendDataToFirstViewController(myData: String)
}
public var delegate: MyDataSendingDelegateProtocol? = nil
Sending a value form framework as
delegate?.sendDataToFirstViewController(myData: "hello world")
In the main app
class ViewController: UIViewController, MyDataSendingDelegateProtocol{
func sendDataToFirstViewController(myData: String) {
print("from frame work \(myData)")
}
}
Accessing my framework VC
public class OOB_View : UIViewController {
public func registraionView() -> UIViewController {
let storyboard = UIStoryboard.init(name: "main", bundle: Bundle(for: oobRegVc.self))
let homeVC = storyboard.instantiateViewController(withIdentifier: "view") as! oobRegVc
return homeVC
}
}
In your framework modify the registraionView method to accept the delegate parameter of type MyDataSendingDelegateProtocol and then set it as homeVC's delegate, i.e.
public class OOB_View : UIViewController {
public func registraionView(delegate: MyDataSendingDelegateProtocol) -> UIViewController {
let storyboard = UIStoryboard.init(name: "main", bundle: Bundle(for: oobRegVc.self))
let homeVC = storyboard.instantiateViewController(withIdentifier: "view") as! oobRegVc
homeVC.delegate = delegate
return homeVC
}
}
Now, in your main app, call the method registraionView(delegate:) like so,
let oobClass = OOB_View()
let x = oobClass.registraionView(delegate: self)
x.modalPresentationStyle = .fullScreen
self.present(x, animated: true, completion: nil)
The above code must be somewhere in the ViewController conforming to MyDataSendingDelegateProtocol.

How to reopen application after login

I'm an android developer. in android, when user login in application, I will re-open the MainActivity class ( controller ) to refresh some views.
in iOS applications, how to do this scenario ?
You can reopen you default/LandingViewController.
Suppose you have a View Controller with name LandingViewController
When you successfully logged in all you need is to re instantiate the LandingViewController
In AppDelegate class make a function with name
func userDidLoggedIn(){
let storyboard = UIStoryboard(name: "Main", bundle: nil)//Replace Main With your own storyboard name containing LandingViewController
let landingViewController = storyboard.instantiateViewController(withIdentifier: "LandingViewControllerIdentifier")//Pass your LandingViewController Identier that you have set in your storyboard file.
guard let subviews = self.window?.subviews else {
return
}
for view in subviews {
view.removeFromSuperview()
}
self.window?.rootViewController = landingViewController
}
Now Simply Call this Function where ever in the entire project like this In your case write below lines in the completion block of login request API.
let delegate = UIApplication.shared.delegate as! AppDelegate
delegate. userDidLoggedIn()
Once user login, you can change your rootviewcontroller like this:
var nav_VC: UIViewController?
func onSuccessfulLogin()
{
let storyboard = UIStoryboard.init(name: "Main", bundle: nil)
nav_VC = nil
if nav_VC == nil {
nav_VC = storyboard.instantiateViewController(withIdentifier: "home_nav")
}
self.window?.rootViewController = nav_VC
self.window?.makeKeyAndVisible()
}

Swift: Change ViewController after Authentication from RESTful API

I have a rails api set up and one test user. I can login/logout view the api.
I have a tab app set up, and I have a LoginViewController set up to authenticate into that. My view controller (basically) looks like the below code. This doesnt work. However, the second block of code does work
I am guessing I am calling something wrong.. The first block executes and then crashes with a *** Assertion failure in -[UIKeyboardTaskQueuewaitUntilAllTasksAreFinished].. I've been spinning my wheels for hours.. tried looking into segues.. etc. Anything would help!!!
//DOES NOT WORK //
class LoginViewController: UIViewController {
#IBOutlet weak var email: UITextField!
#IBOutlet weak var password: UITextField!
#IBAction func Login(sender: AnyObject) {
func switchToTabs() {
let storyboard = UIStoryboard(name: "Main", bundle: nil)
let tabBarController = storyboard.instantiateViewControllerWithIdentifier("TabBarController") as! UITabBarController
let appDelegate = UIApplication.sharedApplication().delegate as! AppDelegate
appDelegate.window?.rootViewController = tabBarController
}
let authCall = PostData()
var authToken = ""
authCall.email = email.text!
authCall.password = password.text!
authCall.forData { jsonString in
authToken = jsonString
if(authToken != "") {
switchToTabs()
}
}
}
// SAME CLASS THAT DOES WORK//
class LoginViewController: UIViewController {
#IBOutlet weak var email: UITextField!
#IBOutlet weak var password: UITextField!
#IBAction func Login(sender: AnyObject) {
let storyboard = UIStoryboard(name: "Main", bundle: nil)
let tabBarController = storyboard.instantiateViewControllerWithIdentifier("TabBarController") as! UITabBarController
let appDelegate = UIApplication.sharedApplication().delegate as! AppDelegate
appDelegate.window?.rootViewController = tabBarController
}
My question is, why can I seemingly navigate to a new view when I don't use my api, but when I do receive my auth token.. I can't log in.
Please excuse the ugly code.. this is my first day with swift
Everytime you every do anything that changed something about the UI inside of a closure or anything that could even possibly be off the main queue, enclose it in this statement.
dispatch_async(dispatch_get_main_queue()) {
//the code that handles UI
}
This includes all code that segues inside a closure like the one you have
authCall.forData { jsonString in
authToken = jsonString
if(authToken != "") {
switchToTabs()
}
}
If you write it like so it would work
authCall.forData {
jsonString in
authToken = jsonString
if(authToken != "") {
dispatch_async(dispatch_get_main_queue()) {
switchToTabs()
}
}
}
then call your segue in switchToTabs()
If you arn't sure what queue you are on, it won't hurt to do it. As you become more familiar with scenarios that take you off the main queue, you will realize when to use it. Anytime you are in a closure and dealing with UI its a safe bet that you could leave. Obviously anything asynchronous would cause it as well. Just place your performSegueWithIdentifier inside of it and it should work.
If you make a segue on the storyboard from the view controller attatched to LoginViewController, then you click the segue you can give it an identifier. Then you just call performSegueWithIdentifier("identifierName", sender: nil) and none of the other code in switchToTabs() is necessary.
If you want to continue down the route you are taking to summon the View Controller, then I think what you are missing is these lines.
Use this below your class outside of it.
private extension UIStoryboard {
class func mainStoryboard() -> UIStoryboard { return UIStoryboard(name: "Main", bundle: NSBundle.mainBundle()) }
class func tabBarController() -> UITabBarController? {
return mainStoryboard().instantiateViewControllerWithIdentifier("TabBarController") as? UITabBarController
}
}
Then change your switchToTabs() function out for this
func switchToTabs() {
let tabBarController = UIStoryboard.tabBarController()
view.addSubview(tabBarController!.view)
//line below can be altered for different frames
//tabBarController!.frame = self.frame
addChildViewController(tabBarController!)
tabBarController!.didMoveToParentViewController(self)
//line below is another thing worth looking into but doesn't really correlate with this
//self.navigationController!.pushViewController(tabBarController!, animated: true)
}
I added in a couple comments for you to look into if you like. I believe this will do the job, but keep in mind that the direction you are headed doesn't remove the previous screen from memory. It is technically still sitting behind your new screen. I wouldn't ever recommend this for what you are trying to do, but it might not hurt if you are building something simplistic.

How can I call a view controller function from an iOS AppDelegate?

I am getting a call from a remote push notification, which includes a path to an image, which I want to display.
The AppDelegate is:
func application(application: UIApplication, didReceiveRemoteNotification userInfo: [NSObject : AnyObject]) {
// get url from notification payload (shortened version)
let url = alert["imgurl"] as? NSString
let vc = ViewController()
vc.loadImage(url as String) // cannot find the imageView inside
}
...and the view looks like this:
func loadImage(url:String) {
downloadImage(url, imgView: self.poolImage)
}
// http://stackoverflow.com/a/28942299/1011227
func getDataFromUrl(url:String, completion: ((data: NSData?) -> Void)) {
NSURLSession.sharedSession().dataTaskWithURL(NSURL(string: url)!) { (data, response, error) in
completion(data: NSData(data: data!))
}.resume()
}
func downloadImage(url:String, imgView:UIImageView){
getDataFromUrl(url) { data in
dispatch_async(dispatch_get_main_queue()) {
imgView.image = UIImage(data: data!)
}
}
}
I'm getting an exception saying imgView is null (it is declared as #IBOutlet weak var poolImage: UIImageView! and I can display an image by calling loadImage("http://lorempixel.com/400/200/") from a button click.
I found a couple of related answers on stackoverflow (one is referenced above) but none work.
If you want to show a new view controller modally, then look into rach's answer further.
If you're ViewController is already on screen, you will need to update it to show the information you need.
How you do this will depend on your root view controller setup. Is your view controller embedded in an UINavigationController? If so then try this:
func application(application: UIApplication, didReceiveRemoteNotification userInfo: [NSObject : AnyObject]) {
if let rootViewController = window?.rootViewController as? UINavigationController {
if let viewController = rootViewController.viewControllers.first as? ViewController {
let url = alert["imgurl"] as? NSString
viewController.loadImage(url as String)
}
}
}
If it is not, then try this:
func application(application: UIApplication, didReceiveRemoteNotification userInfo: [NSObject : AnyObject]) {
if let rootViewController = window?.rootViewController as? ViewController {
let url = alert["imgurl"] as? NSString
rootViewController.loadImage(url as String)
}
}
I believe at the point of time where you are calling
let vc = ViewController()
it has yet to be instantiated. Since you are using #IBOutlet for your UIImageView, I am assuming that you actually have that UIViewController.
Maybe it could be corrected by this:
let vc = self.storyboard?.self.storyboard?.instantiateViewControllerWithIdentifier("ViewControllerStoryboardID")
vc.loadImage("imageUrl")
self.presentViewController(vc!, animated: true, completion: nil)
Let me know if it works. :)
EDIT: You could call an instance of storyboard via this method:
self.window = UIWindow(frame: UIScreen.mainScreen().bounds)
var storyboard = UIStoryboard(name: "Main", bundle: nil)
var vc = storyboard.instantiateViewControllerWithIdentifier("ViewController") as! UIViewController
vc.loadImage("imageUrl")
self.window?.rootViewController = vc
self.window?.makeKeyAndVisible()
By calling #IBOutlet weak var poolImage: UIImageView! you're passing the initialization of poolImage to the storyboard. Since you're initializing the view controller with let vc = ViewController(), vc will know that it is expected to have a reference to poolImage because you declared it in the class, but since you aren't actually initializing vc's view (and subviews) poolImage remains nil until the UI is loaded.
If you need to reference a UIImageView at the time of the initialization of vc, you'll have to manually instantiate it: let poolImage = UIImageView()
EDIT: Your ViewController implementation of poolImage currently looks like this:
class ViewController : UIViewController {
#IBOutlet weak var poolImage : UIImageView!
// rest of file
}
To access poolImage from AppDelegate via let vc = ViewController() you have to make the implementation look like this:
class ViewController : UIViewController {
/* ---EDIT--- add a placeholder where you would like
poolImage to be displayed in the storyboard */
#IBOutlet weak var poolImagePlaceholder : UIView!
var poolImage = UIImageView()
// rest of file
override func viewDidLoad() {
super.viewDidLoad()
/* ---EDIT--- make poolImage's frame identical to the
placeholder view's frame */
poolImage.frame = poolImagePlaceholder.frame
poolImagePlaceholder.addSubView(poolImage)
// then add auto layout constraints on poolImage here if necessary
}
}
That way it poolImage is available for reference when ViewController is created.
The reason I think is because the imageView may not have been initialized. Try the following solution.
Add another init method to your viewController which accepts a URLString. Once you receive a push notification, initialize your viewController and pass the imageURL to the new init method.
Inside the new initMethod, you can use the imageURL to download the image and set it to the imageView. Now your imageView should not be null and the error should not occur.

Test for a UIViewController fails after refactor in Swift

I'm having different results in trying to test a ViewController in Swift.
This first code pass the test.
#testable import VideoAudioExtractor
import XCTest
class SecondViewControllerTest: XCTestCase {
let storyBoardName = "Main"
let viewControllerIdentifier = "SecondViewController"
override func setUp() {
super.setUp()
// Put setup code here. This method is called before the invocation of each test method in the class.
}
func testSelectAudioButtonIsConnected () {
let sut = UIStoryboard(name: storyBoardName, bundle: nil).instantiateViewControllerWithIdentifier("SecondViewController") as! SecondViewController
let dummy = sut.view
if let unpwarppedOptional = sut.selectAudioButton {
XCTAssertEqual(unpwarppedOptional,sut.selectAudioButton, "correct value")
}
else {
XCTFail("Value isn't set")
}
}
override func tearDown() {
// Put teardown code here. This method is called after the invocation of each test method in the class.
super.tearDown()
}
}
If I refactor the test and I move the creation of the view controller to an instance variable the test fails in Line
#testable import VideoAudioExtractor
import XCTest
class SecondViewControllerTest: XCTestCase {
let storyBoardName = "Main"
let viewControllerIdentifier = "SecondViewController"
var sut : SecondViewController {
return UIStoryboard(name: storyBoardName, bundle: nil).instantiateViewControllerWithIdentifier("SecondViewController") as! SecondViewController
}
override func setUp() {
super.setUp()
// Put setup code here. This method is called before the invocation of each test method in the class.
}
func testSelectAudioButtonIsConnected () {
let dummy = sut.view
if let unpwarppedOptional = sut.selectAudioButton {
XCTAssertEqual(unpwarppedOptional,sut.selectAudioButton, "correct value")
}
else {
XCTFail("Value isn't set")
}
}
override func tearDown() {
// Put teardown code here. This method is called after the invocation of each test method in the class.
super.tearDown()
}
}
You need to declare it 'lazy' like this:
lazy var sut : SecondViewController ...
That way it only gets created the first time it's accessed.
What's happening in your code is that every time you access a property of sut, you are creating a new instance of SecondViewController.
You create one instance with sut.view and a completely different one when you access sut.selectAudioButton. The second instance has not had its view loaded because you did not call .view on it!

Resources