Đây là phần 3 cũng là phần cuối của bài viết viết về firebase và swift

Phần 1: (Sử dụng firebase với swift 3.x (phần 1))
Phần 2: (Sử dụng firebase với swift 3.x (phần 2))

Trong phần phần 2 tôi đã giới thiệu về xử lý thêm avatars, tạo image, ... trong bài này tôi sẽ trình bày sâu hơn về xử lý ảnh, trao đổi ảnh thông qua chat.

XII . Gửi Images

Phần xử lý gửi ảnh khá giống với xử lý khi gửi text với một key khác. Hơn nữa lưu trữ dữ liệu ảnh trực tiếp với message, chúng ta dùng Firebase Storage, cái này phù hợp hơn khi lưu trữ files lớn như audio, video hay images.

Đầu tiên hãy thêm thư viện Photos vào file ChatViewController.swift:

import Photos

Tiếp theo, thêm thuộc tính sau:

lazy var storageRef: FIRStorageReference = FIRStorage.storage().reference(forURL: "YOUR_URL_HERE")

Đây là một Firebase storage reference khái niệm này giống với Firebase database references mà bạn đã biết rồi nhưng là đối tượng để lưu trữ. Thay thế YOUR_URL_HERE bằng URL của mình, URL lấy bằng cách nhấp chuột vào Storage trong App Console.

Việc lưu trữ một ảnh tới Firebase storage trả về một URL, nhưng cái này có thể lấy một hai giây hoặc dài hơn tùy theo tốc độ mạng. Hơn nữa nhìn trên khung giao diện người dùng sẽ cảm thấy chậm, gửi photo message với một URL giả và sau đó cập nhật lại message sau khi photo đã được lưu.

Tiếp theo thêm thuộc tính sau:

private let imageURLNotSetKey = "NOTSET"

Sau đó, thêm phương thức sau:

func sendPhotoMessage() -> String? {
  let itemRef = messageRef.childByAutoId()

  let messageItem = [
    "photoURL": imageURLNotSetKey,
    "senderId": senderId!,
  ]

  itemRef.setValue(messageItem)

  JSQSystemSoundPlayer.jsq_playMessageSentSound()

  finishSendingMessage()
  return itemRef.key
}

Bây giờ, cập nhật message sau đó lấy URL của Firebase Storage. Thêm đoạn code sau:

func setImageURL(_ url: String, forPhotoMessageWithKey key: String) {
  let itemRef = messageRef.child(key)
  itemRef.updateChildValues(["photoURL": url])
}

Tiếp theo, để người dùng có thể chọn ảnh để gửi. Một điều rất may mắn là JSQMessagesViewController đã chứa UI để có thể thêm ảnh vào message , tiếp theo cần implement phương thức để handles cái này như sau:

override func didPressAccessoryButton(_ sender: UIButton) {
  let picker = UIImagePickerController()
  picker.delegate = self
  if (UIImagePickerController.isSourceTypeAvailable(UIImagePickerControllerSourceType.camera)) {
    picker.sourceType = UIImagePickerControllerSourceType.camera
  } else {
    picker.sourceType = UIImagePickerControllerSourceType.photoLibrary
  }
     
  present(picker, animated: true, completion:nil)
}

Nếu trong thư viện không có ảnh bạn trường hợp này cần chụp ảnh để gửi nên message. Tiếp thực thi phương thức UIImagePickerControllerDelegate để handle khi người dùng chụp ảnh. Thêm đoạn code sau vào cuối file:

// MARK: Image Picker Delegate
extension ChatViewController: 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
    }
  }
  
  func imagePickerControllerDidCancel(_ picker: UIImagePickerController) {
    picker.dismiss(animated: true, completion:nil)
  }
}

Có hai phương thức handle đó là người dùng chọn ảnh hay hủy chọn ảnh. Khi chọn một ảnh, người dùng có thể làm cả hai việc lấy một ảnh từ thư viện ảnh hay chụp ảnh trực tiếp với camera. Với việc chọn ảnh từ thư việc trong code xử lý như sau:

  1. Đầu tiên, kiểm tra xem URL có năm trong thư viện ảnh hay không.
  2. Tiếp theo, lấy PHAset từ URL của ảnh.
  3. Gọi sendPhotoMessage và nhận Firebase key.
  4. Lấy URL cho ảnh
  5. Tạo một đường dẫn duy nhất trên ID duy nhất của người dùng trong thời gian hiện tại.
  6. Lưu trữ ảnh vào Firebase Storage
  7. Khi ảnh được save, gọi setImageULR() để cập nhật ảnh vào message với URL đúng.

Tiếp theo, thêm đoạn code sau để xử lý trong trường hợp chụp ảnh trực tiếp. Thêm đoạn code sau vào dưới TODO trọng đoạn code trên:

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

Đoạn code trên xử lý như sau:

  1. Lấy ảnh từ dictionary.
  2. Gọi phương thức sendPhotoMessage() để lưu đường dẫn ảnh giả vào Firebase.
  3. Tiếp theo, lấy JPEG của ảnh, gửi tới Firebase Storage.
  4. Tạo một đường dẫn duy nhất trên ID duy nhất của người dùng
  5. tạo đối tượng FIRStorageMetadata và set metadata tới image/ipge.
  6. Lưu ảnh tới Firebase Storage.
  7. Khi ảnh đã được lưu, gọi hàm setImageURL() lần nữa.

XIII . Hiển thị Images

Đầu tiên, thêm thuộc tính tới file ChatViewController:

private var photoMessageMap = [String: JSQPhotoMediaItem]()

Tiếp theo, tạo một phương thức tới addMessage(withId:name:text:) như sau:

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

Phương thức trên lưu JSQPhotoMediaItem vào một thuộc tính mới nếu không có image key. Cái này cho phép truy xuất và cập nhật message khi ảnh set sau.
Có thể lấy dữ liệu ảnh tự Firebase Storage để hiển thị trên UI. Thêm phương thức sau:

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/gif") {
        mediaItem.image = UIImage.gifWithData(data!)
      } else {
        mediaItem.image = UIImage.init(data: data!)
      }
      self.collectionView.reloadData()
      
      // 5
      guard key != nil else {
        return
      }
      self.photoMessageMap.removeValue(forKey: key!)
    })
  }
}

Giải thích về đoạn code trên:

  1. Lấy một rerence để lưu ảnh.
  2. Lấy dữ liệu ảnh từ storage.
  3. Lấy image metadata tự storage.
  4. Nếu metadata là loại ảnh GIF thì sử dụng loại trên UIImage được lấy thông qua SwiftGifOrigin. Cái này cần thiết bởi vì UIImage không handle GIF images ngoài box.
  5. Cuối cùng, xóa key từ photoMessageMap.

Cuối cùng, cập nhật observeMessages(). với câu lệnh if, nhưng trước đó kết thúc bằng điều kiện else, thêm đoạn code sau vào:

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

Ý nghĩa của đoạn code trên như sau:

  1. Kiểm tra xem có photoURL set.
  2. Nếu có thì tạo JSQPhotoMediaItem.
  3. Với media item, gọi addPhotoMessage.
  4. Cuối cùng, kiểm tra xem có chắc chắn photoURL chứa prefix cho Firebase Storage, nếu có thì lấy dữ liệu image.

Tiếp theo, thêm thuộc tính sau:

private var updatedMessageRefHandle: FIRDatabaseHandle?

Bây giờ, thêm đoạn code sau vào cuối của observeMessages().

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

Đoạn code trên xử lý như sau:

  1. Lấy message data từ Firebase snapshot.
  2. Kiểm tra nếu dictionary có photoURL thì set.
  3. Nếu có, lấy JSQPhotoMediaItem
  4. Cuối cùng, lấy dữ liệu ảnh và cập nhật message với ảnh

Cuối cùng để clean up mọi thứ khi ChatViewController. Thêm đoạn code sau vào:

deinit {
  if let refHandle = newMessageRefHandle {
    messageRef.removeObserver(withHandle: refHandle)
  }
  
  if let refHandle = updatedMessageRefHandle {
    messageRef.removeObserver(withHandle: refHandle)
  }
}

Build và chạy ứng dụng; sau đó dùng thử gửi ảnh trong message ta có hình dung như sau:

Kết Luận

Dù bạn là lập trình viên IOS hay mới bắt đầu học IOS đều có thể xây dựng một ứng dụng chat hoàn chỉnh trên IOS thông qua ba bài blogs của tôi. Còn một điều lưu ý nữa khi tương tác giữa Firebase và IOS cũng cần thiết phải setting trong account apple developer để tạo các key theo các bunderID tương ứng vì vậy khi làm việc với firebase bạn nên dành chút thời gian để tìm hiểu về Firebase.

Tham khảo