In the final part of our article series about creating a chat app that supports sending and editing messages in real-time, we enable the inclusion of image attachments with messages.
To this point, we can send and edit messages as well as view the chat history. Now it’s time to add the ability to work with images in our chat. We’ll tell you what to do to send messages with images, save images from received messages, and delete these messages.
The previous parts of the series can be found here: Part 1 and Part 2.
Let’s start from where we stopped last time – you can download the project from the author’s repo at https://github.com/olgadanylova/BackendlessChatApp and switch to the textMessagingPart2 branch.
For displaying messages with image attachments, we need the corresponding cells in the Chat View Controller table. Create and make them look like at the screenshot below:
These cells correspond to the MyImageCell.swift (for messages sent by you) and ImageCell.swift (for messages sent by other users) classes:
class MyImageCell: UITableViewCell {
@IBOutlet weak var dateLabel: UILabel!
@IBOutlet weak var imageButton: UIButton!
override func awakeFromNib() {
super.awakeFromNib()
}
}
class ImageCell: UITableViewCell {
@IBOutlet weak var userNameLabel: UILabel!
@IBOutlet weak var dateLabel: UILabel!
@IBOutlet weak var imageButton: UIButton!
override func awakeFromNib() {
super.awakeFromNib()
}
}
To receive alert notifications and interactions when working with image-messages, we need the following functions in the Alert.swift class:
func showSendImageAlert(onViewController: UIViewController, cameraAction: UIAlertAction, galleryAction: UIAlertAction, cancelAction: UIAlertAction) {
let alert = UIAlertController(title: nil, message: nil, preferredStyle: .actionSheet)
alert.addAction(cameraAction)
alert.addAction(galleryAction)
alert.addAction(cancelAction)
alert.view.tintColor = darkBlueColor
onViewController.view.endEditing(true)
onViewController.present(alert, animated: true)
}
func showSaveImageAlert(onViewController: UIViewController, image: UIImage, saveAction: UIAlertAction) {
let alert = UIAlertController(title: "Save image to Photos", message: "Would you like to save this image in the photo library?", preferredStyle: .alert)
alert.addAction(saveAction)
alert.addAction(UIAlertAction(title: "Cancel", style: .cancel, handler: nil))
alert.view.tintColor = darkBlueColor
onViewController.view.endEditing(true)
onViewController.present(alert, animated: true)
}
func showSavedImageAlert(onViewController: UIViewController) {
let alert = UIAlertController(title: "Saved", message: "Image has been saved in photo library", preferredStyle: .alert)
alert.addAction(UIAlertAction(title: "OK", style: .default, handler: nil))
alert.view.tintColor = darkBlueColor
onViewController.view.endEditing(true)
onViewController.present(alert, animated: true)
}
func showDeleteImageAlert(onViewController: UIViewController, deleteAction: UIAlertAction) {
let alert = UIAlertController(title: "Delete image", message: "Would you like to delete this image from chat?", preferredStyle: .alert)
alert.addAction(deleteAction)
alert.addAction(UIAlertAction(title: "Cancel", style: .cancel, handler: nil))
alert.view.tintColor = darkBlueColor
onViewController.view.endEditing(true)
onViewController.present(alert, animated: true)
}
Further work with images requires the camera or the photo library usage, so we need these permissions in the Info.plist file – NSCameraUsageDescription and NSPhotoLibraryUsageDescription:
Let’s move to the ChatViewController.swift class. We can unlock the addAttachment button and attach an image to a message – either taking it with the camera or by selecting it from the photo library.
We then need to add this property to the ChatViewController.swift class:
private var imageData: Data?
As new types of cells are added to our table, it is necessary to process them correctly. Update the cellForRowAt indexPath function:
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let messageObject = messages[indexPath.row]
let formatter = DateFormatter()
formatter.dateFormat = "HH:mm:ss / MMM d, yyyy"
if messageObject.userId == yourUser.objectId as String? {
// text
if let messageText = messageObject.messageText,
let created = messageObject.created {
let cell = tableView.dequeueReusableCell(withIdentifier: "MyTextMessageCell", for: indexPath) as! MyTextMessageCell
cell.textView.text = messageText
if let updated = messageObject.updated {
cell.dateLabel.text = "updated " + formatter.string(from: updated)
}
else {
cell.dateLabel.text = formatter.string(from: created)
}
let longPress = UILongPressGestureRecognizer(target: self, action: #selector(longPress(sender:)))
longPress.minimumPressDuration = 0.5
cell.addGestureRecognizer(longPress)
return cell
}
// image
if let imagePath = messageObject.imagePath,
let created = messageObject.created {
let cell = tableView.dequeueReusableCell(withIdentifier: "MyImageCell", for: indexPath) as! MyImageCell
cell.imageButton.setTitle(imagePath,for: .normal)
cell.imageButton.tag = indexPath.row
cell.imageButton.addTarget(self,action:#selector(imageButtonClicked(sender:)), for: .touchUpInside)
cell.dateLabel.text = formatter.string(from: created)
return cell
}
}
else if let userName = messageObject.userName {
// text
if let messageText = messageObject.messageText,
let created = messageObject.created {
let cell = tableView.dequeueReusableCell(withIdentifier: "TextMessageCell", for: indexPath) as! TextMessageCell
cell.userNameLabel.text = userName
cell.textView.text = messageText
if let updated = messageObject.updated {
cell.dateLabel.text = "updated " + formatter.string(from: updated)
}
else {
cell.dateLabel.text = formatter.string(from: created)
}
return cell
}
// image
else if let imagePath = messageObject.imagePath,
let created = messageObject.created {
let cell = tableView.dequeueReusableCell(withIdentifier: "ImageCell", for: indexPath) as! ImageCell
cell.imageButton.setTitle(imagePath,for: .normal)
cell.imageButton.tag = indexPath.row
cell.imageButton.addTarget(self,action:#selector(imageButtonClicked(sender:)), for: .touchUpInside)
cell.userNameLabel.text = userName
cell.dateLabel.text = formatter.string(from: created)
return cell
}
}
return UITableViewCell()
}
Sending a message, as before, occurs when you click on the Send button. Backendless FileService is used to store image attachments on the server. The file will be saved to the FileService folder named “chatFiles” with an automatically generated filename. Next, let’s change the pressedSend function so that it looks like this:
@IBAction func pressedSend(_ sender: Any) {
if !editMode {
let messageObject = MessageObject()
if let userName = yourUser.name {
messageObject.userName = userName as String?
}
else if let userEmail = yourUser.email {
messageObject.userName = userEmail as String?
}
messageObject.userId = yourUser.objectId as String?
if let imageData = self.imageData,
let imageName = messageInputField.text {
self.imageData = nil
self.imageModeDisabled()
messageObject.imagePath = "chatFiles/" + imageName
Backendless.sharedInstance().file.uploadFile("chatFiles/" + imageName, content: imageData, response: { uploadedPicture in
if let index = self.messages.firstIndex(where: {$0.imagePath == "chatFiles/" + imageName}) {
DispatchQueue.main.async {
self.tableView.reloadRows(at: [IndexPath(item: index, section: 0)], with: .fade)
}
}
}, error: { fault in
if let errorMessage = fault?.message {
self.alert.showErrorAlert(message: errorMessage, onViewController: self)
}
})
}
else if let messageText = messageInputField.text {
messageObject.messageText = messageText.trimmingCharacters(in: CharacterSet.whitespacesAndNewlines)
self.clearMessageField()
}
messageStore.save(messageObject, response: { savedMessageObject in
Backendless.sharedInstance()?.messaging.publish(self.channelName, message: savedMessageObject, response: { messageStatus in
}, error: { fault in
if let errorMessage = fault?.message {
self.alert.showErrorAlert(message: errorMessage, onViewController: self)
}
})
}, error: { fault in
if let errorMessage = fault?.message {
self.alert.showErrorAlert(message: errorMessage, onViewController: self)
}
})
}
else {
if let messageText = messageInputField.text {
editingMessage?.messageText = messageText.trimmingCharacters(in: CharacterSet.whitespacesAndNewlines)
}
messageStore.save(editingMessage, response: { updatedMessageObject in
self.editModeDisabled()
}, error: { fault in
if let errorMessage = fault?.message {
self.alert.showErrorAlert(message: errorMessage, onViewController: self)
}
})
}
}
As you can see, text messages and messages containing attachments are now processed in different ways.
After sending/receiving a message with an image, the user can view it by clicking on the button with the image name:
@IBAction func imageButtonClicked(sender: UIButton) {
self.performSegue(withIdentifier: "ShowImage", sender: sender)
}
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if segue.identifier == "ShowImage",
let cell = (sender as! UIButton).superview?.superview as? UITableViewCell,
let indexPath = tableView.indexPath(for: cell) {
let message = messages[indexPath.row]
if let imagePath = message.imagePath,
let imageVC = segue.destination as? ImageViewController,
let messageId = message.objectId,
let messageUserId = message.userId {
imageVC.messageId = messageId
imageVC.messageUserId = messageUserId
imageVC.shortImagePath = imagePath
}
else {
alert.showErrorAlert(message: "Image not found", onViewController: self)
}
}
}
As you can see, the ImageViewController class is used to load an image from the Backendless File Service:
Interaction with the message containing the image attachment also occurs through this class. Let’s look at it in more detail.
The Image View Controller consists of ImageView (which displays a picture), buttons to save an image and delete a message, and an indicator of the image loading process. The downloaded image is saved on the device for quick access in the future. When you click the save button, the user will be prompted to save the image to the gallery.
Deleting a message with a picture is available only to the user who sent it – this will delete the message itself, the image file in the FileService and the image from the device’s memory.
The complete implementation of the ImageViewController.swift class is presented below:
class ImageViewController: UIViewController {
@IBOutlet weak var imageView: UIImageView!
@IBOutlet weak var deleteButton: UIBarButtonItem!
@IBOutlet weak var loadingIndicator: UIActivityIndicatorView!
var messageId = ""
var messageUserId = ""
var shortImagePath: String = ""
private var fullImagePath = ""
private let alert = Alert.shared
override func viewDidLoad() {
super.viewDidLoad()
if Backendless.sharedInstance()?.userService.currentUser.objectId as String? != messageUserId {
deleteButton.isEnabled = false
}
if let appId = Backendless.sharedInstance()?.appID,
let apiKey = Backendless.sharedInstance()?.apiKey {
fullImagePath = "https://backendlessappcontent.com/\(appId)/\(apiKey)/files/\(shortImagePath)"
downloadImage(fullImagePath: fullImagePath)
}
}
private func downloadImage(fullImagePath: String) {
if let image = getImageFromUserDefaults(key: shortImagePath) {
DispatchQueue.main.async {
self.imageView.image = image
}
}
else {
loadingIndicator.startAnimating()
URLSession.shared.dataTask(with: NSURL(string: fullImagePath)! as URL, completionHandler: { (data, response, error) -> Void in
if error != nil {
Alert.shared.showErrorAlert(message: error?.localizedDescription ?? "", onViewController: self)
}
else if let imageData = data,
let image = UIImage(data: imageData) {
self.saveImageToUserDefaults(image: image, key: self.shortImagePath)
DispatchQueue.main.async {
self.imageView.image = image
self.loadingIndicator.stopAnimating()
}
}
}).resume()
}
}
private func saveImageToUserDefaults(image: UIImage, key: String) {
let userDefaults = UserDefaults.standard
if let imageData = image.pngData() {
userDefaults.setValue(imageData, forKey: key)
userDefaults.synchronize()
}
}
private func getImageFromUserDefaults(key: String) -> UIImage? {
let userDefaults = UserDefaults.standard
if let imageData = userDefaults.value(forKey: key) as? Data {
return UIImage(data: imageData)
}
return nil
}
private func deleteImageFromUserDefaults(key: String) {
let userDefaults = UserDefaults.standard
userDefaults.removeObject(forKey: key)
userDefaults.synchronize()
}
@objc private func image(_ image: UIImage, didFinishSavingWithError error: Error?, contextInfo: UnsafeRawPointer) {
if let error = error {
alert.showErrorAlert(message: error.localizedDescription, onViewController: self)
} else {
alert.showSavedImageAlert(onViewController: self)
}
}
@IBAction func pressedSave(_ sender: Any) {
if let image = imageView.image {
let saveAction = UIAlertAction(title: "Save to Photos", style: .default, handler: { action in
UIImageWriteToSavedPhotosAlbum(image, self, #selector(self.image(_:didFinishSavingWithError:contextInfo:)), nil)
})
alert.showSaveImageAlert(onViewController: self, image: image, saveAction: saveAction)
}
}
@IBAction func pressedDelete(_ sender: Any) {
let deleteAction = UIAlertAction(title: "Delete", style: .default, handler: { action in
Backendless.sharedInstance()?.file.remove(self.shortImagePath, response: { removed in
Backendless.sharedInstance()?.data.of(MessageObject.self)?.remove(byId: self.messageId, response: { removed in
self.deleteImageFromUserDefaults(key: self.shortImagePath)
self.performSegue(withIdentifier: "unwindToChatVC", sender: nil)
}, error: { fault in
self.alert.showErrorAlert(message: fault?.message ?? "", onViewController: self)
})
}, error: { fault in
self.alert.showErrorAlert(message: fault?.message ?? "", onViewController: self)
})
})
alert.showDeleteImageAlert(onViewController: self, deleteAction: deleteAction)
}
}
We now have a ready-made chat application in which you can send messages with both text and with attached images. You can also edit and delete these messages, saving the necessary data to the device.
The completed project can be found here:
https://github.com/olgadanylova/BackendlessChatApp
Thank you for reading and Happy Coding!