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.
Create a project in Firebase
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
My GitHub
Übung macht den Meister.
Reference