Load UITable Data from another View Controller - ios

I have two Views:
UITableViewController (View A)
UIViewController (View B)
I was wondering, if it's possible to load and setup the table from View B and then segue to View A, when the loading is done. I need this, since the Table View loads Data from Core Data and that takes some time; I would then show a Loading Animation or something. I have a function called loadData() in View A, which fetches all Elements from Core Data and then calls tableView.reloadData().
Does anyone know, how I could implement this? Or should I somehow show the loading View directly from View A with a SubView or something?

Remember to not think about the specifics but instead, think generally:
You want to move from one VC to another and you have some data that needs to be fetched asynchronically. Let's assume you can't know how long it will take.
My suggestion is to contain all data fetching related to a VC inside that VC itself (or services/facades related to it). So basically you should present the UITableViewController and then have it fetch the data while showing skeleton-cells/spinner/etc.
You want to have separation of concerns which means you don't want your ViewController to handle data related to another view controller.
Think about the following use-case: if you have code to fetch data in the previous VC, before presenting the TVC, what happens when you need to re-fetch the data or refresh something? You will have to duplicate the code in both the VC and the TVC.
That's why it's suggested to keep data fetching inside the view controller that needs it.
If, for some reason, you still want to have your answer for this specific question:
You can have the initial VC create the TVC, but not present it yet, call its methods to fetch the data, and have it send a callback (closure/delegate/etc) when it's done fetching. When the fetching is done, present the TVC.
Here is a quick example:
class MyTableVC: UITableViewController {
private var myData: [Int] = []
public func fetchData(completion: () -> Void) {
//Fetch data asyncly
myData = [1, 2 ,3]
completion()
}
}
class MyVC: ViewController {
private func loadTableVC() {
let tableVC = MyTableVC()
tableVC.fetchData { [weak self] in
self?.present(tableVC, animated: true, completion: nil)
}
}
}
Again, I wouldn't use this due to having tight coupling between the 2 view controllers, but it's always up to you to decide how to design your code.

Related

Pass array to another View Controller

I have Tab Bar Controller, where I have few view controllers, but I want to pass array of values (workoutNames) to another view in my Tab Bar Controller. I wonder what's best option to do this and I've decided to use way of passing data with property. But when I try to retrieve data I get empty array. I could also use firestore to retrieve data in my Second View Controller, but It lasts too long, so I decided to passing data between views than retrieve data from firestore every time.
First View Controller
class HomeTableViewController: UIViewController
// I need to pass this array to another view
var workoutsName: [String] = []
...
func sendDataToCalendar() {
// IN THIS FUNCTION I RETRIEVE DATA FROM FIRESTORE AND UPDATE ARRAY WITH VALUES
// After all I print this array to check if everything is correct, and my data is here
print("\(workoutsName)")
}
Here is my Second View Controller when I want to use array from First View Controller
class CalendarViewController: UIViewController {
var arrayOfTitles = [String]()
.
.
.
func getArrayFromHome() {
let homeVC = HomeTableViewController()
homeVC.workoutsName = arrayOfTitles
// NOW I PRINT TO CHECK IF DATA EXISTS
print("\(arrayofTitles)"
}
And its empty, so data didn't pass.
Maybe it's not the best way to pass data, but main idea of this is that I will need to use this array in few view controllers. These controllers won't be connected by segue, so I can't use prepareforSegue. It's not one to one relationship (in future I will need this array in few controllers), so I shouldn't use delegate. I don't think that notification will be ok too, so I think that it's best option to pass data by property. But maybe I am wrong, so please correct me.
The reason why it doesn't work is that you instantiate a new HomeTableViewController with empty data.
If this data will be used on lots of place, why not save it locally? user default seems like it fit your needs.
func sendDataToCalendar() {
UserDefaults.standard.set(workoutsName, forKey: "workoutsName")
}
Then you can read it later on
func getWorkoutNameArray() {
let workoutName = UserDefaults.standard.object(forKey: "workoutsName") as? [String]
}
In your getArrayFromHome() function you are not accessing HomeTableViewController instance where you got the data but creating a new instance. That's why the array is empty. You end up with 2 instances of HomeTableViewController, one in use with the correct array and the dummy one created in the function with an empty array.
Would be better if you pass the data in the same place where you have a reference to CalendarViewController.
Let's say that you are creating and presenting CalendarViewController in your HomeTableViewController like:
let calendarViewController = CalendarViewController()
calendarViewController.arrayOfTitles = workoutNames
// Here is the presentation or push of calendarViewController
It will be useful for you to read this SO question

Retaining Data in a View Controller When Switching from One VC to Another

I have three views, each with its own view controller: VC1, VC2, VC3.
The user will frequently switch back and forth between each of the three views, both forward and backward.
Each view contains data: both shared from the previous view and data unique to that view.
When the user goes back to a View that he has already visited, the data displayed on that view needs to be retained (the same data as he saw the last time he visited that view), and not set to the default values the first time he visited the view.
In the first view controller, VC1, I am using a prepare for segue to push data from VC1 to VC2 or VC3:
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if segue.identifier == "segueToVC2” {
let destinationViewController: VC2 = segue.destination as! VC2;
destinationViewController.passedData1 = firstAmount
destinationViewController.passedData2 = secondAmount
destinationViewController.passedData3 = thirdAmount
} else {
let destinationViewController: VC3 = segue.destination as! VC3;
destinationViewController.passedData1 = firstAmount
destinationViewController.passedData2 = secondAmount
destinationViewController.passedData3 = thirdAmount
destinationViewController.passedData4 = fourthAmount
}
By tapping the GO BACK button on each view, I return to the previous view:
#IBAction func goBackButtonPressed(_ sender: Any) {
print("Back Button Pressed!")
self.view.window?.rootViewController?.dismiss(animated: true, completion: nil)
}
I am having trouble passing data backwards. And when I return to VC2 from VC1, data has been reset to 0. I have no segues going back from VC2 to VC1 or from VC3 to VC2. Would that be the cleanest way to pass the data back: to create another segue in Main.storyboard from VC2 to VC1 and then add another ‘if’ to my prepare for segue that checks for VC1?
I am passing ALL these variables back and forth between view controllers but only using some of them in each view controller. It seems like a waste and I don't think I am on the right track here.
Any help or suggestions?
View controllers should never store data. They are responsible for coordinating between model objects and view objects. That's their whole point. The pattern you're looking for is called MVC (Model-View-Controller) and it's a core part of iOS development.
Move your data out of the view controllers and put it into model classes. Each view controller should fetch data out of the model, and send updates into the model. The only thing the view controllers should pass between themselves is what model objects to work on, and most of the time that only needs to pass in one direction (down the stack).
Delegation can be a useful tool here, and you can also investigate "unwind segues" which are built to help you send data upstream. But again, the data you should be sending is mostly references to the model, and the model itself needs to live outside the view controllers.
It's in Objective-C, but still one of the best simple examples from Apple on MVC design is TheElements, and is worth exploring as a basis. Even without reading the Objective-C, you can see how the various pieces fit together.
I haven't studied it as much as TheElements, but Lister claims to be a good demonstration of MVC patterns in Swift using modern iOS techniques.
Why don't you call a delegate which passes the data to the view controller when you press back button.
Or if the data shared by all view controllers reflect the same value. Make a singleton class and use those values across the app.
example singleton class:
class SomeModel {
static let shared = SomeModel()
private init() {}
}

How to use an array over multiple view controllers?

I have been playing around with a lot of stuff involving arrays and scrollviews. I have mostly stayed within the confines of view controllers, so usually i'll grab data from firebase, add it to an array, and then send it to the tableview or collectionview. What I'm trying to do now is actually navigate between viewcontrollers (or multiple copies of the same view controller) and applying the array items to each view controller.
For example I want to be able to grab some photos from firebase, put them in an array of url strings or whatever. Then I want to put a photo on the background of a view controller. Then when I push the over button it goes navigates to the next view controller and puts the next photo as the background there, etc.
I understand there are probably multiple ways to do this and I was wondering what is the most efficient way? Do I just put an array in a Global class and access that from all the view controllers? Or do I just keep the array in the first view controller, then as I navigate, keep sending it to the next view controller over and over? Also there will be a LOT of items and objects and arrays here so that's why I'm looking for efficiency. Thanks in advance to anyone able to help with this, and I hope I explained it well enough!
This is a very simple way of adding and retrieving String value from a struct, here you are saving the image url string as a value in a dictionary and it's key is going to be the ViewController name.
struct SavedData {
static private var imagesDictionary: [String: String] = [:]
static func image(for viewController: UIViewController) -> String? {
return imagesDictionary["\(type(of: viewController))"]
}
static func add(image name: String, for viewController: UIViewController) {
self.imagesDictionary["\(type(of: viewController))"] = name
}
}
saving a value is very simple, if you're saving the data in a viewController and you want a specific image to be saved for that viewController you can use self
SavedData.add(image: "img1.png", for: self)
And if you want to save an image for a different viewController, do it like this.
SavedData.add(image: "img2.png", for: SecondViewController())
Retrieving the image is also very simple, you should call this method in the viewController that you want to assign the image to.
let savedImage = SavedData.image(for: self)
print(savedImage!)

Pass data between three viewController, all in navigationController, popToRootView

The issue I'm having is this.
I have a navigation controller with 3 viewController. In the 1st controller, I have the user select an image. This image is passed to 2nd and 3rd controller via prepareForSegue.
At the 3rd controller, I have a button that takes the user back to the 1st view controller. I explored 2 ways in doing this:
1) use performSegue, but I don't like this because it just push the 1st controller to my navigation stack. So I have this weird "Back" button at the 1st Viewcontroller now, which is not what I want. I want the app to take user directly to 1st viewcontroller without the back button.
2) I tried Poptorootviewcontroller. This solves the issue of the "back" button. But, when I pop back to the 1st viewcontroller, the user's selected image is still on screen. I want to clear this image when the user goes from the 3rd viewcontroller back to the 1st viewcontroller.
So with approach 2), how do I make sure all memory is refreshed and the image becomes nil in the 1st viewcontroller? Since I'm not using performSegue, 3rd viewcontroller does not have access to the 1st Viewcontroller.
For refresh, you'd have to clear it in viewWillAppear but I find this rather dangerous. Best you can do there is to create a new copy of the view controller everytime and Swift will take care of the rest. I don't know if you are using the storyboard but I would recommend using the class UIStoryboard and the function instiantiateViewControllerWithIdentifier("something") as! YourCustomVC
As long as you stay in the navigation stack, you'll not lose any of the current configurations of previous View Controllers.
As for passing data back to the first controller. You can either just throw it in the global scope which is the easiest way but might be difficult to know when it was updated or if the data is fresh. But you can always just:
var something: String = ""
class someView: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
something = "foo"
}
}
Something will be availabe everywhere then.
You could make a protocol and pass the delegate along the 3 view controllers. So when you are starting it you could do:
func someAction() {
let v = SomeViewController()
v.delegate = self
self.navigationController?.pushViewController(v, animated: true)
}
And then with each following view:
func someOtherAction() {
let v = SomeOtherViewController()
v.delegate = self.delegate
self.navigationController?.pushViewController(v, animated: true)
}
Although personally I find it hard to keep track of this.
Lastly you could use the NSNotificationCenter to pass an object along with all the data and catch it in a function on your first controller.
To do this you first register your VC for the action in viewDidLoad() or something:
NSNotificationCenter.defaultCenter().addObserver(self, selector: "someAction:", name: "someNotification", object: nil)
Then when you are done in the 3rd view make some object or a collection of string and send it back as follows:
NSNotificationCenter.defaultCenter().postNotificationName("someNotification", object: CustomObject())
And then lastly you'll catch it in the function "someAction"
func someAction(note: NSNotification) {
if let object = note.object as? CustomObject {
//Do something with it
}
}
Hope this helps!
Use an unwind segue which provides the functionality to unwind from the 3rd to the 1st (root) view controller.
The unwind segue is tied to an action in the root view controller. Within this action, you simply nil the image:
#IBAction func unwindToRootViewController(sender: UIStoryboardSegue)
{
let sourceViewController = sender.sourceViewController
// Pull any data from the view controller which initiated the unwind segue.
// Nil the selected image
myImageView.image = nil
}
As you can see in the action, segues also let you pass data back from the source view controller. This is a much simpler approach than needing to resort to using delegates, notifications, or global variables.
It also helps keep things encapsulated, as the third view controller should never need to know specifics about a parent view controller, or try to nil any image that belongs to another view controller.
In general, you pass details to a controller, which then acts on it itself, instead of trying to manipulate another controller's internals.

ios swift parse: methods with async results

When I go to a viewController I call within my viewDidAppear Method a function:
override func viewDidAppear(animated: Bool) {
getLessons()
}
This methods loads from parse.com a list of data I want to use in a pickerView.
The function itself:
func getLessons(){
var query = PFQuery(className:"Lesson")
query.orderByAscending("name")
query.findObjectsInBackgroundWithBlock {
(objects: [AnyObject]!, error: NSError!) -> Void in
if error == nil {
for object in objects {
var name = object["name"] as String
self.languagePickerKeys.append(object.objectId)
self.languagePickerValues.append(name)
self.selectedLanguage.text = self.languagePickerValues.first // set the first lessons name into the text field
self.selectedLessonObjectId = self.languagePickerKeys.first // set the first objectId for the lesson
self.languagePicker?.reloadAllComponents()
}
} else {
// Log details of the failure
println("\(error.userInfo)")
}
}
println("getLessons done")
}
The thing is, that the textfield is empty, as the getLesson() gets the data async and the data is not available to the textfield.
I also tried to put the getLesson into the viewDidAppear method, but this doesn't help me, the textfield is empty anyway.
What can I do, to have the data from the getLessons() method ready and loaded its first value into my textfield when the view is shown to the user?
You certainly have to get the data from asyncTask before setting it to pickerView.
Here's the ViewController lifecycle after instantiation:
Preparation if being segued to.
Outlet setting
Appearing and Disappearing.
So, you have two options:
Load the data in previous ViewController and then perform the segue. You need to follow these steps for it.
a. Create a segue from previous ViewController to your ViewController.
b. Call the function when you want to go next ViewController which fetches the data, and the end (after getting the data) call performSegueWithIdentifier which will lead to your ViewController.
c. Set the data in prepareForSegue
let navigationController = segue.destinationViewController as UINavigationController
navigationController.data = yourData //you got from async call
Now when you reach your ViewController, you are sure that your data is present, and you can set it to your pickerView.
If you want to do it in the same ViewController: here's is the lifeCycle of ViewController:so you need to call your function in viewDidLoad, and always set your pickerView after completion of the async network call.
Make sure that you initiate all changes to the UI from the main thread e.g. like so:
dispatch_async(dispatch_get_main_queue(), {
selectedLanguage.text = languagePickerValues.first
self.languagePicker?.reloadAllComponents()
})
The problem is that findObjectsInBackgroundWithBlock is an asynchronous method, so even if you fire it in the ViewDidLoad you will never know when you will receive the response data and you can't be sure that the data will be ready by the time you view appear.
I think you have just 2 possibility:
The first one is to load the data in the previous view controller and then just pass the data that got ready to you view controller.
The second is to use a synchronous method (the findobject method maybe?) and put the call in a method that is fired BEFORE the view appear (like the viewWillAppear: method). But your view will stuck for a moment (I think) while the data is retreiving... However this second solution probably resolve your problem but using synchronous method to retrieve data from a slower data source is usually bad design solution.
D.

Resources