Swift App — Chat with AI

Jru
彼得潘的 Swift iOS / Flutter App 開發教室
8 min readMar 11, 2023

[Part 1] Let’s ask ChatGPT some questions using Swift through ChatGPT’s API.

Part 1 — Features:

  • Connect ChatGPT’s API
  • Define a struct type
  • Create XIB files
  • Adjust the position of the keyboard when it covers a text field
  • Dismiss the keyboard → textFieldShouldReturn(_ textField:), touchesBegan(_ touches:, with event: ), endEditing(_ force:)

Go to see:

Part 2: Create views for login and registration

Part 3: Store the data in Firebase

There are a variety of APIs offered by ChatGPT, and I have connected Completions API on my project.

MVVM

View

Main.Storyboard

I placed the Table View, textField, button on the main storyboard, and then put the textField and button into a stack view.

XIB file

I create two XIB files, one for the user’s cell and another for the ChatGPT’s cell, in order to define different layouts for each cell and manage them separately.

  1. A XIB file is created alongside when creating a new file.
to choose Cocoa Touch Class & click create XIB file

2. Then you’ll get two files: TableViewCell.xib, TableViewCell

TableViewCell.xib: to set UI objects.

TableViewCell: to program, to connect @UBOutlet

3. Identify the cell

identifier of ChatGPT’s cell
identifier of User’s cell

4. Connect @UBOutlet and design UI

4. Add the two XIB files in the main ViewController (ChatViewController)

override func viewDidLoad() {
super.viewDidLoad()

...略...

let chatgptTableViewCellXib = UINib(nibName: "ChatgptTableViewCell", bundle: nil)
tableView.register(chatgptTableViewCellXib, forCellReuseIdentifier: "chatgptCell")
let userTableViewCellXib = UINib(nibName: "UserTableViewCell", bundle: nil)
tableView.register(userTableViewCellXib, forCellReuseIdentifier: "userCell")

}

Auto Layout

Text bubbles adjust their size dynamically based on the length of the text within them.

ChatGPTCell:

The trailing and bottom constraints set with a relation value that is greater than or equal to the size of the superview.

UserCell:

The leading and bottom constraints set with a relation value that is greater than or equal to the size of the superview.

Do not check the scrolling enabled checkbox and allow the text view to adjust automatically based on the text length.

ModelContent.swift

I opened a new file and wrote the model for the request and response from chatGPT API.

Here is a document of API. ☞ ChatGPT API documentation

struct OpenAPIParameters:Encodable{
let model:String = "text-davinci-003"
let prompt:String
let temperature:Double = 0.8
let max_tokens = 3000

}
struct OpenAPIResponse:Decodable{
let choices:[Choices]
}
struct Choices:Decodable{
let text:String
}
Completions API

And I also wrote a model that can be used with on other functions.

enum Name:String{
case chapgpt = "ChatGPT"
case user = "Me"
}

struct Content{
let name:Name
let text:String
}

ViewModelAPICaller.swift

Firstly, you need to register and then obtain a API key for authentication.

to get API key

Next, I established a connection to the API using JSON.

import Foundation

struct APICaller{
static let shared = APICaller()
private let apiKey = "Your API key"
private let baseURL:URL = URL(string: "https://api.openai.com/v1/")!

func fetchChatGPTAPI(prompt:String,completion:@escaping(OpenAPIResponse)->Void)
{
let completionsURL = APICaller.shared.baseURL.appending(component: "completions")
var request = URLRequest(url: completionsURL)
request.httpMethod = "POST"
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
request.setValue("Bearer \(APICaller.shared.apiKey) ", forHTTPHeaderField: "Authorization")
let openAIBody = OpenAPIParameters(prompt: prompt)
request.httpBody = try? JSONEncoder().encode(openAIBody)

URLSession.shared.dataTask(with: request)
{
data, response, error
in
if let data = data
{
do
{
let jsonDecoder = JSONDecoder()
jsonDecoder.keyDecodingStrategy = .convertFromSnakeCase
let openAPIResponse = try jsonDecoder.decode(OpenAPIResponse.self, from: data)
// print(openAPIResponse)
completion(openAPIResponse)
}
catch
{
print(error)
}
}
}.resume()
}
}

Reference

ControllerChatViewController

  1. to request/ response from API

When the user sends a text message by pressing send button, the message will store in content array and then is added to a TableView. ChatGPT will then response to the message via an API, and the response will stored in content array and displayed in the TableView.

The content array store both the user’s message and ChatGPT’s responses. To display each message in its own cell, the TableView uses the sender’s name as a reference.

After adding a new message to the content array, you must call reloadData() to update the TableView. To ensure that the newest message is visible, you can use the scrollToRow(at:,at:,animated:) function to scroll at the bottom of the TableView.

import UIKit

class ChatViewController: UIViewController {

@IBOutlet weak var userMessageTextField: UITextField!
@IBOutlet weak var tableView: UITableView!

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

override func viewDidLoad() {
super.viewDidLoad()

tableView.delegate = self
tableView.dataSource = self
userMessageTextField.delegate = self
}

//按下傳送按鈕
@IBAction func sendMessage(_ sender: UIButton) {
//將輸入的訊息加進content陣列裡
content.append(Content(name: .user, text: userMessageTextField.text ?? ""))
//按下傳送按鈕後,抓取API資料
APICaller.shared.fetchChatGPTAPI(prompt: userMessageTextField.text ?? "")
{ [weak self]
openAPIResponse
in
DispatchQueue.main.async
{
self?.openAPIResponse = openAPIResponse
let choicesText = openAPIResponse.choices[0].text
// print(choicesText)
self?.content.append(Content(name: .chapgpt, text: 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)
}

}

//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 == .chapgpt
{
let chatgptCell = tableView.dequeueReusableCell(withIdentifier: "chatgptCell") as! ChatgptTableViewCell
// print(showContent.name.rawValue)
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
// print(showContent.name.rawValue)
userCell.userLabel.text = showContent.name.rawValue
userCell.userTextView?.text = showContent.text
userCell.userImageView.image = UIImage(systemName: "person.crop.circle")
userCell.updateUserUI()
return userCell
}
}
}

2. when the keyboard covers the textField

I added the text field and button into a stack view and placed it at the bottom of the view. However, when the user types a message, the keyboard can cover the text field. To prevent this, I implemented a feature that moves the entire view up when keyboard appears, ensuring that the text field remains visible to the user.

I have defined a property called stackViewBottomConstraint that stores the bottom of a stack view position. This position will be adjusted upward when the keyboard appears.

stackViewBottomConstraint

I have also obtained two additional values: keyboardHeight, which represents the height of the keyboard, and keyboardTopY , which is the top position of the keyboard.

A property stackViewBottomY is to store the bottom position of the stack view, and bottomSpace is the remaining space below the stack view.

If the stackViewBottomY is greater than the keyboardTopY , it means that the stack view is currently being covered by the keyboard. To adjust for this, the stack view must be moved up by a distance equal to the keyboardHeight , to ensure that it remains visible and does not overlap with the keyboard.

To adjust the position of the stackViewBottomConstraint , I subtract a half of the bottomSpace value from it.

import UIKit

class ChatViewController: UIViewController {

@IBOutlet weak var userMessageTextField: UITextField!
@IBOutlet weak var stackView: UIStackView!
@IBOutlet weak var stackViewBottomConstraint: NSLayoutConstraint!

override func viewDidLoad() {
super.viewDidLoad()

userMessageTextField.delegate = self

setupKeyboard()
}


//鍵盤觀察器
private func setupKeyboard()
{
NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillShow), name: UIResponder.keyboardWillShowNotification, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillHide), name: UIResponder.keyboardWillHideNotification, object: nil)
}

@objc func keyboardWillShow(notification:NSNotification)
{
//print("鍵盤彈出通知\(notification)")

if let keyboardFrame = (notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] ?? 0) as? NSValue
{

//取得鍵盤高度(CGFloat)
let keyboardSize = keyboardFrame.cgRectValue
let keyboardHeight = keyboardSize.height

//鍵盤上方Y的位置
let keyboardTopY = self.view.frame.size.height - keyboardHeight

//stackView下方Y的位置
let stackViewBottomY = stackView.frame.origin.y + stackView.frame.size.height

//畫面最下方剩餘的高度
let bottomSpace = self.view.frame.height - stackViewBottomY

//假設要輸入的地方被鍵盤遮住(鍵盤位置高於輸入框)
if keyboardTopY < stackViewBottomY
{
stackViewBottomConstraint.constant = keyboardHeight - bottomSpace/2
}
}
}

@objc func keyboardWillHide(){
stackViewBottomConstraint.constant = 0
}
}

3. Dismiss the keyboard

The user can dismiss the keyboard by pressing the return button on the keyboard.

//MARK: - TextField
extension ChatViewController:UITextFieldDelegate{

func textFieldShouldReturn(_ textField: UITextField) -> Bool {
textField.resignFirstResponder() //return退鍵盤
}
}

The keyboard can also be dismissed by touching the speech bubble.

//按textView可以退鍵盤
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
tableView.endEditing(true)
}

Reference

There are several areas where further improvements can be made. For example, we could create a speech bubble as shown in the below attached image. Additionally, we could connect the Firebase API to store the message, allowing retrieval of chat data.

speech bubble

Hopefully, I have expressed the content of the article clearly. I have asked for ChatGPT’s help in correcting the grammar of the article.😎

Good Job ChatGPT!😆

My GitHub

Reference

Projects

Access chatGPT’s openAPI

Übung macht den Meister.

→ Go to Part 2: log in /register & take a photo/ choose a picture

--

--