Skip to main content
Version: 4.1

Custom Navigation

If you don't want to use the provided Mealz navigation, you can recreate the navigation yourself. We have documented a simple walkthrough, but feel free to implement your practice of Coordinators.

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/
├── Catalog/
│ ├── CatalogViewController (or CatalogViewPage, etc)
│ ├── CatalogResultsViewController
│ ├── FiltersViewController
│ ├── CatalogSearchViewController
│ ├── ? PreferencesViewController ?
│ └── ? PreferencesSearchViewController ?
├── Basket/
│ ├── MyMealsViewController
│ └── ItemSelectorViewController
└── General/
├── SponsorDetailsViewController
└── RecipeDetailsViewController

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

class ItemSelectorViewController: UIViewController {
private let recipeId: String

init(_ recipeId: String) {
self.recipeId = recipeId
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

CatalogResultsViewController( categoryId: String?, categoryTitle: String? )
CatalogSearchViewController( filterInstance: FilterInstance )
SponsorDetailsViewController( sponsor: Sponsor )
ItemSelectorViewController( recipeId: String )
// if you DON'T plan on adding Meal Planner Feature in the Future
FiltersViewController( filterInstance: FilterInstance )
RecipeDetailsViewController( recipeId: String )
// if you DO plan on adding Meal Planner Feature in the Future
FiltersViewController( filterInstance: FilterInstance, isForMealPlanner: Bool = false )
RecipeDetailsViewController( recipeId: String, isForMealPlanner: Bool = false )

The other Pages do NOT need parameters

2. Implement CatalogView

After preparing the files, the next step is the landing page, the CatalogView. Each page has their own custom parameters, titled the Page name followed by "Parameters." For example, CatalogView expects params: CatalogParameters where you can pass in the default CatalogParameters. The CatalogParameters expects navigation functions & has optional viewOptions for customizing views.

Additionally, CatalogView expects catalogPackageRowParams: CatalogPackageRowParameters which also expects navigation functions & view options.

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

CatalogParameters

CatalogParameters(
actions: CatalogActions(
onFiltersTapped: { [weak self] filterInstance in
guard let strongSelf = self else { return }
strongSelf.navigationController?.pushViewController(FiltersViewController(filterInstance), animated: true)
},
onSearchTapped: { [weak self] filterInstance in
guard let strongSelf = self else { return }
strongSelf.navigationController?.pushViewController(CatalogSearchViewController(filterInstance), animated: true)
},
onFavoritesTapped: { [weak self] in
guard let strongSelf = self else { return }
strongSelf.navigationController?.pushViewController(CatalogResultsViewController(), animated: true)
},
onPreferencesTapped: { [weak self] in // if Preferences isn't implemented, just leave an empty callback
guard let strongSelf = self else { return }
strongSelf.navigationController?.pushViewController(PreferencesViewController(), animated: true)
},
onMealsInBasketButtonTapped: { [weak self] in
guard let strongSelf = self else { return }
strongSelf.navigationController?.pushViewController(MyMealsViewController(), animated: true)
}
)
)

CatalogPackageRowParameters

CatalogPackageRowParameters(
actions: CatalogPackageRowActions(
onSeeAllRecipes: { [weak self] categoryId, categoryTitle in
guard let strongSelf = self else { return }
strongSelf.navigationController?.pushViewController(
CatalogResultsViewController(
categoryId,
categoryTitle: categoryTitle
), animated: true)
},
onShowRecipeDetails: { [weak self] recipeId in
guard let strongSelf = self else { return }
strongSelf.navigationController?.pushViewController(RecipeDetailsViewController(recipeId), animated: true)
}, onRecipeCallToActionTapped: { [weak self] recipeId in
guard let strongSelf = self else { return }
strongSelf.navigationController?.pushViewController(MyMealsViewController(), animated: true)
}
)
)

CatalogRecipesListGridConfig

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 CatalogView. Here is an integration example:

However, because CatalogView is a SwiftUI view, if you are using UIKit, it can get a bit more complicated. We need to wrap the view in a UIHostingController & set the bounds. This will need to be done on all of our views in a swiftUI view.

class CatalogViewController: UIViewController {
var swiftUIView: CatalogView<
CatalogParameters,
CatalogPackageRowParameters> {
return CatalogView.init(
params: /* the CatalogParameters we just made */,
catalogPackageRowParams: /* the CatalogPackageRowParameters we just made */,
gridConfig: /* the CatalogRecipesListGridConfig we just made */
)
}
// The hosting controller for your SwiftUI view
private var hostingController: UIHostingController<CatalogView<
CatalogParameters,
CatalogPackageRowParameters>>?

override func viewDidLoad() {
super.viewDidLoad()
// Initialize the hosting controller with your SwiftUI view
hostingController = UIHostingController(rootView: swiftUIView)
guard let hostingController = hostingController, let hcView = hostingController.view
else { return }
// Since hostingController is optional, using guard to safely unwrap its view
hcView.translatesAutoresizingMaskIntoConstraints = false
addChild(hostingController)
view.addSubview(hcView)
NSLayoutConstraint.activate([ // you can also use Snapkit to set the constraints
hcView.topAnchor.constraint(equalTo: view.topAnchor),
hcView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
hcView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
hcView.trailingAnchor.constraint(equalTo: view.trailingAnchor)
])
hostingController.didMove(toParent: self)
}
}

3. Implement CatalogResults

After the CatalogView, you can implement the CatalogResults Page. CatalogResults can optionally take a categoryId & categoryTitle. CatalogResults expects params: CatalogParameters, the same CatalogParameters as the CatalogView.

Additionally, CatalogResults expects recipesListParams: CatalogRecipesListParameters which also expects navigation functions & view options.

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

CatalogParameters

In our demo applications, we use the same instance of CatalogParameters to share between CatalogView & CatalogResults. However, that is just our implementation, & for simplicity, you can refer to the above CatalogParameters for reference.

CatalogRecipesListParameters

CatalogRecipesListParameters(
actions: CatalogRecipesListActions(
onNoResultsRedirect: { [weak self] in
guard let strongSelf = self else { return }
strongSelf.navigationController?.popViewController(animated: true)
},
onShowRecipeDetails: { [weak self] recipeId in
guard let strongSelf = self else { return }
strongSelf.navigationController?.pushViewController(RecipeDetailsViewController(recipeId), animated: true)
},
onRecipeCallToActionTapped: { [weak self] recipeId in
guard let strongSelf = self else { return }
strongSelf.navigationController?.pushViewController(MyMealsViewController(), animated: true)
}
)
),

CatalogRecipesListGridConfig

In our demo applications, we use the same instance of CatalogRecipesListGridConfig to share between CatalogView & CatalogResults. However, that is just our implementation, & for simplicity, you can refer to the above CatalogRecipesListGridConfig for reference.

Putting it all together

Now we have all the parameters we need for the CatalogResults. The final product will look the same as the above CatalogView, so refer to that for the UIHostingController implementation in UIKit.

CatalogResults(
params: /* the CatalogParameters we just made */,
recipesListParams: /* the CatalogRecipesListParameters we just made */,
categoryId: categoryId /* an optional String? */,
title: categoryTitle /* an optional String? */,
gridConfig: /* the CatalogRecipesListGridConfig we just made */
)

4. 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 */
)

5. Implement CatalogSearch

After the Filters, implement the CatalogSearch Page. CatalogSearch expects a FiltersInstance object. CatalogSearch also expects params: CatalogSearchParameters.

CatalogSearchParameters

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

CatalogSearchParameters(
actions: CatalogSearchActions(
onApplied: { [weak self] in
// this is overly complex so that when the user taps the apply button,
// the next "back button" press will take them to Catalog, instead of back to CatalogSearch
guard let strongSelf = self, let viewA = self?.navigationController?.viewControllers.first else { return }
let viewB = CatalogResultsViewController()
strongSelf.navigationController?.setViewControllers([viewA, viewB], animated: true)
}
)
)

Putting it all together

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

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

6. 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.

7. 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 */
)

8. Implement MyMeals

MyMeals expects params: MyMealsParameters & basketRecipesParams: BasketRecipeParameters. It also expects a gridConfig: BasketRecipesGridConfig.

MyMealsParameters

For the CatalogFeature, the onNoResultsRedirect can just redirect to the view that called it. However, if MyMeals is a standalone page, this should navigate to the Catalog Feature when there are no recipes here.

MyMealsParameters(
actions: MyMealsActions(
onNoResultsRedirect: { [weak self] in
guard let strongSelf = self else { return }
strongSelf.navigationController?.popViewController(animated: true)
}
)
)

BasketRecipeParameters

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), animated: true)
})

BasketRecipesGridConfig

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 MyMeals.

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

9. 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 */
)

10. Implement Preferences - Optional

Optionally, you can implement the Preferences Page. Preferences expects params: PreferencesParameters.

Ensure that you have added the correct navigation function onPreferencesTapped in the CatalogParameters.

PreferencesParameters

PreferencesParameters(
actions: PreferencesActions(
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)
}
)
)

Putting it all together

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

Preferences(
params: /* the PreferencesParameters we just made */
)

11. Implement PreferencesSearch - Optional

After the Preferences, you can implement the PreferencesSearch Page. PreferencesSearch expects params: PreferencesSearchParameters.

PreferencesSearchParameters

PreferencesSearchParameters(
actions: PreferencesSearch(
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 PreferenceSearchs.

PreferencesSearch(
params: /* the PreferencesSearchParameters we just made */
)

Customization

If you would like to customize your components, such as the Background, Loader, or RecipeCard, you can read our documentation here.

Next Steps

After integrating Catalog, you have the bulk of the Miam functionality in your iOS application. However, we also have other features & standalones pages. You can integrate a standalone MyMeals page, Favorites page, or our full feature Meal Planner.