XCTest: The internal class is not casted properly - ios

XCTestCase subclass implementation is below:
import XCTest
#testable import AppName
class SomeTestClass: XCTestCase {
var viewController: AppName.ViewControllerName?
override func setUp() {
super.setUp()
viewController = UIStoryboard(name: "Main", bundle: nil).instantiateViewControllerWithIdentifier("someIdentifier") as? AppName.ViewControllerName // Line 7
_ = viewController?.view
}
override func tearDown() {
// Put teardown code here. This method is called after the invocation of each test method in the class.
super.tearDown()
}
func testIfCaseInteractionLabelIsNotNil() {
XCTAssertNotNil(viewController?.someLabel)
}
}
In the code above the type of view controller object is "AppName.ViewControllerName" if I specify it as just ViewControllerName like below, the test fails because of casting in line 7
var viewController: ViewControllerName?
override func setUp() {
super.setUp()
viewController = UIStoryboard(name: "Main", bundle: nil).instantiateViewControllerWithIdentifier("someIdentifier") as? ViewControllerName // Line 7
_ = viewController?.view
}
I tried making the view controller class public and using #testable annotation in swift2. It doesn't work.
I made "Enable Testability" build setting to YES.
The code works even if I don't use #testable import but in lot of blogs and tutorials, this results in error.
Any help would be appreciated! Thanks in advance

Related

How to Implement ViewController custom init using dependency injection and factory patterns?

I am trying to implement simple example using DI and factory.
ViewController.swift
class VIewController : UIViewController {
private let factory: ViewControllerFactory
init(with factory: Factory) {
self.factory = factory
super.init(nibName: nil, bundle: nil)
}
}
protocol ViewControllerFactory {
func makeViewController() -> ViewController
}
class DependencyContainer {
///
}
extension DependencyContainer: ViewControllerFactory {
func makeViewController() -> ViewController {
return ViewController(with: self)
}
}
AppDelegate.swift
func application(_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions:
[UIApplication.LaunchOptionsKey: Any]?) -> Bool {
self.window = UIWindow(frame: UIScreen.main.bounds)
let container = DependencyContainer()
let rootViewController = container.makeViewController()
self.window?.rootViewController = rootViewController
self.window?.makeKeyAndVisible()
return true
}
I have view controller designed in storyboard. How I can I correct my code to run app successfully?
App is crashing:
I am removing storyboard entry point from view controller, but the outlets are nil and it is crashing when I am using them.
initWithCoder is calling in case when I don't remove that entry point from storyboard.
The behavior you are describing is how storyboards work (ie they call initWithCoder). If you need your constructor to be called you can use a nib file then call your constructor which calls init(nibName:bundle:) and will correctly load the nib file. This works on current iOS versions.
The only way to get constructor injection with a storyboard is to use iOS 13 and use the new #IBSegueAction which will give you a coder that you can pass into your constructor and then call super initWithCoder.
In iOS 13, you can initialise a storyboard view controller as follows:
Add an initialiser to your ViewController class
let property: Property // Your injected property
required init?(coder: NSCoder, property: Property) {
self. property = property
super.init(coder: coder)
}
required init?(coder: NSCoder) { // You'll need to add this
fatalError("init(coder:) has not been implemented")
}
In your factory class:
let viewController = storyboard.instantiateViewController(identifier: "ViewController") { coder in
ViewController(coder: coder, property: <injected property>)
}
There are different types of dependency injection. You're currently trying to use constructor-based dependency injection, which unfortunately doesn't really work with storyboards, since they need to initialise with a decoder. iOS 13 does introduce some additional functionality which will make this approach possible, but for the moment, you could use setter-based dependency injection instead.
Something like the following:
class ViewController: UIViewController {
var factory: ViewControllerFactory!
}
protocol ViewControllerFactory {
func makeViewController() -> ViewController
}
class DependencyContainer {
let storyboard: UIStoryboard = UIStoryboard(name: "Storyboard", bundle: Bundle.main)
}
extension DependencyContainer: ViewControllerFactory {
func makeViewController() -> ViewController {
guard let vc = storyboard.instantiateViewController(withIdentifier: "ViewController") as? ViewController else {
fatalError("Unrecognised viewController")
}
vc.factory = self
return vc
}
}

Unable to call method of ViewController()

I am simply trying to make a method in the ViewController class and be able to call it.
Here is my code (I note the 2 ways I tried calling it, and the errors I got):
import UIKit
class ViewController: UIViewController{
func sayHi(name: String){
print("Hi \(name)")
}
}
/*
let viewcontroller = ViewController()
viewcontroller.sayHi(name: "Bob")
*/
//Error: Expressions are not allowed at the top level
/*
ViewController.sayHi(name: "Bob")
*/
//Error: Expressions are not allowed at the top level
//Error: Instance member 'sayHi' cannot be used on type 'ViewController'; did you mean to use a value of this type instead?
So as you can see in the commenting, I tried to call sayHi as a type method and as an instance method. Neither worked. I will ultimately create a function that can take input from a text input, and manipulate it. Is ViewController.swift even the right file to be doing this? If so, how do I call a method that I have defined?
There will be this delegate in appDelegate which will be called when you app is launched. Create your viewController there and add it to the window.
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
window = UIWindow(frame: UIScreen.main.bounds)
window?.makeKeyAndVisible()
let viewController = ViewController()
window?.rootViewController = viewController
viewController.sayHi()
return true
}
This will work on Playground, but not on Xcode.
Xcode's code is compiled and then you have an app. The first point where a call happens is AppDelegate and from there your first controller and its methods are initialised. Nothing outside a class will be executed.
Use playground for tests or any other online swift playground.
If you want to run sayHi immediately, put it in viewDidLoad and load the app. Delete all further code outside of the class before building again:
override func viewDidLoad() {
super.viewDidLoad()
sayHi()
}
You can create a instance of controller inside a function or a block
when you are working in a class or Xcode projects in Xcode Playground your way of accessing the function sayHi(name: String) in
ViewController works.
For Xcode projects try the following
import UIKit
class ViewController: UIViewController{
func sayHi(name: String){
print("Hi \(name)")
}
}
class SecondViewController: UIViewController{
override func viewDidLoad() {
super.viewDidLoad()
let viewcontroller = ViewController()
viewcontroller.sayHi(name: "Bob")
}
}
When you initialise the SecondViewController you can access the ViewController()
To execute the function sayHi(name: String) immediately when the
ViewController() is initialised you can call it in viewDidLoad() or in
func viewWillAppear()
import UIKit
class ViewController: UIViewController{
override func viewDidLoad(_ animated: Bool) {
super.viewDidLoad(animated)
//Call the function hear
sayHi(name: "Bob")
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
// or call hear
sayHi(name: "Bob")
}
func sayHi(name: String){
print("Hi \(name)")
}
}

Using CoreData and Dependency Injection - Thread 1: Fatal error: init(coder:) has not been implemented

I'm trying to learn how to use CoreData and the correct way to implement it, currently I have watched this video on youtube (link below). At the moment it all makes sense, however when I go from one viewController to my HomeVC (which is part of a tab bar controller) I get the below error. Does anyone have any idea as to why? Any help is greatly appreciated, many thanks!!
https://www.youtube.com/watch?v=OYRo3i9z-lM
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
\\ Thread 1: Fatal error: init(coder:) has not been implemented
}
Function to home:
func toHome() {
self.dismiss(animated: true) {
if let destVC = UIStoryboard(name: "Home", bundle: nil).instantiateViewController(withIdentifier: "TabBarHomeVC") as? UITabBarController {
if let homeVC = destVC.viewControllers?.first as? HomeVC {
homeVC.persistenceManager = PersistenceManager.shared
self.present(destVC, animated: true, completion: nil)
}
}
}
}
HomeVC:
class HomeVC: UIViewController {
var persistenceManager: PersistenceManager
init(persistenceManager: PersistenceManager) {
self.persistenceManager = persistenceManager
super.init(nibName: nil, bundle: nil)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
PersistenceManager:
import Foundation
import CoreData
final class PersistenceManager {
private init() {}
static let shared = PersistenceManager()
lazy var persistentContainer: NSPersistentContainer = {
let container = NSPersistentContainer(name: "CoreDataApp")
container.loadPersistentStores(completionHandler: { (storeDescription, error) in
if let error = error as NSError? {
fatalError("Unresolved error \(error), \(error.userInfo)")
}
})
return container
}()
lazy var context = persistentContainer.viewContext
}
If you work with segues then the UIViewController is instantiated from the Storyboard, which is done through the init?(coder:) initializer. You making it unusable makes the HomeVC unable to instantiate itself from Storyboard since it crashes there.
The property var persistenceManager: PersistenceManager makes Swift unable to inherit init(coder:) from UIViewController (explanation) and so you have to provide one for yourself where you initialize all the variables you added, all to get the UIViewController into a usable state.
Try to make var peristenceManager: PersistenceManager optional since you assign it later anyway or assign it a default value in the same line or assign it a value during init?(coder:) so it is initalized. Also call super.init(coder:) inside your init?(coder:) since there it loads all the settings from the Storyboard.
With Storyboard you can’t give things in any initializer so you have to set it after the initializer did run. You can use a static factory function where you initialize the vc instance, then immediately set what you want and then return the vc in a usable state.
So how can I make homeVC.persistenceManager = PersistenceManager.shared, into HomeVC(persistenceManager: PersistenceManager.shared) by passing it through the tabBarController straight to HomeVC?
You can’t use that initializer since the only one being called is the init?(coder:) initializer. You can change the variable to:
var persistenceManager: PersistenceManager!
Then you can set the variable after init(coder:) was called and you have your instance.
Here the UITabBarController initializes the HomeVC, this guy here had the same issue, where to initialize his UIViewController being embedded, maybe it helps you out. The answer uses a call that is being called before a UIViewController is being shown:
func tabBarController(_ tabBarController: UITabBarController, shouldSelect viewController: UIViewController) -> Bool {
// Initialization code here.
// Also make sure its only called once if you want
}
He even constructs the UIViewController himself if a certain tab is asked for.
You can ofcourse set it as you do already, since the UITabBarController is not shown yet. Keep it simple as you do already.

Cast type not work in generic function scope. Why?

I did a class Router (subclass of UINavigationController) in my project for centralize and minimize the code used to instantiate and navigate between views. But when i try minimize a particular function (buildView), the type casting not work. But this works fine out of scope of function buildView, same whithout the as! operator in function goHome.
enum Routes {
case home
case account
var file: String {
switch self {
case .home:
return "HomeView"
case .account:
return "AccountView"
}
}
protocol HomeInterface: class {
func goTo(view: Routes)
func showModal(view: Routes, caller: UIViewController)
}
class HomePresenter: NSObject, HomeInterface {
init(view: HomeViewInterface) {
self.view = view
}
internal func goTo(view: Routes) { /* Implementation */ }
internal func showModal(view: Routes, caller: UIViewController) {/* Implementation */ }
}
protocol HomeViewInterface: class {
/* Implementation */
}
class HomeViewController: UIViewController, HomeViewInterface {
var presenter: HomeInterface?
override func viewDidLoad() {
super.viewDidLoad()
}
/* Implementation */
}
Working Code
func goHome() {
let viewInstance = buildView(view.file, HomeViewController.identifier, HomeViewController.self)
viewInstance.presenter = HomePresenter(view: viewInstance)
self.view?.pushViewController(viewInstance, animated: true)
}
private func buildView<T>(_ nameFile: String, _ identifier: String, _ viewClass: T.Type) -> T {
return UIStoryboard(name: nameFile, bundle: nil).instantiateViewController(withIdentifier: identifier) as! T
}
Desired final code, but does not work:
func goHome() {
buildViewFinal(view.file, HomeViewController.identifier, HomeViewController.self)
}
func buildViewFinal<T, P>(_ nameFile: String, _ identifier: String, viewClass: T, presenter: P) {
let viewInstance = UIStoryboard(name: nameFile, bundle: nil).instantiateViewController(withIdentifier: identifier) as? T
viewInstance.presenter = P(view: viewInstance)
self.view?.pushViewController(viewInstance, animated: true)
}
When i try minimize the code only to buildViewFinalfunction, the property presenter of viewInstance is not recognize, showing a compile error
Value of type 'T?' has no member 'presenter'
, and in pushViewControllershow error:
Cannot convert value of type 'T?' to expected element type
'UIViewController'
The main goal is turn all code to create and navigate useful and simple.
So, how this works fine in first code, but fails in recognize type inside buildViewFinal scope?
In your first piece of code, you are passing HomeViewController.self as viewClass and so it knows that buildViewFinal is going to return an instance of HomeViewController and that HomeViewController has a presenter property.
In the second code snippet, the compiler doesn't know anything about T, so it can't assume that it will have a presenter property.
You could use type constraint to enforce that T is a HomeViewController or whatever class defines the presenter property:
func buildViewFinal<T: HomeInterface, P>(_ nameFile: String, _ identifier: String, viewClass: T, presenter: P) {
let viewInstance = UIStoryboard(name: nameFile, bundle: nil).instantiateViewController(withIdentifier: identifier) as? T
viewInstance.presenter = P(view: viewInstance)
self.view?.pushViewController(viewInstance, animated: true)
}
but then you will have a problem that viewInstance can't be pushed because the compiler doesn't know that it is an instance of a UIViewController subclass.
Really generics and protocols are just complicating things here.
You are dealing with UIKit which is class oriented, so you might as well just use good old inheritance

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