DEV Community

Simranjot for Canonic Inc.

Posted on

Building a restaurant iOS App

We are building a simple food ordering iOS Application with Swift 5! It would look something like this after we build it:

Image description

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 simple TableViewController. 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, this viewController 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 and Storyboard as our Interface
  • Click Next and create your project!

Image description

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 
Enter fullscreen mode Exit fullscreen mode

Image description

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 to Codable 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 to Codable 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 to Codable 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:

    Image description

Step 3: Setup Restaurant Listings

Create a two new Cocoa Touch Classes:

  • RestaurantListingsViewController of type UIViewController to hold our UI
  • RestaurantTableViewCell of type UITableViewCell to customise our TableCell

Image description

Head over to the Main.storyboard file where we'll create our view of our controller.

  • Assign RestaurantListingsViewController class we just created to a new UIViewController
  • Embed our RestaurantListingsViewController in an UINavigationController
  • 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

Image description

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() } } 
Enter fullscreen mode Exit fullscreen mode

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 } } } 
Enter fullscreen mode Exit fullscreen mode
// 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 } } 
Enter fullscreen mode Exit fullscreen mode
// 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]) } } 
Enter fullscreen mode Exit fullscreen mode
// 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() } } } } 
Enter fullscreen mode Exit fullscreen mode

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 } } } } 
Enter fullscreen mode Exit fullscreen mode

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.

Image description

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.

Image description

Replace the URL you got in our networking code (fetchRestaurants function) in our RestaurantListingsViewController file, run the app and it will show you this:

Image description

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 type UIViewController to hold our UI
  • RestaurantMenuTableViewCell of type UITableViewCell to customise our TableCell

Our folder structure will be looking something like this:

Image description

Head over to the Main.storyboard file where we'll create our view of our controller.

  • Assign RestaurantMenuViewController class we just created to a new UIViewController
  • 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

Image description

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() } } 
Enter fullscreen mode Exit fullscreen mode

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; } } } 
Enter fullscreen mode Exit fullscreen mode
// 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)) } } } 
Enter fullscreen mode Exit fullscreen mode
// 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) } } 
Enter fullscreen mode Exit fullscreen mode
// 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() } } 
Enter fullscreen mode Exit fullscreen mode
// 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 } } } } } 
Enter fullscreen mode Exit fullscreen mode

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 } } 
Enter fullscreen mode Exit fullscreen mode

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) } 
Enter fullscreen mode Exit fullscreen mode

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.

Image description

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.

Top comments (2)

Collapse
 
christhomas412 profile image
christhomas412

"Exciting venture! If you need any insights or assistance during the development of your restaurant iOS app,Qiuck Price feel free to reach out. Best of luck with your project!

Collapse
 
tsevelyna profile image
Evelyn Adams

It seems to me that in the era of covid and restrictions, it is very difficult for restaurants and similar establishments to even survive, let alone develop. But as for me, there is such a cool and convenient thing as barcode menu. It allows you to scan and order food contactless, a great thing in these times.

Some comments may only be visible to logged-in visitors. Sign in to view all comments.