while using a MockTableView this code still not calling reloadData() from the mock,
please i wanna know what is wrong here.
following this book: Test-Driven IOS Development with Swift 4 - Third Edition
page 164, i was as an exercise
full code repo - on github
ItemListViewController.swift
import UIKit
class ItemListViewController: UIViewController, ItemManagerSettable {
#IBOutlet var tableView: UITableView!
#IBOutlet var dataProvider: (UITableViewDataSource & UITableViewDelegate &
ItemManagerSettable)!
var itemManager: ItemManager?
override func viewDidLoad() {
super.viewDidLoad()
itemManager = ItemManager()
dataProvider.itemManager = itemManager
tableView.dataSource = dataProvider
tableView.delegate = dataProvider
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
tableView.reloadData()
}
#IBAction func addItem(_ sender: UIBarButtonItem) {
if let nextViewController =
storyboard?.instantiateViewController(
withIdentifier: "InputViewController")
as? InputViewController {
nextViewController.itemManager = itemManager
present(nextViewController, animated: true, completion: nil)
}
}
}
ItemListViewControllerTest.swift
import XCTest
#testable import ToDo
class ItemListViewControllerTest: XCTestCase {
var sut: ItemListViewController!
var addButton: UIBarButtonItem!
var action: Selector!
override func setUpWithError() throws {
let storyboard = UIStoryboard(name: "Main", bundle: nil)
let vc = storyboard.instantiateViewController(withIdentifier:
"ItemListViewController")
sut = vc as? ItemListViewController
addButton = sut.navigationItem.rightBarButtonItem
action = addButton.action
UIApplication.shared.keyWindow?.rootViewController = sut
sut.loadViewIfNeeded()
}
override func tearDownWithError() throws {}
func testItemListVC_ReloadTableViewWhenAddNewTodoItem() {
let mockTableView = MocktableView()
sut.tableView = mockTableView
guard let addButton = sut.navigationItem.rightBarButtonItem else{
XCTFail()
return
}
guard let action = addButton.action else{
XCTFail()
return
}
sut.performSelector(onMainThread: action, with: addButton, waitUntilDone: true)
guard let inputViewController = sut.presentedViewController as?
InputViewController else{
XCTFail()
return
}
inputViewController.titleTextField.text = "Test Title"
inputViewController.save()
XCTAssertTrue(mockTableView.calledReloadData)
}
}
extension ItemListViewControllerTest{
class MocktableView: UITableView{
var calledReloadData: Bool = false
override func reloadData() {
calledReloadData = true
super.reloadData()
}
}
}
You inject a MockTableview Then you call loadViewIfNeeded(). But because this view controller is storyboard-based and the table view is an outlet, the actual table view is loaded at this time. This replaces your MockTableview.
One solution is:
Call loadViewIfNeeded() first
Inject the MockTableview to replace the actual table view
Call viewDidLoad() directly. Even though loadViewIfNeeded() already called it, we need to repeat it now that we have a different tableview in place.
Another possible solution:
Avoid MockTableview completely. Continue to use a real table view. You can test whether it reloads data by checking whether the number of rows matches the changed data.
Yet another solution:
Avoid storyboards. You can do this with plain XIBs (but these lack table view prototype cells) or programmatically.
By the way, I see all your tearDownWithError() implementations are empty. Be sure to tear down everything you set up. Otherwise you will end up with multiple instances of your system under test alive at the same time. I explain there here: https://qualitycoding.org/xctestcase-teardown/
Related
I am making an app that will display a random quote from a stoic philosopher. Right now, I am stuck on trying to make the correct picture pop up. (User clicks on a Button with the philosopher's name on it, and then a new view pops up with an image of the philosopher and a random quote by him).
class ViewController: UIViewController {
var allQuotes = [String]()
var pictures = [String]()
#IBOutlet var Epictetus: UIButton!
#IBOutlet var Seneca: UIButton!
#IBOutlet var MarcusAurelius: UIButton!
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
// Create a constant fm and assign it the value returned by FileManager.default (built in system type)
let fm = FileManager.default
// Declares a new constant called path that sets the resource path of ours apps buddle.
// A bundle is a directory containing our compiled program and all our assets
let path = Bundle.main.resourcePath!
// items array will be a constant collection of the names of all the files found in the directory of our app
let items = try! fm.contentsOfDirectory(atPath: path)
// create a loop to go through all of our items...
for item in items {
if item.hasSuffix("jpg"){
pictures.append(item)
}
}
print(pictures)
title = "Stoicism"
if let stoicQuotesURL = Bundle.main.url(forResource: "quotes", withExtension: "txt"){
if let stoicQuotes = try? String(contentsOf: stoicQuotesURL) {
allQuotes = stoicQuotes.components(separatedBy: "\n\n")
}
}
}
#IBAction func buttonTapped(_ sender: UIButton) {
if sender.tag == 0 {
if let vc = storyboard?.instantiateViewController(identifier: "Picture") as? PictureViewController {
vc.selectedImage = pictures[0]
navigationController?.pushViewController(vc, animated: true)
}
}
else if sender.tag == 1 {
if let vc = storyboard?.instantiateViewController(identifier: "Picture") as? PictureViewController {
vc.selectedImage = pictures[1]
navigationController?.pushViewController(vc, animated: true)
}
}
else if sender.tag == 2 {
if let vc = storyboard?.instantiateViewController(identifier: "Picture") as? PictureViewController {
vc.selectedImage = pictures[2]
navigationController?.pushViewController(vc, animated: true)
}
}
}
}
That's the code for my main viewController.
import UIKit
class PictureViewController: UIViewController {
#IBOutlet var picture: UIImageView!
#IBOutlet var imageView: UIImageView!
var selectedImage: String?
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
if let imageToLoad = selectedImage {
imageView.image = UIImage(named: imageToLoad)
}
}
override func viewWillAppear(_ animated: Bool) {
// doing it for the parent class
super.viewWillAppear(animated)
// if its a nav Cont then it will hide bars on tap...
}
// now make sure it turns off when you go back to the main screen
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
}
/*
// MARK: - Navigation
// In a storyboard-based application, you will often want to do a little preparation before navigation
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
// Get the new view controller using segue.destination.
// Pass the selected object to the new view controller.
}
*/
}
That's the code for the viewController that has the imageView. Right now, the image that's popping up is always the preset (Marcus Aurelius), even though my code looks correct to me. Obviously it isn't (also, I've already debugged and ensured through print statements that the jpg files add to the pictures array correctly).
Any help would be appreciated.
First of all, this code is really silly:
#IBAction func buttonTapped(_ sender: UIButton) {
if sender.tag == 0 {
if let vc = storyboard?.instantiateViewController(identifier: "Picture") as? PictureViewController {
vc.selectedImage = pictures[0]
navigationController?.pushViewController(vc, animated: true)
}
}
else if sender.tag == 1 {
if let vc = storyboard?.instantiateViewController(identifier: "Picture") as? PictureViewController {
vc.selectedImage = pictures[1]
navigationController?.pushViewController(vc, animated: true)
}
}
else if sender.tag == 2 {
if let vc = storyboard?.instantiateViewController(identifier: "Picture") as? PictureViewController {
vc.selectedImage = pictures[2]
navigationController?.pushViewController(vc, animated: true)
}
}
}
Do you see that everything in those lines is identical except for the numbers? So make the number a variable:
#IBAction func buttonTapped(_ sender: UIButton) {
if let vc = storyboard?.instantiateViewController(identifier: "Picture") as? PictureViewController {
print(sender.tag)
vc.selectedImage = pictures[sender.tag]
navigationController?.pushViewController(vc, animated: true)
}
}
See how much shorter and clearer that is? Okay, I've also added a print statement. This will print the tag to the console. You need to make sure that your buttons do have the right tags. If they do, your code should work.
Good day everyone,
I have root view controller set up as my HomeViewController, in this project i am using token based authentication ( which i am storing in user defaults ) and i am using token for all my API calls.
I have a check in my viewWillAppear method to check if there is access token present and then i make the api call in viewDidAppear to populate the collection view, and this works perfectly fine at all times except the first time.
If I log in for the first time it hits the viewWillAppear, viewDidAppear and then login screen pops up and once i authenticate the user and save it in the UserDefaults, dismiss the login screen and in the HomeViewController all i get is a spinner ( which means that viewDidAppear is also been called ) but if i close the app and open it again it all works fine.
What can i change in my code to make it work in the first time please and thank you!!
class HomeViewController: UIViewController {
// MARK: - Properties
let refreshControl = UIRefreshControl()
var publishedReportList: [ReportListDetail] = []
private let reportsCollectionView: UICollectionView = {
let viewLayout = UICollectionViewFlowLayout()
let collectionView = UICollectionView(frame: .zero, collectionViewLayout: viewLayout)
collectionView.register(ReportsCollectionViewCell.self, forCellWithReuseIdentifier: ReportsCollectionViewCell.identifier)
collectionView.backgroundColor = .systemBackground
return collectionView
}()
// MARK: - Initialisation
override func viewDidLoad() {
super.viewDidLoad()
reportsCollectionView.delegate = self
reportsCollectionView.dataSource = self
print(publishedReportList.count)
refreshControl.tintColor = .blue
refreshControl.addTarget(self, action: #selector(pullToRefresh), for: .valueChanged)
reportsCollectionView.addSubview(refreshControl)
reportsCollectionView.alwaysBounceVertical = true
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
// check auth status
handleNotAuthenticated()
}
override func viewDidAppear(_ animated: Bool) {
// call for reports
getReportUserLayout()
}
// MARK: - Handlers
#objc func pullToRefresh() {
// Code to refresh table view
getReportUserLayout()
}
fileprivate func getReportUserLayout() {
// publishedReportList.removeAll()
whySuchEmptyLabel.isHidden = true
spinner.show(in: view)
spinner.textLabel.text = "Loading Reports.."
DispatchQueue.main.async {
ReportsManager.shared.getReportData { [weak self] (listOfReports) in
guard let strongSelf = self else { return }
strongSelf.publishedReportList = listOfReports
if listOfReports.count == 0 {
strongSelf.whySuchEmptyLabel.isHidden = false
}
strongSelf.reportsCollectionView.reloadData()
strongSelf.spinner.dismiss()
strongSelf.refreshControl.endRefreshing()
}
}
}
private func handleNotAuthenticated() {
if UserDefaults.standard.string(forKey: "accessToken") == nil {
// show login view controller
let loginVC = LoginViewController()
loginVC.modalPresentationStyle = .overCurrentContext
present(loginVC, animated: false)
}
}
}
You are in the HomeViewController and presenting loginVC, so it will not trigger viewDidAppear or viewWillAppear because it is not disappeared from the app. You have to use closure or delegate or notification to communicate back to the HomeViewController. You can also use the Combine framework and save the state. Here is an example of using delegate.
// Add protocol
protocol ViewControllerDelegate {
func loggedIn()
}
class HomeViewController: UIViewController, ViewControllerDelegate {
private func handleNotAuthenticated() {
if UserDefaults.standard.string(forKey: "accessToken") == nil {
let loginVC = LoginViewController()
loginVC.modalPresentationStyle = .overCurrentContext
loginVC.viewDelegate = self
present(loginVC, animated: false)
}
}
}
class LoginViewController: UIViewController {
var viewDelegate: ViewControllerDelegate? = nil
func userLoggedIn() {
self.viewDelegate?.loggedIn()
}
}
I am trying to test whether the method ReloadData() is called by an instance of UITableView when it's dataSource is updated.
I've created a subclass of UITableView called MockTableView. It has a bool called reloadDataGotCalled which is set to true when the overridden function reloadData() is called. I then try access that property from within my test class to test whether it is true.
However when I try to do so the compiler gives me the message that "Value of type 'UITableView' has no member 'reloadDataGotCalled'"
I'm not sure why it's doing that, because as far as I can see I've set that value to be of the type 'MockTableView' which should have that member?
// A ViewController that contains a tableView outlet that I want to test.
class ItemListViewController: UIViewController {
let itemManager = ItemManager()
#IBOutlet var tableView: UITableView!
#IBOutlet var dataProvider: (UITableViewDataSource & UITableViewDelegate & ItemManagerSettable)!
#IBAction func addItem(_ sender: UIBarButtonItem) {
if let nextViewController = storyboard?.instantiateViewController(identifier: "InputViewController") as? InputViewController {
nextViewController.itemManager = itemManager
present(nextViewController, animated: true, completion: nil)
}
}
override func viewDidLoad() {
super.viewDidLoad()
tableView.dataSource = dataProvider
tableView.delegate = dataProvider
dataProvider.itemManager = itemManager
}
}
// My test class
class ItemListViewControllerTest: XCTestCase {
var sut: ItemListViewController!
override func setUp() {
//Given
let storyboard = UIStoryboard(name: "Main", bundle: nil)
let viewController = storyboard.instantiateViewController(identifier: "ItemListViewController")
sut = (viewController as! ItemListViewController)
//When
sut.loadViewIfNeeded()
}
// The test where I'm trying to assign sut.tableView to mockTableView
func test_TableView_IsReloadedWhenItemAddedToItemManger() {
let mockTableView = MockTableView()
sut.tableView = mockTableView
let item = ToDoItem(title: "Foo")
sut.itemManager.add(item)
sut.beginAppearanceTransition(true, animated: true)
sut.endAppearanceTransition()
XCTAssertTrue(sut.tableView.reloadDataGotCalled) // <- this is where I'm getting the compiler message "Value of type 'UITableView' has no member 'reloadDataGotCalled'"
}
}
// My mockTableView subclass in an extension of the ItemListViewControllerTests
extension ItemListViewControllerTest {
class MockTableView: UITableView {
var reloadDataGotCalled = false
override func reloadData() {
super.reloadData()
reloadDataGotCalled = true
}
}
}
I'm expecting that it should compile, and then the test should fail because I've not written the code to make it pass yet?
You have defined tableView instance in ItemListViewController as UITableView. So, you can't access the MockTableView's property with that instance.
You can only access the parent's properties from the children not the vice versa. If you still want to access the property you can try something like the snippet below.
XCTAssertTrue((sut.tableView as! MockTableView).reloadDataGotCalled)
Hope it helps.
I would like to pass data from EditPostViewController to NewsfeedTableViewController using delegates, but func remove(mediaItem:_) is never called in the adopting class NewsfeedTableViewController. What am I doing wrong?
NewsfeedTableViewController: UITableViewController, EditPostViewControllerDelegate {
override func viewDidLoad() {
super.viewDidLoad()
//set ourselves as the delegate
let editPostVC = storyboard?.instantiateViewController(withIdentifier: "EditPostViewController") as! EditPostViewController
editPostVC.delegate = self
}
//remove the row so that we can load a new one with the updated data
func remove(mediaItem: Media) {
print("media is received heeeee")
// it does't print anything
}
}
extension NewsfeedTableViewController {
//when edit button is touched, send the corresponding Media to EditPostViewController
func editPost(cell: MediaTableViewCell) {
let editPostVC = storyboard?.instantiateViewController(withIdentifier: "EditPostViewController") as? EditPostViewController
guard let indexPath = tableView.indexPath(for: cell) else {
print("indexpath was not received")
return}
editPostVC?.currentUser = currentUser
editPostVC?.mediaReceived = cell.mediaObject
self.navigationController?.pushViewController(editPostVC!, animated: true)
}
protocol EditPostViewControllerDelegate: class {
func remove(mediaItem: Media)
}
class EditPostViewController: UITableViewController {
weak var delegate: EditPostViewControllerDelegate?
#IBAction func uploadDidTap(_ sender: Any) {
let mediaReceived = Media()
delegate?.remove(mediaItem: mediaReceived)
}
}
The objects instantiating in viewDidLoad(:) and on edit button click event are not the same objects. Make a variable
var editPostVC: EditPostViewController?
instantiate in in viewDidLoad(:) with delegate
editPostVC = storyboard?.instantiateViewController(withIdentifier: "EditPostViewController") as! EditPostViewController
editPostVC.delegate = self
and then present it on click event
navigationController?.pushViewController(editPostVC, animated: true)
or
present(editPostVC, animated: true, completion: nil)
you can pass data from presenter to presented VC before or after presenting the VC.
editPostVC.data = self.data
I suggest having a property in NewsfeedTableViewController
var editPostViewController: EditPostViewController?
and then assigning to that when you instantiate the EditPostViewController.
The idea is that it stops the class being autoreleased when NewsfeedTableViewController.viewDidLoad returns.
I'm trying to realize the Observer Pattern and I'm experiencing some difficulty as my delegate doesn't seem to be setting properly.
In my Main.storyboard I have a ViewController with a container view. I also have an input box where I'm capturing numbers from a number keypad.
Here's my storyboard:
I'm trying to implement my own Observer Pattern using a protocol that looks like this:
protocol PropertyObserverDelegate {
func willChangePropertyValue(newPropertyValue:Int)
func didChangePropertyValue(oldPropertyValue:Int)
}
My main ViewController.swift
class ViewController: UIViewController {
#IBOutlet weak var numberField: UITextField!
// observer placeholder to be initialized in implementing controller
var observer : PropertyObserverDelegate?
var enteredNumber: Int = 0 {
willSet(newValue) {
print("//Two: willSet \(observer)") // nil !
observer?.willChangePropertyValue(5) // hard coded value for testing
}
didSet {
print("//Three: didSet")
observer?.didChangePropertyValue(5) // hard coded value for testing
}
}
#IBAction func numbersEntered(sender: UITextField) {
guard let inputString = numberField.text else {
return
}
guard let number : Int = Int(inputString) else {
return
}
print("//One: \(number)")
self.enteredNumber = number // fires my property observer
}
}
My ObservingViewController:
class ObservingViewController: UIViewController, PropertyObserverDelegate {
// never fires!
func willChangePropertyValue(newPropertyValue: Int) {
print("//four")
print(newPropertyValue)
}
// never fires!
func didChangePropertyValue(oldPropertyValue: Int) {
print("//five")
print(oldPropertyValue)
}
override func viewDidLoad() {
super.viewDidLoad()
print("view loads")
// attempting to set my delegate
let mainStoryboard: UIStoryboard = UIStoryboard(name: "Main", bundle: nil)
let pvc = mainStoryboard.instantiateViewControllerWithIdentifier("ViewController") as! ViewController
print("//six \(pvc)")
pvc.observer = self
}
}
Here's what my console prints:
What's happening
As you can see when my willSet fires, my observer is nil which indicates that I have failed to set my delegate in my ObservingViewController. I thought I set my delegate using these lines:
let mainStoryboard: UIStoryboard = UIStoryboard(name: "Main", bundle: nil)
let pvc = mainStoryboard.instantiateViewControllerWithIdentifier("ViewController") as! ViewController
print("//six \(pvc)")
pvc.observer = self
However, I must be setting my delegate incorrectly if it's coming back nil.
Question
How do I properly set my delegate?
You are calling into the storyboard to instantiate a view controller and setting it as the observer, however that instantiates a new instance of that view controller, it doesn't mean that it is referencing the one single "view controller" that is in the storyboard. ObservingViewController needs another way to reference the ViewController that has already been created.
So #Chris did reenforce my suspicions which helped me to figure out a solution for assigning my delegate to my view controller properly.
In my ObservingViewController I just need to replace the code in my viewDidLoad with the following:
override func viewDidLoad() {
let app = UIApplication.sharedApplication().delegate as! AppDelegate
let vc = app.window?.rootViewController as! ViewController
vc.observer = self
}
Rather than creating a new instance of my view controller, I'm now getting my actual view controller.