Blog

How to Create a Chat App that Supports Images

by on May 28, 2019

Backendless Chat App with Images

In this series of articles, we are going to show you how to make a beautiful chat app using Xcode Swift that supports sending, editing, and deleting messages with both text and images.

You can download the prepared template from the author’s GitHub repo here and switch to the basicTemplate branch. Go to the root of the downloaded project and run the `pod install`  / `pod update` command to install all necessary dependencies. After the dependencies are installed, open the created .xcworkspace file. The basic project contains a description of ViewControllers, table cells, resources (pictures used in this app) and the keyboard appearance logic.

Main.storyboard contains three ViewControllers: LoginViewController, SignUpViewController and СhatViewController:

Chat App ViewControllers

Firstly, we’ll tell you what each ViewController consists of and then we’ll move on to the code writing.

LoginViewController contains the fields for entering a user’s email and password and a switch to remember the user when logging in. If the switch is on and the user is already logged in, the app won’t ask for an email and will automatically login the next time it launches. If the user is logged out, he/she should enter the credentials again.

Pressing the “Start chatting” button opens the chat view. The “Forgot password?” button allows the user to restore their password. If the user is not yet registered, he/she should press the “New user? Sign up” button to open the registration form (SignUpViewController), where the user can enter his/her name, email, and password. Pressing the “Sign up” button will finish registration.

ChatViewController is the main ViewController of our app. It consists of the table displaying messages, a field to enter a new message, and send and logout buttons. The attachment adding button is currently unavailable; we’ll come back to it later.

Every new message in the chat is the new table cell. The cell with the current user’s (your) message is named MyTextMessageCell and its message has a green background. The cell with another user’s message is named TextMessageCell and its message has a grey background. Every message contains sender name, sending time and the message itself.

Chat Message Cell Structure

Now that we’ve dealt with the ViewControllers contents, let’s get down to the code.

First, let’s configure our app to work with Backendless. Open the Info.plist file and add the App Transport Security Settings (NSAppTransportSecurity key) with Allow Arbitrary Loads = YES:

Backendless Chat App with Images

Then you should initialize Backendless. In the AppDelegate.swift file, change the didFinishLaunchingWithOptions:

func application(_ application: UIApplication, 
didFinishLaunchingWithOptions launchOptions: 
[UIApplication.LaunchOptionsKey: Any]?) -> Bool {
   Backendless.sharedInstance()?.hostURL = "http://api.backendless.com"
   Backendless.sharedInstance()?.initApp(YOUR_APP_ID, apiKey: YOUR_API_KEY)
   return true
}

Now let’s move to the LoginViewController and add the properties necessary for our further coding work.

private var yourUser: BackendlessUser?
private let alert = Alert.shared
private let chatSegue = "segueToChatVC"
  • yourUseris currently logged in user. This property is necessary to pass the current user’s data into the chat.
  • chatSegue – is segue’s name for moving onto the ChatViewController.
  • alert – is the Alert’s class entity, used to display in-app notifications.

The Alert.swift class looks like:

class Alert: NSObject {
    
    static let shared = Alert()
    
    private let darkBlueColor: UIColor
    
    private override init() {
        darkBlueColor = UIColor(rgb: 005493) // color of alert buttons
    }
}

Now let’s describe the user’s login process that occurs by clicking the “Start Chatting” button.

@IBAction func pressedStartChatting(_ sender: Any) {
    if self.rememberMeSwitch.isOn {
        Backendless.sharedInstance()?.userService.setStayLoggedIn(true)
    }
    else {
        Backendless.sharedInstance()?.userService.setStayLoggedIn(false)
    }
        
    if let email = emailField.text, !email.isEmpty,
        let password = passwordField.text, !password.isEmpty {
        Backendless.sharedInstance()?.userService.login(email, password: password, response: { loggedInUser in
            self.clearFields()
            self.yourUser = loggedInUser
            self.performSegue(withIdentifier: self.chatSegue, sender: nil)
        }, error: { fault in
            self.clearFields()
            if let errorMessage = fault?.message {
                self.alert.showErrorAlert(message: errorMessage, onViewController: self)
            }
        })
    }
    else {
        clearFields()
        alert.showErrorAlert(message: "Please check if your email and password are entered correctly", onViewController: self)
    }
}

At first, we need to check if the “remember user mode” is on. If it is, the UserService property is setStayLoggedIn = true . Then we need to check that the email and password fields are filled and call the login function with the specified params. After completing the login, the logged-in user will be saved to the yourUser variable and the chat view will open.

If the value of one of the fields is not entered or an error occurs when trying to log in, the user receives the appropriate notification. Let’s add the showErrorAlert function to the Alert.swift class:

func showErrorAlert(message: String, onViewController: UIViewController) {
    let alert = UIAlertController(title: "Error", message: message, preferredStyle: .alert)
    alert.addAction(UIAlertAction(title: "Dismiss", style: .cancel, handler: nil))
    alert.view.tintColor = darkBlueColor
    onViewController.view.endEditing(true)
    onViewController.present(alert, animated: true)
}

When switching to the chat screen, we need to pass data about the logged-in user to that screen:

override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
    if segue.identifier == chatSegue,
        let chatVC = segue.destination as? ChatViewController {
        chatVC.yourUser = self.yourUser
    }
}

The email and password fields are cleared when the function is called.

func clearFields() {
    self.view.endEditing(true)
    self.emailField.text = ""
    self.passwordField.text = ""
}

Let’s add the auto-login processing for the previously logged in user in the ViewDidAppear function:

override func viewDidAppear(_ animated: Bool) {
    super.viewDidAppear(animated)
    if let currentUser = Backendless.sharedInstance()?.userService.currentUser, Backendless.sharedInstance()?.userService.isValidUserToken() ?? false {
        self.yourUser = currentUser
        performSegue(withIdentifier: chatSegue, sender: nil)
    }
}

If the user forgets his/her password and presses the “Forgot password?” button, the restore password form appears:

@IBAction func pressedForgotPassword(_ sender: Any) {
    alert.showRestorePasswordAlert(onViewController: self)
}

Let’s add the showRestorePasswordAlert function to the Alert.swift class:

func showRestorePasswordAlert(onViewController: UIViewController) {
    let alert = UIAlertController(title: "Restore password", message: "Enter your email and we'll send the instructions on how to reset the password", preferredStyle: .alert)
    alert.addTextField(configurationHandler: { textField in
        textField.placeholder = "email"
    })
    alert.addAction(UIAlertAction(title: "Send", style: .default, handler: { action in
        if let email = alert.textFields?.first?.text, !email.isEmpty {
            Backendless.sharedInstance()?.userService.restorePassword(email, response: {
                let alert = UIAlertController(title: "Password reset email sent", message: "An email has been sent to the provided email address. Follow the email instructions to reset your password", preferredStyle: .alert)
                alert.addAction(UIAlertAction(title: "OK", style: .default, handler: nil))
                alert.view.tintColor = self.darkBlueColor
                onViewController.present(alert, animated: true)
            }, error: { fault in
                if let errorMesssage = fault?.message {
                    self.showErrorAlert(message: errorMesssage, onViewController: onViewController)
                }
            })
        }
        else {
            self.showErrorAlert(message: "Please provide the correct email address to restore password", onViewController: onViewController)
        }
    }))
    alert.addAction(UIAlertAction(title: "Cancel", style: .cancel, handler: nil))
    alert.view.tintColor = darkBlueColor
    onViewController.view.endEditing(true)
    onViewController.present(alert, animated: true)
}

The last thing to do in the LoginViewController is to add the unwindToLoginVC function needed to return on the login screen.

@IBAction func unwindToLoginVC(segue: UIStoryboardSegue) { }

Now let’s move to the SignUpViewController and add the alert property:

private let alert = Alert.shared

Registration of a new user occurs by clicking on the “Sign Up” button:

@IBAction func pressedSignUp(_ sender: Any) {
    if let email = emailField.text, !email.isEmpty,
        let password = passwordField.text, !password.isEmpty {
        let user = BackendlessUser()
        user.email = email as NSString
        user.password = password as NSString
        if let name = nameField.text {
            user.name = name as NSString
        }
        Backendless.sharedInstance()?.userService.register(user, response: { registeredUser in
            self.alert.showRegistrationCompleteAlert(onViewController: self)
        }, error: { fault in
            self.clearFields()
            if let errorMessage = fault?.message {
                self.alert.showErrorAlert(message: errorMessage, onViewController: self)
            }
        })
    }
    else {
        self.clearFields()
        alert.showErrorAlert(message: "Please check if your email and password are entered correctly", onViewController: self)
    }
}

The key fields for registration are email and password, so we need to be sure to check that they are filled out and then call the registration function. After the registration is completed, all text fields will be cleared:

func clearFields() {
    self.view.endEditing(true)
    self.nameField.text = ""
    self.emailField.text = ""
    self.passwordField.text = ""
}

The user will then receive the alert about successful registration. So we need to add the appropriate method in the Alert.swift class:

func showRegistrationCompleteAlert(onViewController: UIViewController) {
    let alert = UIAlertController(title: "Registration complete", message: "Please login to continue", preferredStyle: .alert)
    alert.addAction(UIAlertAction(title: "OK", style: .default, handler: { action in
    onViewController.performSegue(withIdentifier: "unwindToLoginVC", sender: nil)
    }))
    alert.view.tintColor = darkBlueColor
    onViewController.view.endEditing(true)
    onViewController.present(alert, animated: true)
}

If the value of one of the necessary fields is not entered or an error occurs when trying to log in, the user receives the appropriate error notification.

Now that the user can sign up and sign in to chat, let’s head to the ChatViewController.

First, let’s add the necessary properties:

var yourUser: BackendlessUser!
 
private(set) var channel: Channel?
private var messages: [MessageObject]!
    
private let channelName = "MyChannel"
private let alert = Alert.shared
  • channel – channel for chatting,
  • yourUser – your currently logged-in user,
  • messages – an array of messages, dataSource for tableView,
  • channelName – chatting channel name. You can change its value manually.

Also, we need to call the view.endEditing(true) in the clearMessageField function to make the keyboard disappear from the screen when we finish working with the message input field.

func clearMessageField() {
    self.view.endEditing(true)
    ...
}

Let’s add the messages array initialization and channel subscription for receiving messages into the viewDidLoad function:

override func viewDidLoad() {
    super.viewDidLoad()
    ...
    messages = [MessageObject]()
    subscribeForChannel()
}
 
func subscribeForChannel() {
    let channel = Backendless.sharedInstance()?.messaging.subscribe(channelName)
        
    channel?.addMessageListenerDictionary({ message in
        let messageObject = MessageObject()
        if let message = message as? [String : Any] {
            if let userId = message["userId"] as? String {
                messageObject.userId = userId
            }
            if let userName = message["userName"] as? String {
                messageObject.userName = userName
            }
            if let messageText = message["messageText"] as? String {
                messageObject.messageText = messageText
            }
            if let created = message["created"] as? Int {
                messageObject.created = self.intToDate(intVal: created)
            }
            self.messages.append(messageObject)
            self.tableView.reloadData()
        }
    }, error: { fault in
        if let errorMessage = fault?.message {
            self.alert.showErrorAlert(message: errorMessage, onViewController: self)
        }
    })
}
 
func intToDate(intVal: Int) -> Date {
    return Date(timeIntervalSince1970: TimeInterval(intVal / 1000))
}

We receive the message as a dictionary and convert it to the MessageObject to store in the messages array:

class MessageObject: NSObject {
 
    var userId: String?
    var userName: String?
    var messageText: String?
    var created: Date?
    var updated: Date?    
}

Since messages is our table’s dataSource, we should change the numberOfRowsInSection function:

func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
    return messages.count
}

The function cellForRowAt  indexPath is used to display messages in the table:

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?,
        let messageText = messageObject.messageText,
        let created = messageObject.created {
        let cell = tableView.dequeueReusableCell(withIdentifier: "MyTextMessageCell", for: indexPath) as! MyTextMessageCell
        cell.textView.text = messageText
        cell.dateLabel.text = formatter.string(from: created)
            
        let longPress = UILongPressGestureRecognizer(target: self, action: #selector(longPress(sender:)))
        longPress.minimumPressDuration = 1.0
        cell.textView.addGestureRecognizer(longPress)
            
        return cell
    }
    else {
        if  let userName = messageObject.userName,
            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
            cell.dateLabel.text = formatter.string(from: created)
                
            let longPress = UILongPressGestureRecognizer(target: self, action: #selector(longPress(sender:)))
            longPress.minimumPressDuration = 1.0
            cell.textView.addGestureRecognizer(longPress)
               
            return cell
        }
    }
    return UITableViewCell()
}
 
@IBAction func longPress(sender: UILongPressGestureRecognizer) { }

When receiving a message, we should check whether the sender is the current user (you) or not and, depending on this, display a message in MyTextMessageCell or TextMessageCell:

class MyTextMessageCell: UITableViewCell {
 
    @IBOutlet weak var textView: UITextView!
    @IBOutlet weak var dateLabel: UILabel!
    
    override func awakeFromNib() {
        super.awakeFromNib()
        
        self.textView.layer.borderColor = UIColor.white.cgColor
        self.textView.layer.borderWidth = 1
        self.textView.layer.cornerRadius = 10
        self.textView.translatesAutoresizingMaskIntoConstraints = false
    }
}
class TextMessageCell: UITableViewCell {
    
    @IBOutlet weak var userNameLabel: UILabel!
    @IBOutlet weak var textView: UITextView!
    @IBOutlet weak var dateLabel: UILabel!
 
    override func awakeFromNib() {
        super.awakeFromNib()
        
        self.textView.layer.borderColor = UIColor.white.cgColor
        self.textView.layer.borderWidth = 1
        self.textView.layer.cornerRadius = 10
        self.textView.translatesAutoresizingMaskIntoConstraints = false
    }
}

Also, we can include the tap gesture recognizer, but we won’t do anything with it for now.

When clicking the send button, the MessagingService publish function is called and the message is published to the specified channel. Everyone subscribed to this channel receives this message.

@IBAction func pressedSend(_ sender: Any) {
    var message = [String : Any]()
        
    if let messageText = messageInputField.text {
        message["messageText"] = messageText.trimmingCharacters(in: CharacterSet.whitespacesAndNewlines)
    }
    if let userName = yourUser.name {
        message["userName"] = userName
    }
    else if let userName = yourUser.email {
        message["userName"] = userName
    }
    message["userId"] = yourUser.objectId
    message["created"] = Date()
    Backendless.sharedInstance()?.messaging.publish(channelName, message: message, response: { messageStatus in
        self.clearMessageField()
    }, error: { fault in
        if let errorMessage = fault?.message {
            self.alert.showErrorAlert(message: errorMessage, onViewController: self)
        }
    })
}

In the event the user wants to leave the chat and unsubscribe from the channel, the UserService logout and MessagingService unsubscribe functions are used. Let’s return to the LoginViewConrtoller and change the unwindToLoginVC function as follows:

@IBAction func unwindToLoginVC(segue: UIStoryboardSegue) {
    if let chatVC = segue.source as? ChatViewController,
        let channel = chatVC.channel {
        channel.leave()
            Backendless.sharedInstance()?.userService.setStayLoggedIn(false)
        Backendless.sharedInstance()?.userService.logout({ }, error: { fault in
            if let errorMessage = fault?.message {
                self.alert.showErrorAlert(message: errorMessage, onViewController: self)
            }
        })
    }
}

That’s all for today! The app we’ve written today allows users to login, subscribe to a channel, and send and receive text messages. A short example of the application’s usage can be seen on this video:

https://monosnap.com/file/5vc74a2ZQs836EvSCtR7gnzuDrdgh2

Next time we’ll show you how to store the correspondence history and edit and delete messages. You are welcome to modify our app – add the ability to switch between different chats or add user profile pictures (using Backendless File Service to store them of course :)).

Everything we wrote today can be found when switching to the textMessagingPart1 branch of the downloaded project.

Thanks for reading and happy coding!

Continue to Part 2 >

 

Leave a Reply