Building a restaurant iOS App
Simranjot
Posted on December 29, 2021
We are building a simple food ordering iOS Application with Swift 5! It would look something like this after we build it:
Let's quickly go through a rough architecture of the application. We'll have two ViewControllers in our application:
-
RestaurantListingsViewController
This will be the home
viewController
of the app. This is where the user will land after opening the app. It will fetch the restaurants data from our backend and display it in a simpleTableViewController
. When the user will click on any one of them, we'll take him to the menu screen of the restaurant. RestaurantMenuViewController
After clicking on any one of the restaurants from our home scene, thisviewController
will fetch the menu items for it, let the user add them to his cart and place an order!
We'll be using Alamofire networking library to help us communicate with our Backend easily and help us focus on building the actual app.
Step 1: Create & Setup a new Project
Open Xcode and click on Create a new Xcode Project.
- Select the
App
option - Enter the name: Restaurant App
- We'll be using
Swift
as our language andStoryboard
as our Interface - Click
Next
and create your project!
To add Alamofire, we'll be using Swift Package Manager. Xcode comes with built-in support for it and we'll not have to use external package managers like Cocoapods or Carthage. Click on File → Add Packages...
Enter the following Package URLs in the search bar and add: Alamofire & AlamofireImage
https://github.com/Alamofire/AlamofireImage.git
https://github.com/Alamofire/Alamofire.git
Our project is set and ready to be built, let's dive in 🏃♂️
Step 2: Setup Data Modals and Helpers
Before creating any of our views and controllers, we'll set up our data modals that will hold the data that will be used inside our viewControllers
. We'll create three modal files:
-
Restaurant.swift
This will a
struct
confirming toCodable
protocol that will hold the data for our restaurants. When we'll fetch all the restaurants, the data coming in from the backend will be decoded into this struct.
// // Restaurant.swift // Restaurant App // import Foundation struct Restaurants: Codable { let restaurants: [Restaurant] enum CodingKeys: String, CodingKey { case restaurants = "data" } } struct Restaurant: Codable { var id, name, description: String let brandImage: BrandImage? enum CodingKeys: String, CodingKey { case id = "_id" case name, description, brandImage } init() { id = "" name = "" description = "" brandImage = BrandImage() } init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) id = try container.decode(String.self, forKey: .id) name = try container.decode(String.self, forKey: .name) description = try container.decode(String.self, forKey: .description) brandImage = try container.decodeIfPresent(BrandImage.self, forKey: .brandImage) } } struct BrandImage: Codable { let url, alt, name: String? enum CodingKeys: String, CodingKey { case url, alt, name } init() { url = "" alt = "" name = "" } init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) url = try container.decodeIfPresent(String.self, forKey: .url) alt = try container.decodeIfPresent(String.self, forKey: .alt) name = try container.decodeIfPresent(String.self, forKey: .name) } }
-
Menu.swift
This will a
struct
confirming toCodable
protocol that will hold the data for our menu items. When we'll fetch the menu items for any particular restaurant, the data coming in from the backend will be decoded into this struct.
// // Menu.swift // Restaurant App // import Foundation struct Menus: Codable { let menus: [Menu] enum CodingKeys: String, CodingKey { case menus = "data" } } struct Menu: Codable { var id, itemName, description, price: String let itemImage: ItemImage? enum CodingKeys: String, CodingKey { case id = "_id" case itemName, description, itemImage, price } init() { itemName = "" description = "" price = "" id = "" itemImage = ItemImage() } init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) id = try container.decode(String.self, forKey: .id) itemName = try container.decode(String.self, forKey: .itemName) description = try container.decode(String.self, forKey: .description) itemImage = try container.decodeIfPresent(ItemImage.self, forKey: .itemImage) price = try container.decode(String.self, forKey: .price) } } struct ItemImage: Codable { let url, alt, name: String? enum CodingKeys: String, CodingKey { case url, alt, name } init() { url = "" alt = "" name = "" } init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) url = try container.decodeIfPresent(String.self, forKey: .url) alt = try container.decodeIfPresent(String.self, forKey: .alt) name = try container.decodeIfPresent(String.self, forKey: .name) } }
-
Order.swift
This will a
struct
confirming toCodable
protocol that will hold the details of the order the user is placing. We'll send this data to our backend when the user places his confirmed order.
// // Order.swift // Restaurant App import Foundation struct Order: Codable { var restaurant: String var totalAmount: String var orderItems: [OrderItems] init() { restaurant = "" totalAmount = "" orderItems = [] } } struct OrderItems: Codable { var item: String var quantity: String }
Constants & Extensions
-
UIViewController+Extensions.swift
We'll be using a helper function to display alerts. We'll place it the
UIViewController+Extensions.swift
file.
// // UIViewController+Extensions.swift // Restaurant App // import UIKit extension UIViewController { func presentAlert(withTitle title: String, message : String) { let alertController = UIAlertController(title: title, message: message, preferredStyle: .alert) let OKAction = UIAlertAction(title: "OK", style: .default) { action in print("You've pressed OK Button") } alertController.addAction(OKAction) self.present(alertController, animated: true, completion: nil) } }
-
Constants.swift
This will hold StoryboardIdentifiers for our ViewControllers, ReuseIdentifiers for our UITableView Cells and some helper enums!
// // Constants.swift // Restaurant App // import Foundation struct StoryBoardID { static let RestaurantListingsViewController = "RestaurantListingsViewController" static let RestaurantMenuViewController = "RestaurantMenuViewController" } struct CellIdentifiers { static let RestaurantTableViewCell = "RestaurantTableViewCell" static let RestaurantMenuTableViewCell = "RestaurantMenuTableViewCell" } enum NetworkState { case success case loading case failure }
Our folder structure would look something like this after the setup:
Step 3: Setup Restaurant Listings
Create a two new Cocoa Touch Classes:
-
RestaurantListingsViewController
of typeUIViewController
to hold our UI -
RestaurantTableViewCell
of typeUITableViewCell
to customise our TableCell
Head over to the Main.storyboard
file where we'll create our view of our controller.
- Assign
RestaurantListingsViewController
class we just created to a newUIViewController
- Embed our
RestaurantListingsViewController
in anUINavigationController
- Configure the UI
- Loading State UI (When the API call is in progress)
- Retry State UI (When the API call fails)
-
RestaurantsTableView
(When the API call succeeds and data is displayed) - Create a prototype cell and assign it the
RestaurantTableViewCell
class
You can see the view setup on the interface in the sample project repository here
Head over to RestaurantListingsViewController
and add the following code. We are going to:
- Create the outlets and connect them with our interface
- Add some variables to hold data for us
//
// RestaurantListingsViewController.swift
// Restaurant-App
//
import UIKit
import Alamofire
class RestaurantListingsViewController: UIViewController {
var restaurants: [Restaurant] = [] // It'll have the array of all the restaurant items coming from the backend
var fetchState: NetworkState = .loading // It'll have the latest network state of our view controller to control what to show
@IBOutlet weak var retryButton: UIButton!
@IBOutlet weak var retryStackView: UIStackView!
@IBOutlet weak var loadingStackView: UIStackView!
@IBOutlet weak var restaurantsTableView: UITableView!
override func viewDidLoad() {
super.viewDidLoad()
title = "Restaurants"
setupRetryButtton()
setupTableView()
updateViewState()
fetchRestaurants()
}
}
Add the following extensions
below your class to setup our viewController
:
// MARK: View Setup
extension RestaurantListingsViewController {
// Sets up the retry button
fileprivate func setupRetryButtton() {
retryButton.layer.cornerRadius = 8
retryButton.configuration?.contentInsets = NSDirectionalEdgeInsets(top: 10, leading: 20, bottom: 10, trailing: 20)
}
// Sets up the tableView
fileprivate func setupTableView() {
restaurantsTableView.backgroundColor = .clear
restaurantsTableView.tableFooterView = UIView()
}
// Helps to show the correct UI state
fileprivate func updateViewState() {
switch fetchState {
case .success:
retryStackView.isHidden = true
loadingStackView.isHidden = true
restaurantsTableView.isHidden = false
break;
case .loading:
retryStackView.isHidden = true
loadingStackView.isHidden = false
restaurantsTableView.isHidden = true
case .failure:
retryStackView.isHidden = false
restaurantsTableView.isHidden = true
loadingStackView.isHidden = true
break
}
}
}
// MARK: Action Helpers
extension RestaurantListingsViewController {
// Triggers the Restaurant Fetching API call again
@IBAction func retryTapped(_ sender: Any) {
fetchRestaurants()
updateViewState()
}
// Updates the TableView after the API Call
fileprivate func postRestaurantFetchActions() {
switch fetchState {
case .success:
restaurantsTableView.reloadSections(IndexSet(integer: 0), with: .bottom)
break
default:
break
}
updateViewState()
}
// Shows the menu items for the selected restaurant
fileprivate func pushToRestaurantMenuVC(for restaurant: Restaurant) {
// Push to Menu Items
}
}
// MARK: TableView Delegates
extension RestaurantListingsViewController: UITableViewDelegate, UITableViewDataSource {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return restaurants.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: CellIdentifiers.RestaurantTableViewCell, for: indexPath) as! RestaurantTableViewCell
cell.configureCell(for: restaurants[indexPath.row])
return cell
}
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
tableView.deselectRow(at: indexPath, animated: true)
pushToRestaurantMenuVC(for: restaurants[indexPath.row])
}
}
// MARK: Networking
extension RestaurantListingsViewController {
fileprivate func fetchRestaurants() {
fetchState = .loading
AF.request("YOUR API URL")
.validate()
.responseDecodable(of: Restaurants.self) { [weak self] (response) in
guard let availableRestaurants = response.value else { return }
self?.restaurants = availableRestaurants.restaurants
self?.fetchState = (response.error != nil) ? .failure : .success
DispatchQueue.main.async {
self?.postRestaurantFetchActions()
}
}
}
}
Head over to RestaurantTableViewCell
and add the following code to setup our custom cell:
//
// RestaurantTableViewCell.swift
// Restaurant App
//
import UIKit
import Alamofire
import AlamofireImage
class RestaurantTableViewCell: UITableViewCell {
@IBOutlet weak var brandImageView: UIImageView!
@IBOutlet weak var titleLabel: UILabel!
@IBOutlet weak var descriptionLabel: UILabel!
override func awakeFromNib() {
super.awakeFromNib()
brandImageView.contentMode = .scaleToFill
}
func configureCell(for restaurant: Restaurant) {
titleLabel.text = restaurant.name
descriptionLabel.text = restaurant.description
guard let image = restaurant.brandImage else { return }
guard let urlString = image.url else { return }
let request = URLRequest(url: URL(string: urlString)!)
AF.request(request).responseImage { response in
if case .success(let image) = response.result {
self.brandImageView.image = image
}
}
}
}
Step 4: Setup Backend
Let's head to Canonic and find the Restaurant App
sample project from the Marketplace. You can either:
- Use this sample project to and continue, or
- Clone it and Deploy 🚀 . This will then use your data from your own project.
Head on to the Docs and copy the /restaurants
endpoint of the Restaurant Table. This is the Get API that will fetch us the data from the database.
Replace the URL you got in our networking code (fetchRestaurants
function) in our RestaurantListingsViewController
file, run the app and it will show you this:
Step 4: Setup Restaurant Menu View
We'll follow a very similar pattern with this viewController
also. Create a two new Cocoa Touch Classes:
-
RestaurantMenuViewController
****of typeUIViewController
to hold our UI -
RestaurantMenuTableViewCell
of typeUITableViewCell
to customise our TableCell
Our folder structure will be looking something like this:
Head over to the Main.storyboard
file where we'll create our view of our controller.
- Assign
RestaurantMenuViewController
class we just created to a newUIViewController
- Configure the UI
- Loading State UI (When the API call is in progress)
- Retry State UI (When the API call fails)
- Place Order Button
-
RestaurantsTableView
(When the API call succeeds and data is displayed) - Create a prototype cell and assign it the
RestaurantMenuTableViewCell
class
You can see the view setup on the interface in the sample project repository here
Head over to RestaurantMenuViewController
and add the following code. We are going to:
- Create the outlets and connect them with our interface
- Add some variables to hold data for us
//
// RestaurantMenuViewController.swift
// Restaurant App
//
import UIKit
import Alamofire
// Protocol that our custom cell will confoirm to. It'll be triggered everytime the quantity changes of any of the items
protocol OrderUpdates {
func updateOrder( menuItem: inout Menu, quantity: Int)
}
class RestaurantMenuViewController: UIViewController {
var restaurant: Restaurant!
var menuItems: [Menu] = []
var orderSummary = Order()
var fetchState: NetworkState = .loading
@IBOutlet weak var retryButton: UIButton!
@IBOutlet weak var retryStackView: UIStackView!
@IBOutlet weak var loadingStackView: UIStackView!
@IBOutlet weak var menuTableView: UITableView!
@IBOutlet weak var placeOrderButton: UIButton!
override func viewDidLoad() {
super.viewDidLoad()
title = restaurant.name
orderSummary.restaurant = restaurant.id
setupRetryButtton()
setupTableView()
updateViewState()
updatePlaceOrderButton()
fetchMenu()
}
}
Add the following extensions
below your class to setup our viewController
:
// MARK: View Setup
extension RestaurantMenuViewController {
// Sets up the retry button
fileprivate func setupRetryButtton() {
retryButton.layer.cornerRadius = 8
retryButton.configuration?.contentInsets = NSDirectionalEdgeInsets(top: 10, leading: 20, bottom: 10, trailing: 20)
}
// Sets up the tableView
fileprivate func setupTableView() {
menuTableView.backgroundColor = .clear
menuTableView.tableFooterView = UIView()
menuTableView.contentInset = UIEdgeInsets(top: 0, left: 0, bottom: 100, right: 0)
}
// Helps to show the correct UI state
fileprivate func updateViewState() {
switch fetchState {
case .success:
retryStackView.isHidden = true
loadingStackView.isHidden = true
menuTableView.isHidden = false
break;
case .loading:
retryStackView.isHidden = true
loadingStackView.isHidden = false
menuTableView.isHidden = true
break;
case .failure:
retryStackView.isHidden = false
menuTableView.isHidden = true
loadingStackView.isHidden = true
break;
}
}
}
// MARK: Action Helpers
extension RestaurantMenuViewController {
// Triggers the Menu Fetching API call again
@IBAction func retryTapped(_ sender: Any) {
fetchMenu()
updateViewState()
}
// Configures the data and makes and API to the backend to place the order
@IBAction func placeOrderClicked(_ sender: Any) {
let jsonData = try! JSONEncoder().encode(orderSummary)
let json = try? JSONSerialization.jsonObject(with: jsonData, options: [.topLevelDictionaryAssumed]) as? [String: Any]
let parameters = ["input": json]
placeOrder(parameters: parameters as [String : Any])
}
// Updated the Place Order Button with correct amount after the user adds or removes any items
fileprivate func updatePlaceOrderButton() {
let amount = getTotalAmount()
placeOrderButton.setTitle("Place Order: $\(amount)", for: .normal)
placeOrderButton.setTitle("Place Order", for: .disabled)
placeOrderButton.isHidden = !(amount > 0.0)
}
// Updates the TableView after the API Call
fileprivate func postMenuFetchActions() {
switch fetchState {
case .success:
menuTableView.reloadSections(IndexSet(integer: 0), with: .bottom)
break
default:
break
}
updateViewState()
}
// Heleper function to get the price of an menu item
fileprivate func getItemPrice(itemId: String) -> Double {
let item = menuItems.first { item in
item.id == itemId
}
return (Double(item!.price) ?? 0.0)
}
// Heleper function to get the total that amounts up after adding the items
fileprivate func getTotalAmount() -> Double {
return orderSummary.orderItems.reduce(0.0) { result, orderItem in
result + (getItemPrice(itemId: orderItem.item) * (Double(orderItem.quantity) ?? 0.0))
}
}
}
// MARK: TableView Delegates
extension RestaurantMenuViewController: UITableViewDelegate, UITableViewDataSource {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return menuItems.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: CellIdentifiers.RestaurantMenuTableViewCell, for: indexPath) as! RestaurantMenuTableViewCell
cell.delegate = self
cell.selectionStyle = .none
cell.configureCell(for: menuItems[indexPath.row])
return cell
}
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
tableView.deselectRow(at: indexPath, animated: true)
}
}
// MARK: Order Update Delegates
extension RestaurantMenuViewController: OrderUpdates {
internal func updateOrder( menuItem: inout Menu, quantity: Int) {
if let row = orderSummary.orderItems.firstIndex(where: {$0.item == menuItem.id}) {
if (quantity == 0) {
orderSummary.orderItems.remove(at: row)
} else {
orderSummary.orderItems[row].quantity = "\(quantity)"
}
} else {
orderSummary.orderItems.append(OrderItems(item: menuItem.id, quantity: "\(quantity)"))
}
orderSummary.totalAmount = "\(getTotalAmount())"
updatePlaceOrderButton()
}
}
// MARK: Networking
extension RestaurantMenuViewController {
fileprivate func fetchMenu() {
fetchState = .loading
AF.request("https://restaurant-app.can.canonic.dev/api/menus")
.validate()
.responseDecodable(of: Menus.self) { [weak self] (response) in
guard let availableMenus = response.value else { return }
self?.menuItems = availableMenus.menus
self?.fetchState = (response.error != nil) ? .failure : .success
DispatchQueue.main.async {
self?.postMenuFetchActions()
}
}
}
fileprivate func placeOrder(parameters: [String: Any]) {
fetchState = .loading
updateViewState()
AF.request("https://restaurant-app.can.canonic.dev/api/orders", method: .post, parameters: parameters as Parameters, encoding: JSONEncoding.default)
.responseData { [weak self] response in
if ((response.value) != nil) {
DispatchQueue.main.async {
self?.presentAlert(withTitle: "Order Placed 🥳", message: "Sit Tight, your order has been received!")
self?.orderSummary = Order()
self?.orderSummary.restaurant = (self?.restaurant.id)!
self?.updatePlaceOrderButton()
self?.fetchState = .success
self?.updateViewState()
self?.menuTableView.reloadSections(IndexSet(integer: 0), with: .bottom)
}
} else {
DispatchQueue.main.async {
self?.presentAlert(withTitle: "Oops!", message: "Something went wrong, try again later!")
self?.updateViewState()
self?.fetchState = .failure
}
}
}
}
}
Head over to RestaurantMenuTableViewCell
and add the following code to setup our custom cell:
//
// RestaurantMenuTableViewCell.swift
// Restaurant App
//
import UIKit
import Alamofire
import AlamofireImage
class RestaurantMenuTableViewCell: UITableViewCell {
@IBOutlet weak var itemImageView: UIImageView!
@IBOutlet weak var titleLabel: UILabel!
@IBOutlet weak var descriptionLabel: UILabel!
@IBOutlet weak var priceLabel: UILabel!
@IBOutlet weak var quatityLabel: UILabel!
@IBOutlet weak var stepper: UIStepper!
var menuItem: Menu = Menu()
var delegate: OrderUpdates?
override func awakeFromNib() {
super.awakeFromNib()
itemImageView.contentMode = .scaleToFill
}
@IBAction func stepperClicked(_ sender: UIStepper) {
quatityLabel.text = "x\(String(format: "%.0f", sender.value))"
delegate?.updateOrder(menuItem: &menuItem, quantity: Int(sender.value))
}
func configureCell(for menu: Menu) {
menuItem = menu
titleLabel.text = menu.itemName
descriptionLabel.text = menu.description
priceLabel.text = "$\(menu.price)"
quatityLabel.text = "x\(String(format: "%.0f", stepper.value))"
guard let image = menu.itemImage else { return }
guard let urlString = image.url else { return }
let request = URLRequest(url: URL(string: urlString.addingPercentEncoding(withAllowedCharacters: CharacterSet.urlQueryAllowed)!)!)
AF.request(request).responseImage { response in
if case .success(let image) = response.result {
self.itemImageView.image = image
}
}
}
override func prepareForReuse() {
super.prepareForReuse()
itemImageView.image = nil
}
}
Step 5: Take the user to the Menu Listing
Now both of our viewControllers
are set. Let's add the code to push the user to the menu listings when the user clicks on any restaurant. Head to RestaurantListingsViewController
and update the pushToRestaurantMenuVC
function:
fileprivate func pushToRestaurantMenuVC(for restaurant: Restaurant) {
let restaurantMenuVC = UIStoryboard.init(name: "Main", bundle: Bundle.main).instantiateViewController(withIdentifier: StoryBoardID.RestaurantMenuViewController) as! RestaurantMenuViewController
restaurantMenuVC.restaurant = restaurant
self.navigationController?.pushViewController(restaurantMenuVC, animated: true)
}
Build and run your app to see it in action 🥳 Now when you click on the it'll fetch the menu items of the restaurant and let you place your order.
And with that, you have successfully made a basic Food Ordering iOS App for your project. 💃🕺
Congratulations! 🎉
If you want, you can also duplicate this project from Canonic's sample app and easily get started by customizing it as per your experience. Check it out app.canonic.dev.
You can also check out our other guides here.
Join us on discord to discuss or share with our community. Write to us for any support requests at support@canonic.dev. Check out our website to know more about Canonic.
Posted on December 29, 2021
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.
Related
November 29, 2024