Chat with AI — part 2

Jru
9 min readApr 6, 2023

--

Login or Register a new account

Part 1: Connect ChatGPT’s API

part 3: Store the data in Firebase

Part 2

I used the video tutorials to set up my login and register view controller in my app.

Chat APP Part2
Chat APP Part 3

Features:

  • Add CocoaPods → pod init
  • Add login and register view controllers
  • Take a picture or choose a picture from album when registering

LoginViewController

To chat with ChatCPT, the user must first log in to the app.

I have set up UI objects on a scroll view, which is placed on the view.

import UIKit

class LoginViewController: UIViewController {

private let scrollView:UIScrollView = {
let scrollView = UIScrollView()
scrollView.clipsToBounds = true //剪裁超出的部分。在 UIScrollView 中,默認是true
return scrollView
}()

private let iconImageView:UIImageView = {
let imageView = UIImageView()
imageView.image = UIImage(named: "chatBubbleIcon")
imageView.contentMode = .scaleAspectFit
return imageView
}()

private let emailField:UITextField = {
let field = UITextField()
field.autocapitalizationType = .none
field.autocorrectionType = .no //測你所輸入的文字
field.keyboardType = .emailAddress //適用輸入 Email 的鍵盤
field.returnKeyType = .continue //鍵盤上的 return改成continune
field.layer.cornerRadius = 15
field.layer.borderWidth = 2
field.layer.borderColor = UIColor.lightGray.cgColor
field.placeholder = "Email Address"
field.leftView = UIView(frame: CGRect(x: 0, y: 0, width: 10, height: 0))//讓輸入的文字不會與邊框太貼近,text field 的左邊加上想呈現的內容->形成左邊padding空間變大10
field.leftViewMode = .always //永遠顯示
field.backgroundColor = .white //輸入背景變白色
return field
}()

private let passwordField:UITextField = {
let field = UITextField()
field.autocapitalizationType = .none
field.autocorrectionType = .no
field.returnKeyType = .done //鍵盤上的 return改成done
field.layer.cornerRadius = 15
field.layer.borderWidth = 2
field.layer.borderColor = UIColor.lightGray.cgColor
field.placeholder = "Password..."
field.leftView = UIView(frame: CGRect(x: 0, y: 0, width: 10, height: 0))//讓輸入的文字不會與邊框太貼近,text field 的左邊加上想呈現的內容->形成左邊padding空間變大10
field.leftViewMode = .always //永遠顯示
field.backgroundColor = .white //輸入背景變白色
field.isSecureTextEntry = true
return field
}()

private let loginButton: UIButton = {
let button = UIButton()
button.setTitle("Log In", for: .normal)
button.backgroundColor = .link
button.setTitleColor(.white, for: .normal)
button.layer.cornerRadius = 12
button.titleLabel?.font = .systemFont(ofSize: 20, weight: .bold)
return button
}()

private let registerButton:UIButton = {
let button = UIButton()
button.setTitle("Register", for: .normal)
button.setTitleColor(.link, for: .normal)
button.titleLabel?.font = .systemFont(ofSize: 17, weight: .semibold)

return button
}()

override func viewDidLoad() {
super.viewDidLoad()

view.backgroundColor = .white
title = "Log In"

// Add subview
view.addSubview(scrollView)
scrollView.addSubview(iconImageView)
scrollView.addSubview(emailField)
scrollView.addSubview(passwordField)
scrollView.addSubview(loginButton)
scrollView.addSubview(registerButton)
}
}

Then I created a new file and specified the coordinates , width and height on the view.

import Foundation
import UIKit

extension UIView{

public var width:CGFloat{
return self.frame.size.width
}
public var height:CGFloat{
return self.frame.size.height
}
public var top:CGFloat{
return self.frame.origin.y
}
public var bottom:CGFloat{
return self.frame.origin.y + self.frame.size.height
}
public var left:CGFloat{
return self.frame.origin.x
}
public var right:CGFloat{
return self.frame.origin.x + self.frame.size.width
}
}

Once the app has finished loading the viewDidLoad() method, the UI objects are laid out in the viewDidLayoutSubviews() method.

import UIKit

class LoginViewController: UIViewController {

override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()

scrollView.frame = view.bounds //設置extensions檔案

let imageSize = view.width/3

iconImageView.frame = CGRect(x: (scrollView.width-imageSize)/2,
y: 20,
width: imageSize,
height: imageSize)
emailField.frame = CGRect(x: 40,
y: iconImageView.bottom + 40,
width: scrollView.width-80,
height: 50)
passwordField.frame = CGRect(x: 40,
y: emailField.bottom + 20,
width: scrollView.width-80,
height: 50)
loginButton.frame = CGRect(x: 40,
y: passwordField.bottom + 30,
width: scrollView.width-80,
height: 50)
registerButton.frame.origin = CGPoint(x: view.center.x - 50,
y: loginButton.bottom + 30)
registerButton.frame.size = CGSize(width: 100, height: 30)
}
}

When the user taps the login button, the app checks if the text fields are empty or if the password is less than 6 characters. If the user does not enter the correct format, an alert will pop up to prompt them to correct their input.

import UIKit

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()
return
}
// Firebase log in
}
func alertUserLoginError(){
let alert = UIAlertController(title: "Oops!", message: "Please enter all information to log in", preferredStyle: UIAlertController.Style.alert)
alert.addAction(UIAlertAction(title: "Dismiss", style: .cancel))
present(alert, animated: true)

}
}

After the user finishes entering their email and taps the continue button on the keyboard, the user can proceed to enter their password. Once the password is completed, the user can press done button on the keyboard and to log in to their account.

 override func viewDidLoad() {
super.viewDidLoad()

emailField.delegate = self
passwordField.delegate = self
}

extension LoginViewController:UITextFieldDelegate
{
func textFieldShouldReturn(_ textField: UITextField) -> Bool
{

//使用者按下鍵盤上continue按鈕,可以繼續輸入密碼
if textField == emailField{
passwordField.becomeFirstResponder() //focus on password
}
//輸入密碼後按下鍵盤上的done等於按下登入按鈕
else if textField == passwordField{
loginButtonTapped()
}
return true
}
}

The user can create a new account by tapping the Register button located below the Log In button.

override func viewDidLoad() {
super.viewDidLoad()

//按下login按鈕下方的註冊也可以連到與右上角按鈕同一個註冊畫面
registerButton.addTarget(RegisterViewController(), action: #selector(didTapRegister), for: .touchUpInside)

}

@objc private func didTapRegister(){
let registerVC = RegisterViewController()
registerVC.title = "Register Account"
navigationController?.pushViewController(registerVC, animated: true)
}

RegisterViewController

The RegisterViewController’s program is similar to the LoginViewController’s, but with the addition of the firstName and lastName properties. Additionally, the name of the Log In button has been changed to Register and the didTapRegister() method has been removed. The function that checks if the user has correctly entered all the required text fields has also been modified.

import UIKit

class RegisterViewController: UIViewController {

private let scrollView:UIScrollView = {
let scrollView = UIScrollView()
scrollView.clipsToBounds = true //剪裁超出的部分。在 UIScrollView 中,它的默認true
return scrollView
}()

private let iconImageView:UIImageView = {
let imageView = UIImageView()
imageView.image = UIImage(systemName: "person.circle")
imageView.tintColor = .gray
imageView.contentMode = .scaleAspectFill
imageView.layer.masksToBounds = true //imageView.clipsToBounds = true效果相同(裁切)
imageView.layer.borderWidth = 3
imageView.layer.borderColor = UIColor.link.cgColor
return imageView
}()

private let firstName:UITextField = {
let field = UITextField()
field.autocapitalizationType = .none
field.autocorrectionType = .no
field.returnKeyType = .continue
field.layer.cornerRadius = 15
field.layer.borderWidth = 2
field.layer.borderColor = UIColor.lightGray.cgColor
field.placeholder = "First Name"
field.leftView = UIView(frame: CGRect(x: 0, y: 0, width: 10, height: 0))
field.leftViewMode = .always
field.backgroundColor = .white
return field
}()

private let lastName:UITextField = {
let field = UITextField()
field.autocapitalizationType = .none
field.autocorrectionType = .no
field.returnKeyType = .continue
field.layer.cornerRadius = 15
field.layer.borderWidth = 2
field.layer.borderColor = UIColor.lightGray.cgColor
field.placeholder = "Last Name"
field.leftView = UIView(frame: CGRect(x: 0, y: 0, width: 10, height: 0))
field.leftViewMode = .always
field.backgroundColor = .white
return field
}()

private let emailField:UITextField = {
let field = UITextField()
field.autocapitalizationType = .none
field.autocorrectionType = .no //測你所輸入的文字,列出相依性高的單字們(https://ithelp.ithome.com.tw/articles/10220888)
field.keyboardType = .emailAddress //適用輸入 Email 的鍵盤
field.returnKeyType = .continue //鍵盤上的 return改成continune
field.layer.cornerRadius = 15
field.layer.borderWidth = 2
field.layer.borderColor = UIColor.lightGray.cgColor
field.placeholder = "Email Address"
field.leftView = UIView(frame: CGRect(x: 0, y: 0, width: 10, height: 0))//加大輸入的空間,不要貼在框邊上。text field 的左邊加上想呈現的內容->形成左邊padding空間變大10
field.leftViewMode = .always //永遠顯示
field.backgroundColor = .white //輸入背景變白色
return field
}()

private let passwordField:UITextField = {
let field = UITextField()
field.autocapitalizationType = .none
field.autocorrectionType = .no
field.returnKeyType = .done //鍵盤上的 return改成done
field.layer.cornerRadius = 15
field.layer.borderWidth = 2
field.layer.borderColor = UIColor.lightGray.cgColor
field.placeholder = "Password..."
field.leftView = UIView(frame: CGRect(x: 0, y: 0, width: 10, height: 0))//加大輸入的空間,不要貼在框邊上。text field 的左邊加上想呈現的內容->形成左邊padding空間變大10
field.leftViewMode = .always //永遠顯示
field.backgroundColor = .white //輸入背景變白色
field.isSecureTextEntry = true
return field
}()

private let registerButton: UIButton = {
let button = UIButton()
button.setTitle("Register", for: .normal)
button.backgroundColor = .link
button.setTitleColor(.white, for: .normal)
button.layer.cornerRadius = 12
// button.layer.masksToBounds = true //CALayer的属性
button.titleLabel?.font = .systemFont(ofSize: 20, weight: .bold)
return button
}()

override func viewDidLoad() {
super.viewDidLoad()

view.backgroundColor = .white
title = "Register Account"

registerButton.addTarget(self, action: #selector(registerButtonTapped), for: .touchUpInside)


// Add subview
view.addSubview(scrollView)
scrollView.addSubview(iconImageView)
scrollView.addSubview(firstName)
scrollView.addSubview(lastName)
scrollView.addSubview(emailField)
scrollView.addSubview(passwordField)
scrollView.addSubview(registerButton)

}

override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()

scrollView.frame = view.bounds

let imageSize = view.width/3

iconImageView.frame = CGRect(x: (scrollView.width-imageSize)/2,
y: 20,
width: imageSize + 20,
height: imageSize + 20)
iconImageView.layer.cornerRadius = iconImageView.width/2 //圓形頭像

firstName.frame = CGRect(x: 40,
y: iconImageView.bottom + 40,
width: scrollView.width-80,
height: 50)
lastName.frame = CGRect(x: 40,
y: firstName.bottom + 20,
width: scrollView.width-80,
height: 50)
emailField.frame = CGRect(x: 40,
y: lastName.bottom + 20,
width: scrollView.width-80,
height: 50)
passwordField.frame = CGRect(x: 40,
y: emailField.bottom + 20,
width: scrollView.width-80,
height: 50)

registerButton.frame = CGRect(x: 40,
y: passwordField.bottom + 20,
width: scrollView.width-80,
height: 50)
}
//按下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
}
func alertUserLoginError(){
let alert = UIAlertController(title: "Oops!", message: "Please enter all information to create a new account", preferredStyle: UIAlertController.Style.alert)
alert.addAction(UIAlertAction(title: "Dismiss", style: .cancel))
present(alert, animated: true)

}

}

The textField extension has been modified so that the user can continue filling out the next text field by pressing the continue button on the keyboard.

class RegisterViewController: UIViewController {

override func viewDidLoad() {
super.viewDidLoad()

firstName.delegate = self
lastName.delegate = self
emailField.delegate = self
passwordField.delegate = self
}
}

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
}
}

Take a picture or choose form a album

The user can tap the profile picture to add a picture from the their album or taking a picture.

import PhotosUI //PhPickerViewController

class RegisterViewController: UIViewController {

override func viewDidLoad() {
super.viewDidLoad()

//點選imageView跳出選單可選擇照相或從相簿選照片
iconImageView.isUserInteractionEnabled = true
scrollView.isUserInteractionEnabled = true

let gesture = UITapGestureRecognizer(target: self, action: #selector(didTapChangeProfilePic))
iconImageView.addGestureRecognizer(gesture)
}

//點選imageView跳出選單(sheet)
@objc private func didTapChangeProfilePic(){
print("Change pic called")
presentPhotoActionSheet()
}
}

The action sheet will appear, giving the user the option to choose how they want to select a picture, as shown in the picture below.

extension RegisterViewController:UIImagePickerControllerDelegate, UINavigationControllerDelegate,PHPickerViewControllerDelegate
{
//跳出選單
func presentPhotoActionSheet(){
let actionSheet = UIAlertController(title: "Profile Picture", message: "How would you like to select a picture?", preferredStyle: .actionSheet)
actionSheet.addAction(UIAlertAction(title: "Cancel", style: .cancel))
actionSheet.addAction(UIAlertAction(title: "Take Photo", style: .default,handler: { [weak self] _ in
self?.presentCamer()
}))
actionSheet.addAction(UIAlertAction(title: "Choose Photo", style: .default,handler: { [weak self] _ in
self?.presentPhotoPicker()
}))
present(actionSheet, animated: true)
}

.....略.....
}

Add the privacy rules

  1. Take a picture

Conform the UIImagePickerControllerDelegate, UINavigationControllerDelegate protocol

extension RegisterViewController:UIImagePickerControllerDelegate, UINavigationControllerDelegate,PHPickerViewControllerDelegate
{
.....略.....

//tak a picture
func presentCamer(){
let vc = UIImagePickerController()
vc.sourceType = .camera
vc.delegate = self
vc.allowsEditing = true
present(vc, animated: true)
}

func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) {
print(info)
guard let selectedImage = info[UIImagePickerController.InfoKey.editedImage] as? UIImage else {return} //編輯過後的照片
self.iconImageView.image = selectedImage
}

func imagePickerControllerDidCancel(_ picker: UIImagePickerController) {
picker.dismiss(animated: true)
}

}

2. Choose a photo from the album

Don’t forget to import PhotosUI and conform the PHPickerViewControllerDelegate protocol.

extension RegisterViewController:UIImagePickerControllerDelegate, UINavigationControllerDelegate,PHPickerViewControllerDelegate
{
.....略.....

func presentPhotoPicker(){
var configuration = PHPickerConfiguration()
configuration.filter = .images //PHPickerConfiguration 的 filter 預設為 nil,代表可以選擇照片跟影片。
configuration.selectionLimit = 1 //預設只能選一張照片(此行可寫可不寫)
let picker = PHPickerViewController(configuration: configuration) //預設只能選一張照片
picker.delegate = self
present(picker, animated: true)
}


//相簿選一張照片
func picker(_ picker: PHPickerViewController, didFinishPicking results: [PHPickerResult])
{
picker.dismiss(animated: true)

let itemProviders = results.map { $0.itemProvider } //選擇照片的結果在帶在型別 [PHPickerResult] 的參數 results 裡,從 PHPickerResult 的 itemProvider 可載入選擇的照片,得到型別 [NSItemProvider] 的 array。
//使用者選擇的照片數量。先檢查 itemProviders.first 是否有值。loadObject(ofClass:completionHandler:) 載入照片
if let itemProvider = itemProviders.first, itemProvider.canLoadObject(ofClass: UIImage.self)
{
let previousImage = self.iconImageView.image
itemProvider.loadObject(ofClass: UIImage.self)
{
[weak self] image, error
in
DispatchQueue.main.async
{
guard let self = self, let image = image as? UIImage, self.iconImageView.image == previousImage else {return}
self.iconImageView.image = image
}
}
}
}

}

Reference — PHPickerViewController

--

--