How to get data from CloudKit before loading TableData - ios

I'm writing an app in Swift where the first scene has a TableView, I have it setup to display the title and it works fine, I also have it setup to count occurrences in a CloudKit database(or whatever its called) but it performs the count in async so the table defaults to show 0 in the detail pane.
I need to know how to make the app wait before it sets the value for the right detail until the count is completed or how to change them afterwards.
I have attached the code I used to perform the count etc, if I am doing this wrong or inefficiently please let me know
override func viewDidLoad() {
super.viewDidLoad()
self.navigationItem.hidesBackButton = true;
self.textArray.addObject("Link 300")
self.textArray.addObject("Link 410")
self.textArray.addObject("Link 510")
let container = CKContainer.defaultContainer()
let publicData = container.publicCloudDatabase
let query = CKQuery(recordType: "Inventory", predicate: NSPredicate(format: "TRUEPREDICATE", argumentArray: nil))
publicData.performQuery(query, inZoneWithID: nil){results, error in
if error == nil {
for res in results {
let record: CKRecord = res as! CKRecord
if(record.objectForKey(("TrackerModel")) as! String == "Link 300"){
self.count300 = self.count300++
}else if(record.objectForKey(("TrackerModel")) as! String == "Link 410"){
self.count410 = self.count410++
}else if(record.objectForKey(("TrackerModel")) as! String == "Link 510"){
self.count510 = self.count510++
}
}
}else{
println(error)
}
}
self.detailArray.addObject(self.count300.description)
self.detailArray.addObject(self.count410.description)
self.detailArray.addObject(self.count510.description)
}
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return self.textArray.count
}
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) ->UITableViewCell {
var cell: UITableViewCell = self.tableView.dequeueReusableCellWithIdentifier("cell") as! UITableViewCell
cell.textLabel?.text = self.textArray.objectAtIndex(indexPath.row) as? String
cell.detailTextLabel?.text = self.detailArray.objectAtIndex(indexPath.row) as? String
return cell
}
Many thanks - Robbie

The closure associated with the performQuery will complete asynchronously - that is after viewDidLoad has finished. You need to make sure that you reload your table view once the query has completed and you have the data. You also have a problem because you are updating your totals outside the closure - this code will also execute before the data has loaded.
Finally, make sure that any update to the UI (such as reloading the table view) is dispatched on the main queue
override func viewDidLoad() {
super.viewDidLoad()
self.navigationItem.hidesBackButton = true;
self.textArray.addObject("Link 300")
self.textArray.addObject("Link 410")
self.textArray.addObject("Link 510")
let container = CKContainer.defaultContainer()
let publicData = container.publicCloudDatabase
let query = CKQuery(recordType: "Inventory", predicate: NSPredicate(format: "TRUEPREDICATE", argumentArray: nil))
publicData.performQuery(query, inZoneWithID: nil){results, error in
if error == nil {
for res in results {
let record: CKRecord = res as! CKRecord
if(record.objectForKey(("TrackerModel")) as! String == "Link 300"){
self.count300++
}else if(record.objectForKey(("TrackerModel")) as! String == "Link 410"){
self.count410++
}else if(record.objectForKey(("TrackerModel")) as! String == "Link 510"){
self.count510++
}
}
self.detailArray.addObject(self.count300.description)
self.detailArray.addObject(self.count410.description)
self.detailArray.addObject(self.count510.description)
dispatch_async(dispatch_get_main_queue(), { () -> Void in
self.tableView.reloadData()
})
}else{
println(error)
}
}
}

Related

How to use Firestore query pagination with TableVIew

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.

Wait for function to execute first before moving to next line of code

I am using CloudKit in my app and facing problem showing data in table view. In viewDidLoad() I am fetching data from CloudKit database.
Then in table view functions I do CKRecord object count for number of rows.
But count returns 0 to table view and after few seconds returns number of row. Because of this table view does not show the results.
override func viewDidLoad() {
super.viewDidLoad()
loadNewData()
}
func loadNewData() {
self.loadData = [CKRecord]()
let publicData = CKContainer.default().publicCloudDatabase
let qry = CKQuery(recordType: "Transactions", predicate: NSPredicate(format: "TRUEPREDICATE", argumentArray: nil))
qry.sortDescriptors = [NSSortDescriptor(key: "Transaction_ID", ascending: true)]
publicData.perform(qry, inZoneWith: nil) { (results, error) in
if let rcds = results {
self.loadData = rcds
}
if error != nil {
self.showAlert(msg: (error?.localizedDescription)!)
}
}
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return loadData.count
}
func numberOfSections(in tableView: UITableView) -> Int {
return 1
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "Cell2", for: indexPath) as! ViewAllTransactionsTVCell
let pn = loadData[indexPath.row].value(forKey: "Party_Name") as! String
let amt = loadData[indexPath.row].value(forKey: "Amount") as! String
let nrt = loadData[indexPath.row].value(forKey: "Narattions") as! String
let dt = loadData[indexPath.row].value(forKey: "Trans_Date") as! String
cell.partyNameLabel.text = pn
cell.dateLabel.text = dt
cell.narationLabel.text = nrt
cell.amountLabel.text = amt
return cell
}
You shouldn't wait, but instead trigger the reloading of the data when the perform completion handler is called:
publicData.perform(qry, inZoneWith: nil) { (results, error) in
if let rcds = results {
DispatchQueue.main.async {
self.loadData = rcds
self.tableView.reloadData()
}
}
if error != nil {
self.showAlert(msg: (error?.localizedDescription)!)
}
}
Note, I'm dispatching the reload process to the main queue, because you're not guaranteed to have this run on the main thread. As the documentation says:
Your block must be capable of running on any thread of the app ...
And because UI updates must happen on the main thread (and because you want to synchronize your access to loadData), just dispatch this to the main queue, like above.

Flip-flopping text in tableview cells

I am having a peculiar problem when I refresh my tableview. (I am using UIRefreshControl.) If I were to slow-mo what is happening:
Suppose there are 3 cells visible, A, B, and C, in that order from top to bottom.
1) I pull down and the tableview shows that it is refreshing.
2) A gets the text that should go in B. B gets the text that should go in C. C is off-screen.
3) Refresh ends and the table snaps back into place.
4) Incorrect text lingers for a second or so.
5) Each cell's text flips to the right thing.
The flip-flopping is pretty annoying to look at visually. Anyway, I feel like this is the kind of issue that can be fixed with a line of code that I just don't know about.
Here are excerpts of my code:
Function called when refresh occurs (gets records from CloudKit):
func refreshTable(sender: UIRefreshControl) {
var postsPredicate = NSPredicate(format: "%K = %#", VISIBILITY_CODE, WORLD) // default
if sender.tag == 0 {
postsPredicate = NSPredicate(format: "%K = %#", VISIBILITY_CODE, WORLD)
}
else if sender.tag == 1 {
postsPredicate = NSPredicate(format: "%K = %#", VISIBILITY_CODE, PRIVATE)
}
else {
// report problem
}
let query = CKQuery(recordType: POST, predicate: postsPredicate)
let sort = NSSortDescriptor(key: "creationDate", ascending: false)
query.sortDescriptors = [sort]
self.db.perform(query, inZoneWith: nil) { records, error in
if error == nil {
self.list = [records!]
DispatchQueue.main.async(execute: {
self.postTableView.reloadData()
self.refreshControl.endRefreshing()
})
}
else {
if let error = error as? CKError {
print(error)
}
}
}
}
Here's my cellForRowAtIndexPath function:
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
if self.list.count == 1 && self.list[0].count == 0 {
let cell = tableView.dequeueReusableCell(withIdentifier: NO_POSTS, for: indexPath)
return cell
}
let post = self.list[indexPath.section][indexPath.row]
let cell = tableView.dequeueReusableCell(withIdentifier: POST_IN_FEED_CELL, for: indexPath)
if let postCell = cell as? PostInFeedTableViewCell {
// deleted the assignment of values to other UI elements in my custom cell
let predicate = NSPredicate(format: "%K = %#", LIBRARY_CODE, post.object(forKey: LIBRARY_CODE) as! String)
let query = CKQuery(recordType: LIBRARY_ITEM, predicate: predicate)
self.db.perform(query, inZoneWith: nil) { records, error in
if error == nil {
let libraryItem = records?[0]
DispatchQueue.main.async(execute: {
postCell.title.text = libraryItem?.object(forKey: NAME) as! String
})
}
else {
if let error = error as? CKError {
print(error)
}
}
}
}
return cell
}

Refreshing gives Error that "found nil while unwrapping an Optional value"

I'm moving this getCloudKit function from ViewController.swift to Lay.swift so I can keep everything in a single class.
var objects = [Lay]()
override func viewDidLoad() {
super.viewDidLoad()
self.refreshControl?.addTarget(self, action: "handleRefresh:", forControlEvents: UIControlEvents.ValueChanged)
self.getCloudKit()
}
func handleRefresh(refreshControl: UIRefreshControl) {
self.objects.removeAll()
self.getCloudKit()
}
func getCloudKit() {
let now = NSDate()
let predicate = NSPredicate(format: "TimeDate > %#", now)
let sort = NSSortDescriptor(key: "TimeDate", ascending: true)
let container = CKContainer.defaultContainer()
let publicData = container.publicCloudDatabase
let query = CKQuery(recordType: “lay”, predicate: predicate)
query.sortDescriptors = [sort]
publicData.performQuery(query, inZoneWithID: nil) { results, error in
if error == nil {
for lay in results! {
let newlay = Lay()
newLay.tColor = lay["tColor"] as! String
newLay.timeDate = lay["TimeDate"] as! AnyObject
newLay.matchup = lay["Matchup"] as! String
let applicationDict = ["tColor" : newLay.tColor, "Matchup" : newLay.matchup]
let transfer = WCSession.defaultSession().transferUserInfo(applicationDict)
self.objects.append(newLay)
}
dispatch_async(dispatch_get_main_queue(), { () -> Void in
self.refreshControl!.endRefreshing()
self.tableView.reloadData()
})
} else {
print(error)
}
}
}
The problem is when I move it (and the necessary related code):
Error in Lay.swift on TableViewController().refreshControl!.endRefreshing()
saying "fatal error: unexpectedly found nil while unwrapping an
Optional value"
Need to put my WCSession: transferUserInfo code from getCloudKit in my AppDelegate.swift, but keep getting errors when I try
New ViewController.swift:
override func viewDidLoad() {
super.viewDidLoad()
self.refreshControl?.addTarget(self, action: "handleRefresh:", forControlEvents: UIControlEvents.ValueChanged)
Lay().getCloudKit()
}
override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
// #warning Incomplete implementation, return the number of rows
return Lay().objects.count
}
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("reuseIdentifier", forIndexPath: indexPath)
let object = Lay().objects[indexPath.row];
if let label = cell.textLabel{
label.text = object.matchup
}
return cell
}
func handleRefresh(refreshControl: UIRefreshControl) {
Lay().objects.removeAll()
Lay().getCloudKit()
}
New Lay.swift:
var objects = [Lay]()
func getCloudKit() {
let now = NSDate()
let predicate = NSPredicate(format: "TimeDate > %#", now)
let sort = NSSortDescriptor(key: "TimeDate", ascending: true)
let container = CKContainer.defaultContainer()
let publicData = container.publicCloudDatabase
let query = CKQuery(recordType: “lay”, predicate: predicate)
query.sortDescriptors = [sort]
publicData.performQuery(query, inZoneWithID: nil) { results, error in
if error == nil {
for lay in results! {
let newlay = Lay()
newLay.tColor = lay["tColor"] as! String
newLay.timeDate = lay["TimeDate"] as! AnyObject
newLay.matchup = lay["Matchup"] as! String
let applicationDict = ["tColor" : newlay.tColor, "Matchup" : newlay.matchup]
let transfer = WCSession.defaultSession().transferUserInfo(applicationDict)
self.objects.append(newlay)
}
dispatch_async(dispatch_get_main_queue(), { () -> Void in
TableViewController().refreshControl!.endRefreshing()
TableViewController().tableView.reloadData()
})
} else {
print(error)
}
}
New AppDelegate:
private func setupWatchConnectivity() {
if WCSession.isSupported() {
let session = WCSession.defaultSession()
session.delegate = self
session.activateSession()
}
}
private func sendUpdatedDataToWatch(notification: NSNotification) {
if WCSession.isSupported() {
let session = WCSession.defaultSession()
if session.watchAppInstalled
{
let applicationDict = ["TColor" : Lay().tColor, "Matchup" : Lay().matchup]
let transfer = WCSession.defaultSession().transferUserInfo(applicationDict)
NSLog("Transfer AppDelegate: %#", transfer)
NSLog("Trans AppDelegate: %#", applicationDict)
session.transferCurrentComplicationUserInfo(applicationDict)
}
}
}
Your code has ViewController() and Lay() throughout. This will create new instances of those objects. Therefore, although refreshControl is non-nil in your actual view controller, it will be nil in a newly created one.
By splitting out the getCloudKit function, you're allowing the view controller to just manage the view, and the new class to just manage Cloud Kit. This is good, so ideally your Cloud Kit controller should not know anything about the view controller. Therefore, getCloudKit shouldn't be calling reloadData. Instead, you could pass a closure into getCloudKit that gets called when the query finishes. Something along the lines of:
func getCloudKit(completion completionHandler: (([Lay]) -> Void)?) {
...
publicData.performQuery(query, inZoneWithID: nil) { results, error in
if error == nil {
...
if let completion = completionHandler {
completion(self.objects)
}
} else {
print(error)
}
}
Then in ViewController:
let layCloudKit = LayCloudKit()
layCloudKit.getCloudKit(completion: { (objects) -> Void in
dispatch_async(dispatch_get_main_queue(), {
self.objects = objects
self.refreshControl!.endRefreshing()
self.tableView.reloadData()
})
})
Note that I've assumed you would put the Lay Cloud Kit controller into a separate Swift file, as the Lay model class shouldn't need to know about Cloud Kit. If you want to put it in the same file as Lay, then you should mark the func as static or class, because you don't want to create a dummy instance of Lay just to call getCloudKit. In that case, you would call it using Lay.getCloudKit (ie. specifying the Lay class, rather than a Lay instance).

UITableview reloading tableview and cell

I am trying to reload my table view using
self.tableView.reloadData()
It works properly if I'm loading static datasource using array. Everything work properly.
But when I try to use my query function with parse, it loads a new cell but the contents of the tableview cell doesn't change. If I re-open the app, the cells will update properly.
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cellIdentifier = "EmpPostTVCellIdentifier"
let cell: EmpPostTVCell? = tableView.dequeueReusableCellWithIdentifier(cellIdentifier, forIndexPath: indexPath) as? EmpPostTVCell
//If datasource
if dataSource.isEmpty{
fetchDataFromParse()
print("no posts")
}
let itemArr:PFObject = self.dataSource[indexPath.row]
cell?.companyPostLabel.text = (PFUser.currentUser()?.objectForKey("companyName")!.capitalizedString)! as String
cell?.occupationPostLabel.text = itemArr["occupation"]!.capitalizedString as String
cell?.countryPostLabel.text = itemArr["country"]!.capitalizedString as String
let companyImage: PFFile?
companyImage = PFUser.currentUser()?.objectForKey("profileImageEmployer") as? PFFile
companyImage?.getDataInBackgroundWithBlock({ (data, error) -> Void in
if error == nil{
cell?.companyLogoImage.image = UIImage(data: data!)
}
})
let dateArr = createdByDate[indexPath.row]
let strDate = Settings.dateFormatter(dateArr)
cell?.closingDateLabel .text = strDate
return cell!
}
I am using pull to refresh my tableviews contents using this code
func refresh(sender:AnyObject)
{
dispatch_async(dispatch_get_main_queue(), { () -> Void in
self.fetchDataFromParse()
self.tableView.reloadData()
self.refreshControl?.endRefreshing()
})
}
with or without the dispatch_asynch function the results remains the same. It just add new tableviewcell but the contents in it does not change. Any ideas guys?
edit 1 :
func fetchDataFromParse() {
// MARK: - JOB POST QUERY
if PFUser.currentUser()?.objectId == nil{
PFUser.currentUser()?.saveInBackgroundWithBlock({ (success, error) -> Void in
let query = PFQuery(className: "JobPost")
//creating a pointer
let userPointer = PFUser.objectWithoutDataWithObjectId(PFUser.currentUser()?.objectId)
query.whereKey("postedBy", equalTo: userPointer)
query.orderByDescending("createdAt")
let objects = query.findObjects()
for object in (objects as? [PFObject])!{
//print(object.objectId)
self.dataSource.append(object)
self.createdByDate.append((object.objectForKey("closingDate") as? NSDate)!)
print(self.dataSource)
print(self.createdByDate)
}
})
} else {
let query = PFQuery(className: "JobPost")
//creating a pointer
let userPointer = PFUser.objectWithoutDataWithObjectId(PFUser.currentUser()?.objectId)
query.whereKey("postedBy", equalTo: userPointer)
query.orderByDescending("createdAt")
let objects = query.findObjects()
for object in (objects as? [PFObject])!{
//print(object.objectId)
self.dataSource.append(object)
self.createdByDate.append((object.objectForKey("closingDate") as? NSDate)!)
print(self.dataSource)
print(self.createdByDate)
}
}//end of PFUser objectID == nil else clause
}
Let's see the content of the fetchDataFromParse() function where I presume you're filling the self.dataSource array
Try to call self.tableview.reloadData() when fetchDataFromParse() is finished.
Check whether your dataSource array is empty at the end of your fetchDataFromParse method
PFUser.currentUser()?.saveInBackgroundWithBlock is an asynchronus method. So your tableView cell is having no data.

Resources