Passing objectId via prepareForSegue using Parse PFQueryTableViewController - uitableview

This is very likely a simple answer. I am using PFQueryTableViewController to populate a table view. I want to pass the Parse objectId of the current row to my crDetailViewController but can't figure out how. The variable I'm trying to pass is called critterId.
I've updated my code to reflect Race B's #race-b suggestion below and it enables the build to succeed but I get a breakpoint error when I select an actual cell it creates a breakpoint. When I look at the debug code it shows a value of nil for the object, so it's not getting passed somehow since by definition there's an entity selected, otherwise how would the cell be selectable in the first place? Thanks for help!
import UIKit
class crTableViewController: PFQueryTableViewController {
// Initialise the PFQueryTable tableview
override init!(style: UITableViewStyle, className: String!) {
super.init(style: style, className: className)
}
required init(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
// Configure the PFQueryTableView
self.parseClassName = "CritterType"
self.textKey = "Name"
self.imageKey = "mainPhoto"
self.pullToRefreshEnabled = true
self.paginationEnabled = false
}
// Define the query that will provide the data for the table view
override func queryForTable() -> PFQuery! {
var query = PFQuery(className: "CritterType")
return query
}
override func tableView(tableView: UITableView, heightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat {
return 65
}
override func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
self.performSegueWithIdentifier("crDetailSegue", sender: self)
}
override func prepareForSegue(segue: UIStoryboardSegue,
sender: AnyObject?) {
if segue.identifier == "crDetailSegue" {
let detailViewController = segue.destinationViewController
as! crDetailViewController
let myIndexPath = self.tableView.indexPathForSelectedRow()
let row = myIndexPath!.row
println(row)
println(myIndexPath)
detailViewController.critterId = self.objectAtIndexPath(myIndexPath)
}
}
}

There should be an objectAtIndexPath that you can use that will return the object at whatever indexPath.row that you want.

Parse puts the data into an array called objects so to pass the object instead of the ID use the following code:
detailViewController.critter = self.objects[indexPath.row]

Related

How to reload tableView data after data is passed by a Segue

I have two table views. One which the user clicks on and one where data is displayed. When the user clicks on a cell in the first table view a query is made to my firebase database and the query is stored in an Array. I then pass the data through a segue. I used a property observer so I know that the variable is being set. By using break points I was able to determine that my variable obtains its value right before the cellForRowAtIndexPath method. I need help displaying the data in my table view. I do not know where to reload the data to get the table view to update with my data. I am using Swift.
EDIT 2: I have solved my problem. I will post my first and second table views so that you can see my solution.
FirstTableView
import UIKit
import Firebase
import FirebaseDatabase
class GenreTableViewController: UITableViewController {
let dataBase = FIRDatabase.database()
var genreArray = ["Drama","Classic,Comic/Graphic novel","Crime/Detective","Fable,Fairy tale","Fantasy","Fiction narrative", "Fiction in verse","Folklore","Historical fiction","Horror","Humour","Legend","Magical realism","Metafiction","Mystery","Mythology","Mythopoeia","Realistic fiction","Science fiction","Short story","Suspense/Thriller","Tall tale","Western,Biography","Autobiography","Essay","Narrative nonfiction/Personal narrative","Memoir","Speech","Textbook","Reference book","Self-help book","Journalism", "Religon"]
var ResultArray: [NSObject] = []
var infoArray:[AnyObject] = []
override func viewDidLoad() {
super.viewDidLoad()
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
}
// MARK: - Table view data source
override func numberOfSectionsInTableView(tableView: UITableView) -> Int {
// #warning Incomplete implementation, return the number of sections
return 1
}
override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
// #warning Incomplete implementation, return the number of rows
return genreArray.count
}
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("cell", forIndexPath: indexPath)
// Configure the cell...
cell.textLabel?.text = genreArray[indexPath.row]
return cell
}
override func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
}
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
let DestViewController: ResultTableViewController = segue.destinationViewController as! ResultTableViewController
if segue.identifier == "letsGo" {
if let indexPath = self.tableView.indexPathForSelectedRow {
let tappedItem = self.genreArray[indexPath.row]
DestViewController.someString = tappedItem
}
}
}
}
import UIKit
import Firebase
import FirebaseDatabase
class ResultTableViewController: UITableViewController {
let dataBase = FIRDatabase.database()
var SecondResultArray: [FIRDataSnapshot]! = []
var someString: String?{
didSet {
print("I AM A LARGE TEXT")
print(someString)
}
}
override func viewDidLoad() {
let bookRef = dataBase.reference().child("books")
bookRef.queryOrderedByChild("Genre")
.queryEqualToValue(someString)
.observeSingleEventOfType(.Value, withBlock:{ snapshot in
for child in snapshot.children {
self.SecondResultArray.append(child as! FIRDataSnapshot)
//print(self.ResultArray)
}
dispatch_async(dispatch_get_main_queue()) {
self.tableView.reloadData()
}
})
super.viewDidLoad()
// Uncomment the following line to preserve selection between presentations
// self.clearsSelectionOnViewWillAppear = false
// Uncomment the following line to display an Edit button in the navigation bar for this view controller.
// self.navigationItem.rightBarButtonItem = self.editButtonItem()
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
// MARK: - Table view data source
override func numberOfSectionsInTableView(tableView: UITableView) -> Int {
// #warning Incomplete implementation, return the number of sections
return 1
}
override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
// #warning Incomplete implementation, return the number of rows
return SecondResultArray.count
}
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("cell2", forIndexPath: indexPath)
// Configure the cell...
let bookSnapShot: FIRDataSnapshot! = self.SecondResultArray[indexPath.row]
let book = bookSnapShot.value as! Dictionary<String, String>
let Author = book["Author"] as String!
let Comment = book["Comment"] as String!
let Genre = book["Genre"] as String!
let User = book["User"] as String!
let title = book["title"] as String!
cell.textLabel?.numberOfLines = 0
cell.textLabel?.lineBreakMode = NSLineBreakMode.ByWordWrapping
cell.textLabel?.text = "Author: " + Author + "\n" + "Comment: " + Comment + "\n" + "Genre: " + Genre + "\n" + "User: " + User + "\n" + "Title: " + title
let photoUrl = book["bookPhoto"], url = NSURL(string:photoUrl!), data = NSData(contentsOfURL: url!)
cell.imageView?.image = UIImage(data: data!)
return cell
}
}
For better context and troubleshooting here is my current code for the tableView which is supposed to display data:
import UIKit
class ResultTableViewController: UITableViewController {
var SecondResultArray: Array<NSObject> = []{
willSet(newVal){
print("The old value was \(SecondResultArray) and the new value is \(newVal)")
}
didSet(oldVal){
print("The old value was \(oldVal) and the new value is \(SecondResultArray)")
self.tableView.reloadData()
}
}
override func viewDidLoad() {
print ("I have this many elements\(SecondResultArray.count)")
super.viewDidLoad()
}
// MARK: - Table view data source
override func numberOfSectionsInTableView(tableView: UITableView) -> Int {
// #warning Incomplete implementation, return the number of sections
return 1
}
override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
// #warning Incomplete implementation, return the number of rows
return SecondResultArray.count
}
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("cell2", forIndexPath: indexPath)
cell.textLabel?.text = SecondResultArray[indexPath.row] as? String
return cell
}
}
Edit:
Here is my first table view controller. I have tried using the completion handler, but I can't call it correctly and I am constricted by the fact that my query happens in the didSelectRowAtIndexPath method. Please help.
import UIKit
import Firebase
import FirebaseDatabase
class GenreTableViewController: UITableViewController {
let dataBase = FIRDatabase.database()
var genreArray = ["Drama","Classic,Comic/Graphic novel","Crime/Detective","Fable,Fairy tale","Fantasy","Fiction narrative", "Fiction in verse","Folklore","Historical fiction","Horror","Humour","Legend","Magical realism","Metafiction","Mystery","Mythology","Mythopoeia","Realistic fiction","Science fiction","Short story","Suspense/Thriller","Tall tale","Western,Biography","Autobiography","Essay","Narrative nonfiction/Personal narrative","Memoir","Speech","Textbook","Reference book","Self-help book","Journalism", "Religon"]
var ResultArray: [NSObject] = []
var infoArray:[AnyObject] = []
override func viewDidLoad() {
super.viewDidLoad()
// Uncomment the following line to preserve selection between presentations
// self.clearsSelectionOnViewWillAppear = false
// Uncomment the following line to display an Edit button in the navigation bar for this view controller.
// self.navigationItem.rightBarButtonItem = self.editButtonItem()
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
// MARK: - Table view data source
override func numberOfSectionsInTableView(tableView: UITableView) -> Int {
// #warning Incomplete implementation, return the number of sections
return 1
}
override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
// #warning Incomplete implementation, return the number of rows
return genreArray.count
}
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("cell", forIndexPath: indexPath)
cell.textLabel?.text = genreArray[indexPath.row]
return cell
}
override func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
typealias CompletionHandler = (result:NSObject?, error: NSError?) -> Void
func getData(completionHandeler: CompletionHandler){
let bookRef = self.dataBase.reference().child("books")
let GenreSelector = self.genreArray[indexPath.row]
bookRef.queryOrderedByChild("Genre")
.queryEqualToValue(GenreSelector)
.observeSingleEventOfType(.Value, withBlock:{ snapshot in
for child in snapshot.children {
print("Loading group \((child.key!))")
self.ResultArray.append(child as! NSObject)
}
print(self.ResultArray)
self.performSegueWithIdentifier("letsGo", sender: self)
self.tableView.reloadData()
})
}
}
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
var DestViewController: ResultTableViewController = segue.destinationViewController as! ResultTableViewController
DestViewController.SecondResultArray = self.ResultArray
}
You can inject the data to the destination viewController in prepareForSegue Method of the first UIViewController and reload your UITableView in viewDidAppear. If you are getting your data asynchronously, have a completionHandler and reload it in the completionHandler. Here is an example.
func fetchDataWithCompletion(response: (NSDictionary?, error:NSError?)-> Void) -> Void {
//make the API call here
}
How about this:
Assume you have an array (myArray) populated from Firebase and stored in the first tableViewController. There's a second tableViewController and a segue connecting them.
We want to be able to tap on an item in the first tableviewController, have the app retrieve detailed data for the item from Firebase (a 'data' node) and display the detailed data in the second tableViewController.
Firebase structure
some_node
child_node_0
data: some detailed data about child_node_0
child_node_1
data: some detailed data about child_node_1
Within the second tableViewContoller:
var passedObject: AnyObject? {
didSet {
self.configView() // Update the view.
}
}
Tapping an item in the first tableView calls the following function
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
if segue.identifier == "showListInSecondTable" {
if let indexPath = self.tableView.indexPathForSelectedRow {
let tappedItem = myArray[indexPath.row] as! String
let keyOfTappedItem = tappedItem.firebaseKey //child_node_0 for example
doFirebase(keyOfTappedItem)
}
}
}
and the prepareForSegue then calls the following which loads the data from firebase and when the snapshot returns within the block, it populates the passedObject property in the second tableView
func doFirebase(firebaseKey: String) {
ref = myRootRef.childByAppendingPath("\(firebaseKey)/data")
//if we want the detailed data for child_node_0 this would resolve
// to rootRef/child_node_0/data
ref.observeSingleEventOfType(.Value, { snapshot in
let detailObjectToPass = snapshot.Value["data"] as! NSArray or string etc
let controller = (segue.destinationViewController as! UINavigationController).myViewController as! SecondViewController
controller.passedObject = detailObjectToPass
}
and of course in secondController, setting the passedArray calls didSet and sets up the view, and tells the tableView to reload itself, displaying the passed array.
func configView() {
//set up the view and buttons
self.reloadData()
}
I did this super quick so ignore the typos's. The pattern is correct and satisfies the question. (and eliminates the need for an observer to boot!)
P.S. this is way over coded but I wanted to demonstrate the flow and leveraging the asynchronous call to firebase to load the second tableView when the data was valid within the block.
Try updating your closure to include this:
dispatch_async(dispatch_get_main_queue()) {
self.tableView.reloadData()
}
Edit:
On second read, you are already using a completion handler, but I think you didn't see it. Let me correct your code above a bit:
override func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
let bookRef = self.dataBase.reference().child("books")
let GenreSelector = self.genreArray[indexPath.row]
bookRef.queryOrderedByChild("Genre")
.queryEqualToValue(GenreSelector)
.observeSingleEventOfType(.Value, withBlock:{ snapshot in
// This here is your completion handler code!
// I assume it is called asynchronously once your DB is done
for child in snapshot.children {
print("Loading group \((child.key!))")
self.ResultArray.append(child as! NSObject)
}
print(self.ResultArray)
self.performSegueWithIdentifier("letsGo", sender: self)
// self.tableView.reloadData() // is this really needed
})
}
}
You defined a closure, but simply didn't call it. I don't see a reason for that anyways, assuming the block gets called once the database gives you your results. Am I missing something?
That's a good start already, but I think you didn't entirely get how to use a completion handler in this regard, but of course I may be wrong.
I built on top of user3861282's answer and created a small demo project at my github.
In short: You can do all inter-table-communication in the prepareForSegue: method of your first table view controller. Configure the second table view controller there (via its vars). Not any closures/completion handlers there yet.
Then in the second view controller's viewWillAppear: method, start the loading (including an animation if you want). I suggest something like NSURLSession that already defines a completion handler. In that you work with your data from remote, stop any loading animations and you're good.
If the completion handler must be defined in the first table view controller, you can even set it as a var in the second table view controller. That way you "hand over" the closure, i.e. "piece of code".
Alternatively, you can start animations and remote request in the first table view controller and then performSegueWithIdentifier once that is done. In your question you wrote that you want to load in the second table view controller, however, if I understood you correctly.
Your code above properly defines a closure that expects a completion handler (which is also a closure and so kind of doubles what you want), but you never actually call it somewhere. Nor do you call the completion handler in the closure. See my demo for how it can work.
The project I wrote illustrates just one way to do it (minus animations, not enough time). It also shows how you can define your own function expecting a completion handler, but as I said, the standard remote connections in the framework provide one anyways.
Based on additional code that was added to the post, the issue is a controller variable going out of scope.
So here's the issue
class MyClass {
func setUpVars {
let x = 1
}
func doStuff {
print(x)
}
}
Create a class and attempt to print the value of x
let aClass = MyClass()
aClass.setUpVars
aClass.doStuff
This will print nothing (conceptually) as once setUpVars ended, the 'x' variable went out of scope.
whereas
class MyClass {
var x: Int
func setUpVars {
x = 1
}
func doStuff {
print(x)
}
}
will print the value of x, 1.
So the real solution is that your viewControllers need to 'stay alive' during the duration of your class (or app).
Here's the pattern. In the MasterViewController
import UIKit
class MasterViewController: UITableViewController {
var detailViewController: DetailViewController? = nil
then in your MasterViewController viewDidLoad (or wherever), create the detailViewController
override func viewDidLoad() {
super.viewDidLoad()
let controllers = split.viewControllers //this is from a splitViewController
self.detailViewController =
controllers[controllers.count-1].topViewController as? DetailViewController
}
and from there you have it... use prepareForSegue to 'send' the data to the detailViewController
Just wanted to have this posted for future reference.
You can reload the TableView with [tableView reloadData];.

How to trigger a reload of PFTableViewController using Parse with Swift?

I've got a PFTableViewController that displays a table of items. I then segue to a navigation controller with root view controller that allows the user to enter a new item - and after they press save, I dismiss the view controller and return to the PFTableViewController.
I would like to reload the PFTableViewController at this point - so that its list includes the item that the user just added. Currently my attempts to get it to reload are not working; the user has to pull to refresh which is broken - it feels like the added item was not properly saved because it doesn't automatically show up.
I've put the self.tableView.reloadData() in the viewDidLoad() function - and it clearly fires as I added a println to verify that it triggers when the new item view is dismissed. But the table is not reloading the data.
Here's 1 version of the code that I've tried:
override func viewDidAppear(animated: Bool) {
println("going to reload")
self.tableView.reloadData()
}
And another, based on another example I saw:
override func viewDidAppear(animated: Bool) {
println("going to reload")
dispatch_async(dispatch_get_main_queue(), { () -> Void in
self.tableView.reloadData()
})
}
Neither are working.
Here's the majority of the PFTableViewController file set-up in case it's useful as well:
import UIKit
import Parse
import ParseUI
class CategoryViewController: PFQueryTableViewController {
var currentObject : PFObject?
var candidates: Array<AnyObject>?
override init(style: UITableViewStyle, className: String!) {
super.init(style: style, className: className)
}
required init(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
self.parseClassName = "CategoryCandidates"
self.textKey = "candidateTitle"
self.pullToRefreshEnabled = true
self.paginationEnabled = false
}
override func queryForTable() -> PFQuery {
var query = PFQuery(className: "CategoryCandidates")
query.whereKey("categoryID", equalTo: currentObject!)
query.orderByDescending("votes")
return query
}
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath, object: PFObject?) -> CandidateTableViewCell {
var cell = tableView.dequeueReusableCellWithIdentifier("Cell") as! CandidateTableViewCell!
if cell == nil {
cell = CandidateTableViewCell(style: UITableViewCellStyle.Default, reuseIdentifier: "Cell")
}
cell?.voteButton.addTarget(self, action: "buttonAction:", forControlEvents: UIControlEvents.TouchUpInside)
// Extract values from the PFObject to display in the table cell
if let candidateTitle = object?["candidateTitle"] as? String {
cell?.textLabel?.text = candidateTitle
}
if let votesTotal = object?["votes"] as? Int {
if votesTotal > 1 {
cell?.votesLabel.text = "\(votesTotal.description) votes"
} else if votesTotal == 1 {
cell?.votesLabel.text = "\(votesTotal.description) vote"
}
}
cell.voteButton.tag = indexPath.row
return cell
}
Thanks in advance!
The PFQueryTable vc must do two things to refresh the data: (1) rerun the query, and (2) after the query has run, reload the table view. The method loadObjects runs the query, and the query's completion handler reloads the table view.
You can use a delegate to notify the PFTableViewController
protocol CategoryViewControllerDelegate {
func reloadTable()
}
class CategoryViewController: PFQueryTableViewController, CategoryViewControllerDelegate {
// MARK: CategoryViewControllerDelegate
func reloadTable() {
self.tableView.reloadTable()
}
// method to segue into adding of new item
fund addNewItem() {
let view = NewItemViewController()
view.delegate = self
presentViewController(view, animated: true, completion: nil)
}
}
class NewItemViewController: UIViewController {
var delegate:CategoryViewControllerDelegate?
func handleAddNewItem() {
// handle the adding of new item here
// ...
// then call the delegate to reload the table
delegate.reloadTable()
// dismiss and return to parent view
// ...
}
}

PFQueryTableViewController pagination doesn't work with heightForRowAtIndexPath

I am using parse.com framework with Swift and in PFQueryTableViewController when I set the pagination it won't work. If the DB has less rows than the number set in objectPerPage it works fine, but if there are more rows and when I run the app it keeps showing the loading screen and nothing is downloaded, when I do "swipe as refresh" it crash as
Error
*** Terminating app due to uncaught exception 'NSRangeException', reason: '*** -[__NSArrayM objectAtIndex:]: index 5 beyond bounds [0 .. 4]
ImagesTableViewController.swift
import UIKit
import Parse
import ParseUI
import Bolts
class ImagesTableViewController: PFQueryTableViewController {
#IBAction func unwindToSegue (segue : UIStoryboardSegue) {}
// Initialise the PFQueryTable tableview
override init(style: UITableViewStyle, className: String!) {
super.init(style: style, className: className)
}
required init(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
// Configure the PFQueryTableView
self.parseClassName = "Image"
self.pullToRefreshEnabled = true
self.paginationEnabled = true
self.objectsPerPage = 5
}
// Define the query that will provide the data for the table view
override func queryForTable() -> PFQuery {
var query = PFQuery(className: "Image")
query.whereKey("deleted", notEqualTo: 1)
query.orderByDescending("createdAt")
return query
}
//override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath, object: PFObject?) -> PFTableViewCell {
var cell = tableView.dequeueReusableCellWithIdentifier("ImageCell") as! ImageTVCell!
if cell == nil {
cell = ImageTVCell(style: UITableViewCellStyle.Default, reuseIdentifier: "ImageCell")
}
// Extract values from the PFObject to display in the table cell HEADLINE
if let caption = object?["caption"] as? String {
cell?.headlineLabel?.text = caption
}
// Display image
var initialThumbnail = UIImage(named: "question")
cell.postImageView.image = initialThumbnail
if let thumbnail = object?["image"] as? PFFile {
cell.postImageView.file = thumbnail
cell.postImageView.loadInBackground()
}
return cell
}
// if I remove this code pagination work but the cell height is wrong
override func tableView(tableView: UITableView, heightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat {
return calculateHeightForRowAtIndexPath(indexPath)
}
func calculateHeightForRowAtIndexPath(indexPath: NSIndexPath) -> CGFloat {
if let ratio = objectAtIndexPath(indexPath)?["aspect"] as? Float {
println("Ratio: \(ratio)")
return tableView.bounds.size.width / CGFloat(ratio)
} else {
return 50.0
}
}
#IBAction func addNewPhotoButton(sender: UIBarButtonItem) {
self.tabBarController?.tabBar.hidden = true
self.performSegueWithIdentifier("showUploadNewImage", sender: self)
}
}
This problem occurs because of PFQueryTableViewController's implementation of the method tableView:numberOfRowsInSection from the UITableViewDataSource. I've copy/pasted it from the GitHub repo containing PFQueryTableViewController.m
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
NSInteger count = [self.objects count];
if ([self _shouldShowPaginationCell]) {
count += 1;
}
return count;
}
It simply returns the count of objects to display (which makes sense), but if pagination is enabled, then it requires for an extra cell to be shown. This means you have to manually created another cell with the text "Load more data" or something like that, which would trigger a refresh.
A way to overcome this is simply by overriding tableView:numberOfRowsInSection yourself with the following:
override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return self.objects!.count
}
UPDATE 1
The prebuilt Parse pagination button was gone in previous answer
Use the following code snippet for calculating the height of the cells to display the prebuilt Parse pagination button
func calculateHeightForRowAtIndexPath(indexPath: NSIndexPath) -> CGFloat {
// Special case for pagination, using the pre-built one by Parse
if (indexPath.row >= objects!.count) { return 50.0 }
// Determines the height if an image ratio is present
if let ratio = objectAtIndexPath(indexPath)?["aspect"] as? Float {
println("Ratio: \(ratio)")
return tableView.bounds.size.width / CGFloat(ratio)
} else {
return 50.0
}
}
Using Parse 1.11 with iOS 9.2 and Xcode 7.2 Parse Pagination works perfectly.
Problems surface when the user override some funcs used by Parse itself without properly managing the "Load More ..." row added by Parse.
In my case I needed to override tableView-canEditRowAtIndexPath to determine whether the current user can or cannot delete the row according to the object's ACL.
My initial func was:
override func tableView(tableView: UITableView, canEditRowAtIndexPath indexPath: NSIndexPath) -> Bool {
if let curUser = PFUser.currentUser() {
let currentObject = objects![indexPath.row]
if let acl = currentObject.ACL {
return acl.getWriteAccessForUser(curUser)
} else {
return true
}
}
return true
}
but I got the exception of indexpath out of bounds when the Load More line was met during list scrolling.
Problem was solved adding this test:
if (indexPath.row == self.objects!.count) { // row "Load More ..."
return true
}
Without this code the "Load More ..." row was not added by Parse!!
So the complete correct overriding func is:
override func tableView(tableView: UITableView, canEditRowAtIndexPath indexPath: NSIndexPath) -> Bool {
if (indexPath.row == self.objects!.count) { // row "Load More ..."
return true
}
if let curUser = PFUser.currentUser() {
let currentObject = objects![indexPath.row]
if let acl = currentObject.ACL {
return acl.getWriteAccessForUser(curUser)
} else {
return true
}
}
return true
}
Generally speaking all overridden funcs including heightForRowAtIndexpath, must take care of the extra line added by Parse when pagination is enabled.
HTH
Roberto Targa

didSelectRowAtIndexPath indexpath after filter UISearchController - Swift

I’m implementing a search bar with an UISearchController within a sectioned table. So far so good.
The main issue is that when the the filtered results come along, it’s a whole new table with no sections and fewer rows.
When selecting the row, I perform a segue to that position in the array, but the detailed view is expecting that exact row or index from the main array, which I can’t get from the filtered array of objects, which may be [0] [1] [2] in 300 elements.
I guess I can compare the selected object with the main array and assuming there’s no duplicates, get the index from there and pass it over… But these seems pretty inefficient to me.
Apple does something similar (I unfortunately don’t know how) when filtering Contacts, in the Contacts App. How they pass the contact object? That’s pretty much my goal.
Here I let you a snippet of what I’m doing:
func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
if(self.resultSearchController.active) {
customerAtIndex = indexPath.row // Issue here
performSegueWithIdentifier("showCustomer", sender: nil)
}
else {
customerAtIndex = returnPositionForThisIndexPath(indexPath, insideThisTable: tableView)
performSegueWithIdentifier("showCustomer", sender: nil)
}
}
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
if segue.identifier == "showCustomer" {
if let destination = segue.destinationViewController as? CustomerDetailViewController {
destination.newCustomer = false
destination.customer = self.customerList[customerAtIndex!]
destination.customerAtIndex = self.customerAtIndex!
destination.customerList = self.customerList
}
}
}
You can either do in another way, it a trick, but it works. First change your didSelectRowAtIndexPath as below:
func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
var object :AnyObject?
if(self.resultSearchController.active) {
object = filteredArray[indexPath.row]
}
else {
object = self.customerList[indexPath.row]
}
performSegueWithIdentifier("showCustomer", sender: object)
}
Now, in prepareForSegue, get back the object and send it to your detailed view controller
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
if segue.identifier == "showCustomer" {
if let destination = segue.destinationViewController as? CustomerDetailViewController {
destination.newCustomer = false
destination.customer = sender as! CustomerObject
destination.customerAtIndex = self.customerList.indexOfObject(destination.customer)
destination.customerList = self.customerList
}
}
}
Here's the trick I used in my code, I basically load the tableView from the filteredObjects array so then indexPath is always correct:
var selectedObject: Object?
private var searchController: UISearchController!
private var allObjects: [Object]? {
didSet {
filteredObjects = allObjects
}
}
private var filteredObjects: [Object]? {
didSet {
NSOperationQueue.mainQueue().addOperationWithBlock {
self.tableView.reloadData()
}
}
}
override func viewDidLoad() {
super.viewDidLoad()
loadData { objects in
self.allObjects = objects
}
}
// MARK:- UITableView
override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return filteredObjects?.count ?? 0
}
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("Cell", forIndexPath: indexPath) as! UITableViewCell
cell.textLabel?.text = filteredObjects?[indexPath.row].name
return cell
}
override func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
selectedObject = filteredObjects?[indexPath.row]
}
// MARK:- UISearchBarDelegate
func searchBar(searchBar: UISearchBar, textDidChange searchText: String) {
if !searchText.isEmpty {
filteredObjects = allObjects?.filter{ $0.name.lowercaseString.rangeOfString(searchText.lowercaseString) != nil }
} else {
filteredObjects = allObjects
}
Add a new property NSMutableArray *searchArray to your table view class and then pass all search results to this array in -(void)filterContentForSearchText:scope: method. After that you will be able to get the selected object self.searchArray[indexPath.row] in tableView:didSelectRowAtIndexPath:.
I see two solutions -
1) Why not make detailed view look for row or index in filtered array instead of main array. I guess you are concerned only about the object in that row that you want to use in detail.
2) Make each object in the array have a unique id. Pass the unique id on selection thru segue and let detailed view search(predicate) in main array for that id.

Parse.com Query Not Loading

I'm creating an app with displays parse data in a table view. I downloaded a template from https://github.com/Bizzi-Body/HowToDisplayImagesInTableViewFromParse it all worked fine when I ran it but when I put my Parse app id in and Client id it just shows a loading screen (see Screenshot)
So I thought it might be a problem with the app template so downloaded a different one and edited it, but the same problem happen, so I'm think its something wrong with my parse account settings.
import UIKit
class TableViewController: PFQueryTableViewController {
// Initialise the PFQueryTable tableview
override init!(style: UITableViewStyle, className: String!) {
super.init(style: style, className: className)
}
required init(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
// Configure the PFQueryTableView
self.parseClassName = "Photo"
self.textKey = "updatedAt"
self.pullToRefreshEnabled = true
self.paginationEnabled = false
}
// Define the query that will provide the data for the table view
override func queryForTable() -> PFQuery! {
var query = PFQuery(className: "Photo")
query.orderByAscending("updatedAt")
return query
}
//override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath, object: PFObject) -> PFTableViewCell {
var cell = tableView.dequeueReusableCellWithIdentifier("Cell") as CustomCell!
if cell == nil {
cell = CustomCell(style: UITableViewCellStyle.Default, reuseIdentifier: "Cell")
}
// Extract values from the PFObject to display in the table cell
cell.username.text = object["user"] as String!
cell.itemdetail.text = object["description"] as String!
cell.price.text = object["Price"] as String!
var thumbnail = object["Image"] as PFFile
var initialThumbnail = UIImage(named: "question")
cell.productimage.image = initialThumbnail
cell.productimage.file = thumbnail
cell.productimage.loadInBackground()
return cell
}
}
If you enabled Parse LocalDatastore a quite similar issue is here:
https://github.com/ParsePlatform/ParseUI-iOS/issues/26
So try to disable LocalDatastore or update ParseSDK
I think the problem is that the Custom Cell class name does not match the Custom Cell Class detailed in the story board.
If your custom cell class is called "CustomCell" (i.e. - in "CustomCell.swift") you need to make sure that in the story board the custom class setting for the prototype cell is also "CustomCell".
Maybe this changed when you created your one custom class/cell

Resources