Unable to share data between tab view controllers? - swift

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

Related

fatal errors with optionals not making sense

I keep getting a fatal error saying how a value was unwrapped and it was nil and I don't understand how. When I instantiate a view controller with specific variables they all show up, but when I perform a segue to the exact VC, the values don't show up.
Take these functions for example...
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
tableView.deselectRow(at: indexPath, animated: true)
if let displayVC = storyboard?.instantiateViewController(withIdentifier: Constants.Storyboards.TeachStoryboardID) as? SchoolEventDetailsViewController {
displayVC.selectedEventName = events[indexPath.row].eventName
displayVC.selectedEventDate = documentsDate[indexPath.row].eventDate
displayVC.selectedEventCost = documentsCost[indexPath.row].eventCost
displayVC.selectedEventGrade = documentsGrade[indexPath.row].eventGrade
displayVC.selectedEventDocID = documentsID[indexPath.row]?.docID
navigationController?.pushViewController(displayVC, animated: true)
}
}
This combined with this function :
func verifyInstantiation() {
if let dateToLoad = selectedEventDate {
dateEditableTextF.text = dateToLoad
}
if let costToLoad = selectedEventCost {
costEditableTextF.text = costToLoad
}
if let gradesToLoad = selectedEventGrade {
gradesEditableTextF.text = gradesToLoad
}
if let docIDtoLoad = selectedEventDocID {
docIDUneditableTextF.text = docIDtoLoad
}
if let eventNameToLoad = selectedEventName {
eventNameEditableTextF.text = eventNameToLoad
}
}
Helps load the data perfectly, but when I try to perform a segue from a search controller the data is not there.
override func viewDidLoad() {
super.viewDidLoad()
navigationItem.title = selectedEventName
I set the title of the vc to have the event name , and I also recently added a text field to store it as well for experimental purposes (this question).
Now the issue is I want to do a data transfer from an Algolia Search Controller to that VC and I got all the other fields to show up, except for one and that was the document ID. So I created a completion handler function to get the document ID as a string and have it inserted into the vc when the segue is performed, just like how it's there when the vc is instantiated.
Here is the function :
func getTheEventDocID(completion: #escaping ((String?) -> ())) {
documentListener = db.collection(Constants.Firebase.schoolCollectionName).whereField("event_name", isEqualTo: selectedEventName ?? navigationItem.title).addSnapshotListener(includeMetadataChanges: true) { (querySnapshot, error) in
if let error = error {
print("There was an error fetching the documents: \(error)")
} else {
self.documentsID = querySnapshot!.documents.map { document in
return EventDocID(docID: (document.documentID) as! String)
}
let fixedID = "\(self.documentsID)"
let substrings = fixedID.dropFirst(22).dropLast(3)
let realString = String(substrings)
completion(realString)
}
}
}
I thought either selectedEventName or navigationItem.title would get the job done and provide the value when I used the function in the data transfer function which I will show now :
//MARK: - Data Transfer From Algolia Search to School Event Details
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
otherVC.getTheEventDocID { (eventdocid) in
if let id = eventdocid {
if segue.identifier == Constants.Segues.fromSearchToSchoolEventDetails {
let vc = segue.destination as! SchoolEventDetailsViewController
vc.selectedEventName = self.nameTheEvent
vc.selectedEventDate = self.dateTheEvent
vc.selectedEventCost = self.costTheEvent
vc.selectedEventGrade = self.gradeTheEvent
vc.selectedEventDocID = id
}
}
}
}
But it ends up showing nothing when a search result is clicked which is pretty upsetting, I can't understand why they're both empty values when I declared them in the SchoolEventDetailsVC. I tried to force unwrap selectedEventName and it crashes saying there's a nil value and I can't figure out why. There's actually a lot more to the question but I just tried to keep it short so people will actually attempt to read it and help since nobody ever reads the questions I post, so yeah thanks in advance.
I'm a litte confused what the otherVC is, which sets a property of itself in the getTheEventDocID, whilste in the closure you set the properties of self, which is a different controller. But never mind, I hope you know what you are doing.
Since getTheEventDocID runs asynchronously, the view will be loaded and displayed before the data is available. Therefore, viewDidLoad does not see the actual data, but something that soon will be outdated.
So, you need to inform the details view controller that new data is available, and refresh it's user interface. Something like
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
otherVC.getTheEventDocID { (eventdocid) in
if let id = eventdocid {
if segue.identifier == Constants.Segues.fromSearchToSchoolEventDetails {
let vc = segue.destination as! SchoolEventDetailsViewController
vc.selectedEventName = self.nameTheEvent
vc.selectedEventDate = self.dateTheEvent
vc.selectedEventCost = self.costTheEvent
vc.selectedEventGrade = self.gradeTheEvent
vc.selectedEventDocID = id
vc.updateUI()
}
}
}
}
and in the destination view controller:
class SchoolEventDetailsViewController ... {
override func viewDidLoad() {
super.viewDidLoad()
updateUI()
}
func updateUI () {
navigationItem.title = selectedEventName
// and so on
}
}
Ok so I decided to attempt a workaround and completely ditched the getTheEventDocID() method because it was just causing me stress. So I decided to ditch Firebase generated document IDS and just use 10 digit generated ids from a function I made. I also figured out how to add that exact same 10 digit id in the Algolia record by just storing the random 10 digit id in a variable and using it in both places. So now instead of using a query call to grab a Firebase generated document ID and have my app crash everytime I click a search result, I basically edited the Struct of the Algolia record and just added an eventDocID property that can be used with hits.hitSource(at: indexPath.row).eventDocID.
And now the same way I added the other fields to the vc by segue data transfer, I can now do the same thing with my document ID because everything is matching :).

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.

unit testing cell is nil

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.

How do I display the data fetched from called view controller into a dynamic tableviewcell of the calling view controller while using unwind segue.?

I have dynamic tableview, wherein one of the cell (duration) when tapped opens another view controller which is a list of duration viz (30 min, 1 hour, 2 hours and so fort). One of the durations when selected should display the selected duration in the first view controller. I am able to pass the data back to first view controller using unwind segue but unable to display the passed value. DOn't know whats missing.
I am displaying the code below:
FIRST VIEW CONTROLLER (CALLING)
#IBAction func unwindWithSelectedDuration(segue:UIStoryboardSegue) {
var cell = tableView.dequeueReusableCellWithIdentifier("durationCell") as! durationTableViewCell
if let durationTableViewController = segue.sourceViewController as? DurationTableViewController,
selectedDuration = durationTableViewController.selectedDuration {
cell.meetingDurationCell.text = selectedDuration
duration = selectedDuration
}
SECOND VIEW CONTROLLER (CALLED)
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
if segue.identifier == "SaveSelectedDuration" {
if let cell = sender as? UITableViewCell {
let indexPath = tableView.indexPathForCell(cell)
if let index = indexPath?.row {
selectedDuration = durationList[index]
}
}
}
}
tableView.dequeueReusableCellWithIdentifier should only be called within tableView:cellForRowAtIndexPath:. It has no use outside this context.
The easiest fix is to just reload the table once you have stored the selected duration:
#IBAction func unwindWithSelectedDuration(segue:UIStoryboardSegue) {
if let durationTableViewController = segue.sourceViewController as? DurationTableViewController {
selectedDuration = durationTableViewController.selectedDuration
tableView.reloadData()
}
}
Note that this assumes you only need one selectedDuration for your whole table, rather than one per row. If you need one per row, I assume you have them stored in an array somewhere, so it is that array that should be updated instead before the reloadData.

Get double variable when sent variable to next viewcontroller swift

I create global variable just after import statement:
var kontenid = ""
var judulkonten = ""
then I sent to FreeTiles view controller (other view controller) through tableview and prepareForSegue:
func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
//let cell = tableView.dequeueReusableCellWithIdentifier(reuseContentFreeIdentifier, forIndexPath: indexPath) as! FreeTableViewCell
var contentku = contents[indexPath.row] as ContentModel
kontenid = contentku.id
judulkonten = contentku.title
performSegueWithIdentifier("lemparkonten", sender: self)
}
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
// Get the new view controller using segue.destinationViewController.
// Pass the selected object to the new view controller.
if (segue.identifier == "lemparkonten") {
var svc = segue.destinationViewController as! FreeTiles;
svc.idcontent = kontenid
svc.namacontent = judulkonten
}
}
And on FreeTiles view controller, I put this inside class:
var idcontent :String!
var namacontent :String!
But when I println on FreeTiles view controller:
println("konten id nya:\(idcontent)")
println("judul nya:\(namacontent)")
I got two log of idcontent and two log of namacontent, the first is empty and the second filled with correct idcontent and namacontent.
How to avoid get two threw variable result when sent variable between two view controller? What is the correct code to get only one result for each threw variable?
Edited:
it seems like cache on xcode log because when I change println, it show only one log.
if idcontent.isEmpty && namacontent.isEmpty {
//println("Nothing to see here")
}else{
var content_id = idcontent
var content_name = namacontent
println("content_id:\(content_id)")
println("content_name:\(content_name)")
}
So, I make filter on FreeTiles (second view controller) to get the filled variable.
Regards.
First of all, if you are declaring global variables then you need to specify it's scope. Let's say:
public var kontenid = ""
public var judulkonten = ""
Further, once you have declared global variables you can access it from anywhere and it will also reflect when you will change the value.
So, after assigning values to the variables
func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
//let cell = tableView.dequeueReusableCellWithIdentifier(reuseContentFreeIdentifier, forIndexPath: indexPath) as! FreeTableViewCell
var contentku = contents[indexPath.row] as ContentModel
kontenid = contentku.id
judulkonten = contentku.title
performSegueWithIdentifier("lemparkonten", sender: self)
}
In your prepareforsegue method, you don't need to assign the values:
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
// Get the new view controller using segue.destinationViewController.
// Pass the selected object to the new view controller.
if (segue.identifier == "lemparkonten") {
var svc = segue.destinationViewController as! FreeTiles;
}
}
Now you can access the above global variables anywhere so you can directly use it in other class:
if idcontent.isEmpty && namacontent.isEmpty {
//println("Nothing to see here")
}else{
var content_id = kontenid
var content_name = judulkonten
println("content_id:\(content_id)")
println("content_name:\(content_name)")
}
}
Let me know, if you have any issues against this code.
You are printing your variables 2 times out with your code:
override func viewDidLoad() {
super.viewDidLoad()
println("konten id nya:(idcontent)") // first
println("judul nya:(namacontent)") // second
if idcontent.isEmpty && namacontent.isEmpty { //println("Nothing to see here")
} else {
var content_id = idcontent var content_name = namacontent
println("content_id: (content_id)") // 3
println("content_name:(content_name)") // 4
}}
Your Code seems doing well to send data through segues. Otherwise you could check my answer here:
Sending data with Segue with Swift