Table View not reloading in swift - ios

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

Related

image and label in interface builder overlap my data in the TableView cell

I am a beginner in iOS development, and I want to make an instagram clone app, and I have a problem when making the news feed of the instagram clone app.
So I am using Firebase to store the image and the database. after posting the image (uploading the data to Firebase), I want to populate the table view using the uploaded data from my firebase.
But when I run the app, the dummy image and label from my storyboard overlaps the downloaded data that I put in the table view. the data that I download will eventually show after I scroll down.
Here is the gif when I run the app:
http://g.recordit.co/iGIybD9Pur.gif
There are 3 users that show in the .gif
username (the dummy from the storyboard)
JokowiRI
MegawatiRI
After asynchronously downloading the image from Firebase (after the loading indicator is dismissed), I expect MegawatiRI will show on the top of the table, but the dummy will show up first, but after I scroll down and back to the top, MegawatiRI will eventually shows up.
I believe that MegawatiRI is successfully downloaded, but I don't know why the dummy image seems overlaping the actual data. I don't want the dummy to show when my app running.
Here is the screenshot of the prototype cell:
And here is the simplified codes of the table view controller:
class NewsFeedTableViewController: UITableViewController {
var currentUser : User!
var media = [Media]()
override func viewDidLoad() {
super.viewDidLoad()
tabBarController?.delegate = self
// to set the dynamic height of table view
tableView.estimatedRowHeight = StoryBoard.mediaCellDefaultHeight
tableView.rowHeight = UITableViewAutomaticDimension
// to erase the separator in the table view
tableView.separatorColor = UIColor.clear
}
override func viewWillAppear(_ animated: Bool) {
// check wheter the user has already logged in or not
Auth.auth().addStateDidChangeListener { (auth, user) in
if let user = user {
RealTimeDatabaseReference.users(uid: user.uid).reference().observeSingleEvent(of: .value, with: { (snapshot) in
if let userDict = snapshot.value as? [String:Any] {
self.currentUser = User(dictionary: userDict)
}
})
} else {
// user not logged in
self.performSegue(withIdentifier: StoryBoard.showWelcomeScreen, sender: nil)
}
}
tableView.reloadData()
fetchMedia()
}
func fetchMedia() {
SVProgressHUD.show()
Media.observeNewMedia { (mediaData) in
if !self.media.contains(mediaData) {
self.media.insert(mediaData, at: 0)
self.tableView.reloadData()
SVProgressHUD.dismiss()
}
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: StoryBoard.mediaCell, for: indexPath) as! MediaTableViewCell
cell.currentUser = currentUser
cell.media = media[indexPath.section]
// to remove table view highlight style
cell.selectionStyle = .none
return cell
}
}
And here is the simplified code of the table view cell:
class MediaTableViewCell: UITableViewCell {
var currentUser: User!
var media: Media! {
didSet {
if currentUser != nil {
updateUI()
}
}
}
var cache = SAMCache.shared()
func updateUI () {
// check, if the image has already been downloaded and cached then just used the image, otherwise download from firebase storage
self.mediaImageView.image = nil
let cacheKey = "\(self.media.mediaUID))-postImage"
if let image = cache?.object(forKey: cacheKey) as? UIImage {
mediaImageView.image = image
} else {
media.downloadMediaImage { [weak self] (image, error) in
if error != nil {
print(error!)
}
if let image = image {
self?.mediaImageView.image = image
self?.cache?.setObject(image, forKey: cacheKey)
}
}
}
So what makes the dummy image overlaps my downloaded data?
Answer
The dummy images appear because your table view controller starts rendering cells before your current user is properly set on the tableViewController.
Thus, on the first call to cellForRowAtIndexPath, you probably have a nil currentUser in your controller, which gets passed to the cell. Hence the didSet property observer in your cell class does not call updateUI():
didSet {
if currentUser != nil {
updateUI()
}
}
Later, you reload the data and the current user has now been set, so things start to work as expected.
This line from your updateUI() should hide your dummy image. However, updateUI is not always being called as explained above:
self.mediaImageView.image = nil
I don't really see a reason why updateUI needs the current user to be not nil. So you could just eliminate the nil test in your didSet observer, and always call updateUI:
var media: Media! {
didSet {
updateUI()
}
Alternatively, you could rearrange your table view controller to actually wait for the current user to be set before loading the data source. The login-related code in your viewWillAppear has nested completion handers to set the current user. Those are likely executed asynchronously .. so you either have to wait for them to finish or deal with current user being nil.
Auth.auth etc {
// completes asynchronously, setting currentUser
}
// Unless you do something to wait, the rest starts IMMEDIATELY
// currentUser is not set yet
tableView.reloadData()
fetchMedia()
Other Notes
(1) I think it would be good form to reload the cell (using reloadRows) when the image downloads and has been inserted into your shared cache. You can refer to the answers in this question to see how an asynch task initiated from a cell can contact the tableViewController using NotificationCenter or delegation.
(2) I suspect that your image download tasks currently are running in the main thread, which is probably not what you intended. When you fix that, you will need to switch back to the main thread to either update the image (as you are doing now) or reload the row (as I recommend above).
Update your UI in main thread.
if let image = image {
DispatchQueue.main.async {
self?.mediaImageView.image = image
}
self?.cache?.setObject(image, forKey: cacheKey)
}

Firebase logged in user not loading fast enough before segue

I'm hitting this button
override func viewDidLoad() {
FIRAuth.auth()?.signIn(withEmail: "hakuna.matata.kitty#gmail.com", password: "password") {
(user, error) in
// TODO: SET UP A SPINNER ICON
print("Logged into hmk")
self.performSegue(withIdentifier: "login", sender: self)
}
}
As you can see, on callback it redirects to a UITableViewController. The populator code sits in the initializer of UITableViewController, the sole purpose in the closure is to populate self.chatsList
Sometimes, whenever I test out the app, the UITableViewController is blank.
ref.child("users").child(currentUser.uid).observeSingleEvent(of: .value, with: { (snapshot) in
guard snapshot.exists() else {
print("Error: Path not found")
return
}
let value = snapshot.value as? NSDictionary
// Convert a dict of dicts into an array of dicts
// You will lose chatID in the process, so be warned
// Unless you would like to to keep the data in a struct
for (chatIdKey, secondDict) in value?["chats"] as! [String: NSDictionary] {
// This only appends metadata for the last chat
// Does not load every chat message
self.chatsList.append(Chat(chatId: chatIdKey, targetChat: secondDict))
}
})
But when I hit the back key, wait long enough, and login again, the table is populated properly.
How can I make sure that it loads correctly every time? I expected the performSegue on callback would guarantee that we do have a currentUser...
I think your observer should be changed. I see this tableview will be some kind of chat so you might want to keep the reference alive while the user is in that ViewController so instead of using observeSingleEvent you could just observe and you manually remove the observer when leaving the view.
Another thing is that you are observing the value when you should probably observe the childAdded as if any event is added while being in the "chat" view the user will not receive it.
Change the line below and see if this works for you.
ref.child("users").child(currentUser.uid).observe(.childAdded, with: { (snapshot) in
Hope it helps!

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

popViewController does not go back

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.

Refreshing UIViewController Swift

Ok in my app i have a uiviewcontroller for the current user profile and a tableview. When the user clicks a table view cell that contains the current users followers, i refresh the view controller with the new user profile. When i add my code in the viewDidLoad everytime i hit back in the navigation bar it goes back to the last user but when i keep clicking a table view cell the app gets a memory warning and crashes. Now i tried in the viewDidAppear and doing this didnt get no crashes or memory warnings but when i hit back it goes back to the current user
Here is how i set up my code in
override func viewDidAppear(animated: Bool) {
super.viewDidAppear(animated)
userTableView.delegate = self
userTableView.dataSource = self
let query = PFUser.query()
query.whereKey("username", equalTo: ChartsViewController.otherUser.user)
query.getFirstObjectInBackgroundWithBlock {
(object: PFObject?, error: NSError?) -> Void in
if error != nil || object == nil {
println("The getFirstObject request failed.")
} else {
//The find succeeded.
self.userProfile = object as PFUser!
}
}
And my code when the table view cell is clicked
ChartsViewController.otherUser.user = object["username"] as String
self.navigationController!.pushViewController(self.storyboard!.instantiateViewControllerWithIdentifier("OtherUserProfileViewController") as UIViewController, animated: true)

Resources