popViewController does not go back - ios

I am making an app that is retrieving images from parse and showing it to a collection view. When the user taps on the images the image show in detail. I have a textfield that the user can change. When the user changes something they hit the save button and goes back to the collection view. The code that goes back to the collection view controller does not work.
#IBAction func saveButton(sender: AnyObject) {
// Use the sent country object or create a new country PFObject
if let saveInBackWithBlock = currentObject as PFObject? {
updateObject = currentObject! as PFObject
} else {
updateObject = PFObject(className:"Tops")
}
// Update the object
if let updateObject = updateObject {
updateObject["imageText"] = topsLabel.text
// Create a string of text that is used by search capabilites
var searchText = (topsLabel.text)
updateObject["searchText"] = searchText
// Update the record ACL such that the new record is only visible to the current user
updateObject.ACL = PFACL(user: PFUser.currentUser()!)
// Save the data back to the server in a background task
updateObject.saveInBackgroundWithBlock{
(success: Bool, error: NSError?) -> Void in
if (success) {
// The object has been saved.
} else {
// There was a problem, check error.description
}
}
print("saved")
}
self.navigationController?.popViewControllerAnimated(true)
}
I don't know why it doesn't go back to the previous view controller.Thank you

Is your parent view embedded in a navigation controller? also - how has the view been presented?
From the post you just added - you cannot pop a modal VC - try dismissing the view controller instead.

Check if it's embed in Navigation Controller, try:
Click StroryBoard
Select first viewController
Click Editor -> Embed in -> Navigation Controller
Hope it helps.

Related

Table View not reloading in swift

i have a add favorite button feature in my app. when a user adds a shop to his favorites, it goes to his profile.
now the table view works but when a i click the favorite button and navigate to the profile viewcontroller i dont see the new data which is the shop i favorited. but when i close and open the app, the shop becomes visible.
i want it to be once a user favorites something it will be shown immediately in his profile
i used tableView.reloadData() but it is not working. what am i doing wrong?
here is my code:
func loadData(){// i call thiss function later in the viewDidLoad()
let userID = Auth.auth().currentUser!.uid
db.collection("Favorites").whereField("usersID", isEqualTo: userID).getDocuments(){
querySnapshot, error in
if let error = error {
print(error.localizedDescription)
}else
{
if(querySnapshot!.isEmpty){
//here put you didn't favorite any shops image/text
}else{
self.shops = querySnapshot!.documents.compactMap({addShop(dictionary: $0.data())})
DispatchQueue.main.async {
self.faveList.reloadData()
print("reloaded")
}
}
}
}
}
viewDidLoad() is called only once when UIViewController created. You can try calling it on viewWillAppear() method

Dismissing a modal view but keeping the data

I'm trying to dismiss a modal view and return back to the view controller that was "sent" from, while keeping the data that was entered in the modal view. If I understand correctly I need to use delegates/protocols for this but I'm having a lot of trouble understanding how to actually implement it in this situation.
Basically a user can call a modal view to enter some information in text fields, and when they hit save this function is called:
func handleSave() {
guard let newProductUrl = NSURL(string: urlTextField.text!) else {
print("error getting text from product url field")
return
}
guard let newProductName = self.nameTextField.text else {
print("error getting text from product name field")
return
}
guard let newProductImage = self.logoTextField.text else {
print("error getting text from product logo field")
return
}
// Call save function in view controller to save new product to core data
self.productController?.save(name: newProductName, url: newProductUrl as URL, image: newProductImage)
// Present reloaded view controller with new product added
let cc = UINavigationController()
let pController = ProductController()
productController = pController
cc.viewControllers = [pController]
present(cc, animated: true, completion: nil)
}
Which calls the self.productController?.save function to save the newly entered values into core data, and reloads the productController table view with the new product.
However the issue I'm running into, is that the productController table view is dynamically set depending on some other factors, so I just want to dismiss the modal view once the user has entered the data, and return back to the page the modal view was called from.
EDIT: attempt at understanding how to implement the delegate -
ProductController is the parent class that the user gets to the modal view from:
protocol ProductControllerDelegate: class {
func getData(sender: ProductController)
}
class ProductController: UITableViewController, NSFetchedResultsControllerDelegate, WKNavigationDelegate {
override func viewDidLoad() {
super.viewDidLoad()
weak var delegate:ProductControllerDelegate?
}
func getData(sender: ProductController) {
}
And AddProductController is the modally presented controller where the user enters in the data then handleSave is called and I want to dismiss and return to the ProductController tableview it was called from:
class AddProductController: UIViewController, ProductControllerDelegate {
override func viewDidDisappear(_ animated: Bool) {
// error on this line
getData(sender: productController)
}
If the sole purpose of your protocol is to return the final state of the view controller its usually easier and clearer to use an unwind segue instead of a protocol.
Steps:
1) In the parent VC you make a #IBAction unwind(segue: UIStoryboardSegue) method
2) In the storyboard of the presented ViewController you control drag from either the control you want to trigger the exit or from the yellow view controller itself(if performing the segue in code) on to the orange exit icon.
your code should look like:
#IBAction func unwind(segue: UIStoryboardSegue) {
if let source = segue.source as? MyModalViewController {
mydata = source.data
source.dismiss(animated: true, completion: nil)
}
}
see apple documentation
Edit here is the hacky way to trigger and unwind from code without storyboard; I do not endorse doing this:
guard let navigationController = navigationController,
let presenter = navigationController.viewControllers[navigationController.viewControllers.count - 2] as? MyParentViewController else {
return
}
presenter.unwind(UIStoryboardSegue(identifier: String(describing: self), source: self, destination: presenter))
Basically you need to create a delegate into this modal view.
Let's say you have ParentViewController which creates this Modal View Controller. ParentViewController must implement the delegate method, let´s say retrieveData(someData).
On the Modal View Controller, you can use the method viewWillDisappear() to trigger the delegate method which the data you want to pass to the parent:
delegate.retrieveData(someData)
If you have issues understanding how to implement a delegate you can check this link

Core data object get saved, but doesn't show up in table until I pop to related object table and then come forward

I have three view controllers embedded in a navigation controller.
The first view controller is a list of tests, which use fetchedResultsController to populate the table.
The second view controller is a detail view controller, which allows adding a new test, editing it's detail, saving the new test or adding Questions to the test. It also includes a table of questions, which is also populated with a fetchedResultsController.
The problem I have is that when I have an existing Test which is saved, then when I go to add on questions, they correctly populate the table when I pop back to the TestDetailsVC, however, if I'm adding a new test, and then adding new questions to it, the test saves to core data, then in the QuestionDetailsVC questions also save to core data. However, when I pop back to the TestDetailsVC, the new questions don't populate the table. However, when I navigate back to the TestListVC and then go forward, the questions then populate the table correctly. I'm trying to get the questions to populate the table the first time I pop back to the table. Shouldn't the table re-fetche this data when the view loads, and the table work correctly when I navigate back since these objects are saved in core data? Why does it work correctly with a previously saved test object, but not work with a newly saved test object?
My code for saving the new test in the TestDetailsVC is:
//if we tap on a row, then we select that question to edit
//if a question is selected, we will pass that information over to
//the question editor view so it can be edited
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
//make sure there is at least one question
//objs = means object selected
if let objs = controller.fetchedObjects, objs.count > 0 {
//if there is, keep track of the test which is selected
let question = objs[indexPath.row]
//pass along that test to the editor to be edited
//the sender is the selected test at that particular row
performSegue(withIdentifier: "EditQuestionSegue", sender: question)
}
}
//we need to get ready to do the segue before we call it
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if let identifier = segue.identifier {
switch identifier {
case "EditQuestionSegue":
//find the question needing to be edited
//pass this along to the question editing view controller
if let destination = segue.destination as? QuestionDetailsVC {
if let question = sender as? Question {
destination.questionToEdit = question
//pass along the test that the question is
//related to also
if let test = testToEdit {
destination.testToEdit = test
}
}
}
case "AddNewQuestionSegue":
//if no test to edit got passed in, it means
//we're now editing a new test
//once we type in the test title, then we have to
//make sure the test has been saved if it's new test
//before we go on to edit questions
if testToEdit == nil {
saveTestBeforeAddingQuestion()
//make sure we set up as if new
//before we start adding questions
//these are called in the view did load
}
if let destination = segue.destination as? QuestionDetailsVC {
if let test = testToEdit {
destination.testToEdit = test
}
}
default:
print("no segue this time")
}
}
}
func saveTestBeforeAddingQuestion() {
var test: Test!
//if there's not a passed in value into the testToEdit core data
//object test entity, then we're going to edit as if new
if testToEdit == nil {
//then instantiate a new test object ready to be written to
test = Test(context: context)
} else {
test = testToEdit
}
if let title = titleTextField.text {
test.title = title
}
if let abrevTitle = abrevTitleTextField.text {
test.abrevTitle = abrevTitle
}
if let author = authorTextField {
test.author = author.text
}
if let publisher = publisherTextField {
test.publisher = publisher.text
}
ad.saveContext()
//since we have now saved a new test
//let's put that test into our testToEdit variable
//so we can pass it along to the next view controller
//during our segue
if let newTestCreated = test {
testToEdit = newTestCreated
}
}
My code for saving the new question is:
#IBAction func savePressed(_ sender: UIButton) {
var question: Question!
//if there's not a passed in value into the questionToEdit core data
//object test entity, then we're going to edit as if new
if questionToEdit == nil {
//then instantiate a new test object ready to be written to
question = Question(context: context)
} else {
question = questionToEdit
}
//make sure to relate the question added to the testToEdit test
question.test = self.testToEdit
//if there is something in the sentence text field
//then assign that value to the question.sentence attribute
//of the sentence entity in our core data context
if let sentence = questionSentenceTextField.text {
question.sentence = sentence
}
if let identifier = questionIdentifierTextField.text {
question.identifier = identifier
}
if let displayOrder = questionDisplayOrderTextField.text {
question.displayOrder = Int(displayOrder) as NSNumber?
}
// save the context
ad.saveContext()
_ = navigationController?.popViewController(animated: true)
}
I spent days trying to get the question table to load, but finally discovered that it wasn't loading back up because when I pop back to the view controller it doesn't automatically reload the table with the fetchedResultsController. So I had to place a call to refetch the data in the viewWillAppear method and then reload the tableView:
(TestDetailsVC)
override func viewWillAppear(_ animated: Bool) {
guard testToEdit != nil else {return}
attempFetch()
tableView.reloadData()
}

EXC_BAD_ACCESS while passing array to new ViewController

I'm loading some XML from a webservice (car data), create some car objects and would like to display them in a TableViewController.
When the user has selected start and destination location, I'm making an async call to the webservice, show an activity indicator and as soon as the data is loaded, I go to a new view. So I have something like this:
class NewReservationViewController : UIViewController {
#IBAction func searchCarsClicked(sender: UIBarButtonItem) {
//show load cars activity indicator
loadingCarsActivityIndicator.startAnimating()
//load available cars from webservice asyncronously
DataManager.getAvailableCars([...parameter list...], carsLoadedCallback: carsLoaded)
}
func carsLoaded(loadedCars: [Car]) {
//dismiss the waiting widget
//trigger the segue and advance to the next screen
loadingCarsActivityIndicator.stopAnimating()
print("stopped cars loading activity indicator")
print("cars loaded callback called")
print("loaded \(loadedCars.count) distinct cars")
self.cars = loadedCars
performSegueWithIdentifier("showAvailableCars", sender: self)
}
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
if segue.identifier == "showAvailableCars" {
let carTableViewController = segue.destinationViewController as! CarTableViewController
carTableViewController.cars = self.cars
presentViewController(carTableViewController, animated: false, completion: nil)
}
}
}
class DataManager {
class func getAvailableCars([...parameter list...], carsLoadedCallback: ([Car]) -> Void){
Webservice.getAvailability([...parameter list...], completionHandler: { (data, response, error) in
//parse xml
//in the end I get an Array of Car objects
var cars: [Car] = ...
carsLoadedCallback(cars)
})}
}
}
When I populate the TableView with some DummyData I create within the CarTableViewController class, it works fine. However when I try to pass the car arrays to my TableViewController I get a EXC_BAD_ACCESS code = 2 exception. As far as I know this is some kind of Memory exception that is usually caused by a corrupt pointer. So I guess that the car array I created in my static DataManager class within the static method I called gets destroyed. However I'm not sure about that because automatic reference counting should avoid that.
The table view even displays the data but then immediately crashes with the EXC_BAD_ACCESS exception. I tried to set a general breakpoint in the XCode's breakpoints tab but however I don't get a reasonable error message on why the app crashes.
Do you have any ideas on why this happens. How can I get a better error message?
Thanks for your help in advance.
First of all check this function:
func carsLoaded(loadedCars: [Car])
It is returned as callback - what thread is it running? main or backround?
You should call on main thread
dispatch_async(dispatch_get_main_queue(), {
performSegueWithIdentifier("showAvailableCars", sender: self)
})
If doesn't help - provide the line, where it breaks in debugger, so I can help and see more.
UPD: Didn't notice, why do you do manual present?
presentViewController(carTableViewController, animated: false, completion: nil)
Your segue automatically shows view controller, you can change it's style (modal, push) in storyboard.
There are two potential issues that I can see. The first is that you are triggering your segue from a method that might not be on the main thread, you need to ensure that this is done on the main thread. The other issue is that in your prepare for segue you are unwrapping the new ViewController without checking it, so try this instead:
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
if segue.identifier == "showAvailableCars" {
if let carTableViewController = segue.destinationViewController as? CarTableViewController {
carTableViewController.cars = self.cars
presentViewController(carTableViewController, animated: false, completion: nil)
}
}
}
As you said the table view displays the data and then immediately crashes I would assume that the problem is in your CarTableViewController. One thing you could check is when your cells are rendered, if you are trying to access some information that is null from the Car objects

SWIFT / iOS: Data takes a few seconds to load when screen appears

I've created a user profile screen, and when loaded, the data takes a few seconds to appear. I'm looking for a more elegant solution.
Here's the view controller in the storyboard:
As you can see, there is placeholder text. The issue is that this text appears very briefly when this screen is loaded (before the data is retrieved from the database).
Here is what the screen looks like when the data is fully loaded:
I've seen that an Activity Indicator on the previous screen may be a good solution. Can anyone verify this and point me to a good SWIFT tutorial or Stack Overflow solution?
UPDATE:
I'm loading the data on the previous View Controller as suggested. The issue that I'm running into is that the constants which I store my data are not accessible in prepareForSegue -
override func performSegueWithIdentifier(identifier: String, sender: AnyObject?) {
let query = PFQuery(className:"UserProfileData")
query.whereKey("username", equalTo: (PFUser.currentUser()?.username)!)
query.findObjectsInBackgroundWithBlock { (objects: [PFObject]?, error: NSError?) -> Void in
if error == nil {
if let objects = objects! as? [PFObject] {
for object in objects {
let yourselfObject = object["yourself"] as! String?
let brideGroomObject = object["brideGroom"] as! String?
}
}
} else {
print(error)
}
}
}
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
if segue.identifier == "RSVPToUserProfile" {
if let destination = segue.destinationViewController as? UserProfileViewController {
destination.yourselfPassed = yourselfObject
destination.brideGroomPassed = brideGroomObject
}
}
}
This one is actually very simple... Just remove the placeholder text from the storyboard. That way it won't "appear very briefly."
If you don't like the idea of a blank screen, then move the loading code to the place where the view is being presented. Don't present the view until after the loading code is complete. You will have to pass the data to the view controller at presentation time if you do this.
-- EDIT --
Do not override performSegueWithIdentifier:sender:! Doing so will not accomplish your goal.
I say again. Move the loading code to the place where you are calling perform segue, and delay the call until after your data is loaded. (I.E. move the perform segue call into the findObjectsInBackgroundWithBlock block after you get the items.

Resources