Skip to main content
Version: 5.2

Custom Navigation

Steps

1. Create Files & ViewControllers/Pages

When beginning the integration, we advise creating all the pages first to make the navigation implementation easier. Each page will have navigation actions (navigate to new page, go back, etc), so having all the ViewControllers or Pages prebuilt will save you some headache. The files are as follows:

project/

└── Miam/
├── MealPlanner/
│ ├── MealPlannerCTAViewController (or MealPlannerCTAViewPage, etc)
│ ├── MealPlannerFormViewController
│ ├── MealPlannerResultsViewController
│ ├── MealPlannerRecipePickerViewController
│ ├── MealPlannerBasketViewController
│ └── MealPlannerRecapViewController
// The below should already be implemented if you have added Catalog Feature
├── Basket/
│ └── ItemSelectorViewController
├── Catalog/
│ └── FiltersViewController
└── General/
├── SponsorDetailsViewController
└── RecipeDetailsViewController

With these ViewControllers, its important to know that some pages will need parameters. For example:

class MealPlannerRecipePickerViewController: UIViewController {
private let indexOfRecipe: Int

init(_ indexOfRecipe: Int) {
self.indexOfRecipe = indexOfRecipe
super.init(nibName: nil, bundle: nil)
}

required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}

Here's a list of the Pages with the parameters needed

MealPlannerRecipePickerViewController( indexOfRecipe: Int )
FiltersViewController( filterInstance: FilterInstance, isForMealPlanner: Bool = false ) // We implement this to make navigation easier
RecipeDetailsViewController( recipeId: String, isForMealPlanner: Bool = false ) // We implement this for the guest count
SponsorDetailsViewController( sponsor: Sponsor )
ItemSelectorViewController( recipeId: String )

The other Pages do NOT need parameters

2. Implement MealPlannerCTA

First things first, you'll need a Call To Action to launch the Meal Planner. You can create your own button, or implement our protocol MealPlannerCTA. MealPlannerCTA expects a navigation function that takes you to the MealPlannerForm.

MealPlannerCTA

@available(iOS 14, *)
public protocol MealPlannerCTAProtocol {
associatedtype Content: View
@ViewBuilder func content(
onTapGesture: @escaping () -> Void
) -> Content
}
MiamNeutralMealPlannerCallToAction() { [weak self] in
guard let strongSelf = self else { return }
strongSelf.navigationController?.pushViewController(MealPlannerFormViewController(), animated: true)
}

CatalogView

Additionally, the CatalogView has infrastructure to support launching the Meal Planner. To add the MiamNeutral CTA, or your custom CTA, it is quite simple.

First, you'll need to add a callback to the onLaunchMealPlanner parameter in the CatalogParameters. For example:

...
onPreferencesTapped: /* your existing code */,
onLaunchMealPlanner: {
navigationController?.pushViewController(MealPlannerFormViewController(), animated: true)
},
onMealsInBasketButtonTapped: /* your existing code */,

Next, you'll need to update the viewOptions of the CatalogParameters to support the Meal Planner.

...
onMealsInBasketButtonTapped: /* your existing code */,
viewOptions: CatalogViewOptions(useMealPlanner: true)

To add your custom CTA, you can ensure that it implements MealPlannerCTAProtocol & pass it into the viewOptions.

...
onMealsInBasketButtonTapped: /* your existing code */,
viewOptions: CatalogViewOptions(
useMealPlanner: true,
mealPlannerCTA: TypeSafeMealPlannerCTA(YourCustomCTA())
)

3. Implement MealPlannerForm

After the Call To Action, implement the MealPlannerForm Page. MealPlannerForm expects params: MealPlannerFormParameters.

MealPlannerFormParameters

MealPlannerFormParameters(
onNavigateToMealPlannerResults: { [weak self] recipes in
guard let strongSelf = self else { return }
strongSelf.navigationController?.pushViewController(MealPlannerResultsViewController(), animated: true)
}))

Putting it all together

Now we have all the parameters we need for the CatalogSearch.

MealPlannerForm(
params: /* MealPlannerFormParameters we just defined */
)

4. Implement MealPlannerResults

MealPlannerResults expects params: MealPlannerResultsParameters where you can pass in the default MealPlannerResultsParameters. The MealPlannerResultsParameters expects navigation functions & has optional viewOptions for customizing views.

The last parameter is the gridConfig, which sets the recipe dimensions, & spacing. It is an instance of MealPlannerRecipesListGridConfig.

CatalogParameters

MealPlannerResultsParameters(
onShowRecipeDetails: { [weak self] recipeId in
guard let strongSelf = self else { return }
strongSelf.navigationController?.pushViewController(RecipeDetailsViewController(recipeId, isForMealPlanner: true), animated: true)
},
onOpenReplaceRecipe: { [weak self] indexOfRecipe in
guard let strongSelf = self else { return }
strongSelf.navigationController?.pushViewController(MealPlannerRecipePickerViewController(indexOfRecipe), animated: true)
},
onNavigateToBasket: {[weak self] in
guard let strongSelf = self else { return }
strongSelf.navigationController?.pushViewController(MealPlannerBasketViewController(), animated: true)
}),

MealPlannerRecipesListGridConfig

MealPlannerRecipesListGridConfig(
spacing: CGSize(width: 0, height: 0),
recipeCardDimensions: CGSize(width: 300, height: 200)))

Putting it all together

Now we have all the parameters we need for the MealPlannerResults. Here is an integration example:

MealPlannerResults(
params: /* MealPlannerResultsParameters we just defined */,
gridConfig: /* MealPlannerRecipesListGridConfig we just defined */)

5. Implement MealPlannerRecipePicker

MealPlannerRecipePicker expects params: MealPlannerRecipePickerParameters where you can pass in the default MealPlannerRecipePickerParameters. The MealPlannerRecipePickerParameters expects navigation functions & has optional viewOptions for customizing views.

The next parameter is the gridConfig, which sets the number of columns. recipe dimensions, & spacing. It is an instance of CatalogRecipesListGridConfig.

The last parameter is the indexOfReplacedRecipe, which will set the recipe that will be replaced upon being selected. For example, if the user selects the second recipe in the MealPlannerResults to replace, this recipe will replaced by the new recipe.

You will get this Int from the MealPlannerResults page.

PreferencesParameters

PreferencesParameters(
onClosed: { [weak self] in
guard let strongSelf = self else { return }
strongSelf.navigationController?.popViewController(animated: true)
},
onGoToSearch: { [weak self] in
guard let strongSelf = self else { return }
strongSelf.navigationController?.pushViewController(PreferencesSearchViewController(), animated: true)
})

CatalogRecipesListGridConfig

If you have already integrated the Catalog Feature, you can reuse the one you've already created for the CatalogView & CatalogResults.

Of course, if you'd like this page to look different, you can define a new one like below:

CatalogRecipesListGridConfig(
numberOfColumns: 2,
spacing: CGSize(width: 6, height: 6),
recipeCardDimensions: CGSize(width: 300, height: 340),
recipeCardFillMaxWidth: true)

Putting it all together

Now we have all the parameters we need for the MealPlannerRecipePicker.

MealPlannerRecipePicker(
params: /* the MealPlannerRecipePickerParameters we just made */,
gridConfig: /* the CatalogRecipesListGridConfig we just made */,
indexOfReplacedRecipe: indexOfRecipe)

6. Implement Filters

Filters expects a FiltersInstance object. Filters also expects params: FiltersParameters.

Additionally, if you plan on adding Meal Planner Feature, Filters will also be used. The only change is that Filters should be popped instead of navigating to the CatalogResults

FiltersParameters

This object is fairly straightforward, however, because we want to pop the Filters page off the stack as we navigate, there is a little logic.

FiltersParameters(
actions: FiltersAction(
onApplied: { [weak self] in
guard let strongSelf = self else { return }
if strongSelf.isForMealPlanner { // just pop VC if using Meal Planner
strongSelf.navigationController?.popViewController(animated: true)
} else {
// this is overly complex so that when the user taps the apply button,
// the next return will take them to Catalog, instead of back to filters
guard let viewA = strongSelf.navigationController?.viewControllers.first else { return }
let viewB = CatalogResultsViewController()
strongSelf.navigationController?.setViewControllers([viewA, viewB], animated: true)
}
}, onClosed: { [weak self] in
guard let strongSelf = self else { return }
strongSelf.navigationController?.popViewController(animated: true)
}
)
)

Putting it all together

Now we have all the parameters we need for the Filters.

FiltersView(
params: /* the FiltersParameters we just made */,
filterInstance: filterInstance /* the callback navigating to this view will provide this */
)

7. Implement RecipeDetails

The RecipeDetails Page expects a recipeId string. RecipeDetails also expects params: RecipeDetailParameters.

Additionally, RecipeDetails has an optional parameter isForMealPlanner that defaults to false. If you are not implementing Meal Planner now or in the future, you can ignore this.

RecipeDetailParameters

RecipeDetailParameters(
actions: RecipeDetailsActions(
onClosed: { [weak self] in
guard let strongSelf = self else { return }
strongSelf.navigationController?.popViewController(animated: true)
},
onSponsorDetailsTapped: { [weak self] sponsor in
guard let strongSelf = self else { return }
strongSelf.navigationController?.pushViewController(SponsorDetailsViewController(sponsor: sponsor), animated: true)
},
onContinueToBasket: { [weak self] in // this is ignored if the Recipe is already in the basket
guard let strongSelf = self else { return }
strongSelf.navigationController?.pushViewController(MyMealsViewController(), animated: true)
}
)
),

Putting it all together

Now we have all the parameters we need for the RecipeDetails.

RecipeDetails(
params: /* the RecipeDetailParameters we just made */,
recipeId: recipeId /* the callback navigating to this view will provide this */,
isForMealPlanner: isForMealPlanner // defaults to false
)

The default Miam Neutral Recipe Details Footer will NOT show the Call To Action if the item is already in the basket. If you customize your footer, make sure you implement this same functionality.

8. Implement SponsorDetails

After the RecipeDetails, implement the SponsorDetails Page. SponsorDetails expects a sponsor object. SponsorDetails also expects params: SponsorDetailsParameters.

SponsorDetailsParameters

SponsorDetailsParameters does not expect navigation functions, so you don't need to implement anything unless you add custom views.

Putting it all together

Now we have all the parameters we need for the SponsorDetails.

SponsorDetails(
params: SponsorDetailsParameters() /* default */,
sponsor: sponsor /* the callback navigating to this view will provide this */
)

9. Implement MealPlannerBasket

MealPlannerBasket expects params: MealPlannerBasketParameters & basketRecipesParams: BasketRecipeParameters. It also expects a gridConfig: BasketRecipesGridConfig.

Both BasketRecipeParameters & BasketRecipesGridConfig are also expected from MyMeals from the Catalog Feature, so you can reuse these objects for this view too.

MealPlannerBasketParameters

MealPlannerBasketParameters(
onNavigateToRecap: { [weak self] in
guard let strongSelf = self else { return }
strongSelf.navigationController?.pushViewController(MealPlannerRecapPurchaseViewController(), animated: true)
},
onNavigateToBasket: { [weak self] in
// navigate to your Basket
}),

BasketRecipeParameters

Again, this BasketRecipeParameters is the same as MyMeals. However, RecipeDetails should accept isForMealPlanner: true when it is called from the MealPlannerBasket.

BasketRecipeParameters(
onReplaceProduct: { [weak self] recipeId in
guard let strongSelf = self else { return }
strongSelf.navigationController?.pushViewController(ItemSelectorViewController(recipeId), animated: true)
},
onShowRecipeDetails: { [weak self] recipeId in
guard let strongSelf = self else { return }
strongSelf.navigationController?.pushViewController(RecipeDetailsViewController(recipeId, isForMealPlanner: true), animated: true)
})

So for example, you could have an object like this in UIKit to pass into both ViewControllers:

public func sharedBasketRecipeParameters(navigationController: UINavigationController?, isForMealPlanner: Bool) -> BasketRecipeParameters {
return BasketRecipeParameters(
onReplaceProduct: { recipeId in
navigationController?.pushViewController(ItemSelectorViewController(recipeId), animated: true)
},
onShowRecipeDetails: { recipeId in
navigationController?.pushViewController(RecipeDetailsViewController(recipeId, isForMealPlanner: isForMealPlanner), animated: true)
}
)
}

BasketRecipesGridConfig

Again, this BasketRecipesGridConfig is the same as MyMeals.

BasketRecipesGridConfig(
recipeSpacing: CGSize(width: 5, height: 5),
productSpacing: CGSize(width: 6, height: 6),
recipeOverviewDimensions: CGSize(width: 300, height: 150),
isExpandable: true)

Putting it all together

Now we have all the parameters we need for the MealPlannerBasket.

MealPlannerBasket(
params: /* the RecipeDetailParameters we just made */,
basketRecipesParams: /* the BasketRecipeParameters we just made */,
gridConfig: /* the BasketRecipesGridConfig we just made */

10. Implement ItemSelector

ItemSelector expects a recipeId string. ItemSelector also expects params: ItemSelectorParameters.

ItemSelectorParameters

ItemSelectorParameters(
actions: ItemSelectorActions(
onItemSelected: { [weak self] in
guard let strongSelf = self else { return }
strongSelf.navigationController?.popViewController(animated: true)
}
)
)

Putting it all together

Now we have all the parameters we need for the ItemSelector.

ItemSelector(
params: /* the ItemSelectorParameters we just made */,
recipeId: recipeId /* the callback navigating to this view will provide this */
)

11. Implement MealPlannerRecap

MealPlannerRecap expects params: MealPlannerRecapParameters.

MealPlannerRecapParameters

MealPlannerRecapParameters(
onNavigateAwayFromMealPlanner: { [weak self] in
guard let strongSelf = self else { return }
strongSelf.navigationController?.popToRootViewController(animated: true)
// or to your Basket, Catalog Feature, etc
})

Putting it all together

Now we have all the parameters we need for the MealPlannerRecap.

MealPlannerRecap(
params: /* the MealPlannerRecapParameters we just made */
)