Refreshing table view with a UIRefreshControl in swift - ios

I am working on an iOS app in which I have a UITableView which needs to be refreshed on command. I have added a UIRefreshControl object and hooked up a function to it so that whenever I drag down on the table view, my refresh code is supposed to take place. The code fragment is below:
#IBAction func refresh(sender: UIRefreshControl?) {
self.tableView.setContentOffset(CGPointMake(0, self.tableView.contentOffset.y-self.refreshControl!.frame.size.height), animated: true)
sender?.beginRefreshing()
if (profileActivated) {
self.matches = []
let dynamoDBObjectMapper = AWSDynamoDBObjectMapper.defaultDynamoDBObjectMapper()
let queryExpression = AWSDynamoDBScanExpression()
queryExpression.limit = 100;
// retrieving everything
dynamoDBObjectMapper.scan(DDBMatch.self, expression: queryExpression).continueWithBlock({ (task:AWSTask!) -> AnyObject! in
if task.result != nil {
let paginatedOutput = task.result as! AWSDynamoDBPaginatedOutput
//adding all to the matches array
for item in paginatedOutput.items as! [DDBMatch] {
self.matches.append(item)
}
//code to filter and sort the matches array...
self.tableView.reloadData()
}
return nil
})
self.tableView.reloadData()
}
sender?.endRefreshing()
self.tableView.setContentOffset(CGPointMake(0, self.tableView.contentOffset.y - (0.5*self.refreshControl!.frame.size.height)), animated: true)
}
Unfortunately, this code does not quite work right. Whenever I drag down on the refresh control, my table populates but then immediately goes away, and then refreshes about 10-15 seconds later. I inserted some print statements, and the data is all there, it just does not appear for a long time, and I am having trouble making it appear as soon as it is retrieved. I am new to iOS programming so any help would be appreciated.
Thanks in advance

Related

Trying to reloadData() in viewWillAppear

I have a tabBarController and in one of the tabs I can select whatever document to be in my Favourites tab.
So when I go to the Favourites tab, the favourite documents should appear.
I call the reloading after fetching from CoreData the favourite documents:
override func viewWillAppear(_ animated: Bool) {
languageSelected = UserDefaults().string(forKey: "language")!
self.title = "favourites".localized(lang: languageSelected)
// Sets the search Bar in the navigationBar.
let search = UISearchController(searchResultsController: nil)
search.searchResultsUpdater = self
search.obscuresBackgroundDuringPresentation = false
search.searchBar.placeholder = "searchDocuments".localized(lang: languageSelected)
navigationItem.searchController = search
navigationItem.hidesSearchBarWhenScrolling = false
// Request the documents and reload the tableView.
fetchDocuments()
self.tableView.reloadData()
}
The fetchDocuments() function is as follows:
func fetchDocuments() {
print("fetchDocuments")
// We make the request to the context to get the documents we want.
do {
documentArray = try context.fetchMOs(requestedEntity, sortBy: requestedSortBy, predicate: requestedPredicate)
***print(documentArray) // To test it works ok.***
// Arrange the documentArray per year using the variable documentArrayPerSection.
let formatter = DateFormatter()
formatter.dateFormat = "yyyy"
for yearSection in IndexSections.sharedInstance.allSections[0].sections {
let documentsFilteredPerYear = documentArray.filter { document -> Bool in
return yearSection == formatter.string(from: document.date!)
}
documentArrayPerSection.append(documentsFilteredPerYear)
}
} catch {
print("Error fetching data from context \(error)")
}
}
From the statement print(documentArray) I see that the function updates the content. However there is no reload of documents in the tableView.
If I close the app and open it again, then it updates.
Don't know what am I doing wrong!!!
The problem is that you're always appending to documentArrayPerSection but never clearing it so I imagine the array was always getting bigger but only the start of the array which the data source of the tableView was requesting was being used. Been there myself a few times.
I assume that reloadData() is called before all data processing is done. To fix this you will have to call completion handler when fetching is done and only then update tableView.
func fetchDocuments(_ completion: #escaping () -> Void) {
do {
// Execute all the usual fetching logic
...
completion()
}
catch { ... }
}
And call it like that:
fetchDocuments() {
self.tableView.reloadData()
}
Good luck :)

Swift FireStore Listener throws error when loading application the second time

Hi I am in desperate need for some help
All this is happening in a UIViewController child class
I am currently attaching the listener and populating an array and then feeding it to a UICollectionView in the following function (excuse some of the cut off code):
fileprivate func fetchNotes() { // This function is called in vidDidLoad()
let db = Firestore.firestore()
// Attaching listener (ie. listener is an attribute of the class)
listener = db.collection("Courses").document(titleForNavBar).collection("Notes")
.addSnapshotListener { snapshot, error in
// checking for any error
if error != nil {
self.arrayOfNotes.removeAll()
self.allNotesView.arrayOfNotes = self.arrayOfNotes
DispatchQueue.main.async {
self.allNotesView.allNotesCollectionView.reloadData()
}
return
} else {
self.arrayOfNotes.removeAll()
// if there is no error, the array holding all the objects is populated, in a for..loop
for document in (snapshot?.documents)! {
if let noteName = document.data()["noteName"] as? String,
let lectureInformation = document.data()["lectureInformation"] as? String,
let noteDescription = document.data()["noteDescription"] as? String,
let forCourse = document.data()["forCourse"] as? String,
let storageReference = document.data()["storageReference"] as? String,
let noteSize = document.data()["noteSize"] as? Int,
let rating = document.data()["rating"] as? Int
{
self.arrayOfNotes.append(Note(forCourse: forCourse, lectureInformation: lectureInformation, noteDescription: noteDescription, noteName: noteName, noteSize: noteSize, rating: rating, storageReference: storageReference))
self.allNotesView.arrayOfNotes = self.arrayOfNotes
// reloading the UICollectionView (on the main thread) so that it displays new data
DispatchQueue.main.async {
self.allNotesView.allNotesCollectionView.reloadData()
}
}
}
}
}
}
When the view disappears, I am also removing the listener
override func viewDidDisappear(_ animated: Bool) {
super.viewDidDisappear(true)
if listener != nil {
listener.remove()
}
print("listener removed")
}
This works fine, when I install the application for the first time on any device or simulator. When I try to launch the controller, the second time, I get a very nasty error that I have no idea how to debug.
To be accurate the console throws this error:
NoteShare[97230:10528984] *** Assertion failure in -[FSTLevelDBRemoteDocumentCache decodedMaybeDocument:withKey:], third_party/firebase/ios/Source/Firestore/Source/Local/FSTLevelDBRemoteDocumentCache.mm:152
I know this question is quite long (sorry about that), but have you guys come across this error. Please give some hint on how to solve this problem. Thanks! If you need to see any other piece of my code, please let me know.
It seems to be failing here. I don't see what you could be doing wrong in your code to cause that, so you may have hit a bug. It seems very similar to this issue, which has been fixed in the repo but not been released.

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)
}

Swift 2 + Parse: Array index out of range

SOMETIMES THE REFRESH WORKS SOMETIMES IT DOESN'T
I have a UITableViewController which is basically a news feed. I have also implemented a pull to refresh feature. However sometimes when I pull to refresh it gives me the error
'Array index out of range'.
I know this means an item it is trying to get does not exist but can you tell me why? Here is my code:
override func viewDidLoad() {
super.viewDidLoad()
refresher = UIRefreshControl()
refresher.attributedTitle = NSAttributedString(string: "Pull to refresh")
refresher.addTarget(self, action: "refresh", forControlEvents: UIControlEvents.ValueChanged)
self.tableView.addSubview(refresher)
refresh()
tableView.delegate = self
tableView.dataSource = self
}
and the refresh() function:
func refresh() {
//disable app while it does stuff
UIApplication.sharedApplication().beginIgnoringInteractionEvents()
//get username and match with userId
let getUser = PFUser.query()
getUser?.findObjectsInBackgroundWithBlock({ (objects, error) -> Void in
if let users = objects {
//clean arrays and dictionaries so we dont get indexing error???
self.messages.removeAll(keepCapacity: true)
self.users.removeAll(keepCapacity: true)
self.usernames.removeAll(keepCapacity: true)
for object in users {
if let user = object as? PFUser {
//make userId = username
self.users[user.objectId!] = user.username!
}
}
}
})
let getPost = PFQuery(className: "Posts")
getPost.findObjectsInBackgroundWithBlock { (objects, error) -> Void in
if error == nil {
if let objects = objects {
self.messages.removeAll(keepCapacity: true)
self.usernames.removeAll(keepCapacity: true)
for object in objects {
self.messages.append(object["message"] as! String)
self.usernames.append(self.users[object["userId"] as! String]!)
self.tableView.reloadData()
}
}
}
}
self.refresher.endRefreshing()
UIApplication.sharedApplication().endIgnoringInteractionEvents()
}
and:
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let myCell = tableView.dequeueReusableCellWithIdentifier("SinglePostCell", forIndexPath: indexPath) as! PostCell
//ERROR GETS REPORTED ON THE LINE BELOW
myCell.usernamePosted.text = usernames[indexPath.row]
myCell.messagePosted.text = messages[indexPath.row]
return myCell
}
You have a race condition given you are doing two background tasks, where the second depends on values returned from the first. getUser?.findObjectsInBackgroundWithBlockwill return immediately, and getPost.findObjectsInBackgroundWithBlock will start executing. The getPost should be inside the block for getUser, to ensure the sequence is correct.
Similarly, the following two lines should be inside the second block:
self.refresher.endRefreshing()
UIApplication.sharedApplication().endIgnoringInteractionEvents()
Given the error line, you probably also have a race condition between the two background tasks and displaying the tableView. I would be inclined to try:
func tableView(tableView:UITableView!, numberOfRowsInSection section:Int) {
return self.refresher.refreshing ? 0 : self.usernames.count
}
This way you won't touch self.usernames until the background refresh is finished (as long as you remember to put endRefreshing inside the second block, which is also put inside the first block).
I Believe that in self.users[user.objectId!] = user.username! the user.ObjectId is some random value assigned by parse which looks like this: "34xcf4". This is why you might be getting 'Array index out of range'.
There are two required methods for configuring a UITableView:
tableView(_:cellForRowAtIndexPath:)
and
tableView(_:numberOfRowsInSection:)
In your code you are presenting only one required method, if you don't implement the second method then it that may cause errors.
Check the documentation at:
https://developer.apple.com/library/ios/documentation/UIKit/Reference/UITableViewDataSource_Protocol/#//apple_ref/occ/intfm/UITableViewDataSource/tableView:cellForRowAtIndexPath:
You are calling self.tableView.reloadData() on every addition to your array and doing so in a background thread.
As a general rule, you should not do UI updates in a background thread. When you clear self.messages and self.usernames, because you are in background thread, nothing prevents the tableview from trying to get a cell at an index that no longer has any data in the array.
If you want to keep your code in the background thread (risky as it may be), you should at least call .beginUpdates before reloading your arrays and wait until they're all done before calling reload and endUpdates.

UITableView not updating when switching between tabs

Preface: I've tried adding tableView.reloadData() to viewWillAppear (...and viewDidLoad, viewDidAppear, etc.) of the UITableViewController that's not updating. I threw in setNeedsDisplay for S's & G's, too.
I have a UITabBarController with 3 tabs on it. Each tab is a TableViewController is backed by Core Data and is populated with NSManagedObjects from one NSManagedObjectContext.
In TableViewController1 I make changes to the cells, the tableView reloads properly and reflects the changes. If I click the tab for TableViewController2, the changes made on TVC1 aren't reflected.
The changes made on TVC1 are persisting between launches, as I see them on TVC2 when I close the app and relaunch it.
What am I missing? Any ideas would be greatly appreciated.
Update
Here's the code in question:
func markFavorite(sender: AnyObject) {
// Store the sender in case you need it later. Might not need this.
clickedFavoriteButton = sender as! UIButton
if resultsSearchController.active {
let indexPath = sender.tag
let selectedSound = self.filteredSounds[indexPath]
print("markFavorite's sender tag is \(indexPath)")
if selectedSound.favorite == 1 {
selectedSound.favorite = 0
} else {
selectedSound.favorite = 1
}
saveManagedObjectContext()
} else {
let indexPath = sender.tag
let selectedSound = self.unfilteredSounds[indexPath]
print("markFavorite's sender tag is \(indexPath)")
if selectedSound.favorite == 1 {
selectedSound.favorite = 0
} else {
selectedSound.favorite = 1
}
saveManagedObjectContext()
}
}
func saveManagedObjectContext() {
if managedObjectContext.hasChanges {
do {
try self.managedObjectContext.save()
} catch {
// catch error here
}
}
self.tableView.reloadData()
}
You should always use a NSFetchedResultsController to display Core Data items in a table view. It comes with delegate methods that update your table as the underlying data changes (even before saving).
To get started, examine the Xcode template (Master-Detail) implementation. Once you get the hang of it you will love it. Everything works pretty much out of the box.
You may have to trigger context.save() manually because core-data isn't saving the data right away.
let context = (UIApplication.sharedApplication().delegate as! AppDelegate).managedObjectContext!
runOnMain() {
do {
try! self.context.save()
} catch let error as NSError {
//Handle any upcoming errors here.
}
}
Its important to run the method on the main thread otherwise you will get an error.
this method should do the job:
func runOnMain(block: dispatch_block_t) {
if NSThread.isMainThread() {
block()
}else{
dispatch_sync(dispatch_get_main_queue(), block)
}
}
Please let me know if this approach worked for you.
You should not try to reload data at any point in view controller lifecycle. Instead create delegates for each tab bar controller, set them properly and call delegate methods only when something really change in your data source. If you are not familiar with delegation you can learn more about it here

Resources