Did you know you can use the Backendless Code Generator, which is a part of Backendless Console, to easily generate a simple real-time chat application for iOS, Android and JavaScript? You get full source code and can see how to use Backendless APIs for sending text-based chat messages in real time. In this article, we will describe how to modify the generated iOS application so you can send both pictures and text messages.
If you would like to jump to the completed app, we put the source code for the finished project into the author’s personal GitHub repository at https://github.com/olgadanylova/ChatWithImages.git.
To get started, use the code generator to get the basic project. If you have not used the code generator, it is very easy to use:
Now that the project is set up, run it and make sure it works – you should be able to send text-based messages into the chat channel. To see the full effect, you would need to run the app in at least two emulators/devices.
Let’s move on to the fun part, adding support for sending images into the chat.
@objcMembers class ChatMessage: NSObject { var userName: String? var messageText: String? var pictureUrl: String? var picture: UIImage? }
Since messages now can contain both text and a picture, this class will be used to represent messages sent through Backendless.
To send pictures, the Backendless File Service will be used – for each sent image a link is generated and is sent into the chat. That link will be set in the pictureUrl field in the ChatMessage objects. To send a text-based message, you need to create a ChatMessage object with the necessary text in the messageText field.
class MessageCell: UITableViewCell { @IBOutlet var nameLabel: UILabel! @IBOutlet var messageLabel: UILabel! override func awakeFromNib() { super.awakeFromNib() } }
class PictureCell: UITableViewCell { @IBOutlet var nameLabel: UILabel! @IBOutlet var pictureView: UIImageView! @IBOutlet var activityIndicator: UIActivityIndicatorView! override func awakeFromNib() { super.awakeFromNib() activityIndicator.isHidden = true } }
Below is the source code of the class ChatViewController – it is slightly different from what was originally in the generated project – added code working with the table and the ability to select images.
class ChatViewController: UIViewController, UITableViewDelegate, UITableViewDataSource, UITextFieldDelegate, UINavigationControllerDelegate, UIImagePickerControllerDelegate { @IBOutlet var tableView: UITableView! @IBOutlet var messageInputField: UITextField! var channel: Channel? var userName: String? private var activeTextField: UITextField? private var messages: Array<ChatMessage>? override func viewDidLoad() { super.viewDidLoad() addMessageListeners() messages = Array<ChatMessage>() let message = ChatMessage() message.userName = userName message.messageText = "joined" Backendless.sharedInstance().messaging.publish(self.channel?.channelName, message: message, response: { messageStatus in }, error: { fault in self.showErrorAlert(fault!) }) } override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) messageInputField.delegate = self navigationItem.title = userName navigationItem.hidesBackButton = true navigationItem.backBarButtonItem = nil NotificationCenter.default.addObserver(self, selector: #selector(keyboardDidShow(_:)), name: .UIKeyboardWillShow, object: nil) NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillBeHidden(_:)), name: .UIKeyboardWillHide, object: nil) } override func viewWillDisappear(_ animated: Bool) { super.viewWillDisappear(animated) NotificationCenter.default.removeObserver(self, name: .UIKeyboardWillShow, object: nil) NotificationCenter.default.removeObserver(self, name: .UIKeyboardWillHide, object: nil) } override func viewDidLayoutSubviews() { super.viewDidLayoutSubviews() } @objc func keyboardDidShow(_ notification: NSNotification) { let infoValue = notification.userInfo![UIKeyboardFrameEndUserInfoKey] as! NSValue let keyboardSize = infoValue.cgRectValue.size UIView.animate(withDuration: 0.3, animations: { var viewFrame = self.view.frame viewFrame.size.height -= keyboardSize.height self.view.frame = viewFrame }) DispatchQueue.main.async { if (self.tableView.numberOfRows(inSection: 0) > 0) { let indexPath = IndexPath(row: self.tableView.numberOfRows(inSection: 0) - 1, section: 0) self.tableView.scrollToRow(at: indexPath, at: .bottom, animated: true) } } } @objc func keyboardWillBeHidden(_ notification: NSNotification) { UIView.animate(withDuration: 0.3, animations: { let screenFrame = UIScreen.main.bounds var viewFrame = CGRect(x: 0, y: 0, width: screenFrame.size.width, height: screenFrame.size.height) viewFrame.origin.y = 0 self.view.frame = viewFrame }) } func textFieldDidBeginEditing(_ textField: UITextField) { activeTextField = textField } func textFieldDidEndEditing(_ textField: UITextField) { textField.resignFirstResponder() activeTextField = nil } func textFieldShouldReturn(_ textField: UITextField) -> Bool { if (!(messageInputField.text?.isEmpty)!) { let message = ChatMessage() message.userName = userName message.messageText = messageInputField.text publishMessage(message) messageInputField.text = "" } textField.resignFirstResponder() return true } func numberOfSections(in tableView: UITableView) -> Int { return 1 } func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { return (messages?.count)! } func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { let message = self.messages![indexPath.row] if (message.messageText != nil) { let cell = tableView.dequeueReusableCell(withIdentifier: "MessageCell", for: indexPath) as! MessageCell cell.nameLabel.text = String(format: "[%@]:", message.userName!) cell.messageLabel.text = message.messageText return cell } else if (message.pictureUrl != nil) { let cell = tableView.dequeueReusableCell(withIdentifier: "PictureCell", for: indexPath) as! PictureCell cell.nameLabel.text = String(format: "[%@]:", message.userName!) if (message.picture != nil) { cell.pictureView.image = message.picture } else { cell.pictureView.image = nil cell.activityIndicator.isHidden = false cell.activityIndicator.startAnimating() let url = URL(string: message.pictureUrl!)! let session = URLSession(configuration: .default) let downloadPictureTask = session.dataTask(with: url) { (data, response, error) in if let imageData = data { let image = UIImage(data: imageData) message.picture = image DispatchQueue.main.async { cell.pictureView.image = image cell.activityIndicator.isHidden = true cell.activityIndicator.stopAnimating() } } } downloadPictureTask.resume() } return cell } return UITableViewCell() } func addMessageListeners() { channel?.addMessageListenerCustomObject({ receivedMessage in if let message = receivedMessage as? ChatMessage { self.messages?.append(message) self.tableView.reloadData() } }, error: { fault in self.showErrorAlert(fault!) }, class: ChatMessage.ofClass()) } func publishMessage(_ message: ChatMessage) { Backendless.sharedInstance().messaging.publish(channel?.channelName, message: message, response: { messageStatus in }, error: { fault in self.showErrorAlert(fault!) }) } func showErrorAlert(_ fault: Fault) { let alert = UIAlertController(title: "Error", message: fault.message, preferredStyle: .alert) let dismissButton = UIAlertAction(title: "Dismiss", style: .cancel, handler: nil) alert.addAction(dismissButton) present(alert, animated: true, completion: nil) } func showImagePicker() { let alert = UIAlertController(title: "Select image", message: nil, preferredStyle: .alert) alert.addAction(UIAlertAction(title: "Use camera", style: .default, handler: { alertAction in if (!UIImagePickerController.isSourceTypeAvailable(.camera)) { let fault = Fault(message: "No device found. Camera is not available") self.showErrorAlert(fault!) } else { let cameraPicker = UIImagePickerController() cameraPicker.sourceType = .camera cameraPicker.delegate = self self.present(cameraPicker, animated: true, completion: nil) } })) alert.addAction(UIAlertAction(title: "Select from gallery", style: .default, handler: { alertAction in if UIImagePickerController.isSourceTypeAvailable(.photoLibrary) { let imagePicker = UIImagePickerController() imagePicker.sourceType = .photoLibrary imagePicker.delegate = self imagePicker.allowsEditing = false self.present(imagePicker, animated: true, completion: nil) } })) alert.addAction(UIAlertAction(title: "Cancel", style: .cancel, handler: nil)) self.present(alert, animated: true) } func imagePickerControllerDidCancel(_ picker: UIImagePickerController) { dismiss(animated: true, completion: nil) } func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [String : Any]) { let image = info[UIImagePickerControllerOriginalImage] as? UIImage let img = UIImage(cgImage: (image?.cgImage)!, scale: CGFloat(1.0), orientation: .right) let imagePath = String(format: "tmpChatFiles/%@.png", UUID().uuidString) Backendless.sharedInstance().file.uploadFile(imagePath, content: UIImagePNGRepresentation(img), response: { uploadedPicture in let message = ChatMessage() message.userName = self.userName message.pictureUrl = uploadedPicture?.fileURL self.publishMessage(message) }, error: { fault in self.showErrorAlert(fault!) }) dismiss(animated: true, completion: nil) } @IBAction func attachFile(_ sender: Any) { view.endEditing(true) showImagePicker() } }
Instructions to use the enhanced chat app:
Now the RT Chat application can send and receive picture images. In the next article in this series, we will show how to save pictures from the chat to the device and how to delete already sent messages.
Happy Coding!
If you have any questions about this procedure, please post your questions in our support forum (https://support.backendless.com) or on Slack (http://slack.backendless.com).