Shakeology
Are you a cocktail lover tired of spending $15 to $20 per drink? Why not take control and get more for your money? Shakeology makes it easy to craft your own cocktails—even if you’ve never mixed one before!
My partner and I aren’t heavy drinkers, but we enjoy indulging in a couple of glasses now and then. At one point, I found myself thinking, “Is this really worth $15?” That’s when the idea struck: “I don’t know anything about mixing cocktails, but why not try making our own cocktails instead?”
This project came together in about 6 days, and I couldn’t be happier with the outcome. Creating this app was as enjoyable as mixing cocktails, and I’m thrilled to showcase it as a portfolio piece!
Main.
private func setUpVideoBackground() {
let path = Bundle.main.path(forResource: "background1", ofType: "mp4")
player = AVPlayer(url: URL(fileURLWithPath: path!))
player!.actionAtItemEnd = AVPlayer.ActionAtItemEnd.none
let playerLayer = AVPlayerLayer(player: player)
playerLayer.frame = self.view.frame
playerLayer.videoGravity = AVLayerVideoGravity.resizeAspectFill
self.player?.isMuted = true
self.view.layer.insertSublayer(playerLayer, at: 0)
NotificationCenter.default.addObserver(self, selector: #selector(playerItemDidReachEnd), name: NSNotification.Name.AVPlayerItemDidPlayToEndTime, object: player!.currentItem)
player!.seek(to: CMTime.zero)
player!.play()
}
@objc func playerItemDidReachEnd() {
player!.seek(to: CMTime.zero)
}
func configureLogoImageView() {
logoImageView.translatesAutoresizingMaskIntoConstraints = false
logoImageView.image = Images.shakeLogo
logoImageView.contentMode = .scaleAspectFit
NSLayoutConstraint.activate([
logoImageView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 100),
logoImageView.centerXAnchor.constraint(equalTo: view.centerXAnchor),
logoImageView.widthAnchor.constraint(equalToConstant: 300),
logoImageView.heightAnchor.constraint(equalToConstant: 200)
])
}
@objc func pushCocktailListVC() {
guard !isSearchButtonClicked else {
presentCTAlertOnMainThread(title: "Something went wrong", message: "Something happened. Please try again later", buttonTitle: "OK")
return
}
let cocktailListVC = CocktailListVC()
cocktailListVC.title = "Searching for drinks"
navigationController?.pushViewController(cocktailListVC, animated: true)
}
@objc func pushFavoriteListsVC() {
let favoriteListVC = FavoriteListVC()
favoriteListVC.title = "Favorite Drinks"
navigationController?.pushViewController(favoriteListVC, animated: true)
}
Search Cocktail List.
func configureCollectionView() {
collectionView = UICollectionView(frame: view.bounds, collectionViewLayout: UIHelper.createTwoColumnFlowLayout(in: view))
view.addSubview(collectionView)
collectionView.delegate = self
collectionView.backgroundColor = .systemBackground
collectionView.register(CocktailCell.self, forCellWithReuseIdentifier: CocktailCell.reuseID)
}
func configureSearchController() {
let searchController = UISearchController()
searchController.searchResultsUpdater = self
searchController.searchBar.placeholder = "Search for cocktail..."
searchController.obscuresBackgroundDuringPresentation = false
navigationItem.searchController = searchController
}
func displayCocktails() {
showLoadingView()
NetworkManager.shared.getCocktails() { [weak self] result in
guard let self = self else { return }
self.dismissLoadingView()
switch result {
case .success(let cocktails):
self.updateUI(with: cocktails)
case .failure(let error):
self.presentCTAlertOnMainThread(title: "Bad Stuff Happened", message: error.rawValue, buttonTitle: "OK")
}
}
}
func updateUI(with cocktails: [Cocktail]) {
self.cocktails.append(contentsOf: cocktails)
self.updateData(on: self.cocktails)
}
func configureDataSource() {
dataSource = UICollectionViewDiffableDataSource(collectionView: collectionView, cellProvider: { collectionView, indexPath, cocktail in
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: CocktailCell.reuseID, for: indexPath) as! CocktailCell
cell.set(cocktail: cocktail)
return cell
})
}
func updateData(on cocktails: [Cocktail]) {
var snapshot = NSDiffableDataSourceSnapshot()
snapshot.appendSections([.main])
snapshot.appendItems(cocktails)
DispatchQueue.main.async { self.dataSource.apply(snapshot, animatingDifferences: true) }
}
extension CocktailListVC: UICollectionViewDelegate {
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
let activeArray = isSearching ? filteredCocktails : cocktails
let cocktail = activeArray[indexPath.item]
let destVC = DrinkInfoVC(drinkID:cocktail.idDrink)
destVC.drinkID = cocktail.idDrink
let navController = UINavigationController(rootViewController: destVC)
present(navController, animated: true)
}
}
extension CocktailListVC: UISearchResultsUpdating {
func updateSearchResults(for searchController: UISearchController) {
guard let filter = searchController.searchBar.text, !filter.isEmpty else {
filteredCocktails.removeAll()
updateData(on: cocktails)
isSearching = false
return
}
isSearching = true
filteredCocktails = cocktails.filter { $0.strDrink.lowercased().contains(filter.lowercased()) }
updateData(on: filteredCocktails)
}
}
Quick Fun Fact
After finishing the project, I took another look at the cocktail list and thought it looked a bit tacky compared to the other screens. A new design popped into my mind, and I quickly iterated the UI.
Technically, the design was done inside the code 😎.
Ingredients +
Instructions.
func configureScrollView() {
view.addSubview(scrollView)
scrollView.addSubview(contentView)
scrollView.pinToEdges(of: view)
contentView.pinToEdges(of: scrollView)
NSLayoutConstraint.activate([
contentView.widthAnchor.constraint(equalTo: scrollView.widthAnchor),
contentView.heightAnchor.constraint(equalToConstant: 1200)
])
}
func getDrinkInfo() {
NetworkManager.shared.getDrinkInfo(for: drinkID) { [weak self] result in
guard let self = self else { return }
switch result {
case .success(let cocktail):
DispatchQueue.main.async { self.configureUIElements(with: cocktail) }
case .failure(let error):
self.presentCTAlertOnMainThread(title: "Something went wrong!!!", message: error.rawValue, buttonTitle: "OK")
}
}
}
func configureUIElements(with cocktail: DrinkInfo) {
self.add(childVC: CTDrinkInfoMainImageVC(drink: cocktail), to: self.headerView)
self.add(childVC: CTDrinkDescItemVC(drink: cocktail, delegate: self), to: self.itemViewOne)
}
func add(childVC: UIViewController, to containerView: UIView) {
addChild(childVC)
containerView.addSubview(childVC.view)
childVC.view.frame = containerView.bounds
childVC.didMove(toParent: self)
}
private func configureItems() {
descriptionInfoView.set(withDrink: drink.strDrink, withTag: drink.strTags ?? "", withCategory: drink.strCategory, withIBA: drink.strIBA ?? "")
ingredientButton.set(backgroundColor: .clear, title: "INGREDIENTS", titleColor: .systemPink)
instructionButton.set(backgroundColor: .clear, title: "INSTRUCTIONS", titleColor: .systemGray)
ingredientInfoView.setIngredient(withDescription: configureIngredientInfo())
instructionInfoView.setInstruction(withDescription: configureInstructionInfo())
}
func configureIngredientInfo() -> String {
var ingredientString = ""
ingredientString += "Prepare \(drink.strGlass)\n\n"
for (ingredient, measurement) in drink.ingredients {
ingredientString += "\u{2022} \(measurement ?? "") \(ingredient)\n"
}
return ingredientString
}
func configureInstructionInfo() -> String {
var instructionString = ""
let instructions = drink.strInstructions
instructionString += "Here is how you make \(drink.strDrink):\n\n"
let sentences = instructions.components(separatedBy: ". ")
for (index, sentence) in sentences.enumerated() {
instructionString += "\(index + 1). \(sentence)\n\n"
}
instructionString += "\nEnjoy your \(drink.strDrink) cocktail!"
return instructionString
}
override func ingredientButtonTapped() {
delegate.didTapIngredientButton(for: drink)
ingredientButton.set(backgroundColor: .clear, title: "INGREDIENTS", titleColor: .systemPink)
instructionButton.set(backgroundColor: .clear, title: "INSTRUCTIONS", titleColor: .systemGray)
ingredientInfoView.isHidden = false
instructionInfoView.isHidden = true
}
override func instructionButtonTapped() {
delegate.didTapInstructionButton(for: drink)
instructionButton.set(backgroundColor: .clear, title: "INSTRUCTIONS", titleColor: .systemPink)
ingredientButton.set(backgroundColor: .clear, title: "INGREDIENTS", titleColor: .systemGray)
ingredientInfoView.isHidden = true
instructionInfoView.isHidden = false
}
@objc func dismissVC() {
if let navigationController = navigationController {
if navigationController.viewControllers.count > 1 {
navigationController.popViewController(animated: true)
} else {
dismiss(animated: true)
}
} else {
dismiss(animated: true)
}
}
Favorite List.
func configureTableView() {
view.addSubview(tableView)
tableView.frame = view.bounds
tableView.rowHeight = 80
tableView.delegate = self
tableView.dataSource = self
tableView.removeExcessCells()
tableView.register(FavoriteCell.self, forCellReuseIdentifier: FavoriteCell.reuseID)
}
func getFavorites() {
PersistenceManager.retrieveFavorites { [weak self] result in
guard let self = self else { return }
switch result {
case .success(let favorites):
self.updateUI(with: favorites)
case .failure(let error):
self.presentCTAlertOnMainThread(title: "Something went wrong", message: error.rawValue, buttonTitle: "OK")
}
}
}
func updateUI(with favorites: [Cocktail]) {
if favorites.isEmpty {
self.showEmptyStateView(with: "No Favorites?\nAdd one on the Cocktail Screen.", in: self.view)
} else {
self.favorites = favorites
DispatchQueue.main.async {
self.tableView.reloadData()
self.view.bringSubviewToFront(self.tableView)
}
}
}
extension FavoriteListVC: UITableViewDataSource, UITableViewDelegate {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return favorites.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: FavoriteCell.reuseID) as! FavoriteCell
let favorite = favorites[indexPath.row]
cell.set(favorite: favorite)
return cell
}
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
let favorite = favorites[indexPath.row]
let destVC = DrinkInfoVC(drinkID: favorite.idDrink)
navigationController?.pushViewController(destVC, animated: true)
}
func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCell.EditingStyle, forRowAt indexPath: IndexPath) {
guard editingStyle == .delete else { return }
PersistenceManager.updateWith(favorite: favorites[indexPath.row], actionType: .remove) { [weak self] error in
guard let self = self else { return }
guard let error = error else {
self.favorites.remove(at: indexPath.row)
tableView.deleteRows(at: [indexPath], with: .left)
return
}
self.presentCTAlertOnMainThread(title: "Unable to remove", message: error.rawValue, buttonTitle: "OK")
}
}
}
Empty Favorites.
func updateUI(with favorites: [Cocktail]) {
if favorites.isEmpty {
self.showEmptyStateView(with: "No Favorites?\nAdd one on the Cocktail Screen.", in: self.view)
} else {
self.favorites = favorites
DispatchQueue.main.async {
self.tableView.reloadData()
self.view.bringSubviewToFront(self.tableView)
}
}
}
func showEmptyStateView(with message: String, in view: UIView) {
let emptyStateView = CTEmptyStateView(message: message)
emptyStateView.frame = view.bounds
view.addSubview(emptyStateView)
}
private func configureMessageLabel() {
messageLabel.numberOfLines = 3
messageLabel.textColor = .secondaryLabel
let labelCenterYConstant: CGFloat = DeviceTypes.isiPhoneSE || DeviceTypes.isiPhone8Zoomed ? -80 : -150
NSLayoutConstraint.activate([
messageLabel.centerYAnchor.constraint(equalTo: self.centerYAnchor, constant: labelCenterYConstant),
messageLabel.leadingAnchor.constraint(equalTo: self.leadingAnchor, constant: 40),
messageLabel.trailingAnchor.constraint(equalTo: self.trailingAnchor, constant: -40),
messageLabel.heightAnchor.constraint(equalToConstant: 200)
])
}