I am trying to use Firestore pagination with swift TableView. I used the outline of the code provided by Google in their Firestore docs. Here is my code which loads the first 4 posts from Firestore.
func loadMessages(){
let postDocs = db
.collectionGroup("userPosts")
.order(by: "postTime", descending: false)
.limit(to: 4)
postDocs.addSnapshotListener { [weak self](querySnapshot, error) in
self?.q.async{
self!.posts = []
guard let snapshot = querySnapshot else {
if let error = error {
print(error)
}
return
}
guard let lastSnapshot = snapshot.documents.last else {
// The collection is empty.
return
}
//where do I use this to load the next 4 posts?
let nextDocs = Firestore.firestore()
.collectionGroup("userPosts")
.order(by: "postTime", descending: false)
.start(afterDocument: lastSnapshot)
if let postsTemp = self?.createPost(snapshot){
DispatchQueue.main.async {
self!.posts = postsTemp
self!.tableView.reloadData()
}
}
}
}
}
func createPost(_ snapshot: QuerySnapshot) ->[Post]{
var postsTemp = [Post]()
for doc in snapshot.documents{
if let firstImage = doc.get(K.FStore.firstImageField) as? String,
let firstTitle = doc.get(K.FStore.firstTitleField) as? String,
let secondImage = doc.get(K.FStore.secondImageField) as? String,
let secondTitle = doc.get(K.FStore.secondTitleField) as? String,
let userName = doc.get(K.FStore.poster) as? String,
let uID = doc.get(K.FStore.userID) as? String,
let postDate = doc.get("postTime") as? String,
let votesForLeft = doc.get("votesForLeft") as? Int,
let votesForRight = doc.get("votesForRight") as? Int,
let endDate = doc.get("endDate") as? Int{
let post = Post(firstImageUrl: firstImage,
secondImageUrl: secondImage,
firstTitle: firstTitle,
secondTitle: secondTitle,
poster: userName,
uid: uID,
postDate: postDate,
votesForLeft: votesForLeft,
votesForRight:votesForRight,
endDate: endDate)
postsTemp.insert(post, at: 0)
}else{
}
}
return postsTemp
}
Here is my delegate which also detects the end of the TableView:
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let post = posts[indexPath.row]
let cell = tableView.dequeueReusableCell(withIdentifier: K.cellIdentifier, for: indexPath) as! PostCell
cell.delegate = self
let seconds = post.endDate
let date = NSDate(timeIntervalSince1970: Double(seconds))
let formatter = DateFormatter()
formatter.dateFormat = "M/d h:mm"
if(seconds <= Int(Date().timeIntervalSince1970)){
cell.timerLabel?.text = "Voting Done!"
}else{
cell.timerLabel?.text = formatter.string(from: date as Date)
}
let firstReference = storageRef.child(post.firstImageUrl)
let secondReference = storageRef.child(post.secondImageUrl)
cell.firstTitle.setTitle(post.firstTitle, for: .normal)
cell.secondTitle.setTitle(post.secondTitle, for: .normal)
cell.firstImageView.sd_setImage(with: firstReference)
cell.secondImageView.sd_setImage(with: secondReference)
cell.userName.setTitle(post.poster, for: .normal)
cell.firstImageView.layer.cornerRadius = 8.0
cell.secondImageView.layer.cornerRadius = 8.0
if(indexPath.row + 1 == posts.count){
print("Reached the end")
}
return cell
}
Previously I had an addSnapshotListener without a limit on the Query and just pulled down all posts as they came. However I would like to limit how many posts are being pulled down at a time. I do not know where I should be loading the data into my model. Previously it was being loaded at the end of the addSnapshotListener and I could still do that, but when do I use the next Query? Thank you for any help and please let me know if I can expand on my question any more.
I’m assuming that your method of detecting when user reaches the bottom of the tableView items is correct.
In my personal opinion setting real-time listeners for pagination would be quite a challenge. I recommend you using a bunch of get calls to do this.
If done in that way, what you need is a function that every time it’s called, it brings the next set of posts. For example, first time it’s called, it’ll fetch 4 latest docs A.K.A posts. Second time it’s called, it’ll fetch the next latest set of posts (4). To clarify the resulting posts from first call is newer than second call. Hopefully this is making sense.
How to?
Maintain two properties, one that keeps track of last document fetched, And one that stores all the posts fetched up to now(array or any applicable data structures). If the function gets called 4 times the array I’m talking about here would have 16posts (provided that there are >= 16 posts in firestore).
Now since we have the point to which we fetched the posts from firestore now, we can use the Firestore API to configure the query to fetch the next set, first call onwards. Each time a set of documents/posts is received it’s appended to the array.
Oh almost forgot, the function I’m speaking of here, has to be called every time the User reaches tableView end.
This solution may or may not be ideal for you, but hopefully it at-least leads you down some path to finding a solution. Any questions are welcome, happy to help..
//I have this solution working in a project, the approach is to detect when the user scrolls and the offset is getting close to the top
//When this happens, you get the next bunch of elements from firestore, insert them in your data source and finallly reload the tableview keeping the scroll offset.
//below are the related methods, hope it helps.
func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
if scrollView.contentOffset.y < 300{
self.stoppedScrolling()
}
}
func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) {
if !decelerate {
if scrollView.contentOffset.y < 300{
self.stoppedScrolling()
}
}
}
//When the tableview stops scrolling you call your method getNextPosts which should be very similar to your loadMessages, maybe you dont need a listener, you just need the next posts.
func stoppedScrolling() {
getNextPosts { posts in
self.insertNextPosts(posts)
}
}
//Insert the new messages that you just got
private func insertNextPosts(_ posts: [Post]){
self.messages.insert(contentsOf: posts, at: 0)
self.messagesCollectionView.reloadDataAndKeepOffset()
}
//This function es from MessageKit: https://messagekit.github.io, take it only as reference, besides is for a collectionview but you can adapt it to tableview
public func reloadDataAndKeepOffset() {
// stop scrolling
setContentOffset(contentOffset, animated: false)
// calculate the offset and reloadData
let beforeContentSize = contentSize
reloadData()
layoutIfNeeded()
let afterContentSize = contentSize
// reset the contentOffset after data is updated
let newOffset = CGPoint(
x: contentOffset.x + (afterContentSize.width - beforeContentSize.width),
y: contentOffset.y + (afterContentSize.height - beforeContentSize.height))
setContentOffset(newOffset, animated: false)
}
Related
I am trying to use Firestore pagination with swift TableView. Here is my code which loads the first 4 posts from firestore.
func loadMessages(){
let postDocs = db
.collectionGroup("userPosts")
.order(by: "postTime", descending: false)
.limit(to: 4)
postDocs.addSnapshotListener { [weak self](querySnapshot, error) in
self?.q.async{
self!.posts = []
guard let snapshot = querySnapshot else {
if let error = error {
print(error)
}
return
}
guard let lastSnapshot = snapshot.documents.last else {
// The collection is empty.
return
}
let nextDocs = Firestore.firestore()
.collectionGroup("userPosts")
.order(by: "postTime", descending: false)
.start(afterDocument: lastSnapshot)
if let postsTemp = self?.createPost(snapshot){
DispatchQueue.main.async {
self!.posts = postsTemp
self!.tableView.reloadData()
}
}
}
}
}
func createPost(_ snapshot: QuerySnapshot) ->[Post]{
var postsTemp = [Post]()
for doc in snapshot.documents{
if let firstImage = doc.get(K.FStore.firstImageField) as? String,
let firstTitle = doc.get(K.FStore.firstTitleField) as? String,
let secondImage = doc.get(K.FStore.secondImageField) as? String,
let secondTitle = doc.get(K.FStore.secondTitleField) as? String,
let userName = doc.get(K.FStore.poster) as? String,
let uID = doc.get(K.FStore.userID) as? String,
let postDate = doc.get("postTime") as? String,
let votesForLeft = doc.get("votesForLeft") as? Int,
let votesForRight = doc.get("votesForRight") as? Int,
let endDate = doc.get("endDate") as? Int{
let post = Post(firstImageUrl: firstImage,
secondImageUrl: secondImage,
firstTitle: firstTitle,
secondTitle: secondTitle,
poster: userName,
uid: uID,
postDate: postDate,
votesForLeft: votesForLeft,
votesForRight:votesForRight,
endDate: endDate)
postsTemp.insert(post, at: 0)
}else{
}
}
return postsTemp
}
Here is my delegate which also detects the end of the TableView:
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let post = posts[indexPath.row]
let cell = tableView.dequeueReusableCell(withIdentifier: K.cellIdentifier, for: indexPath) as! PostCell
cell.delegate = self
let seconds = post.endDate
let date = NSDate(timeIntervalSince1970: Double(seconds))
let formatter = DateFormatter()
formatter.dateFormat = "M/d h:mm"
if(seconds <= Int(Date().timeIntervalSince1970)){
cell.timerLabel?.text = "Voting Done!"
}else{
cell.timerLabel?.text = formatter.string(from: date as Date)
}
let firstReference = storageRef.child(post.firstImageUrl)
let secondReference = storageRef.child(post.secondImageUrl)
cell.firstTitle.setTitle(post.firstTitle, for: .normal)
cell.secondTitle.setTitle(post.secondTitle, for: .normal)
cell.firstImageView.sd_setImage(with: firstReference)
cell.secondImageView.sd_setImage(with: secondReference)
cell.userName.setTitle(post.poster, for: .normal)
cell.firstImageView.layer.cornerRadius = 8.0
cell.secondImageView.layer.cornerRadius = 8.0
if(indexPath.row + 1 == posts.count){
print("Reached the end")
}
return cell
}
Previously I had an addSnapshotListener without a limit on the Query and just pulled down all posts as they came. However I would like to limit how many posts are being pulled down at a time. I do not know where I should be loading the data into my model. Previously it was being loaded at the end of the addSnapshotListener and I could still do that, but when do I use the next Query? Thank you for any help and please let me know if I can expand on my question any more.
There is a UITableViewDelegate method called tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) that will be called just before a cell is loading.
You could use this one to check if the row at IndexPath is in fact the cell of the last object in your tableview's datasource. Something like datasource.count - 1 == IndexPath.row (The -1 is to account for item 0 being the first item in an array, where as it already counts as 1).
If that object is indeed the last one in your datasource, you could make a call to Firebase and add items to the datasource. Before mutating the datasource, make sure to check the new number of objects the show (the ones already loaded + new ones) has to be larger than the current number of objects in the datasource, otherwise the app will crash.
You also might want to give your user a heads up that you're fetching data. You can trigger that heads up also in the delegate method.
I am using Firebase to populate a TableView in my iOS app. The first few objects are loaded but once I get to the third item in my list the app crashes with the exception:
'NSRangeException', reason: '*** __boundsFail: index 3 beyond bounds [0 .. 2]'
I know that this means that I am referring to an array at an index that it does not contain however I do not know why.
I create the TableView with a TableViewController and initialize it like so:
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
print(posts.count)
return posts.count
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let post = posts[indexPath.row]
print(post)
let cell = tableView.dequeueReusableCell(withIdentifier: K.cellIdentifier, for: indexPath) as! PostCell
let firstReference = storageRef.child(post.firstImageUrl)
let secondReference = storageRef.child(post.secondImageUrl)
cell.firstTitle.setTitle(post.firstTitle, for: .normal)
cell.secondTitle.setTitle(post.secondTitle, for: .normal)
cell.firstImageView.sd_setImage(with: firstReference)
cell.secondImageView.sd_setImage(with: secondReference)
// Configure the cell...
return cell
}
I believe that the first function creates an array with the number of objects in posts and that the second function assigns values to the template for the cell. The print statement in the first method prints 4 which is the correct number of objects retrieved from firebase. I assume that means an array is created with 4 objects to be displayed in the TableView. This is what is really confusing because the error states that there are only 3 objects in the array. Am I misunderstanding how the TableView is instantiated?
Here is the code that fills the TableView:
func loadMessages(){
db.collectionGroup("userPosts")
.addSnapshotListener { (querySnapshot, error) in
self.posts = []
if let e = error{
print("An error occured trying to get documents. \(e)")
}else{
if let snapshotDocuments = querySnapshot?.documents{
for doc in snapshotDocuments{
let data = doc.data()
if let firstImage = data[K.FStore.firstImageField] as? String,
let firstTitle = data[K.FStore.firstTitleField] as? String,
let secondImage = data[K.FStore.secondImageField] as? String,
let secondTitle = data[K.FStore.secondTitleField] as? String{
let post = Post(firstImageUrl: firstImage, secondImageUrl: secondImage, firstTitle: firstTitle, secondTitle: secondTitle)
self.posts.insert(post, at: 0)
print("Posts: ")
print(self.posts.capacity)
DispatchQueue.main.async {
self.tableView.reloadData()
}
}
}
}
}
}
The app builds and runs and displays the first few items but crashes once I scroll to the bottom of the list. Any help is greatly appreciated.
Edit:
override func viewDidLoad() {
super.viewDidLoad()
tableView.dataSource = self
tableView.register(UINib(nibName: K.cellNibName, bundle: nil), forCellReuseIdentifier: K.cellIdentifier)
loadMessages()
}
You're getting an out-of-bounds error because you're dangerously populating the datasource. You have to remember that a table view is constantly adding and removing cells as it scrolls which makes updating its datasource a sensitive task. You reload the table on each document iteration and insert a new element in the datasource at index 0. Any scrolling during an update will throw an out-of-bounds error.
Therefore, populate a temporary datasource and hand that off to the actual datasource when it's ready (and then immediately reload the table, leaving no space in between an altered datasource and an active scroll fetching from that datasource).
private var posts = [Post]()
private let q = DispatchQueue(label: "userPosts") // serial queue
private func loadMessages() {
db.collectionGroup("userPosts").addSnapshotListener { [weak self] (snapshot, error) in
self?.q.async { // go into the background (and in serial)
guard let snapshot = snapshot else {
if let error = error {
print(error)
}
return
}
var postsTemp = [Post]() // setup temp collection
for doc in snapshot.documents {
if let firstImage = doc.get(K.FStore.firstImageField) as? String,
let firstTitle = doc.get(K.FStore.firstTitleField) as? String,
let secondImage = doc.get(K.FStore.secondImageField) as? String,
let secondTitle = doc.get(K.FStore.secondTitleField) as? String {
let post = Post(firstImageUrl: firstImage, secondImageUrl: secondImage, firstTitle: firstTitle, secondTitle: secondTitle)
postsTemp.insert(post, at: 0) // populate temp
}
}
DispatchQueue.main.async { // hop back onto the main queue
self?.posts = postsTemp // hand temp off (replace or append)
self?.tableView.reloadData() // reload
}
}
}
}
Beyond this, I would handle this in the background (Firestore returns on the main queue) and only reload the table if the datasource was modified.
After some fiddling around and implementing #bsod's response I was able to get my project running. The solution was in Main.Storyboard under the Attributes inspector I had to set the content to Dynamic Prototypes.
In my WalletTableViewController I have two functions, used to calculate the Wallet Value:
A. updateCellValue() Is called by reloadData() with the tableView and uses indexPath.row to fetch a value (price) and an amount (number of coins) corresponding to the cell and make a calculation to get the total value of that coin (amountValue = value * amount). That is then saved with Core Data.
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath) as! WalletTableViewCell
cell.delegate = self
cell.amountTextField.delegate = self
updateCellValue(cell, atRow: indexPath.row)
return cell
}
func updateCellValue(_ walletTableViewCell: WalletTableViewCell, atRow row: Int) {
var newCryptos : [CryptosMO] = []
var doubleAmount = 0.0
if CoreDataHandler.fetchObject() != nil {
newCryptos = CoreDataHandler.fetchObject()!
}
cryptoPrice = cryptos[row].code!
guard let cryptoDoublePrice = CryptoInfo.cryptoPriceDic[cryptoPrice] else { return }
let selectedAmount = newCryptos[row]
guard let amount = selectedAmount.amount else { return }
var currentAmountValue = selectedAmount.amountValue
doubleAmount = Double(amount)!
let calculation = cryptoDoublePrice * doubleAmount
currentAmountValue = String(calculation)
CoreDataHandler.editObject(editObject: selectedAmount, amount: amount, amountValue: currentAmountValue)
updateWalletValue()
}
B. updateWalletValue() Is a function that fetches all the amountValue objects in Core Data and adds them together to calculate the Wallet Value.
func updateWalletValue() {
var items : [CryptosMO] = []
if CoreDataHandler.fetchObject() != nil {
items = CoreDataHandler.fetchObject()!
}
total = items.reduce(0.0, { $0 + Double($1.amountValue)! } )
WalletTableViewController.staticTotal = total
}
In my MainViewController, the Wallet Value is displayed too, but how can I refresh it's value?
func updateMainVCWalletLabel() {
//... what can I do here??
}
This works great for the WalletViewController of course with the TableView and indexPath, but how can I call updateCellValue from the MainViewController to keep the value updated?
The WalletViewController is instantiated and pushed from the MainViewController :
#IBAction func walletButtonTapped(_ sender: Any) {
let walletViewController = self.storyboard?.instantiateViewController(withIdentifier: "walletTableViewController")
self.present(walletViewController!, animated: true)
}
If you want to use a single method in multiple view controllers you should implement that method where you can call that method from anywhere. For example you can use singleton class here.
Create a swift file and name it as your wish (like WalletHelper or WalletManager)
Then you will get a file with the following format
class WalletHelper: NSObject
{
}
Create a shared instance for that class
static let shared = WalletHelper()
Implement the method you want
func getWalletValue() -> Float {
// write your code to get wallet value`
// and return the calculated value
}
Finally call that method like
let walletValue = WalletHelper.shared. getWalletValue()
WalletHelper.swift looks like
import UIKit
class WalletHelper: NSObject
{
static let shared = WalletHelper()
func getWalletValue() -> Float {
// write your code to get wallet value
// and return the calculated value
}
}
Update (old answer below)
To me it is absolutly unclear what you want to achieve: Which value do you want to be updated? The staticTotal?
Seems a litte like an XYProblem to me. As #vadian commented yesterday, please clearly describe where the data is stored, how the controllers are connected, what you want to update when in order to achieve what. You could also provide a MCVE which makes clear what you are asking, instead of adding more and more code snippets.
And, even more interesting: Why do you modify CoreData entries (CoreDataHandler.editObject) when you are in the call stack of tableView(_: cellForRowAt:)? Never ever ever do so! You are in a reading case - reloadData is intended to update the table view to reflect the data changes after the data has been changed. It is not intended to update the data itself. tableView(_: cellForRowAt:) is called many many times, especially when the user scrolls up and down, so you are causing large write impacts (and therefore: performance losses) when you write into the database.
Old Post
You could just call reloadData on the table view, which then will update it's cells.
There are also a few issues with your code:
Why do you call updateWalletValue() that frequently? Every time a cell is being displayed, it will be called, run through the whole database and do some reduce work. You should cache the value and only update it if the data itself is modified
Why do you call fetchObject() twice (in updateWalletValue())?
You should do this:
func updateWalletValue() {
guard let items:[CryptosMO] = CoreDataHandler.fetchObject() else {
WalletTableViewController.staticTotal = 0.0
return
}
total = items.reduce(0.0, { $0 + Double($1.amountValue)! } )
WalletTableViewController.staticTotal = total
}
I have a table view where depending on the cell class it will download an image from Firebase. I've noticed when using the app that cells with the same cell identifier will show the previous downloaded image before showing the new one. This is what I have before changing it.
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
if tableData[indexPath.row]["Image"] != nil {
let cell = tableView.dequeueReusableCell(withIdentifier: "imageNotesData", for: indexPath) as! ImageNotesCell
cell.notes.delegate = self
cell.notes.tag = indexPath.row
cell.notes.text = tableData[indexPath.row]["Notes"] as! String
guard let imageFirebasePath = tableData[indexPath.row]["Image"] else {
return cell }
let pathReference = Storage.storage().reference(withPath: imageFirebasePath as! String)
pathReference.getData(maxSize: 1 * 1614 * 1614) { data, error in
if let error = error {
print(error)
} else {
let image = UIImage(data: data!)
cell.storedImage.image = image
}
}
return cell
}
else {
let cell = tableView.dequeueReusableCell(withIdentifier: "notesData", for: indexPath) as! NotesCell
//let noteString = tableData[indexPath.row]["Notes"] as! String
cell.notes.text = tableData[indexPath.row]["Notes"] as! String
cell.notes.delegate = self
cell.notes.tag = indexPath.row
return cell
}
}
Knowing that this is not a good user experience and that it looks clunky, I tried to move the pathReference.getData to where I setup the data but the view appears before my images finish downloading. I have tried to use a completion handler but I'm still having issues.
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(true)
getSectionData(userID: userID, city: selectedCity, completion: {(sectionString) in
self.setupTableCellView(userID: userID, city: selectedCity, section: sectionString) { (tableData) in
DispatchQueue.main.async(execute: {
self.cityName?.text = selectedCity
self.changeSections.setTitle(sectionString, for: .normal)
self.currentSectionString = sectionString
self.setupTableData(tableDataHolder: tableData)
})
}
})
}
func setupTableCellView(userID: String, city: String, section: String, completion: #escaping ([[String:Any]]) -> () ) {
let databaseRef = Database.database().reference().child("Users").child(userID).child("Cities").child(city).child(section)
var indexData = [String:Any]()
var indexDataArray = [[String:Any]]()
databaseRef.observeSingleEvent(of: .value, with: { (snapshot) in
for dataSet in snapshot.children {
let snap = dataSet as! DataSnapshot
//let k = snap.key
let v = snap.value
indexData = [:]
for (key, value) in v as! [String: Any] {
//indexData[key] = value
if key == "Image" {
//let pathReference = Storage.storage().reference(withPath: value as! String)
print("before getImageData call")
self.getImageData(pathRef: value as! String, completion: {(someData) in
print("before assigning indexData[key]")
indexData[key] = someData
print("after assigning indexData[key]")
})
} else {
indexData[key] = value
}
}
indexDataArray.append(indexData)
}
completion(indexDataArray)
})
}
func getImageData(pathRef: String, completion: #escaping(UIImage) -> ()) {
let pathReference = Storage.storage().reference(withPath: pathRef as! String)
pathReference.getData(maxSize: 1 * 1614 * 1614, completion: { (data, error) in
if let error = error {
print(error)
} else {
let image = UIImage(data:data!)
print("called before completion handler w/ image")
completion(image!)
}
})
}
I don't know if I am approaching this the right way but I think I am. I'm also guessing that the getData call is async and that is why it will always download after showing the table view.
You can't do this.
Make the request from Firebase.
Over time, you will get many replies - all the information and all the changing information.
When each new item arrives - and don't forget it may be either an addition or deletion - alter your table so that it displays all the current items.
That's OCC!
OCC is "occasionally connected computing". A similar phrase is "offline first computing". So, whenever you use any major service you use every day like Facebook, Snapchat, etc that is "OCC": everything stays in sync properly whether you do or don't have bandwidth. You know? The current major paradigm of device-cloud computing.
Edit - See Fattie's comments about prepareForReuse()!
With reusable table cells, the cells will at first have the appearance they do by default / on the xib. Once they're "used", they have whatever data they were set to. This can result in some wonky behavior. I discovered an issue where in my "default" case from my data, I didn't do anything ecause it already matched the xib, but if the data's attributes were different, I updated the appearance. The result was that scrolling up and down really fast, some things that should have had the default appearance had the changed appearance.
One basic solution to just not show the previous image would be to show a place holder / empty image, then call your asynchronous fetch of the image. Not exactly what you want because the cell will still show up empty...
Make sure you have a local store for the images, otherwise you're going to be making a server request for images you already have as you scroll up and down!
I'd recommend in your viewDidLoad, call a method to fetch all of your images at once, then, once you have them all, in your success handler, call self.tableview.reloadData() to display it all.
I have searched every source I know of for help on this problem. I want to make individual rows within a tableview disappear after a certain amount of time expires. Even if the app is not open, I want the rows to delete as soon as the timer reaches zero. I have been trying to arrange each post into an dictionary with timer pairings to handle the row deletion when time occurs. I have looked at this post for guidance but no solutions. Swift deleting table view cell when timer expires.
This is my code for handling the tableview and the timers:
var nextID: String?
var postsInFeed = [String]()
var postTimer = [Timer: String]()
var timeLeft = [String: Int]()
(in view did load)
DataService.ds.REF_FEED.observe(.value, with: { (snapshot) in
self.posts = []
self.postsInFeed = []
self.nextID = nil
if let snapshot = snapshot.children.allObjects as? [DataSnapshot] { //Gets us into our users class of our DB
for snap in snapshot { // Iterates over each user
if let postDict = snap.value as? Dictionary<String, Any> { // Opens up the dictonary key value pairs for each user.
let key = snap.key //
let post = Post(postID: key, postData: postDict) // Sets the properties in the dictionary to a variable.
self.posts.append(post) // appends each post which contains a caption, imageurl and likes to the empty posts array.
self.nextID = snap.key
let activeID = self.nextID
self.postsInFeed.append(activeID!)
print(self.postsInFeed)
print(activeID!)
}
}
}
self.tableView.reloadData()
})
//////////////////////////////////////////////////////////////////////////////////////////////
// Sets up our tableview
func numberOfSections(in tableView: UITableView) -> Int {
return 1
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return postsInFeed.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let post = posts[indexPath.row] // We get our post object from the array we populate when we call the data from the database up above.
if let cell = tableView.dequeueReusableCell(withIdentifier: "tableCell") as? TableViewCell { //Specifies the format of the cell we want from the UI
// cell.cellID = self.postsInFeed[indexPath.row]
cell.cellID = self.postsInFeed[indexPath.row]
cell.homeVC = self
if let img = HomeVC.imageCache.object(forKey: post.imageUrl as NSString){
cell.configureCell(post: post, img: img as? UIImage)
print(postTimer)
print(self.timeLeft)
} else {
cell.configureCell(post: post)
print(postTimer)
print(self.timeLeft)
}
return cell
} else {
return TableViewCell()
}
}
func handleCountdown(timer: Timer) {
let cellID = postTimer[timer]
// find the current row corresponding to the cellID
let row = postsInFeed.index(of: cellID!)
// decrement time left
let timeRemaining = timeLeft[(cellID!)]! - 1
timeLeft[cellID!] = timeRemaining
if timeRemaining == 0 {
timer.invalidate()
postTimer[timer] = nil
postsInFeed.remove(at: row!)
tableView.deleteRows(at: [IndexPath(row: row!, section: 0)], with: .fade)
} else {
tableView.reloadRows(at: [IndexPath(row: row!, section: 0)], with: .fade)
}
}
In the tableviewcell:
weak var homeVC: HomeVC?
var cellID: String!
func callTime() {
homeVC?.timeLeft[cellID] = 25
let timer = Timer.scheduledTimer(timeInterval: 1.0, target: self, selector: #selector(homeVC?.handleCountdown(timer:)), userInfo: nil, repeats: true)
homeVC?.postTimer[timer] = cellID
}
Any help would be really appreciated!
Timers don't run when your app is not running or is suspended.
You should rework your code to save a date (to UserDefaults) when you start your timer, and then each time the timer fires, compare the current date to the saved date. Use the amount of elapsed time to decide how many entries in your table view's data model to delete. (In case more than one timer period elapsed while you were suspended/not running.)
You should implement applicationDidEnterBackground() in your app delegate (or subscribe to the equivalent notification, UIApplicationDidEnterBackground) and stop your timer(s).
Then implement the app delegate applicationDidBecomeActive() method (or add a notification handler for the UIApplicationDidBecomeActive notification), and in that method, check the amount of time that has elapsed, update your table view's data model, and tell the table view to reload if you've removed any entries. Then finally restart your timer to update your table view while your app is running in the foreground.