View on GitHub

recipepuppy

A proof of concept app by using the free to use RecipePuppy API

Building a Recipes Book

The challenge

This challenge should be done by using the free to use RecipePuppy API. We would like you to retrieve some recipes from there, display the recipes and perform certain operations on those recipes. Hereby the details:

  1. API connection, you should use their search endpoint and perform recipe searches with one or multiple ingredients (ie: http://www.recipepuppy.com/api/?i=onions,garlic&p=1) and parse the results. We would like you to use the networking tools iOS provides and not an external library.
  2. Use a searchbar as user input for the first point and show the results in a collection view with a layout like the following. Each recipe should show the image on top, the recipe name, ingredients (this one could have multiple lines so the layouts should support dynamic heights) and a label in a 45% angle that would show only if it contains lactose (to simplify consider that only milk and cheese contain lactose).

  1. Add pagination to the list whenever the user scrolls,this should be as seamless as possible.
  2. Each recipe has an href parameter that is an URL pointing to a website with the recipe details. Whenever the user clicks on a recipe use this parameter to open the website in a new view without leaving the app.
  3. Offline functionality, each recipe should have a favorite button and clicking it should save the full recipe offline. Create a separate screen and a way to access it to show the favorite recipes.

Implicit requiments of the task

Development Story

Initial API Analysis

For the analysis of the API has been used the vscode extension Rest Client and the app Insomnia

Optional Parameters:

GET http://www.recipepuppy.com/api/?i=onions,garlic&p=2

Example Result

{
  "title": "Recipe Puppy",
  "version": 0.1,
  "href": "http:\/\/www.recipepuppy.com\/",
  "results": [ 
        {
            "title": "Roasted Garlic Grilling Sauce \r\n\t\t\r\n\t\r\n\t\t\r\n\t\r\n\t\t\r\n\t\r\n\t\r\n\r\n",
            "href": "http:\/\/www.kraftfoods.com\/kf\/recipes\/roasted-garlic-grilling-sauce-56344.aspx",
            "ingredients": "garlic, onions, hot sauce",
            "thumbnail": "http:\/\/img.recipepuppy.com\/634118.jpg"
        }
   ]
}

Observations

    {
       "title": String,
        "href": String,
        "ingredients": String,
        "thumbnail": String,
    }

But, while asking for the 3 page, it produces results:

GET http://www.recipepuppy.com/api/?i=onions,garlic&q=&p=101

Documentation of the process

Project architecture (VIPER)

Why choosing to use it even if it’s not enforced?

PROS CONS
It’s used by many of the recent companies in the field Extra Boilerplate.
It’s seen as an improvement over MVVM. It would take some research time to master.
It could solve some of the problems I’m facing with MVVM. I may make several mistakes
Personal opportunity to learn with a hands on strategy. Some degree of uncertainty regarding being able to finish in time the task in hand

The takeaways:

UI (Snapkit)

Why choosing to use it over Storyboards?

PROS CONS
Less conflicts when merging code from several sources (team work) Need to add everything UI related manually
The intention is clear and readable at any moment No visual editor
Finding an offensive Autolayout rule is easier Needed to run the project to see the changes reflected and compare with desired result
The developer tends to think more about what it writes It takes more time
Easier to review in a Pull Request (team work) The file containing the view gets bigger

Networking

At the beginning, and as in many examples around the web, the Networking was placed inside the Interactor. This is not a good idea for big projects, or in general, it’s good to have a facade over it to allow changes in the API and separation of concerns.

For example, in this case, the API was not reliable enough, so, some considerations had to be made in order to remove that concern from the Interactor which intention is to obtain/process/prepare the data for the presenter.

typealias APIResponse = (Result<[RecipeData], Error>) -> Void
protocol APIServiceInterface {
    func fetchRecipes(ingredients: String, page: String, completionHandler: @escaping APIResponse)
}

Adding responsabilities of the Network layer in the Interactor was smelling really bad 😷, a really lightweight Services approach was used instead. Injecting the needed dependencies in every case making the code as easily testable and single responsability as posible.

Why lightweight? There are several examples of using Services in VIPER, going trough all the process of registering them in a ServicesBuilder, and it sounds awesome, but for only two services that I was planning to use in this constrained exercise, it was not worthed to add that complexity and to maintain those new classes and boilerplate needed.

protocol ServicesCatalog: class {
    var api: APIServiceInterface { get }
    var persistence: PersitenceServiceInterface { get }
}

In a project aiming to production, I see it as the way to go, to make Services and to register them via a centralized Injection. Or Services Builder as called in some places.

For this case, I used just a Factory for it, no rigid schemas of what a Service should include or extra added rules, those are better in other kind of projects.

final class ServiceFactory: ServicesCatalog {
    var api: APIServiceInterface { RecipeAPIService(session: URLSession.shared) }
    var persistence: PersitenceServiceInterface { LocalPersistenceService() }
}

The networking service handles:

The RecipesListInteractor handles then:

Data Persistence

Since the very beginning I was thinking on using Realm for Data Persistence. But, where’s the fun on it? Using CoreData is a good oportunity to showcase threading management and enjoy some nice headaches 🤕.

As it was intended only to save favorites, the entity Recipe would save the data needed for reconstructing the full object and the contents of the saved webpage. Maybe it would have be better to naming it as FavoritedRecipe.

The protocol defines some of the base use cases of a CRUD:

protocol PersitenceServiceInterface {
    func save(recipe: ModelRecipe) -> Bool
    func remove(recipe: ModelRecipe) -> Bool
    func loadAll() -> [ModelRecipe]
}

And includes, an extension for handling threading. That guarantees that all calls are made on the same thread, in an async way, with a high priority and that it returns with a completion handler executed in the Main Thread.

Maybe I could have moved this logic into an unique private func, working with generics and higher order functions. Something like:

extension PersitenceServiceInterface {
    private var persistenceThread: DispatchQueue { DispatchQueue.global(qos: DispatchQoS.QoSClass.userInitiated) }

    func save(recipe: ModelRecipe, completionHandler: @escaping (Bool) -> Void) {
        threadSafe(action: { self.save(recipe: recipe) }) { completionHandler($0) }
    }

    func remove(recipe: ModelRecipe, completionHandler: @escaping (Bool) -> Void) {
        threadSafe(action: { self.remove(recipe: recipe) }) { completionHandler($0) }
    }

    func loadAll(completionHandler: @escaping ([ModelRecipe]) -> Void) {
        threadSafe(action: { self.loadAll() }) { completionHandler($0) }
    }

    private func threadSafe<ResultType>(action: @escaping () -> ResultType, completionHandler: @escaping (ResultType) -> Void) {
        persistenceThread.async {
            let result = action()
            DispatchQueue.main.async {
                completionHandler(result)
            }
        }
    }
}

But this is something I just got out of my head now, from here the importance of revisiting code and refactor when needed. This reduces the amounts of duplicated code and hence, the chances of commiting errors, and, of course, is suuuper swifty 🚀

This service is injected via the constructor of the Interactor, as well as it’s done and explained before with the API Service. So, no need to repeat myself in this matter.

Other choices

There are other choices that I made but to explain it here it would be, maybe too obvious/tedious and I guess it would be wasting space and time, I’ll just point some of them out and I’m open to discuss them.

What would I have done better with more time

protocol RecipesListPresenterRouterInterface: PresenterRouterInterface {}

Final takeaway

I have learned a lot, and I feel like.. as that is always part of my objectives, that this is a total success.

I’ll continue working on this in my spare time, from time to time, as it can be a good point for mentoring, training, or just experiment with some crazy ideas. And, of course, those tests will be added ✅