Drinks on me(Part one)

For my final iOS training project, I’ve developed an app for booking drinks. Simple idea, right? But there’s a lot more to it. It’s more than just ordering a drink; there are some nuances and cool features to explore. Let’s dive in and see what it’s all about. Ready?

Yes, we’re going to build an app to book drinks from kebuke.

Requirement

  • Be able to add drinks
  • Be able to choose drinks
  • Be able to add your name to the order
  • Has sweetness option
  • Has temperature option
  • Has size option
Demonstration

Topic

  • What’s escaping
  • Table view reload
  • Sort parameter
  • Auto update
  • Return to fold the keyboard

What’s escaping

What on earth is escaping? Who are we running from? I tried to connect to the endpoint API with a function. This function takes in a closure which will give me the access to decide what to do with the fetched data. But if I run the function without adding @escaping then Swift will complain loudly.

Now, here’s where the concept of escaping comes in. In Swift, when we pass a closure as an argument to a function and that closure is called after the function returns, we label it as escaping. Why? Because it "escapes" the function it was passed to. Say we’re making an API call: the function we use to initiate that call finishes its job way before the server responds. If we want to do something with the server's response, that action (represented by the closure) has to "live on" or "escape" beyond the lifetime of the initial function call. By marking a closure with the @escaping syntax, we’re essentially saying: "Hey, this closure might be called sometime in the future, Be ready!"

// MARK: - Fetch and unwrap API

/// Fetches the order details from a specified API endpoint.
/// - Parameter completion: A completion handler that is triggered after fetching data,
/// passing the parsed `ResponseRoot` object as a parameter.
func fetchOrder(_ completion: @escaping ((_ fetchedOrderRoot:ResponseRoot) -> Void)){

// Define the endpoint URL string.
let urlStr = "https://api.airtable.com/v0/appN21f5f7mgnzUIi/order"
if let url = URL(string: urlStr){

// Create a URLRequest object with the specified URL.
// The cache policy is set to ignore local cache, ensuring fresh data is always fetched.
var urlRequest = URLRequest(url: url, cachePolicy: .reloadIgnoringLocalCacheData)

// Specify that the request method is "GET".
urlRequest.httpMethod = "GET"

// Set the authorization header with the bearer token.
// Note: It's crucial to keep tokens like this secret in a real-world application.
urlRequest.setValue("Bearer patVvuJLhCGDlIA5N.bb43e3d5bf2d60015a897eff3ed89c044143a0c5fc967a59bcb9d20d8cc5043a", forHTTPHeaderField: "Authorization")

// Create a URLSession data task with the request.
URLSession.shared.dataTask(with: urlRequest) { data, _, _ in
// Check if data is received from the API.
if let data = data {
let decoder = JSONDecoder()
// Uncomment below if you need to decode dates in ISO8601 format.
//decoder.dateDecodingStrategy = .iso8601

do {
// Decode the received data into a `ResponseRoot` object.
let orderRoot = try decoder.decode(ResponseRoot.self, from: data)

// Switch to the main thread, as UI changes should be made on the main thread.
DispatchQueue.main.async {

// Pass the decoded `ResponseRoot` object to the completion handler.
completion(orderRoot)

// Uncomment below if you want to update some UI components.
//self.resultRecords = responseRecords
//self.tableView.reloadData()
}

} catch {
// Print any decoding errors.
print(error)
}
}
}.resume() // Start the URLSession data task.
}
}

TableView.reload and reloadRows

I came across situations where I need to update the content displayed in a UITableViewa lot. The UITableView offers few methods to reload its content, but the two most commonly used are tableView.reloadData() and tableView.reloadRows(at:with:). Let's dive into the nuances of each.

  1. tableView.reloadData()
  • Purpose: It’s the go-to method when we want to reload all the rows in the table view.
  • Performance: Depending on the size of our data source, this method can be heavy because it dequeued all current cells and re-queries all the cells and sections.
  • Use case: In the updateOrders function, after fetching the order data, the entire table view is reloaded. This ensures all rows reflect the most recent data.

2. tableView.reloadRows(at:with:)

  • Purpose: This method is more targeted and allows us to reload specific rows in the table view. It’s ideal for scenarios where only a few rows need updating, leaving the rest untouched.
  • Performance: Typically more efficient than reloadData(), especially when the table view has many rows and only a subset needs refreshing.
  • Use case: In the reviseOrder function, when the order of a specific row is revised, only that particular row is refreshed. This method provides a smoother user experience.

So that sounds easy enough. But I still manage to spend half a day on it somehow… In short, use TableView.reload when the table is short and simple. Use TableView.reloadRows when the table is long and complex.

@IBAction func reviseOrder(_ sender: UIStepper) {

print(sender.value)

let point = sender.convert(CGPoint.zero, to: inlineTableView)
if let indexPath = inlineTableView.indexPathForRow(at: point),
let originalCup = orderRoot?.records[indexPath.row].fields.cups {


// Update local data source
patchInfo.cups = originalCup + Int(sender.value)

orderRoot?.records[indexPath.row].fields.cups = patchInfo.cups

// Reload the specific row with animation
inlineTableView.reloadRows(at: [indexPath], with: .automatic)

// Fetch the record ID to be revised
let reviseID = orderRoot?.records[indexPath.row].id
recordsToRevise = reviseID
reviseRecord()
}
sender.value = 0

calculateSum()
}
@objc func updateOrders() {

//Call the 'fetchOrder' method to retrieve order data.
fetchOrder { fetchedOrderRoot in

//Once the fetch is complete, assign the fetched data to the 'orderRoot' property of the current object.
self.orderRoot = fetchedOrderRoot

//Perform a UI animation block to reload the tableView data.
UIView.transition(with: self.inlineTableView,
duration: 0.4,
options: .transitionCrossDissolve,
animations: {
// Within the animation block, reload the tableView data.
self.inlineTableView.reloadData()
})
}

//After fetching the order and updating the tableView, recalculate the total sum.
calculateSum()
}

Sort parameter

The query parameter. These are the key-value pairs we’ll often spot in a URL, typically coming after a ? and separated by & symbols. They allow us to refine our requests, fetching data to our special needs.

Here, the query parameters sort[0][field] and sort[0][direction] are being used to dictate how the fetched data should be sorted. The former specifies the field we're sorting by (defaulting to "tag"), while the latter tells us the direction of the sort.

Here’s a portal to airtable rest API documentation.

let urlStr = "https://api.airtable.com/v0/appN21f5f7mgnzUIi/menu?sort%5B0%5D%5Bfield%5D=\(sortField)&sort%5B0%5D%5Bdirection%5D=\(sortDirection)"
// MARK: - API Integration

func fetchData(sortField:String = "tag", sortDirection:String = "asc") {

// Construct the API endpoint URL string with provided sort field and direction.
let urlStr = "https://api.airtable.com/v0/appN21f5f7mgnzUIi/menu?sort%5B0%5D%5Bfield%5D=\(sortField)&sort%5B0%5D%5Bdirection%5D=\(sortDirection)"
if let url = URL(string: urlStr) {

// Initialize a URLRequest object with the URL.
var urlRequest = URLRequest(url: url)

// Set the HTTP method to GET since we are fetching data.
urlRequest.httpMethod = "GET"

// Set the Authorization header with the bearer token for API authentication.
urlRequest.setValue("Bearer patVvuJLhCGDlIA5N.bb43e3d5bf2d60015a897eff3ed89c044143a0c5fc967a59bcb9d20d8cc5043a", forHTTPHeaderField: "Authorization")

// Start a data task to fetch data from the API.
URLSession.shared.dataTask(with: urlRequest) { data, _, _ in

// Check if data was received from the API.
if let data = data {

// Create a JSONDecoder object to decode the received data.
let decoder = JSONDecoder()

// Attempt to decode the received data into the 'SearchRoot' model.
do {
let searchRecords = try decoder.decode(SearchRoot.self, from: data)

// If decoding is successful, update the UI on the main thread.
DispatchQueue.main.async {
// Assign the decoded data to the 'resultRecords' property.
self.resultRecords = searchRecords

// Reload the tableView to reflect the fetched data.
self.tableView.reloadData()
}
} catch {
// If decoding fails, print the error.
print(error)
}
}

// Resume the data task. This actually starts the network request.
}.resume()
}
}

Auto update

Okay, this one is a bit longer than I thought. The piece of code below showed a list of ordered items in the cart. But most importantly. This cart can update its content automatically. In the following code, we activate and deactivate a timer several times. Let me point it out for ya.

In viewDidLoad We schedule a timer to repeatedly call the updateOrders method every 10 seconds. The purpose? To fetch the latest orders and update the UI seamlessly. This ensures that the user always sees the most up-to-date content without any manual intervention.

updateTimer = Timer.scheduledTimer(timeInterval: 10, target: self, selector: #selector(updateOrders), userInfo: nil, repeats: true)

In the function updateOrders . It fetches the latest orders and then uses a cross-dissolve animation to update the table view.

@objc func updateOrders() {
fetchOrder { fetchedOrderRoot in
self.orderRoot = fetchedOrderRoot
UIView.transition(with: self.inlineTableView, duration: 0.4, options: .transitionCrossDissolve, animations: { self.inlineTableView.reloadData() })
}
calculateSum()
}

In function viewDidAppear and viewDidDisappear . It’s essential to manage the timer’s lifecycle, ensuring they’re active only when needed. Hence, when our view appears (viewDidAppear), the timer starts, set to refresh every 10 seconds. But when the view disappears (viewDidDisappear), the timer is stopped. By disabling the timer, we ensure that it no longer fires, consumes resources, and prevents potential memory costs.

// When the view appears, start the timer to fetch orders every 12 seconds.
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
updateTimer = Timer.scheduledTimer(timeInterval: 10.0, target: self, selector: #selector(updateOrders), userInfo: nil, repeats: true)
}

// When the view disappears, invalidate (stop) the timer to avoid unnecessary API calls.
override func viewDidDisappear(_ animated: Bool) {
super.viewDidDisappear(animated)
updateTimer?.invalidate()
updateTimer = nil
}
// MARK: - CartViewController

import UIKit

// Define a view controller for the cart screen.
class CartViewController: UIViewController, UITableViewDataSource, UITableViewDelegate {

// Outlets for the UI elements.
@IBOutlet var orderButton: UIButton!
@IBOutlet var footerView: UIView!
@IBOutlet var cartView: UIView!

// Data properties to store information about the orders and other related data.
var patchInfo = PatchInfo()
var recordsToDelete: String?
var recordsToRevise: String?
var orderRoot: ResponseRoot?

// Timer used to periodically fetch and update orders.
var updateTimer: Timer?
@IBOutlet var inlineTableView: UITableView!

override func viewDidLoad() {
super.viewDidLoad()

// Set the background color for the UI elements.
inlineTableView.backgroundColor = UIColor.init(red: 23/255, green: 61/255, blue: 80/255, alpha: 1)
cartView.backgroundColor = inlineTableView.backgroundColor
footerView.backgroundColor = inlineTableView.backgroundColor

// Initialize a timer to fetch orders every 10 seconds.
updateTimer = Timer.scheduledTimer(timeInterval: 10, target: self, selector: #selector(updateOrders), userInfo: nil, repeats: true)

// Fetch the initial order data when the view loads.
fetchOrder { fetchedOrderRoot in
self.orderRoot = fetchedOrderRoot
print("\(String(describing: self.orderRoot))")
self.inlineTableView.reloadData()

// Calculate the total price of the orders.
self.calculateSum()
}
}

// Function to show an alert for confirming the order.
func showAlert() {
let alertController = UIAlertController(title: "訂單確認", message: "確定要撥打電話?", preferredStyle: .actionSheet)
let callAlertAction = UIAlertAction(title: "02-26562288", style: .default)
let cancelAlertAction = UIAlertAction(title: "Cancel", style: .destructive)

alertController.addAction(callAlertAction)
alertController.addAction(cancelAlertAction)

present(alertController, animated: true)
}

// Action triggered when the order button is tapped.
@IBAction func orderButton(_ sender: Any) {
showAlert()
}

// Timer function to update orders periodically.
@objc func updateOrders() {
fetchOrder { fetchedOrderRoot in
self.orderRoot = fetchedOrderRoot
UIView.transition(with: self.inlineTableView, duration: 0.4, options: .transitionCrossDissolve, animations: { self.inlineTableView.reloadData() })
}
calculateSum()
}

// When the view appears, start the timer to fetch orders every 12 seconds.
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
updateTimer = Timer.scheduledTimer(timeInterval: 12.0, target: self, selector: #selector(updateOrders), userInfo: nil, repeats: true)
}

// When the view disappears, invalidate (stop) the timer to avoid unnecessary API calls.
override func viewDidDisappear(_ animated: Bool) {
super.viewDidDisappear(animated)
updateTimer?.invalidate()
updateTimer = nil
}

// Function to calculate the total sum of the orders in the cart.
func calculateSum() {
let records = orderRoot?.records ?? []
totalPrice = 0

for record in records {
let pricePerCup = record.fields.originPrice
let cups = record.fields.cups
totalPrice += pricePerCup * cups
}
orderButton.setTitle("Order for $ \(totalPrice)", for: .normal)
}

// Action to revise the number of items in an order.
@IBAction func reviseOrder(_ sender: UIStepper) {
// Logic to update the order based on the stepper value...
// This logic finds the corresponding order in the table view and updates its quantity.
// After updating, it sends a request to the server to revise the order.
}

// Function to send a PATCH request to revise an order's data.
func reviseRecord() {
// API logic to revise the record...
}

// Action to delete a specific order.
@IBAction func deleteOrder(_ sender: UIButton) {
// Logic to delete an order...
}

// Function to send a DELETE request to remove an order.
func deleteRecord(at indexPath: IndexPath) {
// API logic to delete the record...
}

// MARK: - Table View Data Source and Delegate

// Define the number of sections in the table view.
func numberOfSections(in tableView: UITableView) -> Int {
return 1
}

// Define the number of rows in a section.
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return orderRoot?.records.count ?? 20
}

// Define the cell for a specific row.
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
guard let cell = tableView.dequeueReusableCell(withIdentifier: CartTableViewCell.reuseIdentifier, for: indexPath) as? CartTableViewCell else { fatalError("unable to find cell") }

// Configure the cell based on the data...

return cell
}
}

Return to fold the keyboard

To use this delegate function within the controller. It must conform the UITextFieldDelegate protocol. Then we will be able to access a function call textFieldShouldReturn which lets us dictate what additional action we want to perform when the return key is pressed. In our case we want the current active text field to be deactivated.

class DetailTableViewController: UITableViewController,UITextFieldDelegate{ 
.
.
.
}
    // MARK: - implement text field delegate function
func textFieldShouldReturn(_ textField: UITextField) -> Bool {
textField.resignFirstResponder()
return true
}

Conclusion/ GIT

Oh man, this project is something else! Don’t get me wrong, I’m totally vibing with the process. But wow, I’ve thrown in every trick I’ve got to build this app, and it feels so dang satisfying!

--

--