Building a restaurant iOS App

iamsimranjot

Simranjot

Posted on December 29, 2021

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.

💖 💪 🙅 🚩
iamsimranjot
Simranjot

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