Chat with AI — part 3

Jru
11 min readApr 21, 2023

--

Store the data in Firebase

Part3 — Features:

  • Add CocoaPods → install Firebase SDK
  • Create a project in Firebase
  • Store the register account in the Firebase Authentication
  • Log out the account
  • Store & load messages in Firebase Realtime Database
  • Store & load a profile picture in Firebase Storage

Go to see:

Part1: Access ChatGPT’s API.

Part2: Create views for login and registration.

Add CocoaPods — Firebase

I added the key words pod‘Firebase/Core’, pod‘Firebase/Auth’, pod‘Firebase/Database’ , pod ‘Firebase/Storage’ in the Podfile. Then I run the command pod install using the terminal. Finally, I opened the project with the path of extension .xcworkspace to keep doing my project.

Store the register account in the Firebase Authentication

The new user creates an account, and the account information is stored in the Firebase database. The steps on how to create an account can be found in the following article below.

MVVM

I created a new file called DatabaseManager.swift , which is used to connect the program between the view and model.

import Foundation
import FirebaseDatabase
import FirebaseStorage

struct ChatAppUser{
let firstName:String
let lastName:String
let emailAddress:String

//computed property
var safeEmail:String{
var safeEmail = emailAddress.replacingOccurrences(of: ".", with: "-")
safeEmail = safeEmail.replacingOccurrences(of: "@", with: "-")
return safeEmail
}
let profilePictureUrl:String?
}

struct Message{
let account:String
let userMessage:String
let chatgptMessage:String
//computed property
var safeEmail:String{
var safeEmail = account.replacingOccurrences(of: ".", with: "-")
safeEmail = safeEmail.replacingOccurrences(of: "@", with: "-")
return safeEmail
}
}

//final class: this class cannot be subclassed called
final class DatabaseManager{
//singleton
static let shared = DatabaseManager()
//從數據庫讀取或寫入數據realtime database
private let databaseReference:DatabaseReference = Database.database().reference()
//上傳檔案到storage
private let storageReference:StorageReference = Storage.storage().reference()
}

//MARK: - Account Management
extension DatabaseManager{
//if the user email not exist
public func userExists(with email: String,
completion: @escaping ((Bool)->Void))
{
//not contain '.' '#' '$' '[' or ']'
var safeEmail = email.replacingOccurrences(of: ".", with: "-")
safeEmail = safeEmail.replacingOccurrences(of: "@", with: "-")

databaseReference.child("User").child(safeEmail).observeSingleEvent(of: .value) { snapshot in

guard snapshot.value as? String != nil else {
completion(false)
return
}
completion (true)
}
}

///Inserts new user to database
public func insertUser(with user:ChatAppUser)
{
databaseReference.child("User").child(user.safeEmail).setValue(
[
"first_name": user.firstName,
"last_name": user.lastName,
"profile":user.profilePictureUrl
]
)
}

///Insert conversations to database
public func insertContent(with user:Message)
{
let message:[String:Any] =
[
"userMessage": user.userMessage,
"chatgptMessage":user.chatgptMessage
]

let date = Date()

databaseReference.child("User").child(user.safeEmail).child("conversations").child("\(date)").setValue(message)

}
///Insert picture to database
public func uploadPhoto(image:UIImage?, completion:@escaping (Result<URL, Error>)-> Void)
{
let fileReference = storageReference.child(UUID().uuidString + ".jpg")
if let imageData = image?.jpegData(compressionQuality: 0.7)
{
fileReference.putData(imageData, metadata: nil)
{ result in
switch result
{
case .success:
fileReference.downloadURL(completion: completion)
case .failure(let error):
completion(.failure(error))
}
}
}
}

public func loadConversations(email:String, completion:@escaping (DataSnapshot)->Void){
var safeEmail = email.replacingOccurrences(of: ".", with: "-")
safeEmail = safeEmail.replacingOccurrences(of: "@", with: "-")
databaseReference.child("User").child(safeEmail).child("conversations").observeSingleEvent(of:.value)
{ snapshot in
guard snapshot.children.allObjects as? [DataSnapshot] != nil else{
print("no data in Firebase")
return
}
completion(snapshot)
}
}
public func loadProfile(email:String, completion:@escaping (DataSnapshot?)-> Void){
var safeEmail = email.replacingOccurrences(of: ".", with: "-")
safeEmail = safeEmail.replacingOccurrences(of: "@", with: "-")
databaseReference.child("User").child(safeEmail).child("profile").observeSingleEvent(of: .value) { snapshot in
guard snapshot.children.allObjects as? [DataSnapshot] != nil else{
print("no data in Firebase")
return
}
completion(snapshot)
}
}
}

Register

I made a new account and store it in Firebase.

import FirebaseAuth

class RegisterViewController: UIViewController {

override func viewDidLoad() {
super.viewDidLoad()
registerButton.addTarget(self, action: #selector(registerButtonTapped), for: .touchUpInside)
}

//按下register按鈕時確認輸入的資料是否完整
@objc private func registerButtonTapped(){
firstName.resignFirstResponder()
lastName.resignFirstResponder()
emailField.resignFirstResponder()
passwordField.resignFirstResponder()

guard let firstName = firstName.text, let lastName = lastName.text, let email = emailField.text, let password = passwordField.text, !firstName.isEmpty, !lastName.isEmpty, !email.isEmpty, !password.isEmpty, password.count >= 6 else {
alertUserLoginError()
return
}
// Firebase log in
DatabaseManager.shared.userExists(with: email,completion:
{ [weak self] exists in

guard let strongSelf = self else {
return
}
//user already exists
guard !exists else{
//alert帳號已存在
strongSelf.alertUserLoginError(message:"This email address already exists.")
return
}
FirebaseAuth.Auth.auth().createUser(withEmail: email, password: password) { authResult, error in

guard authResult != nil, error == nil else {
print("Error creating user:\(String(describing: error?.localizedDescription))")
return
}

//update the user profile picture
....略....
//The program will be showen in the "Store a profile picture in Storage Firebase" section

//insert the user's information in the database
DatabaseManager.shared.insertUser(with: ChatAppUser(firstName: firstName, lastName: lastName, emailAddress: email))

self?.navigationController?.dismiss(animated: false)

}
})

}

private func alertUserLoginError(message:String = "Please enter all information to create a new account"){
let alert = UIAlertController(title: "Oops!", message: message, preferredStyle: UIAlertController.Style.alert)
alert.addAction(UIAlertAction(title: "Dismiss", style: .cancel))
present(alert, animated: true)

}
}

When the user presses the continue button , they can continue to type data into the text field.

extension RegisterViewController:UITextFieldDelegate{

func textFieldShouldReturn(_ textField: UITextField) -> Bool {
switch textField{
case firstName:
lastName.becomeFirstResponder()
case lastName:
emailField.becomeFirstResponder()
case emailField:
passwordField.becomeFirstResponder()
case passwordField:
registerButtonTapped()
default:
return true
}
return true
}
}

Login

You can test the newly registered account by signing in through Firebase. If the user has successfully signed up for an account, they can use their credentials to log in.

import Firebase

class LoginViewController: UIViewController {

override func viewDidLoad() {
super.viewDidLoad()
loginButton.addTarget(self, action: #selector(loginButtonTapped), for: .touchUpInside)
}

//按下login按鈕時確認輸入的資料是否完整
@objc private func loginButtonTapped()
{
//按下login按鈕後,不會出現鍵盤
emailField.resignFirstResponder()
passwordField.resignFirstResponder()
//判斷輸入是否符合規定
guard let email = emailField.text, let password = passwordField.text, !email.isEmpty, !password.isEmpty, password.count >= 6 else {
alertUserLoginError(message: "Please enter all information to log in")
return
}
//確認是否已有帳號
DatabaseManager.shared.userExists(with: email)
{ [weak self] exists in
print("exists:\(exists)")
//帳號已存在
guard !exists else{
return
}
// Firebase log in
Firebase.Auth.auth().signIn(withEmail: email, password: password)
{ [weak self] authResult, error in

guard let strongSelf = self else{
return
}

guard let result = authResult, error == nil else{
self?.alertUserLoginError(message: "Please make a new account.")
print("Failed:\(String(describing: error?.localizedDescription)) and failed to log in with email: \(email)")
return
}
let user = result.user
print("Success, logged in User(成功登入): \(String(describing: user.email))")

//Check whether the user is logged in or not.
if let user = Auth.auth().currentUser
{
print("logged in(已登入):\(user.uid),\(String(describing: user.email)),\(String(describing: user.photoURL))")
}
//Receive the login status changes接收登入狀態改變的通知
FirebaseAuth.Auth.auth().addStateDidChangeListener
{ auth, user in
if let user = user
{
print("\(String(describing: user)) login(要登入)")

//Dismiss the view 登入成功後退掉畫面
strongSelf.navigationController?.dismiss(animated: true)
}else{
print("not login")
}
}

}

}
}

func alertUserLoginError(message:String){
let alert = UIAlertController(title: "Oops!", message: message, preferredStyle: UIAlertController.Style.alert)
alert.addAction(UIAlertAction(title: "Dismiss", style: .cancel))
present(alert, animated: true)

}
}

Check whether the user has logged in or not

ChatViewController → Firstly, you need to import FirebaseAuth. In the main view controller, ChatViewController, I wrote the method that checks whether the user has logged in or not when the view appears. If the user has not logged in, the Login view will appear. On the other hand, if the user has already logged in, the data stored in Firebase will be downloaded and appeared in chat bubble.

Before downloading the data from Firebase, I clear the content property, which is responsible for storing the messages sent by the user and ChatGPT’s responses, to ensure it is empty.

import FirebaseAuth

class ChatViewController: UIViewController {

override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)

validateAuth()
}

//判斷是否登入
private func validateAuth(){
self.content.removeAll()
if FirebaseAuth.Auth.auth().currentUser == nil
{
let vc = LoginViewController()
let nav = UINavigationController(rootViewController: vc)
nav.modalPresentationStyle = .fullScreen
present(nav, animated: false)
}
else
{
//download conversations data from firebase
let currentUserEmail = Auth.auth().currentUser?.email ?? ""
DatabaseManager.shared.loadConversations(email: currentUserEmail)
{ [weak self] snapshot in

self?.content.removeAll()

if let allSnapshot = snapshot.children.allObjects as? [DataSnapshot]
{
for message in allSnapshot
{
let message = message.value as? [String:Any]
let userMessage = message?["userMessage"]
let chatgptMessage = message?["chatgptMessage"]

self?.content.append(Content(name: .user, text: userMessage as! String))
self?.content.append(Content(name: .chatgpt, text: chatgptMessage as! String))
DispatchQueue.main.async {
self?.tableView.reloadData()
//讓句子出現在最底層的對話中
let contentCount = (self?.content.count ?? 1 ) - 1
let indexPath = IndexPath(row: contentCount, section: 0)
self?.tableView.scrollToRow(at: indexPath, at: .bottom, animated: true)
}

}

}

}
//Check the profile pircture 確認是否有大頭照的路徑
....略....
//The program will be showen in the "Store a profile picture in Storage Firebase" section

print("download data from firebase")
}
}
}

Dismiss the view

RegisterViewController → After pressing the register button and the view controller dismiss.

LoginViewController → After pressing the log in button and logging in successfully, the login view controller is dismissed.

Tip: when you want to restart the simulator, you can choose Device → Erase all content and Settings on tool bar to clear all caches.

Log out the account

The logout button is located in the upper right corner of the chat view. When the user logs out, the messages — which are stored in the content property and displayed in the chat bubble — will be cleared.

class ChatViewController: UIViewController { 

var content = [Content]()

override func viewDidLoad() {
super.viewDidLoad()
navigationItem.rightBarButtonItem = UIBarButtonItem(title: "Logout", style: .done, target: self, action: #selector(logout))

}
@objc private func logout(){
do{
try FirebaseAuth.Auth.auth().signOut()
content.removeAll()
let vc = LoginViewController()
let navi = UINavigationController(rootViewController: vc)
navi.modalPresentationStyle = .fullScreen
present(navi, animated: false)
}
catch{
print("登出失敗\(error)")
}
}
}

Store messages in Firebase Realtime Database

Insert conversation messages into Firebase Realtime Database

When the user presses the sent button, the message will not only be saved in the content property but also in Firebase.

//MARK: - Send Message
//按下傳送按鈕
class ChatViewController: UIViewController {

var content = [Content]()
var openAPIResponse:OpenAPIResponse?

@IBAction func sendMessage(_ sender: UIButton) {
let userMessage = userMessageTextField.text ?? "no message"
//將輸入的訊息加進content陣列裡
content.append(Content(name: .user, text: userMessage))

//抓chatgpt API
APICaller.shared.fetchChatGPTAPI(prompt: userMessage)
{ [weak self]
openAPIResponse
in
DispatchQueue.main.async
{
self?.openAPIResponse = openAPIResponse
let choicesText = openAPIResponse.choices[0].text

self?.content.append(Content(name: .chatgpt, text: choicesText ))

//存入database
let currentUserEmail = Auth.auth().currentUser?.email
var safeEmail = currentUserEmail?.replacingOccurrences(of: ".", with: "-")
safeEmail = safeEmail?.replacingOccurrences(of: "@", with: "-")
DatabaseManager.shared.insertContent(with: Message(account: safeEmail ?? "no user account", userMessage: userMessage, chatgptMessage: choicesText ))

self?.tableView.reloadData() //更新資料
//讓句子出現在最底層的對話中
let contentCount = (self?.content.count ?? 1) - 1
let indexPath = IndexPath(row: contentCount, section: 0)
self?.tableView.scrollToRow(at: indexPath, at: .bottom, animated: true)

}

}

userMessageTextField.text = "" //按下傳送按鈕後,textField輸入前先清空文字
self.tableView.reloadData() //按下傳送按鈕,將傳入的文字更新table資料並顯示在畫面上
view.endEditing(true) //按下傳送按鈕後退鍵盤
//句子出現在最底層的對話中
let indexPath = IndexPath(row: content.count-1, section: 0)
self.tableView.scrollToRow(at: indexPath, at: .bottom, animated: true)
}
}

Download data from Firebase Realtime Database

The data is loaded when the user log into the app.

import UIKit
import FirebaseAuth
import Firebase

class ChatViewController: UIViewController {

override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)

validateAuth()
}

//判斷是否登入
private func validateAuth(){
if{ ....略....}
else{

//download conversations data from firebase
let currentUserEmail = Auth.auth().currentUser?.email ?? ""
DatabaseManager.shared.loadConversations(email: currentUserEmail)
{ [weak self] snapshot in
self?.content.removeAll()

if let allSnapshot = snapshot.children.allObjects as? [DataSnapshot]
{
for message in allSnapshot
{
let message = message.value as? [String:Any]

let userMessage = message?["userMessage"]
let chatgptMessage = message?["chatgptMessage"]

self?.content.append(Content(name: .user, text: userMessage as! String))
self?.content.append(Content(name: .chatgpt, text: chatgptMessage as! String))

DispatchQueue.main.async {
self?.tableView.reloadData()
//讓句子出現在最底層的對話中
let contentCount = (self?.content.count ?? 1 ) - 1
let indexPath = IndexPath(row: contentCount, section: 0)
self?.tableView.scrollToRow(at: indexPath, at: .bottom, animated: true)
}

}

}

}
}
}

Display the messages in the chat bubble

/MARK: - TableView
extension ChatViewController:UITableViewDelegate,UITableViewDataSource{

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

func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let showContent = content[indexPath.row]

if showContent.name == .chatgpt
{
let chatgptCell = tableView.dequeueReusableCell(withIdentifier: "chatgptCell") as! ChatgptTableViewCell

chatgptCell.chatgptTextView.text = showContent.text
chatgptCell.chatgptLabel.text = showContent.name.rawValue
chatgptCell.chatgptImageView.image = UIImage(named: "chatgpt")
chatgptCell.updateChatGPTUI()

return chatgptCell
}
else{
let userCell = tableView.dequeueReusableCell(withIdentifier: "userCell") as! UserTableViewCell

userCell.userLabel.text = showContent.name.rawValue
userCell.userTextView?.text = showContent.text

//downloade the profile picture from Firebase Storage
...略...

userCell.updateUserUI()

return userCell
}
}

}

Reference

Store a profile picture in Firebase Storage

Upload the profile picture

DatabaseManager:

When the user presses the register button, the user’s information, including their profile picture, will be saved in the Firebase database. The profile picture will be given a random name and given a URL path before being saved in Firebase Storage.


final class DatabaseManager{
//singleton
static let shared = DatabaseManager()
//從數據庫讀取或寫入數據realtime database
private let databaseReference:DatabaseReference = Database.database().reference()
//上傳檔案到storage
private let storageReference:StorageReference = Storage.storage().reference()
}

extension DatabaseManager{

///Inserts new user to database
public func insertUser(with user:ChatAppUser)
{
databaseReference.child("User").child(user.safeEmail).setValue(
[
"first_name": user.firstName,
"last_name": user.lastName,
"profile":user.profilePictureUrl
]
)
}

///Insert picture to database
public func uploadPhoto(image:UIImage?, completion:@escaping (Result<URL, Error>)-> Void)
{
let fileReference = storageReference.child(UUID().uuidString + ".jpg")
if let imageData = image?.jpegData(compressionQuality: 0.7)
{
fileReference.putData(imageData, metadata: nil)
{ result in
switch result
{
case .success:
fileReference.downloadURL(completion: completion)
case .failure(let error):
completion(.failure(error))
}
}
}
}

}

RegisterViewController:

This program is written to execute the upload of the user’s information, including their profile picture’s path, when they press the register button.

//按下register按鈕時確認輸入的資料是否完整
@objc private func registerButtonTapped(){

.... 略 ....

//上傳註冊資料、照片到realtime database
DatabaseManager.shared.uploadPhoto(image: self?.iconImageView.image) { result in
switch result{
case .success(let url):
let imageUrlString = url.absoluteString

//entry the database
DatabaseManager.shared.insertUser(with: ChatAppUser(firstName: firstName, lastName: lastName, emailAddress: email,profilePictureUrl: imageUrlString))

case .failure(let error):
print(error)
}
}

}

Download & show the profile picture

ChatViewController:

Firstly, I checked if a profile picture’s URL path exists when the user logs into the app.

class ChatViewController: UIViewController {

var havePicture = false

private func validateAuth(){

....略....

//確認是否有大頭照的路徑
DatabaseManager.shared.loadProfile(email: currentUserEmail) { [weak self] snapShot in
if snapShot?.value as? String != nil{
self?.havePicture = true
}
}
}
}

If a profile picture’s URL path is stored in Firebase Realtime Database, it will be loaded, and the user’s profile picture will show up. Otherwise, a system picture will be displayed on the profile.

extension ChatViewController:UITableViewDelegate,UITableViewDataSource{

...略...

func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let showContent = content[indexPath.row]

if showContent.name == .chatgpt
{
...略...
//ChatGPT's messages are displayed in the chat bubble.
}
else{
let userCell = tableView.dequeueReusableCell(withIdentifier: "userCell") as! UserTableViewCell

userCell.userLabel.text = showContent.name.rawValue
userCell.userTextView?.text = showContent.text

//download a profile picture from Firebase Storage.
if self.havePicture == true{
let currentEmail = Auth.auth().currentUser?.email ?? ""
DatabaseManager.shared.loadProfile(email: currentEmail)
{ snapShot in
if let value = snapShot?.value as? String{
let pathUrl = URL(string: value)!
URLSession.shared.dataTask(with: pathUrl) { data, response, error in
guard let data = data, error == nil else{
return
}
DispatchQueue.main.async {
userCell.userImageView.image = UIImage(data: data)
}
print("大頭照路徑:\(pathUrl)")
}.resume()
}
}
}
else{
userCell.userImageView.image = UIImage(systemName: "person.circle" )
}
userCell.updateUserUI()

return userCell
}
}
}

Reference

--

--