Each time I add a new post to the database, the amount of times the posts show is increased by one. For instance, when I add one new post, the number of times the posts are duplicated is once. When I add another post (the view is reloaded) I see all the posts three times. I assume that the problem is with the function fetchPosts(), as each time the view loads it collects all the data from the firebase and appends it to the array. I have already tried emptying the array in the view did load, but that only makes all the posts show even more times. Also, I have tried using observe(.childAdded) and that results in no posts showing at all.
var ref: DatabaseReference!
var postList = [Post]()
var refHandle : UInt!
#IBOutlet weak var tableView: UITableView!
override func viewDidLoad() {
super.viewDidLoad()
checkForSignedIn ()
ref = Database.database().reference().child("posts")
fetchPosts()
}
public func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return postList.count
}
public func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath) as! PostTableViewCell
//set cell content
let contentOfCellPost = postList[indexPath.row]
cell.label?.text = contentOfCellPost.post_words
cell.revealCount.text = contentOfCellPost.Reveals
return cell
}
public func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
let tableSize = tableView.bounds.height
return tableSize
}
func fetchPosts () {
let query = ref.queryOrdered(byChild: "timestamp").queryLimited(toFirst: 10)
query.observe(.value) { (snapshot) in
for child in snapshot.children.allObjects as! [DataSnapshot] {
if let value = child.value as? NSDictionary {
let post = Post()
let poster = value["poster"] as? String ?? "Name not found"
let post_content = value["post"] as? String ?? "Content not found"
let post_reveals = value["Reveals"] as? String ?? "Reveals not found"
post.post_words = post_content
post.poster = poster
post.Reveals = post_reveals
post.postID = child.key
self.postList.append(post)
print (post.post_words ?? "none")
DispatchQueue.main.async { self.tableView.reloadData() }
//make this for when child is added but so that it also shows psots already there something like query.observre event type of
}
}
}
}
The console log at first, for instance, will show the correct number of posts :
Thrice
Twice
Once
Tttt
Bloop
Decimal
9
7
3
When I add a new post, it shows this:
Tattoo
Thrice
Twice
Once
Tttt
Bloop
Decimal
9
7
3
Tattoo
Thrice
Twice
Once
Tttt
Bloop
Decimal
9
7
3
Tattoo
Thrice
Twice
Once
Tttt
Bloop
Decimal
9
7
3
Tattoo
Thrice
Twice
Once
Tttt
Bloop
Decimal
9
7
3
You need to clear your model (self.postList) at the beginning of the .observe block like so:
query.observe(.value) { (snapshot) in
self.postList.removeAll() //or however you can clear it
for child in snapshot.children.allObjects as! [DataSnapshot] {
if let value = child.value as? NSDictionary {
let post = Post()
let poster = value["poster"] as? String ?? "Name not found"
let post_content = value["post"] as? String ?? "Content not found"
let post_reveals = value["Reveals"] as? String ?? "Reveals not found"
post.post_words = post_content
post.poster = poster
post.Reveals = post_reveals
post.postID = child.key
self.postList.append(post)
print (post.post_words ?? "none")
DispatchQueue.main.async { self.tableView.reloadData() }
//make this for when child is added but so that it also shows psots already there something like query.observre event type of
}
}
Currently, each time the database is updated with a post, you add all posts to your model once again. Therefore you must clear your model each time you fetch all posts.
The reason why this doesn't work in viewDidLoad is because viewDidLoad is called only once, in the beginning, and not everytime the view appears -- thus the data will not be cleared upon adding a post.
Alternatively, you can use .childAdded -- but then you need to change the way you parse it because each snapshot with .childAdded returns a single post, not all the posts together.
Related
I am writing swift code with the goal of displaying a increasing number on every tableview cell. Right now the int is not being display. So the first tableview cell should say 1 and the 2nd should say 2. You can see in the gif below what is going along with the tableview cell and nothing is appearing in them when the button is clicked. The func below is when the button is clicked.
var pageNumber = 1
var itemName : [Player] = []
func enterData() {
theScores.reloadData()
let appDeldeaget = UIApplication.shared.delegate as! AppDelegate
let context = appDeldeaget.persistentContainer.viewContext
// Simpler way to create a new Core Data object
let theTitle = Player(context: context)
// Simpler way to set the position attribute
theTitle.positon = Int64(pageNumber)
print(pageNumber)
// pageNumber must be of type Int64, otherwise use Int64(pageNumber)
do {
try context.save()
itemName.append(theTitle)
pageNumber += 1
} catch {
// handle errors
}
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let title = itemName[indexPath.row]
let cell = theScores.dequeueReusableCell(withIdentifier: "MyCell", for : indexPath)
cell.selectionStyle = .default
let attr5 = title.value(forKey: "positon") as? String
let text = [" Item :", attr5].compactMap { $0 }.reduce("", +)
cell.textLabel?.text = "\(text)"
cell.textLabel?.textAlignment = .center
cell.layoutMargins = UIEdgeInsets.zero
cell.preservesSuperviewLayoutMargins = false
cell.separatorInset = UIEdgeInsets.zero
cell.layoutMargins = UIEdgeInsets.zero
return cell
}
Here's why it doesn't work. You have this:
let attr5 = title.value(forKey: "positon") as? String
let text = [" Item :", attr5].compactMap { $0 }.reduce("", +)
This is a really complicated way to try and do this, and it doesn't work as written. The problem is that the value of position is an Int64 and you need a string. But using as? like that doesn't turn it into a string. When that line of code runs, Swift says, can I just make this into a string? But it can't. So the as? String is nil, and your table cells don't include the number because the conversion failed.
A better way would be something like
if let position = title.value(forKey: "positon") {
cell.textLabel?.text = "Item : \(positon))"
}
But that's only if you really want to use value(forKey:) for some reason. You probably don't need that because normally Xcode creates a subclass of NSManagedObject for each entity with named properties. So even better would be
cell.textLabel?.text = "Item: \(title.position)"
These both work because string interpolation knows how to convert an integer to a string.
You probably should call .reloadData() after context.save()
Im having a issue where I can't view anything that I write to my Firebase database in my table view. I previously had some working code to view my database entries but had to revise how I write data to the database so I can save the unique ID generated by childByAutoID(), so I may delete an entry later. Here's my code:
Heres how I write to Firebase:
ref = Database.database().reference() // sets the variable "ref" to connect to our Firebase database
let key = ref?.child("task").childByAutoId().key
let post = ["uid": key, // Gets auto generated ID for database entry
"title": input.text, // Saves the title field to Firebase
"description": inputField.text] // Saves the description field to Firebase
ref?.child("task").child(key!).setValue(post) // Saves the task for Firebase, ties each post with a unique ID
var arr : [(String, String)] = [];
for (key, value) in post {
arr.append((key, value!));
Heres my TableViewController:
public func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int{
return (arr.count)
}
public func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell{
let cell = UITableViewCell(style:
UITableViewCell.CellStyle.default, reuseIdentifier: "cell")
let (key, value) = arr[indexPath.row]; //read element for the desired cell
cell.textLabel?.text = key
cell.detailTextLabel?.text = value
return (cell)
}
override func viewDidAppear(_ animated: Bool) {//change "viewDidLoad()" back to "viewDidAppear()"
ref = Database.database().reference() // sets the variable "ref" to connect to our Firebase database
list.removeAll() // Deletes all older data, so only data thats on the Firebase Database will be added to the "list" array
desc.removeAll() // Deletes all older data, so only data thats on the Firebase Database will be added to the "desc" array
handle = ref?.child("task").observe(.childAdded, with: { (snapshot) in
if let item = snapshot.value as? String
{
arr.append(item)
list.append(item)
//desc.removeAll()
self.myTableView.reloadData()
}
})
}
Firebase returns the items in form of NSArray or NSDictionary or NSString. (Refer here)
Since your post object is NSDictionary, parse the snapshot value as a Array of Dictionary. try the following:
if let itemArray = snapshot.value as? [[String : Any]] {
// Looping logic on 'itemArray' to get your sent dictionaries on the firebase
// After looping logic add Ui logic out of loop
}
I would like two display Object data gotten from Parse in swift. I have tried using label in this way but it only displays the last element in the object. Please how can I make it display all the element in the object in the label. Like one element to one label. Thanks
let query = PFQuery(className: "Questionnaire")
query.findObjectsInBackground { (objects, error) -> Void in
if error == nil {
// There were no errors in the fetch
if let returnedObjects = objects {
// var text = ""
// Objects Array is not nil
// loop through the array to get each object
for object in returnedObjects {
print(object["question"] as! String)
// text.append(object["question"] as! String)
self.Label.text = (object["question"] as! String)
}
}
}
}
You can do in one line like that and join all question with , separator , you can change separator to any (empty, -,...etc)
if let returnedObjects = returnedObjects {
self.Label.text = returnedObjects.map {($0["question"] as? String) ?? nil}.compactMap({$0}).joined(separator: ",")
}
Use tableview for this.
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath) as! YouTableViewCell
cell.textLabel.text = yourArray[indexpath.row] as? String ?? ""
return cell
}
If it's important to use UILabel
var concatenatedString = ""
for object in returnedObjects {
concatenatedString += object["question"] as! String
}
self.Label.text = concatenatedString
You are looping through the array and setting each value to Label.text. However, setting Label.text will replace what was on the label before. That's why you only see the last item.
One solution is to display the string representation of the array:
self.Label.text = "\(object)"
Another solution is to display the items in a table view Suganya Marlin has suggested. You would need to conform to UITableViewDatasource and implement the various methods. Here is a guide.
Below I have my existing query download and cell for table row code...
publicDB.perform(query, inZoneWith: nil)
{
(results, error) -> Void in
if (error != nil)
{
self.present(alert, animated: true, completion: nil)
}
else
{
for result in results!
{
self.restaurantArray.append(result)
}
OperationQueue.main.addOperation( { () -> Void in
self.tableView.reloadData()
}) } }}
downloadRestaurants()
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "restaurantcell") as? RestaurantTableCell
let restaurant: CKRecord = restaurantArray[indexPath.row]
cell?.name?.text = restaurant.value(forKey: "Name") as? String
let asset = restaurant.value(forKey: "Picture") as! CKAsset
let data = try! Data(contentsOf: asset.fileURL)
_ = UIImage(data: data)
cell?.picture?.image = UIImage(data: data)
return cell!
}
When I run this code, the app remains functional but scrolling through the 10 or so table cells is incredibly choppy. I am unsure what is causing this - all records, each containing an image, are downloaded during the query download portion of the top function. However, a problem or concept I'm missing is ever present during runtime. What am I missing here? Lazy loading? cache? something else? Unsure at this point, so any help would be incredibly helpful.
Update 1:
I've updated my code with a large thank you going to Pierce. I've had to update my code ever so slightly from his answer to maintain a ckrecord array to segue over to another controller via - restaurantArray but also create a new array for the NSObject class - tablerestaurantarray to be displayed in the current table controller.
var restaurantArray: Array<CKRecord> = []
var tablerestaurantarray: [Restaurant] = []
for result in results!
{
let tablerestaurant = Restaurant()
if let name = result.value(forKey: "Name") as! String? {
tablerestaurant.name = name
}
// Do same for image
if let imageAsset = result.object(forKey: "Picture") as! CKAsset? {
if let data = try? Data(contentsOf: imageAsset.fileURL) {
tablerestaurant.image = UIImage(data: data)
}
}
self.tablerestaurantarray.append(tablerestaurant)
self.restaurantArray.append(result)
}
OperationQueue.main.addOperation( { () -> Void in
self.tableView.reloadData()
})
}
}
}
downloadRestaurants()
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return restaurantArray.count
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "restaurantcell") as? RestaurantTableCell
let restaurant: Restaurant = tablerestaurantarray[indexPath.row]
cell?.name?.text = restaurant.name
cell?.picture?.image = restaurant.image
return cell!
}
The way your code is setup, whenever you scroll in your UITableView, your program is converting a CKAsset into Data, and then converting that into a UIImage, and that's within every cell! That's a rather inefficient process, so try creating an NSObject called something like Restaurant that has an image property, and when you go through all the records returned from your CKQuery, parse each record into a new Restaurant object. To create a new NSObject, go to File -> New -> File -> select 'Swift File' and add something like this:
import UIKit
class Restaurant: NSObject {
// Create a UIImage property
var image: UIImage?
// Add any other properties, i.e. name, address, etc.
var name: String = ""
}
Now for your query:
// Create an empty array of Restaurant objects
var restaurantArray: [Restaurant] = []
publicDB.perform(query, inZoneWith: nil) { (results, error) -> Void in
if (error != nil) {
self.present(alert, animated: true, completion: nil)
} else {
for result in results! {
// Create a new instance of Restaurant
let restaurant = Restaurant()
// Use optional binding to check if value exists
if let name = result.value(forKey: "Name") as! String? {
restaurant.name = name
}
// Do same for image
if let imageAsset = result.object(forKey: "Picture") as! CKAsset? {
if let data = try? Data(contentsOf: imageAsset.fileURL) {
restaurant.image = UIImage(data: data)
}
}
// Append the new Restaurant to the Restaurants array (which is now an array of Restaurant objects, NOT CKRecords)
self.restaurantArray.append(restaurant)
}
OperationQueue.main.addOperation( { () -> Void in
self.tableView.reloadData()
})
}
}
Now your cell setup is much simpler:
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "restaurantcell") as? RestaurantTableCell
let restaurant: Restaurant = restaurantArray[indexPath.row]
cell?.name?.text = restaurant.name
cell?.picture?.image = restaurant.image
return cell!
}
You should use CKQueryOperation in order to implements pagination for your UITableView.
You have to set the resultLimit property to a number equals to the cell quantity visiable at one time on you table plus 3 or 4
Set recordFetchedBlock property where you have to implement the code that will apply to one CKRecord
Set queryCompletionBlock property. This is the most important part on your pagination code because this closure receive an Optional CKQueryCursor parameter.
If this CKQueryCursor is nil then you have reach the last record available for you query but if it's a non nil value, then you have more records to fetch using this CKQueryCursor as indicator to your next fetch.
When user scroll on your TableView and reach the last element you should perform another fetch with CKQueryCursor.
Other performance advice is CKAssets should be treated on separated execution queues.
Now I'm doing a normal topic and comment integrated app. Already carried out the functions like posting topic, posting comment, and setting the pointer of my comment to the Topic class.
But now the problem that trouble me now for the past few days, is the comments displayed cannot be classified to each topic, they are all gathering together no matter what different topic they're from.
My PFObjects are Topics and Comment, two classes totally.
Topics has objects:Title, content and user.
Comment has objects: content, parent(pointer to Topics) and user.
Already check out the parse database, seeming like each comment has a same parent key from its own topic's object ID. But still wonder why they cannot be shown accordingly.
Here's my coding. I'd be grateful if ones could lend ones' helping hand.
var timelineCommentData:NSMutableArray = NSMutableArray()
func loadData(){
timelineCommentData.removeAllObjects()
var findCommentData:PFQuery = PFQuery(className: "Comment")
findCommentData.findObjectsInBackgroundWithBlock({
(objects:[AnyObject]!,error:NSError!)->Void in
if (error == nil) {
for object in objects {
self.timelineCommentData.addObject(object)
}
let array:NSArray = self.timelineCommentData.reverseObjectEnumerator().allObjects
self.timelineCommentData = array.mutableCopy() as NSMutableArray
self.tableView.reloadData()
}
})
}
override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return timelineCommentData.count + 1 //+1 is for displaying the topic
}
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let identifier = indexPath.row == 0 ? "storyCell" : "CommentCell"
let cell = tableView.dequeueReusableCellWithIdentifier(identifier) as UITableViewCell
if let storyCell = cell as? TopicTableViewCell{
storyCell.titleLabel.text = topic?.objectForKey("title") as? String
storyCell.contentLabel.text = topic?.objectForKey("content") as? String
storyCell.timestampLabel.text = timeAgoSinceDate(topic!.createdAt, true)
}
storyCell.delegate = self
}
if let CommentCell = cell as? CommentsTableViewCell {
let comment:PFObject = self.timelineCommentData.objectAtIndex(indexPath.row - 1) as PFObject ///// Is anything wrong here??
var query = PFQuery(className: "Comment")
query.whereKey("parent", equalTo:topic)
query.findObjectsInBackgroundWithBlock({
(objects:[AnyObject]!,error:NSError!)->Void in
if (error == nil) {
CommentCell.commentLabel.text = comment.objectForKey("commentContent") as? String /////Or here
CommentCell.timeLabel.text = timeAgoSinceDate(comment.createdAt, true)
}
else{
println("no comment")
}
})
CommentCell.delegate = self
}
return cell
}