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! :)
Related
I keep getting a fatal error saying how a value was unwrapped and it was nil and I don't understand how. When I instantiate a view controller with specific variables they all show up, but when I perform a segue to the exact VC, the values don't show up.
Take these functions for example...
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
tableView.deselectRow(at: indexPath, animated: true)
if let displayVC = storyboard?.instantiateViewController(withIdentifier: Constants.Storyboards.TeachStoryboardID) as? SchoolEventDetailsViewController {
displayVC.selectedEventName = events[indexPath.row].eventName
displayVC.selectedEventDate = documentsDate[indexPath.row].eventDate
displayVC.selectedEventCost = documentsCost[indexPath.row].eventCost
displayVC.selectedEventGrade = documentsGrade[indexPath.row].eventGrade
displayVC.selectedEventDocID = documentsID[indexPath.row]?.docID
navigationController?.pushViewController(displayVC, animated: true)
}
}
This combined with this function :
func verifyInstantiation() {
if let dateToLoad = selectedEventDate {
dateEditableTextF.text = dateToLoad
}
if let costToLoad = selectedEventCost {
costEditableTextF.text = costToLoad
}
if let gradesToLoad = selectedEventGrade {
gradesEditableTextF.text = gradesToLoad
}
if let docIDtoLoad = selectedEventDocID {
docIDUneditableTextF.text = docIDtoLoad
}
if let eventNameToLoad = selectedEventName {
eventNameEditableTextF.text = eventNameToLoad
}
}
Helps load the data perfectly, but when I try to perform a segue from a search controller the data is not there.
override func viewDidLoad() {
super.viewDidLoad()
navigationItem.title = selectedEventName
I set the title of the vc to have the event name , and I also recently added a text field to store it as well for experimental purposes (this question).
Now the issue is I want to do a data transfer from an Algolia Search Controller to that VC and I got all the other fields to show up, except for one and that was the document ID. So I created a completion handler function to get the document ID as a string and have it inserted into the vc when the segue is performed, just like how it's there when the vc is instantiated.
Here is the function :
func getTheEventDocID(completion: #escaping ((String?) -> ())) {
documentListener = db.collection(Constants.Firebase.schoolCollectionName).whereField("event_name", isEqualTo: selectedEventName ?? navigationItem.title).addSnapshotListener(includeMetadataChanges: true) { (querySnapshot, error) in
if let error = error {
print("There was an error fetching the documents: \(error)")
} else {
self.documentsID = querySnapshot!.documents.map { document in
return EventDocID(docID: (document.documentID) as! String)
}
let fixedID = "\(self.documentsID)"
let substrings = fixedID.dropFirst(22).dropLast(3)
let realString = String(substrings)
completion(realString)
}
}
}
I thought either selectedEventName or navigationItem.title would get the job done and provide the value when I used the function in the data transfer function which I will show now :
//MARK: - Data Transfer From Algolia Search to School Event Details
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
otherVC.getTheEventDocID { (eventdocid) in
if let id = eventdocid {
if segue.identifier == Constants.Segues.fromSearchToSchoolEventDetails {
let vc = segue.destination as! SchoolEventDetailsViewController
vc.selectedEventName = self.nameTheEvent
vc.selectedEventDate = self.dateTheEvent
vc.selectedEventCost = self.costTheEvent
vc.selectedEventGrade = self.gradeTheEvent
vc.selectedEventDocID = id
}
}
}
}
But it ends up showing nothing when a search result is clicked which is pretty upsetting, I can't understand why they're both empty values when I declared them in the SchoolEventDetailsVC. I tried to force unwrap selectedEventName and it crashes saying there's a nil value and I can't figure out why. There's actually a lot more to the question but I just tried to keep it short so people will actually attempt to read it and help since nobody ever reads the questions I post, so yeah thanks in advance.
I'm a litte confused what the otherVC is, which sets a property of itself in the getTheEventDocID, whilste in the closure you set the properties of self, which is a different controller. But never mind, I hope you know what you are doing.
Since getTheEventDocID runs asynchronously, the view will be loaded and displayed before the data is available. Therefore, viewDidLoad does not see the actual data, but something that soon will be outdated.
So, you need to inform the details view controller that new data is available, and refresh it's user interface. Something like
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
otherVC.getTheEventDocID { (eventdocid) in
if let id = eventdocid {
if segue.identifier == Constants.Segues.fromSearchToSchoolEventDetails {
let vc = segue.destination as! SchoolEventDetailsViewController
vc.selectedEventName = self.nameTheEvent
vc.selectedEventDate = self.dateTheEvent
vc.selectedEventCost = self.costTheEvent
vc.selectedEventGrade = self.gradeTheEvent
vc.selectedEventDocID = id
vc.updateUI()
}
}
}
}
and in the destination view controller:
class SchoolEventDetailsViewController ... {
override func viewDidLoad() {
super.viewDidLoad()
updateUI()
}
func updateUI () {
navigationItem.title = selectedEventName
// and so on
}
}
Ok so I decided to attempt a workaround and completely ditched the getTheEventDocID() method because it was just causing me stress. So I decided to ditch Firebase generated document IDS and just use 10 digit generated ids from a function I made. I also figured out how to add that exact same 10 digit id in the Algolia record by just storing the random 10 digit id in a variable and using it in both places. So now instead of using a query call to grab a Firebase generated document ID and have my app crash everytime I click a search result, I basically edited the Struct of the Algolia record and just added an eventDocID property that can be used with hits.hitSource(at: indexPath.row).eventDocID.
And now the same way I added the other fields to the vc by segue data transfer, I can now do the same thing with my document ID because everything is matching :).
I'm trying to share a record with other users in CloudKit but I keep getting an error. When I tap one of the items/records on the table I'm presented with the UICloudSharingController and I can see the iMessage app icon, but when I tap on it I get an error and the UICloudSharingController disappears, the funny thing is that even after the error I can still continue using the app.
Here is what I have.
Code
var items = [CKRecord]()
var itemName: String?
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
let item = items[indexPath.row]
let share = CKShare(rootRecord: item)
if let itemName = item.object(forKey: "name") as? String {
self.itemName = item.object(forKey: "name") as? String
share[CKShareTitleKey] = "Sharing \(itemName)" as CKRecordValue?
} else {
share[CKShareTitleKey] = "" as CKRecordValue?
self.itemName = "item"
}
share[CKShareTypeKey] = "bundle.Identifier.Here" as CKRecordValue
prepareToShare(share: share, record: item)
}
private func prepareToShare(share: CKShare, record: CKRecord){
let sharingViewController = UICloudSharingController(preparationHandler: {(UICloudSharingController, handler: #escaping (CKShare?, CKContainer?, Error?) -> Void) in
let modRecordsList = CKModifyRecordsOperation(recordsToSave: [record, share], recordIDsToDelete: nil)
modRecordsList.modifyRecordsCompletionBlock = {
(record, recordID, error) in
handler(share, CKContainer.default(), error)
}
CKContainer.default().privateCloudDatabase.add(modRecordsList)
})
sharingViewController.delegate = self
sharingViewController.availablePermissions = [.allowPrivate]
self.navigationController?.present(sharingViewController, animated:true, completion:nil)
}
// Delegate Methods:
func cloudSharingControllerDidSaveShare(_ csc: UICloudSharingController) {
print("saved successfully")
}
func cloudSharingController(_ csc: UICloudSharingController, failedToSaveShareWithError error: Error) {
print("failed to save: \(error.localizedDescription)")// the error is generated in this method
}
func itemThumbnailData(for csc: UICloudSharingController) -> Data? {
return nil //You can set a hero image in your share sheet. Nil uses the default.
}
func itemTitle(for csc: UICloudSharingController) -> String? {
return self.itemName
}
ERROR
Failed to modify some records
Here is what I see...
Any idea what could be wrong?
EDIT:
By the way, the error is generated in the cloudSharingController failedToSaveShareWithError method.
Looks like you're trying to share in the default zone which isn't allowed. From the docs here
Sharing is only supported in zones with the
CKRecordZoneCapabilitySharing capability. The default zone does not
support sharing.
So you should set up a custom zone in your private database, and save your share and records there.
Possibly it is from the way you're trying to instantiate the UICloudSharingController? I cribbed my directly from the docs and it works:
let cloudSharingController = UICloudSharingController { [weak self] (controller, completion: #escaping (CKShare?, CKContainer?, Error?) -> Void) in
guard let `self` = self else {
return
}
self.share(rootRecord: rootRecord, completion: completion)
}
If that's not the problem it's something with either one or both of the records themselves. If you upload the record without trying to share it, does it work?
EDIT TO ADD:
What is the CKShareTypeKey? I don't use that in my app. Also I set my system fields differently:
share?[CKShare.SystemFieldKey.title] = "Something"
Try to add this to your info.plist
<key>CKSharingSupported</key>
<true/>
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.
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)")
}
}
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
}