unit testing cell is nil - swift

Issue:
I loaded collectionView with 3 Dummy items. However Cell came back nil, is it because view was never loaded? How do you guys test your collectionViewCell type?
Code
var window: UIWindow?
var sut: QuestsDataProvider!
var collectionView: UICollectionView!
override func setUp() {
super.setUp()
bulletinController = BulletinController(collectionViewLayout: UICollectionViewFlowLayout())
sut = QuestsDataProvider(acceptedQuests: false, completedQuests: false)
bulletinController.collectionView?.dataSource = sut
bulletinController.collectionView?.delegate = sut
window = UIWindow()
window?.makeKeyAndVisible()
window?.rootViewController = bulletinController
}
func testCellIsQuestCell() {
let indexPath = IndexPath(item: 1, section: 0)
let cell = collectionView.cellForItem(at: indexPath)
guard let count = sut.questManager?.quests.count else {return XCTFail()}
XCTAssertTrue(cell is QuestCell)
}
Edit:
Upon Further testing, I'm able to see the dummy Cell inside my simulator and get a accurate count from numberOfitems(InSection: Int). However I have no visible Cell.
2nd Edit:
After further research, I found out the issue is collectionView.cellForItem(at: indexPath) only shows visible cell. Is there any other method for unit testing collection view cell type?

You need to access the view object of the view controller before it and its subview components will be fully initialised.
You should be able to just do let _ = bulletinController.view in your setup function. it is quite a common approach, see here
Relevant parts included below
func setupCreateOrderViewController()
{
let bundle = NSBundle(forClass: self.dynamicType)
let storyboard = UIStoryboard(name: "Main", bundle: bundle)
createOrderViewController = storyboard.instantiateViewControllerWithIdentifier("CreateOrderViewController") as! CreateOrderViewController
_ = createOrderViewController.view
}
Quote from link:
But there are two very, very important things happening on the last line:
Asking for the view property of createOrderViewController causes the view to be loaded. The viewDidLoad() method is called as a result.
After the view is loaded, all the IBOutlets are also set up and ready to be used in out tests. For example, you can assert that a text field outlet’s text equal to a string you expect.
EDIT:
You can also just call loadViewIfNeeded() on the view controller, which will do the same thing.
Loads the view controller’s view if it has not yet been loaded.

Related

Going from one storyboard to another from a tableView in Swift 5 / Cocoa

have search on that topic without finding a solution that work.
I am building a accounting application with several storyboard. Main, Customer( clients), invoice (factures)... etc. I can go from the main storyboard to the customer of Invoice storyboard by click a button no problem... The button (main SB) is linked to the Customer or Invoice storyboard reference.
In the clients storyboard, I have a tableView with that list the purchased historic of that customer. I would like to to be able to double clic on a specific invoice, and open that invoice in the Invoice storyboard.
The double clic part work fine, print message work... but the program crash after with the message: Could not cast value of type '__NSCFBoolean' (0x7fffaab000c8) to '__C.NSViewControllerPresentationAnimator'
That code was taken andadapted from another post. I have tried different variation withou success ie same error message.
I have not work on the part where I transfer the Invoice number from the client SB to the Invoice SB. I will likely transfer the Invoice number with a segue and have the Invoices program look if that variable if not nil, after loading
Invoice storyboard filename : factures.storyboard
facture ViewController Class : FacturesVC
ViewController storyboardID : facturesVC_id
#objc func tableViewDoubleClick(_ sender:AnyObject) {
if tableView.selectedRow >= 0 {
print ("VC545:", tableView.selectedRow)
//let storyboard = NSStoryboard(name: "factures", bundle: nil)
//let VC = storyboard.instantiateViewController(withIdentifier: "facturesVC_id") // give same error
let VC = NSStoryboard(name: "factures", bundle: nil).instantiateController(withIdentifier: "facturesVC_id") as! FacturesVC
self.present(VC as NSViewController, animator: true as! NSViewControllerPresentationAnimator)
}
}
Your code does not make sense.
It looks like you are trying to call present(_:animator:). If you call that, you need to pass it an animator (an object of type NSViewControllerPresentationAnimator.)
Your code does not create a NSViewControllerPresentationAnimator.
Here is an outline of how you need to change it:
let vc = NSStoryboard(name: "factures", bundle: nil).instantiateController(withIdentifier: "facturesVC_id") as! FacturesVC
let animator = // Code to create an NSViewControllerPresentationAnimator
self.present(vc, animator: animator)
I haven't worked with NSViewControllerPresentationAnimators before. (I mostly work with iOS these days.) You should probably search for tutorials on NSViewControllerPresentationAnimator if you are unsure how to proceed.
Finally, I have found the answer I was looking for...
Here is the code.
#objc func tableViewDoubleClick(_ sender:AnyObject) {
if tableView.selectedRow >= 0 {
let srow = tableView.selectedRow
//print ("VC551:", srow)
fact_nb = Int(fact_tbv[srow].id_f) ?? 0 // invoice nb that you want to segue
let storyboard = NSStoryboard(name: "factures", bundle: nil)
let VC = storyboard.instantiateController(withIdentifier: "facturesVC_id")
//self.presentAsSheet(VC as! NSViewController) work fine for sheet
// self.presentingViewController // data are laoded but nothing show up
// self.presentAsModalWindow(VC as! NSViewController) // OK for modal, cannot be resize , yellow button missing on bar
// self.present(VC as! NSViewController, animator: false as! NSViewControllerPresentationAnimator) // true or false... need a animator
let window = NSWindow(contentViewController: VC as! NSViewController)
window.center()
let windowController = NSWindowController(window: window)
windowController.showWindow(nil)
//see How to Perform Segue https://www.youtube.com/watch?v=JL0xuZ4TXrM
self.performSegue(withIdentifier: "gotofact", sender: nil) // segue identifier name : gotofact
}
}
override func prepare(for segue: NSStoryboardSegue, sender: Any?) {
let sb = segue.destinationController as! FacturesVC
print ("VC569:", fact_nb)
sb.factnb = fact_nb
}

Unable to share data between tab view controllers?

I am making an app where I take in user input and display it as a chart, which requires an array of data. I have managed to save data in an array using core data and I cannot figure out how to share that data from one tab to the other TabViewController.
here is how the data is stored and fetched in the FirstViewController
let number = Numbers(context: PersistenceService.context)
number.numberInArray = Int16(numberEnteredInSlider)
PersistenceService.saveContext()
testArray.append(Int(Double(number.numberInArray)))
var numbers = [Numbers]() // Where Numbers = NSManaged Class
var fetchRequest = NSFetchRequest<NSFetchRequestResult>(entityName: "Numbers")
do {try numbers = PersistenceService.context.fetch(fetchRequest) as! [Numbers]
for number in numbers {
print(number.numberInArray)
}
}catch {
print("error")
}
and here is the output(printed testarray):
SAVED
2
5
6
5
Now I want to share this test array from one view controller to another(chartsViewController)
this is what I have tried
class chartsViewController: UIViewController {
let mainVC = mainViewController(nibName: "mainViewController", bundle: nil)
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
print(mainVC.testArray)
updateGraph()
func updateGraph() {
var lineChartEntry = [ChartDataEntry]() //this is the Array that will eventually be displayed on the graph.
for i in 0..<mainVC.testArray.count {
//
let value = ChartDataEntry(x: Double(i), y: Double(mainVC.testArray[i]))
// here we set the X and Y status in a data chart entry
lineChartEntry.append(value)
// here we add it to the data set
}}
//only showing the part needed. I have tried the same solution with another array and it worked.
}
and the output comes as [0]
I have also tried making a singleton but that didn't work out.
To pass data between tabs on UITabBarController /tabBar, what needs to be done is to have an intermediate. (This is usually the main UITabBarController)
Pic of UITabBarController and the child tabbar
Create a Class and link it to this TabBarController within IB
class BaseTBController: UITabBarController {
// Provide the variable which we want to pass
var workoutTitle: String = "Select a Workout"
override func viewDidLoad() {
super.viewDidLoad()
}
}
Assuming you want to pass data from TabBar2 to TabBar1, then on TabBar2 (in this case, I have it as a UITableView). In the delegate method:
extension VCLibrary: UITableViewDelegate{
// method to run when table view cell is tapped
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
// prepare to store the data to be passed to another TabBar
let tabbar = tabBarController as! BaseTBController
tabbar.workoutTitle = jsonErgWorkouts[indexPath.row].title
// Automatically select Tab1 after choosing
self.tabBarController?.selectedIndex = 0
// Deselect the selected row once we move to Tab1
tableView.deselectRow(at: indexPath, animated: true)
}
}
After selecting the data to be passed, the code (above) will automatically switch to Tab1. Within Tab1, the following code is aimed to receive the passed data
override func viewDidAppear(_ animated: Bool) {
// Obtain Passed in values from BaseTBController
let tabbar = tabBarController as! BaseTBController
// populate the Title as passed from Tab2
workoutTitleLabel.text = tabbar.workoutTitle
}
I learned this from:
https://www.youtube.com/watch?v=GL8-eM93EvQ

ScrollView IBOutlet nil when using protocol to call function in viewDidDisappear

I have a scrollView that contains a dynamic amount of WeatherViewControllers each displaying the weather data of a different city the user has saved. The user can segue from the WeatherViewControllers to a CityListViewController. Where they can add and remove cities from their list which in turn should add and remove WeatherViewControllers from the scrollView upon dismissing the CityListViewController, this is where I am running into a problem.
Currently I am trying to use a protocol in to call the func reloadScrollView which calls viewDidLoad in the scrollViewController upon dismissing(viewDidDisappear) the CityListViewController but am getting an error:
Fatal error: Unexpectedly found nil while unwrapping an Optional value: file
when it gets to:
totalScrollView.addSubview(weatherScreen.view)
Using debugger I have found that totalScrollView is nil and that is causing the problem. Is there a way to make the scrollView load so it is not nil when dismissing the other viewController
OR
is the a better time to call use this protocol to call this function?
Side Note: Upon initially opening the app the scrollView loads properly with all the correct WeatherViewControllers in the UIScrollView and the correct cities in the list.
class ScrollViewController: UIViewController, ScrollReloadProtocol {
func reloadScrollView() {
print("SCROLL RELOADED!!!!!*******")
self.viewDidLoad()
}
#IBOutlet var totalScrollView: UIScrollView!
var pages = [ViewController]()
var x = 0
var weatherScreensArray = [SavedCityEntity]()
var weatherScreenStringArray = [String]()
let context = (UIApplication.shared.delegate as! AppDelegate).persistentContainer.viewContext
var horizString = "H:|[page1(==view)]"
let defaults = UserDefaults.standard
override func viewDidLoad() {
super.viewDidLoad()
//userDefaults used to keep track of which screen is which to put different cities on different viewControllers
defaults.set(0, forKey: "screenNumber")
//load cities to get number of cities saved
loadCities()
var views : [String: UIView] = ["view": view]
//create all weatherWeatherControllers
while x <= weatherScreensArray.count {
pages.append(createAndAddWeatherScreen(number: x))
weatherScreenStringArray.append("page\(x+1)")
views["\(weatherScreenStringArray[x])"] = pages[x].view
let addToHoriz = "[\(weatherScreenStringArray[x])(==view)]"
horizString.append(addToHoriz)
x+=1
}
horizString.append("|")
let verticalConstraints = NSLayoutConstraint.constraints(withVisualFormat: "V:|[page1(==view)]|", options: [], metrics: nil, views: views)
let horizontalConstraints = NSLayoutConstraint.constraints(withVisualFormat: horizString, options: [.alignAllTop, .alignAllBottom], metrics: nil, views: views)
NSLayoutConstraint.activate(verticalConstraints + horizontalConstraints)
}
//Function to create and add weatherViewController
func createAndAddWeatherScreen(number: Int) -> ViewController {
defaults.set(number, forKey: "screenNumber")
let story = UIStoryboard(name: "Main", bundle: nil)
let weatherScreen = story.instantiateViewController(identifier: "View Controller") as! ViewController
weatherScreen.view.translatesAutoresizingMaskIntoConstraints = false
totalScrollView.addSubview(weatherScreen.view)
addChild(weatherScreen)
weatherScreen.didMove(toParent: self)
return weatherScreen
}
}
Skipping that fact that your are not doing it right, let's forcus on the one issue at a time. You are trying to access the totalScrollView implicitly in the viewDidLoad where if the outlet is linked it should be loaded at that point. If it is nil you should:
Make sure that you have the .storyboard or .xib file defining the ScrollViewController layout.
Make sure you are loading this controller from that storyboard/xib.
Make sure that the view controller in the storyboard/xib file has set its class to ScrollViewController, similar to the following print screen:
Make sure that the outlet is linked in the storyboard/xib to this property in your code file (probably ScrollViewController.swift). If not:
open storyboard and sorucecode file in separate editors
drag and drop from the dot on the left of the property declaration to the UIScrollView in the storyboard
make sure that there is added a link to Referencing Outlets

How to call a function from another controller in swift

I set the Show Charts button on the DetailView Controller which triggers the getChartData function and shows me the values in display view in charts, now I want to call that function in the didselectrow on the main Viewcontroller so that the chart is loaded automatically, but it fails.
When I tried to call that function in didselectrow (DVC.getChartsData) I got the error "Thread 1: Fatal error: Unexpectedly found nil while implicitly unwrapping an Optional value"
DVC.getChartsData
Thread 1: Fatal error: Unexpectedly found nil while implicitly unwrapping an Optional value
ViewController:
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
let Storyboard = UIStoryboard(name: "Main", bundle: nil)
let DVC = Storyboard.instantiateViewController(withIdentifier: "DetailViewController") as! DetailViewController
DVC.getDetailName = coin[indexPath.row].name
let formatedRoundingPrice = (coin[indexPath.row].price as NSString).floatValue * currencymodel.indexValue
let formatedPrice = String (format: "%.3f", formatedRoundingPrice)
DVC.getDetailPrice = formatedPrice
self.navigationController?.pushViewController(DVC, animated: true)
let percentage = String ((coin[indexPath.row].percent as NSString).floatValue)
DVC.getDetailPercent = percentage
tableView.deselectRow(at: indexPath, animated: true)
//DVC.getChartData()
}
DetailViewController:
#IBAction func tapLineChart(_ sender: Any) {
getChartData()
}
func getChartData () {
let chart = HITLineChartView(frame: CGRect(x: 0, y: 0, width: UIScreen.main.bounds.width, height: displayView.bounds.height))
displayView.addSubview(chart)
let max = String((priceResult.max() ?? 0.0).rounded(.up))
let min = String((priceResult.min() ?? 0.0).rounded(.down))
let maxChange = abs((listOfChanges.max()) ?? 0.0).rounded(.up)
let minChange = abs((listOfChanges.min()) ?? 0.0).rounded(.up)
absMaxPercentage = Int(maxChange > minChange ? maxChange : minChange)
titles = ["\(getDetailName) closing price is \(getDetailPrice)"]
print(data)
chart.draw(absMaxPercentage,
values: listOfChanges,
label: (max: max, center: "", min: min),
dates: namesArray,
titles: titles)
addCloseEvent(chart)
finalURL = baseURL + "bitcoin" + "/market_chart?vs_currency=usd&days=5"
print(finalURL)
getBitcoinData(url: finalURL)
}
How to load my charts tap on a specific tableview cell instead of tapping on tapLineChart.
https://imgur.com/fg2502P
https://imgur.com/C4AzaRY
https://imgur.com/jOrwujy
if you want to call a function on viewControllerB that you declare from viewController A.
just create the object of the class file you want to use the function from
var obj mainVC = MainViewController()
class MainViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
}
func commonMethod() {
print("From the main class")
}
}
Using that object, call the function in another file where you mean to use it
class OtherViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
mainVC.commonMethod()
// Do any additional setup after loading the view, typically from a nib.
}
}
Additionally, You can also create a new swift file, name it Global.swift, create all your functions that you want to use throughout the application here. They become "global functions"
You will want to use delegates or observers to pass data between view controllers.
I'm new to tutorials, but I wrote a bit about this here: https://www.eankrenzin.com/swift-blog/pass-data-throughout-your-app-with-observers-and-notifications-xcode-11-amp-swift-5
You should use optional binding to unwrap your VC let DVC = Storyboard.instantiateViewController(withIdentifier: "DetailViewController") as! DetailViewController
Your code is crashing because of that line. Check your interface builder to make sure the identifier is correct. Edit: this line was not causing a crash, but it is still better to use optional binding.The line is: https://imgur.com/CVP1x6H
NOTE: It is terrible practice to litter your app with instances when delegates and observers could work. Also do NOT have globals. Globals are disastrous for debugging and create tech debt.

My cells are duplicating themselves

I am new to swift and I am trying to make this note app. I have split view controller that goes in my first view controller and that view controller connects to a table view controller. Everything works perfectly is just that when I launch the app I have all the notes like I want but when I try to go back to my first view controller and come back to my table view controller, all the notes are duplicated every single time I do it. I tried everything I can try, is there anyone who can help me
my MasterViewController is
import UIKit
class MasterViewController: UITableViewController {
var detailViewController: DetailViewController? = nil
override func viewDidLoad()
{
super.viewDidLoad()
Note.loadNotes() // The problem is here, I think
noteTable = self.tableView
// Do any additional setup after loading the view, typically from a nib.
let addButton = UIBarButtonItem(barButtonSystemItem: .add, target: self, action: #selector(insertNewObject(_:)))
navigationItem.rightBarButtonItem = addButton
if let split = splitViewController
{
let controllers = split.viewControllers
detailViewController = (controllers[controllers.count-1] as! UINavigationController).topViewController as? DetailViewController
}
}
My loadNotes function is
class func loadNotes()
{
let defaults:UserDefaults = UserDefaults.standard
let saveData: [NSDictionary]? = defaults.object(forKey: kAllNotes) as? [NSDictionary]
if let data:[NSDictionary] = saveData
{
for i:Int in 0 ..< data.count
{
let n:Note = Note()
n.setValuesForKeys(data[i] as! [String : Any])
allNotes.append(n)
}
}
}
Your loadNotes method keeps appending. The first line of loadNotes should be:
allNotes = [Note]()
Then it starts with an empty array and fills it up.
And why is loadNotes a static method? That's a bad design. Make Notes a normal class and make loadNotes an instance method.
On an unrelated note (no pun intended), do not use UserDefaults to store app data. Only use it to store little bits of information.