I have added chatting/messaging to my application. I can go to a users profile, select message and message them.
I also added a tab that show all the users I messaged which is in a table view and you can click a user and see messages between you and them.
The issue happens when I open this tab to see the users Ive messaged when I've messaged more than 10 users. The simulator crashes and I get this error:
"Thread 1: Exception: "Invalid Query. 'in' filters support a maximum of 10 elements in the value array."
here is my code:
import UIKit
import FirebaseFirestore
class MessagingVC: UIViewController {
#IBOutlet weak var tableView: UITableView! { didSet {
tableView.tableFooterView = UIView()
tableView.contentInset.top = 10
}}
#IBOutlet weak var noDataLabel: UILabel!
private let db = Firestore.firestore()
var users = [AppUser]()
var user: AppUser {
return UserManager.currentUser!
}
private var lastMessageDict: [String: Date]?
private var unreadMessageDict: [String: Bool]?
var chatListener: ListenerRegistration?
override func viewDidLoad() {
super.viewDidLoad()
getChats()
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
noDataLabel.isHidden = true
// remove any blocked users when entering the screen.
users.removeAll() { self.user.blockedUsers?.contains($0.uid) == true }
// filter hidden users
users = users.filter { $0.isHidden ?? false == false }
tableView.reloadData()
// getChats()
}
// Get all users id that the current user has been chating with.
func getChats() {
let chatCollection = db.collection("chat")
chatListener = chatCollection.whereField("participants", arrayContains: user.uid).addSnapshotListener { [unowned self] (querySnapshot, error) in
guard error == nil else {
print(error!.localizedDescription)
return
}
var userChatIds = [String]()
self.lastMessageDict = [String: Date]()
self.unreadMessageDict = [String: Bool]()
for chat in querySnapshot?.documents ?? [] {
let chatData = chat.data()
if let participants = chatData["participants"] as? [String] {
for particiant in participants {
if particiant != self.user.uid,
!(self.user.blockedUsers?.contains(particiant) == true),
!userChatIds.contains(particiant) {
userChatIds.append(particiant)
if let lastMessageDate = chatData["last message"] as? Timestamp {
self.lastMessageDict![particiant] = lastMessageDate.dateValue()
}
if let unreadMessageDict = chatData["unread message"] as? [String: Bool],
let unreadMesage = unreadMessageDict[self.user.uid] {
self.unreadMessageDict![particiant] = unreadMesage
}
}
}
}
}
if !userChatIds.isEmpty {
self.getChatsInfo(chatIds: userChatIds)
} else {
self.tableView.reloadData()
self.noDataLabel.isHidden = self.users.count > 0
}
}
}
func getChatsInfo(chatIds: [String]) {
getUsersForChat(chatIds) { (users, error) in
guard error == nil else {
print(error?.localizedDescription ?? "")
return
}
for user in users {
if let index = self.users.firstIndex(of: user) {
self.users[index] = user
} else {
self.users.append(user)
}
}
// self.users = users
self.users.sort { (first, second) -> Bool in
let firstDate = self.lastMessageDict?[first.uid]
let secondDate = self.lastMessageDict?[second.uid]
if firstDate == nil { return false }
else if secondDate == nil { return true }
else {
return firstDate! > secondDate!
}
}
self.users = self.users.filter { $0.isHidden ?? false == false }
self.tableView.reloadData()
self.noDataLabel.isHidden = self.users.count > 0
}
}
func getUsersForChat(_ ids: [String], completion:#escaping (_ users: [AppUser], _ error: Error?)->()) {
var allUsers = [AppUser]()
let allids = self.users.map { $0.uid }
let ids = ids.filter { !allids.contains($0) }
if ids.count == 0 {
completion(allUsers, nil)
return
}
var error: Error?
let userCollection = db.collection("users")
let dispatchGroup = DispatchGroup()
dispatchGroup.enter()
userCollection.whereField("uid", in: ids).getDocuments {querySnapshot,err in
error = err
var users = [AppUser]()
for document in querySnapshot?.documents ?? [] {
if let restaurant = Restaurant(snapshot: document) {
users.append(restaurant)
}
}
allUsers.append(contentsOf: users)
dispatchGroup.leave()
}
dispatchGroup.enter()
let userCollection2 = db.collection("users2")
userCollection2.whereField("uid", in: ids).getDocuments {querySnapshot,err in
error = err
var users = [AppUser]()
for document in querySnapshot?.documents ?? [] {
if let user = AppUser(snapshot: document) {
users.append(user)
}
}
allUsers.append(contentsOf: users)
dispatchGroup.leave()
}
dispatchGroup.notify(queue: DispatchQueue.main) {
completion(allUsers, error)
}
}
deinit {
chatListener?.remove()
}
}
extension MessagingVC: UITableViewDelegate, UITableViewDataSource {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return users.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "MessageCell", for: indexPath) as! MessagingTableViewCell
let user = users[indexPath.row]
cell.nameLabel.text = user.firstName + " " + user.lastName
if let unreadMessage = unreadMessageDict?[user.uid],
unreadMessage == true {
cell.unreadMessageIndicatorLabel.isHidden = false
} else {
cell.unreadMessageIndicatorLabel.isHidden = true
}
cell.photoImageView.image = nil
user.getProfileImage { (image) in
DispatchQueue.main.async {
if let cell = tableView.cellForRow(at: indexPath) as? MessagingTableViewCell {
cell.photoImageView.image = image
}
}
}
if let rest = user as? Restaurant {
cell.categoryImageView.image = UIImage(named: rest.Categories1)
}
return cell
}
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
tableView.deselectRow(at: indexPath, animated: true)
let user = users[indexPath.row]
// unreadMessageDict?[user.uid] = false
performSegue(withIdentifier: "messagingToChatSegue", sender: user)
}
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if segue.identifier == "messagingToChatSegue" {
let chatVC = segue.destination as! ChatViewController
let otherUser = sender as! AppUser
chatVC.currentUser = user
chatVC.otherUser = otherUser
chatVC.channelId = ClickedResturantVC.generateIdForMessages(user1Id: user.uid, user2Id: otherUser.uid)
}
}
}
Here is a picture of the error I am getting
Here is a picture of my Firestore database
The error message is telling you that Firestore doesn't support more than 10 items in the ids array that you pass to this query filter:
userCollection.whereField("uid", in: ids)
According to the documentation for this sort of query:
Note the following limitations for in and array-contains-any:
in and array-contains-any support up to 10 comparison values.
If you need more than 10, you will need to batch them into multiple queries.
Related
Premise
I am currently making SNS with Swift.
I encountered the following error message while implementing user added functionality on it.
Error message
Thread 1: Fatal error: Unexpectedly found nil while unwrapping an Optional value
Code
Swift4
import UIKit
class PeopleViewController: UIViewController {
#IBOutlet weak var tableView: UITableView!
var users = [UserModel]()
var userUid = ""
override func viewDidLoad() {
super.viewDidLoad()
tableView.dataSource = self
tableView.tableFooterView = UIView(frame: .zero)
tableView.rowHeight = 80
loadUser()
}
func loadUser() {
UserApi.shared.observeUser { (user) in
self.isFollowing(userUid: user.uid!, completed: { (value) in
**if user.uid != UserApi.shared.CURRENT_USER_UID! {** <-errorPoint
user.isFollowing = value
self.users.append(user)
self.tableView.reloadData()
}
})
}
}
func isFollowing(userUid: String, completed: #escaping (Bool) -> Void ) {
FollowApi.shared.isFollowing(withUser: userUid, completed: completed)
}
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if segue.identifier == "ShowUserInfoSegue" {
let showUserInfoVC = segue.destination as! ShowUserinfoViewController
showUserInfoVC.userUid = self.userUid
}
}
}
extension PeopleViewController: PeopleCellDelegate {
func didTappedShowUserInfo(userUid: String) {
self.userUid = userUid
performSegue(withIdentifier: "ShowUserInfoSegue", sender: self)
}
}
extension PeopleViewController: UITableViewDataSource {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return users.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "PeopleTableViewCell", for: indexPath) as! PeopleTableViewCell
cell.user = users[indexPath.row]
cell.delegate = self
return cell
}
}
Code
Swift4
import Foundation
import FirebaseDatabase
import FirebaseAuth
class UserApi {
var REF_USERS = Database.database().reference().child("users")
static var shared: UserApi = UserApi()
private init() {
}
var CURRENT_USER_UID: String? {
if let currentUserUid = Auth.auth().currentUser?.uid {
return currentUserUid
}
return nil
}
var CURRENT_USER: User? {
if let currentUserUid = Auth.auth().currentUser {
return currentUserUid
}
return nil
}
func observeUser(uid: String, completion: #escaping (UserModel) -> Void) {
REF_USERS.child(uid).observeSingleEvent(of: .value) { (snapshot) in
guard let dic = snapshot.value as? [String: Any] else { return }
let newUser = UserModel(dictionary: dic)
completion(newUser)
}
}
func observeUser(completion: #escaping (UserModel) -> Void ) {
REF_USERS.observe(.childAdded) { (snapshot) in
guard let dic = snapshot.value as? [String: Any] else { return }
let user = UserModel(dictionary: dic)
completion(user)
}
}
func observeCurrentUser(completion: #escaping (UserModel) -> Void ) {
guard let currentUserUid = CURRENT_USER_UID else { return }
REF_USERS.child(currentUserUid).observeSingleEvent(of: .value) { (snapshot) in
guard let dic = snapshot.value as? [String: Any] else { return }
let currentUser = UserModel(dictionary: dic)
completion(currentUser)
}
}
func queryUser(withText text: String, completion: #escaping(UserModel) -> Void ) {
REF_USERS.queryOrdered(byChild: "username_lowercase").queryStarting(atValue: text).queryEnding(atValue: text + "\u{f8ff}").queryLimited(toLast: 5).observeSingleEvent(of: .value) { (snapshot) in
snapshot.children.forEach({ (data) in
let child = data as! DataSnapshot
guard let dic = child.value as? [String: Any] else { return }
let user = UserModel(dictionary: dic)
completion(user)
})
}
}
}
What I tried
How can I fix "Thread 1: Fatal error: Unexpectedly found nil while unwrapping an Optional value" in Swift
https://learnappmaking.com/found-nil-while-unwrapping-optional-value/
I browsed and examined these sites, but it did not work.
I think that user information can not be taken out successfully.
Supplementary information
We will add additional information if we have other missing information.
Since I often do not understand Swift in 3 weeks, I would like you to tell me with concrete code etc.
Also, I am happy if you can tell me the cause of the error.
FW / tool version
You're trying to access the CURRENT_USER_UID from UserApi Singleton class which is Optional computed property which seems to be returning nil.
If there's not current user signed-in than Firebase Auth returns nil instead of uid
Auth.auth().currentUser?.uid // Because of Optional Chaining
I'd Suggest you to safely unwrap Optionals.
func loadUser() {
UserApi.shared.observeUser { (user) in
self.isFollowing(userUid: user.uid!, completed: { (value) in
if let currentUser = UserApi.shared.CURRENT_USER_UID {
if user.uid != currentUser {
user.isFollowing = value
self.users.append(user)
self.tableView.reloadData()
}
} else {
// Current user not Signed-In
}
})
}
}
I am using collection view for displaying result.I want to delete particular cell when that cell is selected. To keep record of selected cell, i am using struct.Everything is working perfectly. But when i click delete button selected cell are not getting deleted.Some other cell gets deleted. But when i go back to the previous screen and come back again ,the cell which i wanted to be deleted is actually removed. I don't know why and how this is happening?
import UIKit
import GRDBCipher
class AdvisoryViewController:
UIViewController,UICollectionViewDelegate,UICollectionViewDataSource {
#IBOutlet weak var new_advisory_label: UILabel!
var getname=String()
var array=[String]()
var date_array=[String]()
var crop_code:String=""
var dbQueue:DatabaseQueue? = nil
var ss = [Int]()
var struct_array :[myObject] = []
var msg_stat:[oldMsg] = []
var msg_count_array=[Int]()
var msg_array=[String]()
var selected_date_array=[String]()
var srno_array=[String]()
var selected_srno_array=[String]()
#IBOutlet weak var collection_view: UICollectionView!
struct myObject
{
var date: String
var advisory:String
var status: String
var srno: String
}
struct oldMsg
{
var is_checked: Bool
}
override func viewDidLoad() {
super.viewDidLoad()
do{
let longpress1=UILongPressGestureRecognizer(target: self, action: #selector(longpress))
// longpress1.minimumPressDuration=1
collection_view.addGestureRecognizer(longpress1)
let
path=NSSearchPathForDirectoriesInDomains(.documentDirectory,.userDomainMask,
true).first!
dbQueue = try DatabaseQueue(path: "\(path)/pest_messenger.sqlite")
try dbQueue?.inDatabase { db in
let crop_sursor = try Row.fetchCursor(db, "SELECT * FROM crop_master where crop_name='\(getname)'")
while let row = try crop_sursor.next() {
let title: String = row.value(named: "srno")
crop_code=title
}
let rows = try Row.fetchCursor(db, "SELECT distinct brief_advisory,advisory_date,srno FROM crop_insect_advisory_issued where crop_code='\(crop_code)'")
while let row = try rows.next() {
let title: String = row.value(named: "brief_advisory")
let da:String=row.value(named:"advisory_date")
let srno:Int = row.value(named:"srno")
let date_f=DateFormatter()
date_f.dateFormat="yyyy-MM-dd"
let d=date_f.date(from: da)
date_f.dateFormat="dd-MM-yyyy"
let dd=date_f.string(from: d!)
array.append(title)
date_array.append(dd)
srno_array.append("\(srno)")
}
}
for i in 0 ..< array.count
{
struct_array.append(myObject(date:date_array[i],advisory:array[i],
status:"N",srno:srno_array[i]))
}
for i in 0 ..< array.count
{
msg_stat.append(oldMsg(is_checked: false))
}
collection_view.delegate = self
collection_view.dataSource = self
}catch{
print(error)
}
}
func collectionView(_ collectionView: UICollectionView,
numberOfItemsInSection section: Int) -> Int {
return array.count
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt
indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier:
"AdvisoryCollectionViewCell", for: indexPath) as!
AdvisoryCollectionViewCell
cell.msg_label.text = struct_array[indexPath.item].advisory
cell.date_label.text = struct_array[indexPath.item].date
let imagee = UIImage.init(named: "rsz_tick")
cell.check_image.image = imagee
cell.check_image.isHidden = true
if( msg_stat[indexPath.row].is_checked == false){
cell.check_image.isHidden = true
}
else if(msg_stat[indexPath.row].is_checked == true)
{
cell.check_image.isHidden = false
}
else{
}
return cell
}
func longpress(gestureRecognizer: UIGestureRecognizer)
{
let righbarButton = UIBarButtonItem(image: UIImage(named:
"rsz_delete")?.withRenderingMode(.alwaysOriginal), style:
UIBarButtonItemStyle.plain, target: self, action:
#selector(myRightTapped(sender:)))
self.navigationItem.rightBarButtonItem = righbarButton
let longpress = gestureRecognizer as! UILongPressGestureRecognizer
let state = longpress.state
let locationInview = longpress.location(in: collection_view)
var indexpath=collection_view.indexPathForItem(at: locationInview)
if(gestureRecognizer.state == UIGestureRecognizerState.began)
{
let cell = collection_view.cellForItem(at: indexpath!) as!
AdvisoryCollectionViewCell
cell.check_image.isHidden = false
msg_stat[(indexpath?.row)!].is_checked = true
ss.append((indexpath?.item)!)
print("index of deleting = \(indexpath?.item)")
msg_count_array.append((indexpath?.item)!)
msg_array.append(struct_array[(indexpath?.item)!].advisory)
selected_date_array.append(struct_array[(indexpath?.item)!].date)
selected_srno_array.append(struct_array[(indexpath?.item)!].srno)
}
}
func collectionView(_ collectionView: UICollectionView, didSelectItemAt
indexPath: IndexPath) {
let cell = collection_view.cellForItem(at: indexPath)
as!AdvisoryCollectionViewCell
if(!(ss.isEmpty)){
cell.check_image.isHidden = !cell.check_image.isHidden
if(cell.check_image.isHidden == true)
{
msg_stat[indexPath.row].is_checked = false
print("why removing.....")
if(!(msg_count_array.isEmpty)){
msg_count_array.remove(at: (indexPath.item))}
if(!(msg_array.isEmpty)){
msg_array.remove(at: (indexPath.item))
}
if(!(selected_srno_array.isEmpty))
{
selected_srno_array.remove(at: (indexPath.item))
}
if(!(selected_date_array.isEmpty))
{
selected_date_array.remove(at: (indexPath.item))
}
}
else{
msg_stat[indexPath.row].is_checked = true
msg_count_array.append((indexPath.item))
msg_array.append(array[(indexPath.item)])
selected_srno_array.append(struct_array[(indexPath.item)].srno)
}
}
}
func myRightTapped(sender:UIBarButtonItem!)
{
do{
for i in 0 ..< msg_count_array.count
{
struct_array.remove(at: i)
msg_stat.remove(at: i)
}
msg_count_array.removeAll()
for i in 0 ..< msg_array.count
{
var msg = msg_array[i]
var srno = selected_srno_array[i]
let
path=NSSearchPathForDirectoriesInDomains(.documentDirectory,.userDomainMask,
true).first!
let dbQueue = try DatabaseQueue(path: "\(path)/pest_messenger.sqlite")
try dbQueue.inDatabase { db in
try db.execute("DELETE FROM crop_insect_advisory_issued where srno='\
(srno)'")
}
}
msg_array.removeAll()
array.removeAll()
date_array.removeAll()
srno_array.removeAll()
// let dbQueue = try DatabaseQueue(path: "\(path)/p_messenger.sqlite")
try dbQueue?.inDatabase { db in
let rows = try Row.fetchCursor(db, "SELECT distinct brief_advisory,advisory_date,srno FROM crop_insect_advisory_issued where crop_code='\(crop_code)'")
while let row = try rows.next() {
let title: String = row.value(named: "brief_advisory")
let da:String=row.value(named:"advisory_date")
let srno:Int = row.value(named:"srno")
let date_f=DateFormatter()
date_f.dateFormat="yyyy-MM-dd"
let d=date_f.date(from: da)
date_f.dateFormat="dd-MM-yyyy"
let dd=date_f.string(from: d!)
array.append(title)
date_array.append(dd)
srno_array.append("\(srno)")
}
}
for i in 0 ..< array.count
{
struct_array.append(myObject(date:date_array[i],advisory:array[i],
status:"N",srno:srno_array[i]))
}
for i in 0 ..< array.count
{
msg_stat.append(oldMsg(is_checked: false))
}
collection_view.reloadData()
}
catch{
print(error)
}
}
}
I have implemented private chat in my ios app. However, it is not so private. When I send a message that I intend to send to one person, everyone in the app can see it. I have three view controllers in play here.
The FirstViewController has a list of users, and when the cell is clicked it is segued to the DetailedViewController. In this viewController, it only lists the details of the user clicked on. Next, when I press the compose button in the DetailedViewController, the goal is to segue to MessageUserController. This is where I am stuck. This is the code to segue to the MessageUserController:
var username: String?
#IBAction func sendMessage(_ sender: Any) {
performSegue(withIdentifier: "sendMessageToUser", sender: self.username)
}
override public func prepare(for segue: UIStoryboardSegue, sender: Any?) {
guard segue.identifier == "sendMessageToUser", let chatVc = segue.destination as? MessageViewController else {
return
}
chatVc.senderId = self.loggedInUser?.uid
chatVc.senderDisplayName = self.username
}
I assume the sender could be the username because it is unique to the user. When I click on a user to chat with, it works fine but when I click on another user, the chat between the first users are already displayed in the new user's chatController
In the firstViewController, username is passed like this:
if segue.identifier == "UsersProfile" {
if let indexPath = sender as? IndexPath{
let vc = segue.destination as! UsersProfileViewController
let post = self.posts[indexPath.row] as! [String: AnyObject]
let username = post["username"] as? String
vc.username = username
}
}
entire view controller:
import UIKit
import Photos
import Firebase
import FirebaseDatabase
import JSQMessagesViewController
class SendMessageViewController: JSQMessagesViewController {
var username: String?
//var receiverData = AnyObject?()
var messages = [JSQMessage]()
private var photoMessageMap = [String: JSQPhotoMediaItem]()
private let imageURLNotSetKey = "NOTSET"
lazy var outgoingBubbleImageView: JSQMessagesBubbleImage = self.setupOutgoingBubble()
lazy var incomingBubbleImageView: JSQMessagesBubbleImage = self.setupIncomingBubble()
var rootRef = FIRDatabase.database().reference()
var messageRef = FIRDatabase.database().reference().child("messages")
private var newMessageRefHandle: FIRDatabaseHandle?
private lazy var usersTypingQuery: FIRDatabaseQuery =
self.rootRef.child("typingIndicator").queryOrderedByValue().queryEqual(toValue: true)
lazy var storageRef: FIRStorageReference = FIRStorage.storage().reference(forURL: "gs://gsignme-14416.appspot.com")
private var updatedMessageRefHandle: FIRDatabaseHandle?
private lazy var userIsTypingRef: FIRDatabaseReference =
self.rootRef.child("typingIndicator").child(self.senderId) // 1
private var localTyping = false // 2
var isTyping: Bool {
get {
return localTyping
}
set {
// 3
localTyping = newValue
userIsTypingRef.setValue(newValue)
}
}
override func viewDidLoad() {
super.viewDidLoad()
self.senderId = FIRAuth.auth()?.currentUser?.uid
// Do any additional setup after loading the view.
self.navigationController?.navigationBar.barTintColor = UIColor(red:0.23, green:0.73, blue:1.00, alpha:1.0)
self.navigationController?.navigationBar.titleTextAttributes = [NSForegroundColorAttributeName: UIColor.white]
self.navigationItem.title = senderDisplayName
self.navigationItem.rightBarButtonItem?.tintColor = UIColor.white
self.navigationItem.leftBarButtonItem?.tintColor = UIColor.white
// No avatars
collectionView!.collectionViewLayout.incomingAvatarViewSize = CGSize.zero
collectionView!.collectionViewLayout.outgoingAvatarViewSize = CGSize.zero
observeMessages()
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
observeTyping()
}
deinit {
if let refHandle = newMessageRefHandle {
messageRef.removeObserver(withHandle: refHandle)
}
if let refHandle = updatedMessageRefHandle {
messageRef.removeObserver(withHandle: refHandle)
}
}
override func collectionView(_ collectionView: JSQMessagesCollectionView!, messageDataForItemAt indexPath: IndexPath!) -> JSQMessageData! {
return messages[indexPath.item]
}
override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return messages.count
}
private func setupOutgoingBubble() -> JSQMessagesBubbleImage {
let bubbleImageFactory = JSQMessagesBubbleImageFactory()
return bubbleImageFactory!.outgoingMessagesBubbleImage(with: UIColor.jsq_messageBubbleBlue())
}
private func setupIncomingBubble() -> JSQMessagesBubbleImage {
let bubbleImageFactory = JSQMessagesBubbleImageFactory()
return bubbleImageFactory!.incomingMessagesBubbleImage(with: UIColor.jsq_messageBubbleLightGray())
}
override func collectionView(_ collectionView: JSQMessagesCollectionView!, messageBubbleImageDataForItemAt indexPath: IndexPath!) -> JSQMessageBubbleImageDataSource! {
let message = messages[indexPath.item] // 1
if message.senderId == senderId { // 2
return outgoingBubbleImageView
} else { // 3
return incomingBubbleImageView
}
}
override func collectionView(_ collectionView: JSQMessagesCollectionView!, avatarImageDataForItemAt indexPath: IndexPath!) -> JSQMessageAvatarImageDataSource! {
return nil
}
override func collectionView(_ collectionView: JSQMessagesCollectionView!, layout collectionViewLayout: JSQMessagesCollectionViewFlowLayout!, heightForMessageBubbleTopLabelAt indexPath: IndexPath!) -> CGFloat {
return 15
}
override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = super.collectionView(collectionView, cellForItemAt: indexPath) as! JSQMessagesCollectionViewCell
let message = messages[indexPath.item]
if message.senderId == senderId {
cell.textView?.textColor = UIColor.white
} else {
cell.textView?.textColor = UIColor.black
}
return cell
}
//ADD A NEW MESSAGE
private func addMessage(withId id: String, name: String, text: String) {
if let message = JSQMessage(senderId: id, displayName: name, text: text) {
messages.append(message)
}
}
override func didPressSend(_ button: UIButton!, withMessageText text: String!, senderId: String!, senderDisplayName: String!, date: Date!) {
let itemRef = rootRef.child("messages").childByAutoId() // 1
let messageItem = [ // 2
"senderId": senderId!,
"ReceiverName": senderDisplayName!,
"text": text!,
]
itemRef.setValue(messageItem) // 3
JSQSystemSoundPlayer.jsq_playMessageSentSound() // 4
finishSendingMessage() // 5
isTyping = false
}
private func observeMessages() {
// 1.
let messageQuery = rootRef.child("messages").queryLimited(toLast: 25)
// 2. We can use the observe method to listen for new
// messages being written to the Firebase DB
newMessageRefHandle = messageQuery.observe(.childAdded, with: { (snapshot) -> Void in
// 3
let messageData = snapshot.value as! Dictionary<String, String>
if let id = messageData["senderId"] as String!, let name = messageData["ReceiverName"] as String!, let text = messageData["text"] as String!, text.characters.count > 0 {
// 4
self.addMessage(withId: id, name: name, text: text)
// 5
self.finishReceivingMessage()
} else if let id = messageData["senderId"] as String!,
let photoURL = messageData["photoURL"] as String! { // 1
// 2
if let mediaItem = JSQPhotoMediaItem(maskAsOutgoing: id == self.senderId) {
// 3
self.addPhotoMessage(withId: id, key: snapshot.key, mediaItem: mediaItem)
// 4
if photoURL.hasPrefix("gs://") {
self.fetchImageDataAtURL(photoURL, forMediaItem: mediaItem, clearsPhotoMessageMapOnSuccessForKey: nil)
}
}
} else {
print("Error! Could not decode message data")
}
})
// We can also use the observer method to listen for
// changes to existing messages.
// We use this to be notified when a photo has been stored
// to the Firebase Storage, so we can update the message data
updatedMessageRefHandle = messageRef.observe(.childChanged, with: { (snapshot) in
let key = snapshot.key
let messageData = snapshot.value as! Dictionary<String, String> // 1
if let photoURL = messageData["photoURL"] as String! { // 2
// The photo has been updated.
if let mediaItem = self.photoMessageMap[key] { // 3
self.fetchImageDataAtURL(photoURL, forMediaItem: mediaItem, clearsPhotoMessageMapOnSuccessForKey: key) // 4
}
}
})
}
override func textViewDidChange(_ textView: UITextView) {
super.textViewDidChange(textView)
// If the text is not empty, the user is typing
isTyping = textView.text != ""
}
private func observeTyping() {
let typingIndicatorRef = rootRef.child("typingIndicator")
userIsTypingRef = typingIndicatorRef.child(senderId)
userIsTypingRef.onDisconnectRemoveValue()
usersTypingQuery = typingIndicatorRef.queryOrderedByValue().queryEqual(toValue: true)
// 1
usersTypingQuery.observe(.value) { (data: FIRDataSnapshot) in
// 2 You're the only one typing, don't show the indicator
if data.childrenCount == 1 && self.isTyping {
return
}
// 3 Are there others typing?
self.showTypingIndicator = data.childrenCount > 0
self.scrollToBottom(animated: true)
}
}
func sendPhotoMessage() -> String? {
let itemRef = messageRef.childByAutoId()
let messageItem = [
"photoURL": imageURLNotSetKey,
"senderId": senderId!,
]
itemRef.setValue(messageItem)
JSQSystemSoundPlayer.jsq_playMessageSentSound()
finishSendingMessage()
return itemRef.key
}
func setImageURL(_ url: String, forPhotoMessageWithKey key: String) {
let itemRef = messageRef.child(key)
itemRef.updateChildValues(["photoURL": url])
}
override func didPressAccessoryButton(_ sender: UIButton) {
let picker = UIImagePickerController()
picker.delegate = self as! UIImagePickerControllerDelegate & UINavigationControllerDelegate
if (UIImagePickerController.isSourceTypeAvailable(UIImagePickerControllerSourceType.camera)) {
picker.sourceType = UIImagePickerControllerSourceType.camera
} else {
picker.sourceType = UIImagePickerControllerSourceType.photoLibrary
}
present(picker, animated: true, completion:nil)
}
private func addPhotoMessage(withId id: String, key: String, mediaItem: JSQPhotoMediaItem) {
if let message = JSQMessage(senderId: id, displayName: "", media: mediaItem) {
messages.append(message)
if (mediaItem.image == nil) {
photoMessageMap[key] = mediaItem
}
collectionView.reloadData()
}
}
private func fetchImageDataAtURL(_ photoURL: String, forMediaItem mediaItem: JSQPhotoMediaItem, clearsPhotoMessageMapOnSuccessForKey key: String?) {
// 1
let storageRef = FIRStorage.storage().reference(forURL: photoURL)
// 2
storageRef.data(withMaxSize: INT64_MAX){ (data, error) in
if let error = error {
print("Error downloading image data: \(error)")
return
}
// 3
storageRef.metadata(completion: { (metadata, metadataErr) in
if let error = metadataErr {
print("Error downloading metadata: \(error)")
return
}
// 4
if (metadata?.contentType == "image") {
mediaItem.image = UIImage.init(data: data!)
} else {
mediaItem.image = UIImage.init(data: data!)
}
self.collectionView.reloadData()
// 5
guard key != nil else {
return
}
self.photoMessageMap.removeValue(forKey: key!)
})
}
}
}
extension SendMessageViewController: UIImagePickerControllerDelegate, UINavigationControllerDelegate {
func imagePickerController(_ picker: UIImagePickerController,
didFinishPickingMediaWithInfo info: [String : Any]) {
picker.dismiss(animated: true, completion:nil)
// 1
if let photoReferenceUrl = info[UIImagePickerControllerReferenceURL] as? URL {
// Handle picking a Photo from the Photo Library
// 2
let assets = PHAsset.fetchAssets(withALAssetURLs: [photoReferenceUrl], options: nil)
let asset = assets.firstObject
// 3
if let key = sendPhotoMessage() {
// 4
asset?.requestContentEditingInput(with: nil, completionHandler: { (contentEditingInput, info) in
let imageFileURL = contentEditingInput?.fullSizeImageURL
// 5
let path = "\(FIRAuth.auth()?.currentUser?.uid)/\(Int(Date.timeIntervalSinceReferenceDate * 1000))/\(photoReferenceUrl.lastPathComponent)"
// 6
self.storageRef.child(path).putFile(imageFileURL!, metadata: nil) { (metadata, error) in
if let error = error {
print("Error uploading photo: \(error.localizedDescription)")
return
}
// 7
self.setImageURL(self.storageRef.child((metadata?.path)!).description, forPhotoMessageWithKey: key)
}
})
}
} else {
// Handle picking a Photo from the Camera - TODO
// 1
let image = info[UIImagePickerControllerOriginalImage] as! UIImage
// 2
if let key = sendPhotoMessage() {
// 3
let imageData = UIImageJPEGRepresentation(image, 1.0)
// 4
let imagePath = FIRAuth.auth()!.currentUser!.uid + "/\(Int(Date.timeIntervalSinceReferenceDate * 1000)).jpg"
// 5
let metadata = FIRStorageMetadata()
metadata.contentType = "image/jpeg"
// 6
storageRef.child(imagePath).put(imageData!, metadata: metadata) { (metadata, error) in
if let error = error {
print("Error uploading photo: \(error)")
return
}
// 7
self.setImageURL(self.storageRef.child((metadata?.path)!).description, forPhotoMessageWithKey: key)
}
}
}
}
func imagePickerControllerDidCancel(_ picker: UIImagePickerController) {
picker.dismiss(animated: true, completion:nil)
}
}
Looks to me like this isn't a privacy issue as you state, it's simply that you're not clearing the data on your messages view controller when you load the new conversation.
Ultimately it really depends on how secure you want this to be; if you're happy having the private messages saved in memory, then don't destroy them until the user logs out — you can even keep multiple private conversations saved in a CoreData database. It's still relatively secure this way, and it's convenient for users and performant. If you prefer to destroy messages sooner, clear the data on viewDidDisappear, then checking in your prepareForSegue method that the data is again cleared. You could also destroy the entire messages controller each time you dismiss it, if storing a strong reference isn't what you want to do.
An example of this, as a storyboard:
App loads
User1 is logged in
User1 selects private messages
User1 has conversation with User2
User1 switches to a conversation with User3
[pseudo-code]
userDidChangeRecipient {
// destroy messages view controller
// or destroy Firebase array data and destroy the reference to the message/conversation ID
}
And each time you load the view controller:
prepareForSegue {
if strongRefToMessagesVC == nil {
// instantiate a new instance of vc from nib or scratch
// load the appropriate message/conversation ID
// load messages
}
}
More digging:
There's two possibilities here:
You're not destroying the view controller when you switch messages, and this tutorial expects you to. In that case, you need to look at when the segue ends or the user closes the messages view controller and either destroy it, or empty the array.
You're trying to write all the private messages into the same JSQMessage array. I notice in that view controller you have:
var messageRef = FIRDatabase.database().reference().child("messages")
Is that the database connection you're working with? Each private message conversation should have a unique reference ID so that they do not overlap, otherwise every user will load up the same set of messages from Firebase.
I am having a hard time trying to figure out, how I can change/update a specific part of my firebase database through swift. To give you an example of how my firebase database is structured, here you have a photo:
I am trying to update the likesForPost +1 everytime someone hits the like button that I have in my tableViewController. The important part is that every likesForPost should not be updates, just the one where the button is. I hope you understand my situation and that you can help me :-)
My struct
struct Sweet {
let key: String!
let content: String!
let addedByUser: String!
let profilePhoto: String!
var likesForPost: String!
let itemRef: FIRDatabaseReference?
init (content: String, addedByUser: String, profilePhoto: String!, likesForPost: String!, key: String = "") {
self.key = key
self.content = content
self.addedByUser = addedByUser
self.profilePhoto = profilePhoto
self.likesForPost = likesForPost
self.itemRef = nil
}
init (snapshot: FIRDataSnapshot) {
key = snapshot.key
itemRef = snapshot.ref
if let theFeedContent = snapshot.value!["content"] as? String {
content = theFeedContent
} else {
content = ""
}
if let feedUser = snapshot.value!["addedByUser"] as? String {
addedByUser = feedUser
} else {
addedByUser = ""
}
if let feedPhoto = snapshot.value!["profilePhoto"] as? String! {
profilePhoto = feedPhoto
} else {
profilePhoto = ""
}
if let feedLikes = snapshot.value!["likesForPost"] as? String! {
likesForPost = feedLikes
} else {
likesForPost = "0"
}
}
func toAnyObject() -> AnyObject {
return ["content":content, "addedByUser":addedByUser, "profilePhoto":profilePhoto!, "likesForPost":likesForPost]
}
}
My UITableViewController
import UIKit
import FirebaseDatabase
import FirebaseAuth
import FBSDKCoreKit
class feedTableViewController: UITableViewController {
#IBOutlet weak var loadingSpinner: UIActivityIndicatorView!
var facebookProfileUrl = ""
var dbRef: FIRDatabaseReference!
var updates = [Sweet]()
override func viewDidLoad() {
super.viewDidLoad()
loadingSpinner.startAnimating()
dbRef = FIRDatabase.database().reference().child("feed-items")
startObersvingDB()
}
func startObersvingDB() {
dbRef.observeEventType(.Value, withBlock: { (snapshot: FIRDataSnapshot) in
var newUpdates = [Sweet]()
for update in snapshot.children {
let updateObject = Sweet(snapshot: update as! FIRDataSnapshot)
newUpdates.append(updateObject)
}
self.updates = newUpdates
self.tableView.reloadData()
}) { (error: NSError) in
print(error.description)
}
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
}
#IBAction func addToFeed(sender: AnyObject) {
let feedAlert = UIAlertController(title: "New update", message: "Enter your update", preferredStyle: .Alert)
feedAlert.addTextFieldWithConfigurationHandler { (textField:UITextField) in
textField.placeholder = "Your update"
}
feedAlert.addAction(UIAlertAction(title: "Send", style: .Default, handler: { (action:UIAlertAction) in
if let feedContent = feedAlert.textFields?.first?.text {
if let user = FIRAuth.auth()?.currentUser {
let name = user.displayName
//let photoUrl = user.photoURL
let accessToken = FBSDKAccessToken.currentAccessToken()
if(accessToken != nil) //should be != nil
{
let req = FBSDKGraphRequest(graphPath: "me", parameters: ["fields":"id"], tokenString: accessToken.tokenString, version: nil, HTTPMethod: "GET")
req.startWithCompletionHandler({ (connection, result, error : NSError!) -> Void in
if(error == nil)
{
let userId: String! = result.valueForKey("id") as? String!
let userID = userId
self.facebookProfileUrl = "http://graph.facebook.com/\(userID)/picture?type=large"
let likes = "0"
let feed = Sweet(content: feedContent, addedByUser: name!, profilePhoto: self.facebookProfileUrl, likesForPost: likes)
let feedRef = self.dbRef.child(feedContent.lowercaseString)
feedRef.setValue(feed.toAnyObject())
}
else
{
print("error \(error)")
}
})
}
// LAV FEEDCONTENT OM TIL OGSÅ AT MODTAGE PROFIL BILLEDE URL I STRING OG GIV SÅ facebookProfileUrl STRING LIGE HERUNDER I feed
} else {
// No user is signed in.
}
}
}))
self.presentViewController(feedAlert, animated: true, completion: nil)
}
// MARK: - Table view data source
override func numberOfSectionsInTableView(tableView: UITableView) -> Int {
return 1
}
override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return updates.count
}
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell:updateTableViewCell = tableView.dequeueReusableCellWithIdentifier("Cell", forIndexPath: indexPath) as! updateTableViewCell
let update = updates[indexPath.row]
//cell.textLabel?.text = update.content
//cell.detailTextLabel?.text = update.addedByUser
cell.nameLabel.text = update.addedByUser
cell.updateLabel.text = update.content
cell.likesLabel.text = "\(update.likesForPost) hi-fives"
if update.profilePhoto! != "" {
if let url = NSURL(string: update.profilePhoto!) {
if let data = NSData(contentsOfURL: url) {
cell.picView.image = UIImage(data: data)
cell.picView.layer.cornerRadius = cell.picView.frame.size.width/2
cell.picView.clipsToBounds = true
}
}
} else {
print("Empty facebookProfileUrl")
}
loadingSpinner.stopAnimating()
return cell
}
}
Modify your struct to include one more variable (lets say let path : String!)that will include the value of the node key retrieved from your DB(megaTest or test).
Your Struct
struct Sweet {
let key: String!
let content: String!
let addedByUser: String!
let profilePhoto: String!
var likesForPost: String!
let itemRef: FIRDatabaseReference?
let path : String!
init (content: String, addedByUser: String, profilePhoto: String!, likesForPost: String!, key: String = "",dataPath : String!) {
self.key = key
self.content = content
self.addedByUser = addedByUser
self.profilePhoto = profilePhoto
self.likesForPost = likesForPost
self.itemRef = nil
self.path = dataPath
}
init (snapshot: FIRDataSnapshot) {
key = snapshot.key
itemRef = snapshot.ref
path = key
if let theFeedContent = snapshot.value!["content"] as? String {
content = theFeedContent
} else {
content = ""
}
if let feedUser = snapshot.value!["addedByUser"] as? String {
addedByUser = feedUser
} else {
addedByUser = ""
}
if let feedPhoto = snapshot.value!["profilePhoto"] as? String! {
profilePhoto = feedPhoto
} else {
profilePhoto = ""
}
if let feedLikes = snapshot.value!["likesForPost"] as? String! {
likesForPost = feedLikes
} else {
likesForPost = "0"
}
}
func toAnyObject() -> AnyObject {
return ["content":content, "addedByUser":addedByUser, "profilePhoto":profilePhoto!, "likesForPost":likesForPost,"pathInTheDB" : path]
}
}
In cellForIndexPath just add this
cell. pathDB = self.structArray![indexPath.row].path
Modify your customCell class like this
class customTableViewCell : UITableViewCell{
var pathDB : String! //megaTest or test
#IBAction func likeBtn(sender : UIButton!){
//Update like's
}
}
For updating the value you can use either runTransactionBlock:-
FIRDatabase.database().reference().child(pathDB).child("likesForPost").runTransactionBlock({ (likes: FIRMutableData) -> FIRTransactionResult in
// Set value and report transaction success
likes.value = likes.value as! Int + 1
return FIRTransactionResult.successWithValue(likes)
}) { (err, bl, snap) in
if let error = error {
print(error.localizedDescription)
}
}
Or observe that node with .observeSingleEventOfType, retrieve the snap and then update
let parentRef = FIRDatabase.database().reference().child(pathDB).child("likesForPost")
parentRef.observeSingleEventOfType(.Value,withBlock : {(snap) in
if let nOfLikes = snap.value as? Int{
parentRef.setValue(nOfLikes+1)
}
})
Tried so many times to find out what causes the fatal error. But, still can't figure it out. The first table (result table) causes this error when I try to refresh the table with pull. The second table (favoriteProductTableView) works perfect, so I didn't put any code about the second one. Wondering why. Thank you for your help.
var followArray = [String]()
var resultsNameArray = [String]()
var resultsImageFiles = [PFFile?]()
var resultsDetailsArray = [String]()
var resultsDetailsImageFiles = [PFFile?]()
var resultsObjectID = [String]()
var resultsTitle = [String]()
var personPriceArray = [String]()
var personQuantityArray = [String]()
var personOrderTypeArray = [String]()
var refresher:UIRefreshControl!
override func viewDidLoad() {
super.viewDidLoad()
favoriteProductTableView.hidden = true
refresher = UIRefreshControl()
refresher.tintColor = UIColor.blackColor()
refresher.addTarget(self, action: "refresh", forControlEvents: UIControlEvents.ValueChanged)
self.resultsTable.addSubview(refresher)
}
override func viewDidAppear(animated: Bool) {
refreshResults()
}
func refresh(){
refreshResults()
}
func refreshResults(){
switch(segmentedControl.selectedSegmentIndex){
case 0:
followArray.removeAll(keepCapacity: false)
resultsNameArray.removeAll(keepCapacity: false)
resultsImageFiles.removeAll(keepCapacity: false)
resultsDetailsArray.removeAll(keepCapacity: false)
resultsDetailsImageFiles.removeAll(keepCapacity: false)
resultsObjectID.removeAll(keepCapacity: false)
resultsTitle.removeAll(keepCapacity: false)
personPriceArray.removeAll(keepCapacity: false)
personQuantityArray.removeAll(keepCapacity: false)
personOrderTypeArray.removeAll(keepCapacity: false)
let followQuery = PFQuery(className: "follow")
followQuery.whereKey("user", equalTo: (PFUser.currentUser()!.username)!)
followQuery.whereKey("userToFollow", notEqualTo: (PFUser.currentUser()!.username)!)
followQuery.findObjectsInBackgroundWithBlock { (objects:[PFObject]?, error: NSError?) -> Void in
if error != nil {
}
for object in objects! {
self.followArray.append(object.objectForKey("userToFollow") as! String)
}
let query = PFQuery(className: "products")
query.whereKey("userName", containedIn: self.followArray)
query.findObjectsInBackgroundWithBlock { (catchobjects:[PFObject]?, error:NSError?) -> Void in
if error != nil {
}
for catchobject in catchobjects! {
if catchobject.objectForKey("selling_price") != nil {
self.personPriceArray.append(catchobject.objectForKey("selling_price") as! String)
self.personOrderTypeArray.append("Selling")
} else {
self.personPriceArray.append(catchobject.objectForKey("buying_price") as! String)
self.personOrderTypeArray.append("Buying")
}
self.personQuantityArray.append(catchobject.objectForKey("quantity") as! String)
self.resultsNameArray.append(catchobject.objectForKey("unique_username") as! String)
self.resultsImageFiles.append(catchobject.objectForKey("profile_picture") as? PFFile)
self.resultsDetailsArray.append(catchobject.objectForKey("details") as! String)
self.resultsDetailsImageFiles.append(catchobject.objectForKey("detailsImage") as? PFFile)
self.resultsTitle.append(catchobject.objectForKey("title") as! String)
self.resultsObjectID.append(catchobject.objectId!)
}
dispatch_async(dispatch_get_main_queue()) {
self.resultsTable.reloadData()
}
self.loadEmptyLabel(self.resultsTable)
}
self.refresher.endRefreshing()
}
break
case 1:
...
break
default:
break
}
}
func loadEmptyLabel(tableView: UITableView) {
let emptyLabel = UILabel(frame: CGRectMake(0, 0, UIScreen.mainScreen().bounds.width, UIScreen.mainScreen().bounds.height))
emptyLabel.textAlignment = NSTextAlignment.Center
emptyLabel.textColor = UIColor.blackColor()
emptyLabel.text = "No matched result found."
tableView.backgroundView = emptyLabel
tableView.separatorStyle = UITableViewCellSeparatorStyle.None
var resultCount = Int()
if tableView == resultsTable {
resultCount = resultsNameArray.count
} else {
resultCount = resultsTitleArray.count
}
if resultCount == 0 {
tableView.reloadData()
emptyLabel.hidden = false
} else {
emptyLabel.hidden = true
}
}
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
var numRow: Int = 0
switch(segmentedControl.selectedSegmentIndex){
case 0:
numRow = resultsNameArray.count
break
case 1:
numRow = resultsTitleArray.count
break
default:
break
}
return numRow
}
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
if tableView == resultsTable {
let cell:favoritedTableViewCell = resultsTable.dequeueReusableCellWithIdentifier("Cell") as! favoritedTableViewCell
cell.profileLbl.text = self.resultsNameArray[indexPath.row]
cell.messageTxt.text = self.resultsDetailsArray[indexPath.row]
cell.priceLabel.text = "\(self.personOrderTypeArray[indexPath.row]) \(self.personQuantityArray[indexPath.row]) for $\(self.personPriceArray[indexPath.row])"
cell.titleLabel.text = self.resultsTitle[indexPath.row]
if resultsImageFiles[indexPath.row] != nil {
resultsImageFiles[indexPath.row]!.getDataInBackgroundWithBlock { (imageData:NSData?, error:NSError?) -> Void in
if error == nil{
let image = UIImage(data: imageData!)
cell.imgView.image = image
}
}
} else {
cell.imgView.image = UIImage(named: "Profile Picture")
}
if resultsDetailsImageFiles[indexPath.row] != nil{
resultsDetailsImageFiles[indexPath.row]?.getDataInBackgroundWithBlock({ (imageData:NSData?, error:NSError?) -> Void in
if error == nil{
let image = UIImage(data: imageData!)
cell.detailsImg.image = image
}
})
} else {
cell.detailsImg.image = UIImage(named: "Profile Picture")
}
return cell
} else {
....
}
}
Your numberOfRowsInSection function returns one of two array lengths based on segmentedControl.selectedSegmentIndex, whereas cellForRowAtIndexPath indexes the arrays based on the tableView being displayed. This doesn't look right, especially given your referencing `` which doesn't appear to be populated anywhere - should it just be resultsTitle?.
Also, you're calling self.resultsTable.reloadData() from a background thread. This is bad - it must be called from the main thread using:
dispatch_async(dispatch_get_main_queue()) {
self.resultsTable.reloadData()
}
Nevertheless, it's not clear why you've got this inside the loop either.