Reputation: 1829
I am working on a messaging app and I want to display the location where a message was sent from under the message content. I try to do this by sending the user's location data to Firebase, and then attempting to retrieve the data to display it as a string.
Getting the users location works fine (I am using CoreLocation
to do so) as does uploading data to my Firebase realtime database. I save the location along with each message as such:
let itemRef = messageRef.childByAutoId() // 1 let messageItem = [ // 2 "text": text, "senderId": senderId, "location": getLocation() ] itemRef.setValue(messageItem) // 3
And then attempt to retrieve the data in another method as such:
override func collectionView(collectionView: JSQMessagesCollectionView!, attributedTextForCellBottomLabelAtIndexPath indexPath: NSIndexPath!) -> NSAttributedString! {
var locationId: String = ""
let messagesQuery = messageRef
let message = messages[indexPath.item]
messagesQuery.observeEventType(.ChildAdded) { (snapshot: FIRDataSnapshot!) in
locationId = snapshot.value!["location"] as! String
print(locationId)
}
if message.senderId == senderId {
return nil
} else {
return NSAttributedString(string: locationId)
}
}
The correct location gets printed out to the console but nothing gets displayed in my app. However if I replace the variable locationId
with any other string, it works.
My problem I believe is with Firebase retrieval.
If anybody could help me fix this issue, that would be greatly appreciated.
Here is the rest of the code for my class for reference (just in case):
class ChatViewController: JSQMessagesViewController, CLLocationManagerDelegate {
// MARK: Properties
//Firebase
var rootRef = FIRDatabase.database().reference()
var messageRef: FIRDatabaseReference!
var locationRef: FIRDatabaseReference!
//JSQMessages
var messages = [JSQMessage]()
var outgoingBubbleImageView: JSQMessagesBubbleImage!
var incomingBubbleImageView: JSQMessagesBubbleImage!
var purp = UIColor.init(red:47/255, green: 53/255, blue: 144/255, alpha: 1)
var roastish = UIColor.init(red: 255/255, green: 35/255, blue: 35/255, alpha: 1.0)
var orangish = UIColor.init(red: 231/255, green: 83/255, blue: 55/255, alpha: 1.0)
var gray = UIColor.init(red: 241/255, green: 251/255, blue: 241/255, alpha: 1)
//Location
var city: String = ""
var state: String = ""
var country: String = ""
var locationManager = CLLocationManager()
func getLocation() -> String {
if city == ("") && state == ("") && country == (""){
return "Planet Earth"
}
else {
if country == ("United States") {
return self.city + ", " + self.state
}
else {
return self.city + ", " + self.state + ", " + self.country
}
}
}
override func viewDidLoad() {
super.viewDidLoad()
// Initialize location
locationManager.delegate = self
locationManager.requestWhenInUseAuthorization()
if CLLocationManager.locationServicesEnabled() {
//collect user's location
locationManager.desiredAccuracy = kCLLocationAccuracyThreeKilometers
locationManager.requestLocation()
locationManager.startUpdatingLocation()
}
// Change the navigation bar background color
navigationController!.navigationBar.barTintColor = gray
self.navigationController!.navigationBar.titleTextAttributes = [ NSFontAttributeName: UIFont(name: "Avenir Next", size: 20)!]
title = "RoastChat"
setupBubbles()
// No avatars
// Remove file upload icon
self.inputToolbar.contentView.leftBarButtonItem = nil;
// Send button
self.inputToolbar.contentView.rightBarButtonItem.setTitle("Roast", forState: UIControlState.Normal)
// Send button color
self.inputToolbar.contentView.rightBarButtonItem.setTitleColor(roastish, forState: UIControlState.Normal)
// Input bar text placeholder
self.inputToolbar.contentView.textView.placeHolder = "RoastChat"
collectionView!.collectionViewLayout.incomingAvatarViewSize = CGSizeZero
collectionView!.collectionViewLayout.outgoingAvatarViewSize = CGSizeZero
//Firebase reference
messageRef = rootRef.child("messages")
locationRef = rootRef.child("locations")
}
override func viewDidAppear(animated: Bool) {
super.viewDidAppear(animated)
observeMessages()
}
override func viewDidDisappear(animated: Bool) {
super.viewDidDisappear(animated)
}
func locationManager(manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
//--- CLGeocode to get address of current location ---//
CLGeocoder().reverseGeocodeLocation(manager.location!, completionHandler: {(placemarks, error)->Void in
if let pm = placemarks?.first
{
self.displayLocationInfo(pm)
}
})
}
func displayLocationInfo(placemark: CLPlacemark?)
{
if let containsPlacemark = placemark
{
//stop updating location
locationManager.stopUpdatingLocation()
self.city = (containsPlacemark.locality != nil) ? containsPlacemark.locality! : ""
self.state = (containsPlacemark.administrativeArea != nil) ? containsPlacemark.administrativeArea! : ""
self.country = (containsPlacemark.country != nil) ? containsPlacemark.country! : ""
}
}
func locationManager(manager: CLLocationManager, didFailWithError error: NSError) {
print("Error while updating location " + error.localizedDescription)
}
override func collectionView(collectionView: JSQMessagesCollectionView!,
messageDataForItemAtIndexPath indexPath: NSIndexPath!) -> JSQMessageData! {
return messages[indexPath.item]
}
override func collectionView(collectionView: JSQMessagesCollectionView!,
messageBubbleImageDataForItemAtIndexPath indexPath: NSIndexPath!) -> JSQMessageBubbleImageDataSource! {
let message = messages[indexPath.item] // 1
if message.senderId == senderId { // 2
return outgoingBubbleImageView
} else { // 3
return incomingBubbleImageView
}
}
override func collectionView(collectionView: UICollectionView,
numberOfItemsInSection section: Int) -> Int {
return messages.count
}
override func collectionView(collectionView: JSQMessagesCollectionView!,
avatarImageDataForItemAtIndexPath indexPath: NSIndexPath!) -> JSQMessageAvatarImageDataSource! {
return nil
}
private func setupBubbles() {
let factory = JSQMessagesBubbleImageFactory()
outgoingBubbleImageView = factory.outgoingMessagesBubbleImageWithColor(
UIColor.jsq_messageBubbleBlueColor())
incomingBubbleImageView = factory.incomingMessagesBubbleImageWithColor(
roastish)
}
func addMessage(id: String, text: String) {
let message = JSQMessage(senderId: id, displayName: "", text: text)
messages.append(message)
}
override func collectionView(collectionView: UICollectionView,
cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {
let cell = super.collectionView(collectionView, cellForItemAtIndexPath: indexPath)
as! JSQMessagesCollectionViewCell
let message = messages[indexPath.item]
if message.senderId == senderId {
cell.textView!.textColor = UIColor.whiteColor()
} else {
cell.textView!.textColor = UIColor.whiteColor()
}
return cell
}
override func didPressSendButton(button: UIButton!, withMessageText text: String!, senderId: String!,
senderDisplayName: String!, date: NSDate!) {
let itemRef = messageRef.childByAutoId() // 1
let messageItem = [ // 2
"text": text,
"senderId": senderId,
"location": getLocation()
]
itemRef.setValue(messageItem) // 3
// 4
JSQSystemSoundPlayer.jsq_playMessageSentSound()
// 5
finishSendingMessage()
Answers.logCustomEventWithName("Message sent", customAttributes: nil)
}
private func observeMessages() {
// 1
let messagesQuery = messageRef.queryLimitedToLast(25)
// 2
messagesQuery.observeEventType(.ChildAdded) { (snapshot: FIRDataSnapshot!) in
// 3
let id = snapshot.value!["senderId"] as! String
let text = snapshot.value!["text"] as! String
// 4
self.addMessage(id, text: text)
// 5
self.finishReceivingMessage()
Answers.logCustomEventWithName("Visited RoastChat", customAttributes: nil)
}
}
override func collectionView(collectionView: JSQMessagesCollectionView!, attributedTextForCellBottomLabelAtIndexPath indexPath: NSIndexPath!) -> NSAttributedString! {
var locationId: String = ""
let messagesQuery = messageRef
let message = messages[indexPath.item]
messagesQuery.observeEventType(.ChildAdded) { (snapshot: FIRDataSnapshot!) in
locationId = snapshot.value!["location"] as! String
print(locationId)
}
if message.senderId == senderId {
return nil
} else {
return NSAttributedString(string: locationId)
}
}
override func collectionView(collectionView: JSQMessagesCollectionView, layout collectionViewLayout: JSQMessagesCollectionViewFlowLayout, heightForCellBottomLabelAtIndexPath indexPath: NSIndexPath) -> CGFloat {
return kJSQMessagesCollectionViewCellLabelHeightDefault
}
override func collectionView(collectionView: JSQMessagesCollectionView!, didTapMessageBubbleAtIndexPath indexPath: NSIndexPath!) {
super.collectionView(collectionView, didTapMessageBubbleAtIndexPath: indexPath)
let data = self.messages[indexPath.row]
print("They tapped: " + (data.text))
}
}
Upvotes: 0
Views: 385
Reputation: 1829
I wasn't able to do the retrieval any other way. Instead I exploited JSQMessagesViewControllers other built in methods to solve my problem. The framework has a built in variable called senderDisplayName. Since the nature of this app I am making is anonymous I didn't need to use it, so I manipulated the senderDisplay name to display the user's location instead.
func addMessage(id: String, text: String, displayName: String) {
let message = JSQMessage(senderId: id, displayName: displayName, text: text)
messages.append(message)
}
Then I set displayName to be the location:
private func observeMessages() {
// 1
let messagesQuery = messageRef.queryLimitedToLast(25)
// 2
messagesQuery.observeEventType(.ChildAdded) { (snapshot: FIRDataSnapshot!) in
// 3
let id = snapshot.value!["senderId"] as! String
let text = snapshot.value!["text"] as! String
let locationId = snapshot.value!["location"] as! String
// 4
// self.addMessage(id, text: locationId.lowercaseString + ": \n" + text)
self.addMessage(id, text: text, displayName: locationId)
// 5
self.finishReceivingMessage()
Answers.logCustomEventWithName("Visited RoastChat", customAttributes: nil)
}
}
Then I just used the built in attributedTextForCellBottomLabelAtIndexPath
to set my bottom label as the user's location:
override func collectionView(collectionView: JSQMessagesCollectionView!, attributedTextForCellBottomLabelAtIndexPath indexPath: NSIndexPath!) -> NSAttributedString! {
let message = messages[indexPath.item]
if message.senderId == senderId {
return nil
} else {
return NSAttributedString(string: message.senderDisplayName)
}
}
It works perfect! This is what programming is all about, problem solving.
Thanks to everyone who helped and pointed me in the right direction.
Upvotes: 0
Reputation: 248
99% of the time when you retrieve some data from the internet - as is the case here, you observe a Firebase event to get a location which was sent by some other user - the code works in an asynchronous manner.
What that means is that the closure which is fired when you retrieve the location is getting called independently of the rest of the attributedTextForCellBottomLabelAtIndexPath function.
So by the time you actually get the location the function has already returned the NSAttributedString created with an empty locationId; why empty? because you have set it's initial value to "". The closure passed when calling messagesQuery.observeEventType captures the locationId property locally, so you are able to set it to what you retrieve from the API and print works fine. But right after the flow reaches the end of the closure's code this value disappears from memory and is used nowhere.
What you have to do to make your app work is to update the cell's bottom label after you get the locationId. You can go about it in a few different ways. I will suggest two similar ones.
The easiest and most straight forward would be if the cell was able to observe on the Firebase event and update itself.
func collectionView(collectionView: UICollectionView, willDisplayCell cell: UICollectionViewCell, forItemAtIndexPath indexPath: NSIndexPath) {
let cell = collectionView.dequeueReusableCellWithReuseIdentifier(YourCellClass.identifier, forIndexPath: indexPath) as! YourCellClass
cell.messagesQuery = messagesRef
return cell
}
And inside of your cell's code you would have something like that:
class YourCellClass: UICollectionViewCell {
static let identifier = String(YourCellClass)
var messagesQuery: FIRDatabaseReference {
didSet {
messagesQuery.observeEventType(.ChildAdded) { [weak self] snapshot in
guard let `self` = self, value = snapshot.value as? [String: AnyObject], locationId = value["location"] as? String else { return }
dispatch_async(dispatch_get_main_queue()) {
self.bottomLabel.attributedText = NSAttributedText(string: locationId)
}
}
}
}
// all your other cell code goes here
}
A different, better option would be to use MVVM and the cell's view model would be responsible for fetching the location, the cell would observe on it's view model and update itself, but I think that's a bit too much to learn right now for you ;)
P.S. Please keep in mind that I wrote this code of the top of my head so I can't guarantee in 100% that it will compile right away.
Upvotes: 2