如何使用 firebase 在 ios 中实现私人聊天

How to implement private chat in ios using firebase

我在我的 ios 应用程序中实现了私人聊天。然而,它并不是那么私密。当我发送一条我打算发送给一个人的消息时,应用程序中的每个人都可以看到它。我在这里使用了三个视图控制器。

FirstViewController 有一个用户列表,当单击单元格时,它会转到 DetailedViewController。在这个viewController中,只列出了被点击的用户的详细信息。接下来,当我按下 DetailedViewController 中的撰写按钮时,目标是转到 MessageUserController。这就是我被困的地方。这是转到 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
}

我假设发件人可以是用户名,因为它对用户来说是唯一的。当我点击一个用户聊天时,它工作正常但是当我点击另一个用户时,第一个用户之间的聊天已经显示在新用户的 chatController

在第一个ViewController中,用户名是这样传递的:

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 
    }
}

整个视图控制器:

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)
    }
}

在我看来,这不是您所说的隐私问题,只是您在加载新对话时没有清除消息视图控制器上的数据。

最终这真的取决于你希望它有多安全;如果您很高兴将私人消息保存在内存中,那么在用户注销之前不要销毁它们——您甚至可以将多个私人对话保存在 CoreData 数据库中。这种方式还是比较安全的,方便用户和性能。如果您希望尽快销毁消息,请清除 viewDidDisappear 上的数据,然后检查您的 prepareForSegue 方法以再次清除数据。如果存储强引用不是您想要的,您也可以在每次关闭时销毁整个消息控制器。

这方面的一个例子,作为故事板:

  1. 应用加载
  2. 用户 1 已登录
  3. 用户1选择私信
  4. User1User2
  5. 进行了对话

  1. User1 切换到与 User3
  2. 的对话
  3. [伪代码]

    userDidChangeRecipient {
        // destroy messages view controller
        // or destroy Firebase array data and destroy the reference to the message/conversation ID
    }
    

并且每次加载视图控制器时:

prepareForSegue {
    if strongRefToMessagesVC == nil {
        // instantiate a new instance of vc from nib or scratch
        // load the appropriate message/conversation ID
        // load messages
    }
} 

更多挖掘:

这里有两种可能:

  1. 切换消息时并没有破坏视图控制器,本教程希望您这样做。在这种情况下,您需要查看 segue 何时结束或用户关闭消息视图控制器并销毁它或清空数组。

  2. 您正在尝试将所有私人消息写入同一个 JSQMessage 数组。我注意到在那个视图控制器中你有:

    var messageRef = FIRDatabase.database().reference().child("messages")
    

    这是您正在使用的数据库连接吗?每个私人消息对话都应该有一个唯一的参考 ID,这样它们就不会重叠,否则每个用户都会从 Firebase 加载同一组消息。