How to update UITableviewcell label in other method in swift3? - ios

I am trying to update a UITableviewcell's label in other method. I also saw many post on stackoverflow, non was work for me. i am using Swift 3 and I tried this one :
let indexPath = IndexPath.init(row: 0, section: 0)
let cell : CategoryRow = tableView.cellForRow(at: indexPath) as! CategoryRow
and i am getting this error :
fatal error: unexpectedly found nil while unwrapping an Optional value
i am using this in other method where i update tableviewcell label text after 24hr.
code is here
var myQuoteArray = ["“If you have time to breathe you have time to meditate. You breathe when you walk. You breathe when you stand. You breathe when you lie down. – Ajahn Amaro”","We are what we repeatedly do. Excellence, therefore, is not an act but a habit. – Aristotle","The best way out is always through. – Robert Frost","“If you have time to breathe you have time to meditate. You breathe when you walk. You breathe when you stand. You breathe when you lie down. – Ajahn Amaro”","We are what we repeatedly do. Excellence, therefore, is not an act but a habit. – Aristotle","The best way out is always through. – Robert Frost"
]
func checkLastRetrieval(){
// let indexPath = IndexPath.init(row: 0, section: 0)
// let cell : CategoryRow = tableView.cellForRow(at: indexPath) as? CategoryRow
let indexPath = IndexPath.init(row: 0, section: 0)
guard let cell = tableView.cellForRow(at: indexPath) as? CategoryRow else { return }
print("Getting Error line 320")// This is not printing on console
let userDefaults = UserDefaults.standard
if let lastRetrieval = userDefaults.dictionary(forKey: "lastRetrieval") {
if let lastDate = lastRetrieval["date"] as? NSDate {
if let index = lastRetrieval["index"] as? Int {
if abs(lastDate.timeIntervalSinceNow) > 86400 { // seconds in 24 hours
// Time to change the label
var nextIndex = index + 1
// Check to see if next incremented index is out of bounds
if self.myQuoteArray.count <= nextIndex {
// Move index back to zero? Behavior up to you...
nextIndex = 0
}
cell?.quotationLabel.text = self.myQuoteArray[nextIndex]
let lastRetrieval : [NSObject : AnyObject] = [
"date" as NSObject : NSDate(),
"index" as NSObject : nextIndex as AnyObject
]
userDefaults.set(lastRetrieval, forKey: "lastRetrieval")
userDefaults.synchronize()
}
// Do nothing, not enough time has elapsed to change labels
}
}
} else {
// No dictionary found, show first quote
cell?.quotationLabel.text = self.myQuoteArray.first!
print("line 357")
// Make new dictionary and save to NSUserDefaults
let lastRetrieval : [NSObject : AnyObject] = [
"date" as NSObject : NSDate(),
"index" as NSObject : 0 as AnyObject
]
userDefaults.set(lastRetrieval, forKey: "lastRetrieval")
userDefaults.synchronize()
}
}
and Calling this method in ViewDidLoad()
override func viewDidLoad() {
super.viewDidLoad()
checkLastRetrieval()
}
what should i do?
Thanks in Advance

The function cellForRow(at:) returns nil if the requested cell is not currently onscreen. Since you are calling your function in viewDidLoad there will be no cells on screen. This results in the crash because you have force downcast the result of cellForRow(at:).
You should never use a force unwrap or a force downcast unless you are absolutely
certain the result cannot be nil and/or the only possible action if it is nil is for your app to crash.
Generally you should not update cells directly. It is better to update the table's data model and then call reloadRows(at:,with:) and let your data source cellForRow(at:) function handle updating the cell.
If you are going to update the cell directly then make sure you use a conditional downcast with if let or guard let to avoid a crash if the cell isn't visible. In this case you still need to update your data model so that the cell data is correct when the cell does come on screen.

You should use guard let statement:
let indexPath = IndexPath(row: 0, section: 0)
guard let cell = tableView.cellForRow(at: indexPath) as? CategoryRow else { return }
If your cell type is not CategoryRow, you will return from the method. Try to find out why is your cell type not a CategoryRow.

Related

How to paginate queries with a TableView in swift

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

Issue trying to complete Firebase Storage download before showing tableview

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.

Removing rows from Tableview after time expires

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.

Swift: app crashes after deleting a column in Parse

In Parse I accidentally deleted a column called "likes" that counts the number of a likes a user receives for their blog post. I created the column again with the same name but now when I run my app it crashes and I receive this message "unexpectedly found nil while unwrapping an Optional value". It points to my code where its suppose to receive the "likes" in my cellForRowAtIndexPath. I pasted my code below. Is there any way I could fix this issue and stop it from crashing?
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath?) -> PFTableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("BCell", forIndexPath: indexPath!) as! BlogCell
if let object : PFObject = self.blogPosts.objectAtIndex(indexPath!.row) as? PFObject {
cell.author.text = object["blogger"] as? String
cell.post.text = object["blogPost"] as? String
let dateUpdated = object.createdAt! as NSDate
let dateFormat = NSDateFormatter()
dateFormat.dateFormat = "h:mm a"
cell.timer.text = NSString(format: "%#", dateFormat.stringFromDate(dateUpdated)) as String
let like = object[("likes")] as! Int
cell.likeTracker.text = "\(like)"
}
return cell
}
I would inspect what's going on with the object if I were you. You clearly aren't getting data that you expected to be there. As a stopgap, you can change let like = object["likes"] as! Int to
if let like = object["likes"] as? Int {
cell.likeTracker.text = "\(like)"
}
If you do that, you will also want to implement the prepareForReuse method in BlogCell to set that label's text to nil or else you might have some weird cell reuse bugs.
Where you delete a column from uitableview , you need to delete data from data source, and update the delete index or reload the whole table .
Look for if you are missing that step

Casting of a custom class cell for UIViewTable fails with optional unwrap error

I got UITableView with two types of cells. The first type presents existing data, the second is used to add new items to the table. So I created a subclass "AddListTableViewCell" and put in "#IBOutlet weak var addListTextBox: UITextField!" property to read it's contents.
This "Add List" cell was drawn in the first row with no runtime errors. It was pushed by:
#IBAction func AddListButtonPressed(sender: UIBarButtonItem) {
if !isAddingList {
isAddingList = true
ListsTableView.reloadData()
let path = NSIndexPath(forRow: 0, inSection: 0)
let cell = tableView.cellForRowAtIndexPath(path) as? AddListTableViewCell
cell.addListTextBox.becomeFirstResponder()
}
}
}
On ListsTableView.reloadData() method "tableView(_:cellForRowAtIndexPath:" has created additional cell. Again: no errors.
Then I changed it's order wishing it to be presented at the end of the table and added more demo items to observe it's interaction (forRow in AddListButtonPressed changed too :).
Now when I ran the app and pressed AddListButton, that fires func AddListButtonPressed, I got "fatal error: unexpectedly found nil while unwrapping an Optional value" with "let cell = tableView.cellForRowAtIndexPath(path) as? AddListTableViewCell".
Futhermore, if view was scrolled after the app starts, there are no errors, but warnig "[UIWindow endDisablingInterfaceAutorotationAnimated:] called on > without matching -beginDisablingInterfaceAutorotation. Ignoring." in the console.
I found a solution on this site to replace faulty line
let cell = tableView.cellForRowAtIndexPath(path) as? AddListTableViewCell
with
let cell = tableView.dequeueReusableCellWithIdentifier("AddList", forIndexPath: path) as! AddListTableViewCell
and it works well but I still generates a warning.
But why am I supposed to create cell again, when it's already set with "tableView(_:cellForRowAtIndexPath:" method upon "ListsTableView.reloadData()"? Is it possible to somehow keep "let cell = tableView.cellForRowAtIndexPath(path) as? AddListTableViewCell" to avoid warnings?
EDIT One more function, where it's used (sorry for no indention, posted from phone):
private func addList () {
let path = NSIndexPath(forRow: brain.tree.count, inSection: 0)
let cell = tableView.cellForRowAtIndexPath(path) as! AddListTableViewCell
if let listName = cell.addListTextBox.text {
if listName.characters.filter({!" ".characters.contains($0)}).count > 0 {
brain.newList(listName)
cell.addListTextBox.text = ""
isAddingList = false
ListsTableView?.reloadData()
performSegueWithIdentifier("ShowTasksSegue", sender: nil)
}
}
}

Resources