How to make a prototype cell 'disappear' when it is 'empty' (not clickable) - swift

I am making an application where the user can see certain items/users within his x km radius (much like Tinder where you can set the radius of girls/guys you want to see in your area). So in my cellForRowAtIndexPath function I am determining whether a cell can be shown.
If he is in the radius, the event is shown. If the location is too far away, it shouldn't be using a cell.
My current code just hides the cell, but it is still clickable. I want it to NOT use a cell in the first place, but I couldn't find how to do it. Any ideas?
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath, object: PFObject?) -> AllGeneralMeetsTableViewCell {
//get location from backend
let locLati = object?["coordLat"] as? Double
let locLongi = object?["coordLong"] as? Double
let currentLocation:CLLocation = CLLocation(latitude: localLati , longitude: localLongi)
let meetLocation:CLLocation = CLLocation(latitude: locLati! , longitude: locLongi!)
let meters:CLLocationDistance = currentLocation.distanceFromLocation(meetLocation)
// make distance in km
let distInKm = meters/1000
//get distance that user has set in his settings
let cell = tableView.dequeueReusableCellWithIdentifier("GeneralMeetsIdentifier") as! AllGeneralMeetsTableViewCell!
if (distInKm <= settingsKm) {
// Extract values from the PFObject to display in the table cel
if let title = object?["title"] as? String {
cell?.titleCell?.text = title
}
if let message = object?["message"] as? String {
cell?.messageCell?.text = message
}
if let image = object?["image"] as? PFFile {
image.getDataInBackgroundWithBlock({ (imageData: NSData?,error: NSError?) -> Void in
if (error == nil) {
let image1 = UIImage(data:imageData!)
cell.imageCell.image = image1
}
})
}
return cell
}
else {
return cell
}
}
}
The data is being returned by the following query
override func queryForTable() -> PFQuery {
let query = PFQuery(className: self.parseClassName!)
let FBID = myUser.objectForKey("facebookID")!
query.whereKey("facebookID", equalTo: FBID)
query.whereKey("private", equalTo: "false")
return query
}

In your cellForRowAtIndexPath-
if cell is to be displayed {
tableView.rowHeight = 120
//Replace 120 with desired rowHeight
} else {
tableView.rowHeight = 0
}
Hope this helps :-)

You want to make the cell disappear when there is no data. If I am right, then you can use this library called as 'DZNEmptyDataSet' to display some images telling the user that there is no data to load. Use Cocoapods to install it, or just drag the files and create a bridging header. The usage is pretty straightforward as well - just follow the documentation at the GitHub.

The proper solution is to remove the data from your data source so that it won't be displayed in the first place. But sometimes this is a lot of work.
If you just want to hide a cell you can give it a zero height using the tableView(_:heightForRowAtIndexPath:) delegate method:
func tableView(tableView: UITableView, heightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat {
if indexPath.row == 1 {
return 0.0
}
return tableView.rowHeight
}

Parse provides a type PFGeopoint and it is even supported in the Query, with Parse framework you can also obtain location the same way you download stuff from database, this way you can download only those posts that are available for the user in his range...
PFGeoPoint.geoPointForCurrentLocationInBackground { (geoPoint: PFGeoPoint?, error: NSError?) -> Void in
if error == nil { //asynchronous, U have access to GPS location
// create Parse Query
let query: PFQuery = PFQuery(className: "Post")
// 5km range
query.whereKey("gps", nearGeoPoint: geoPoint, withinKilometers: 5.0)
//rest of query....
} else { // dont have access to GPS or whatever
print("Eror location: \(error)")
}
}

Related

CollectionView Cell and Progress Bar - Progress Bar showing in wrong Cell after scrolling

Ive been searching for a answer to this one for days now and cant seem to figure it out. I have a Collection View with custom cell. When you double tap a cell in the Collection View it will either download a file or delete it if its been downloaded before.
During the download a progress bar displays the progress of the download then displays a small icon in the top left corner. When deleting it removes the icon.
If you download from one cell and delete from another while first download is in progress it works fine but only if both cells were visible within the collection view.
if i download from one cell, then scroll offscreen and delete from a cell that is not in same screen as the cell that is being download from, it removes the corner image as usual then displays the progress bar of the cell that is being download from.
I don't know if this is an error with how i am reusing cells??? It doesn't seem to have anything to do with how i am updating the cell or collection view which works in all cases except after scrolling.
Below is 2 functions that download or delete file:
func downloadDataToDevice(cell: JourneyCollectionViewCell, selectedIndexPath: IndexPath){
let downloadedAudio = PFObject(className: "downloadedAudio")
// save all files with unique name / object id
let selectedObjectId = self.partArray[selectedIndexPath.item].id
let selectedPartName = self.partArray[selectedIndexPath.item].name
let query = PFQuery(className: "Part")
query.whereKey("objectId", equalTo: selectedObjectId)
query.getFirstObjectInBackground { (object, error) in
if error != nil || object == nil {
print("No object for the index selected.")
} else {
//print("there is an object, getting the file.")
downloadedAudio.add(object?.object(forKey: "partAudio") as! PFFile, forKey: selectedPartName)
let downloadedFile = object?.object(forKey: "partAudio") as! PFFile
// get the data first so we can track progress
downloadedFile.getDataInBackground({ (success, error) in
if (success != nil) {
// pin the audio if there is data
downloadedAudio.pinInBackground(block: { (success, error) in
if success {
// reload the cell
self.reloadCell(selectedIndexPath: selectedIndexPath, hideProgress: true, hideImage: false, cell: cell)
self.inProgress -= 1
cell.isUserInteractionEnabled = true
}
})
}
// track the progress of the data
}, progressBlock: { (percent) in
self.activityIndicatorView.stopAnimating()
cell.progessBar.isHidden = false
//cell.progessBar.transform = cell.progessBar.transform.scaledBy(x: 1, y: 1.1)
cell.contentView.bringSubview(toFront: cell.progessBar)
cell.progessBar.setProgress(Float(percent) / Float(100), animated: true)
cell.isUserInteractionEnabled = false
})
}
}
}
func removeDataFromDevice(cell: JourneyCollectionViewCell, selectedIndexPath: IndexPath, object: PFObject) {
let selectedPartName = self.partArray[selectedIndexPath.item].name
// unpin the object from the LocalDataStore
PFObject.unpinAll(inBackground: [object], block: { (success, error) in
if success {
// reduce inProgress
self.inProgress -= 1
self.reloadCell(selectedIndexPath: selectedIndexPath, hideProgress: true, hideImage: true, cell: cell)
}
})
}
and this is how I'm reloading the cell
func reloadCell(selectedIndexPath: IndexPath, hideProgress: Bool, hideImage: Bool, cell: JourneyCollectionViewCell) {
cell.progessBar.isHidden = hideProgress
cell.imageDownloaded.isHidden = hideImage
self.collectionView.reloadItems(at: [selectedIndexPath])
}
----------- EDIT -------------
This is my cellForItem at function. Presently i am using a query to look on local drive and see if the file exists and then adding the corner image if it is. This is the first time i have used a query in this place, usually it is a query at login to populate an array but that is for a more static collection of data than what i am trying to achieve here by letting the user download and delete files.
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell: JourneyCollectionViewCell = collectionView.dequeueReusableCell(withReuseIdentifier: reuseIdentifier, for: indexPath) as! JourneyCollectionViewCell
cell.imageCell.file = self.partArray[indexPath.item].image
cell.imageCell.loadInBackground()
cell.imageCell.layer.masksToBounds = true
// not sure if its good to run a query here as its constantly updated.
// query if file is on LDS and add image to indicate
let cellPartName = self.partArray[indexPath.item].name
let checkQuery = PFQuery(className: "downloadedAudio")
checkQuery.whereKeyExists(cellPartName)
checkQuery.fromLocalDatastore()
checkQuery.getFirstObjectInBackground(block: { (object, error) in
if error != nil || object == nil {
//print("The file does not exist locally on the device, remove the image.")
cell.imageDownloaded.isHidden = true
cell.imageDownloaded.image = UIImage(named: "")
cell.progessBar.isHidden = true
} else {
//print("the file already exists on the device, add the image.")
cell.contentView.bringSubview(toFront: cell.imageDownloaded)
cell.imageDownloaded.isHidden = false
cell.imageDownloaded.image = UIImage(named: "download-1")
}
})
return cell
}
This is a normal feature of "reuse" cells, for efficient memory management purposes. What you need to do is reset the cell values in below function:
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
}
By reset, I mean set the cells to their default state, prior to you making any updates such as adding the left corner icon or the status bar.
You need to make sure the arrays that you are feeding the collectionview data from is maintained properly. For example, if you have an array A =[1,2,3] and you delete A[1], then array A needs to be [1,3].
So i tried placing the progress view programatically, i tried prepareForReuse in the custom cell class, neither resolved this issue directly, though i will keep using prepareForReuse as i think its a cleaner way to manage the cell than i had been.
What seems to have worked was relocating the cell within the progressBlock
if let downloadingCell = self.collectionView.cellForItem(at: selectedIndexPath) as? JourneyCollectionViewCell { downloadingCell.progessBar.isHidden = false
downloadingCell.contentView.bringSubview(toFront: downloadingCell.progessBar)
downloadingCell.progessBar.setProgress(Float(percent) / Float(100), animated: true)
downloadingCell.setNeedsDisplay()
downloadingCell.isUserInteractionEnabled = false
}

Slow CloudKit table scrolling - altering existing code?

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.

Convert PFGeopoint lat and long from Parse to display current users distance in radius to other users onto a text label

The first image is an example of what the result is returning and the second image is an example of the results page, 'distance' is the label that I need to change in order to display my users distance. I have all my users locations stored on Parse as PFGeoPoint called "location" in lat and long. I then have a tabelViewCell with a textLabel. All users are shown on the VC and I am trying to show how far these users are from the current user like in Tinder.
I have the other users locations running in the logs as lat and long coordinates and I have the text label updating from "distance" to "[] km away!" So I must be getting the array back but its returning empty.
I have searched the internet and can't seem to figure it out. All the tutorials are all obj c or json or to add annotations in mapView. Here is my code on my usersResultsViewController:
var locationManager : CLLocationManager!
var latitude: CLLocationDegrees = 0
var longitude: CLLocationDegrees = 0
#IBAction func done(sender: UIBarButtonItem) {
self.performSegueWithIdentifier("backToProfile", sender: self)
}
#IBOutlet var resultsPageTableView: UITableView!
var imageFiles = [PFFile]()
var instrumentText = [String]()
var nameText = [String]()
var ageText = [String]()
var locationText = [PFGeoPoint]()
var usersLocations = Double
let roundedTwoDigitDistance = Double
override func viewDidLoad() {
super.viewDidLoad()
locationManager = CLLocationManager()
locationManager.delegate = self
locationManager.desiredAccuracy = kCLLocationAccuracyBest
locationManager.requestWhenInUseAuthorization()
locationManager.startUpdatingLocation()
// start of tableView:
let query = PFQuery(className: "_User")
query.whereKey("username", notEqualTo:PFUser.currentUser()!.username!)
query.findObjectsInBackgroundWithBlock { (users: [AnyObject]?, error: NSError?) -> Void in
if error == nil {
// success
print(users!.count)
for user in users! {
self.imageFiles.append(user["image"] as! PFFile)
self.instrumentText.append(user["instrument"] as! String)
self.nameText.append(user["name"] as! String)
self.ageText.append(user["age"] as! String)
// self.locationText.append(user["location"] as! PFGeoPoint)
}
// reload the table forget this it will load nothing
self.resultsPageTableView.reloadData()
} else {
print("error")
}
}
}
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
// *** note to self: for the return here it must be a mandatory field for user look at this again nd change it to mandatory age or username or something.
return imageFiles.count
}
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let singleCell: CustomCell = tableView.dequeueReusableCellWithIdentifier("mySingleCellid") as! CustomCell
// text
singleCell.usersInstrument.text = instrumentText[indexPath.row]
singleCell.userName.text = nameText[indexPath.row]
singleCell.userAge.text = ageText[indexPath.row]
let query = PFUser.query()!
if let latitude = PFUser.currentUser()?["location"]?.latitude {
if let longitude = PFUser.currentUser()?["location"]?.longitude {
print(latitude)
print(longitude)
query.whereKey("username", notEqualTo:PFUser.currentUser()!.username!)
query.whereKey("location", withinGeoBoxFromSouthwest: PFGeoPoint(latitude: latitude - 10, longitude: longitude - 10), toNortheast:PFGeoPoint(latitude:latitude + 10, longitude: longitude + 10))
query.findObjectsInBackgroundWithBlock { (users: [AnyObject]?, error: NSError?) -> Void in
if error == nil {
// success
for user in users! {
singleCell.userDistance.text = "\(self.locationText) km away!"
here are some of the forums i have found helpful but I am still stuck!!!:
http://www.scriptscoop.com/t/a2d00e357960/ios-converting-a-pfgeopoint-lat-and-long-from-parse-into-a-cllocation-lat-.html
trying to access the subscript of a Parse query array in SWIFT
Two Query Constraints On One Key with Parse and Swift
PFGeopoints have methods called "distanceInMilesTo:" and "distanceInKilometersTo:". These are what you're going to want to use. Call that method on the PFGeopoint storing the current user's location, and pass in each user's location from your query. Store the result in the appropriate label.
Here is the link to the API reference for this method: http://parse.com/docs/ios/api/Classes/PFGeoPoint.html#//api/name/distanceInKilometersTo:
Use the "near" feature and start getting all users within, say, 1 mile, then save that information, then get all users within 2 miles, then 5 miles, etc. Continuing to put geo-fences around a specific location and growing in concentric circles with increasing radius/distance, you can save the estimated distance for each user.
It's also possible to query for the set of objects that are contained within a particular area. To find the objects in a rectangular bounding box, add the withinGeoBox restriction to your Parse.Query.
var query = new Parse.Query(PlaceObject);
query.withinMiles("location", userGeoPoint, 10.0);
query.find().then(function(placesObjects) {
// Get a list of objects within 10 miles of a user's location
});
More examples are at https://www.parse.com/docs/js/guide

Can't see the messages I'm posting in Parse

I'm creating a yik yak clone and I can't seem to see the messages I post in the textField(string) on Parse. Is there something wrong I'm doing in my code that's not letting it show up on Parse?
#IBAction func postPressed(sender: AnyObject) {
if(currLocation != nil){
let testObj = PFObject(className: "BubbleTest")
testObj["userName"] = PFUser.currentUser()?.username
//testObj["profileName"] = PFUser.valueForKey("profileName") as! String
//testObj["photo"] = PFUser.currentUser()?.valueForKey("photo") as! PFFile
testObj["textField"] = self.textField.text
testObj["location"] = PFGeoPoint(latitude: currLocation!.latitude , longitude: currLocation!.longitude)
testObj["count"] = 0
testObj["replies"] = 0
testObj.saveInBackground()
self.dismissViewControllerAnimated(true, completion: nil)
}
else {
alert()
}
The reason you are not seeing anything because you post it into the wrong class. According to the picture BubbleTest is the name of the class not YikYakTest
replace this line
let testObj = PFObject(className: "YikYakTest")
by
let testObj = PFObject(className: "BubbleTest")
your code should look like :
Note use saveInBackgroundWithBlock method so you could check if there is an error while saving
let testObj = PFObject(className: "BubbleTest")
let username = PFUser.currentUser()?.username
testObj["userName"] = username
testObj["textField"] = self.textField.text
testObj["Location"] = PFGeoPoint(latitude:currLocation.latitude , longitude: currLocation.longitude)
testObj["count"] = 0
testObj["replies"] = 0
testObj.saveInBackgroundWithBlock { (success:Bool, error :NSError?) -> Void in
if error == nil
{
print("detail is saved")
self.dismissViewControllerAnimated(true, completion: nil)
}
else
{
print("error")
}
}
when you are saving PFGeopoint coordinates save it into Location column not location
I know many developer friends of mine who ran into a similar issue. I myself had this problem as well, now resolved. So hopefully I can provide some insight from what I learned querying data from Parse:
Try changing the numberOfSectionsInTableView method to return 1 instead of 0 like so:
override func numberOfSectionsInTableView(tableView: UITableView) -> Int {
return 1
}
You may need to also have some data structure to hold the users' posts (messages):
var userPosts:NSMutableArray! = NSMutableArray()
Also, your table view could then have as many rows as you will have posts stored in userPosts:
override func tableView(tableView: UITableView?, numberOfRowsInSection section: Int) -> Int {
return userPosts.count
}
In cellForRowAtIndexPath, replace this:
let object = PFObject(className: "BubbleTest")
WITH THIS:
let userPost : PFObject = self.posts.objectAtIndex(indexPath!.row) as! PFObject
...
cell.message.text = userPost.objectForKey("message") as! String
return cell
}
This will set the text of your custom cell's message property to whatever the user's message is (i.e.: "Testing 1 2").
Note: These steps aren't intended to be the only steps needed to solve your problem. It is meant to guide you in the right direction with some basic steps.
Hope that helps! :)

No returns for Parse query

I'm trying to pull locations near a user via Parse for tableview, except I only get either a white screen or an empty table.
Edited: I neglected to add the call for location before query near geopoint is submitted. In testing, I found that location was determined under 'geoPontForCurrentLocationInBackground'. However, once the geopoint is established and the query is submitted, the user location returns no latitude or longitude within the query. Moreover, the query doesn't return any objects and I'm not sure why:
override func queryForTable() -> PFQuery! {
var query = PFQuery(className: "User")
PFGeoPoint.geoPointForCurrentLocationInBackground {
(userLocation: PFGeoPoint!, error: NSError!) -> Void in
if error == nil {
}
}
let point: PFGeoPoint = PFGeoPoint(latitude: self.userLoc.latitude, longitude: self.userLoc.longitude)
query.whereKey("location", nearGeoPoint: point, withinMiles: 50.0)
query.limit = 10
return query
}
override func tableView(tableView: UITableView!, cellForRowAtIndexPath indexPath: NSIndexPath!, object: PFObject!) -> PFTableViewCell! {
let cell = tableView.dequeueReusableCellWithIdentifier("Cell", forIndexPath: indexPath) as Shops
if let object = object {
if let shopName = object.valueForKey("shopName") as? String {
cell.shopList.text = shopName
}
}
return cell as Shops
}
I believe the issue is that Parse treats the User class as a special class. You may have difficulty querying User class if you treat it as if it was a Custom Class.
The correct and tested way to do this is to the the query function of PFUser.
var query = PFUser.query()
You are setting up the query, but never actually executing it. Per the Parse documents, you need to add:
// Final list of objects
queryResults = query.findObjects()
Before your queryForTable() return statement in order to get the query objects.