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:
- UIKit
- SwiftUI
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")
}
struct ItemSelectorView: View {
private let recipeId: String
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
- UIKit
- SwiftUI
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)
}
)
)
// TODO
CatalogPackageRowParameters
- UIKit
- SwiftUI
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)
}
)
)
// TODO
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:
- UIKit
- SwiftUI
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)
}
}
// TODO
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
- UIKit
- SwiftUI
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)
}
)
),
// TODO
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.
- UIKit
- SwiftUI
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)
}
)
)
// TODO
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.
- UIKit
- SwiftUI
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)
}
)
)
// TODO
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
- UIKit
- SwiftUI
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)
}
)
),
// TODO
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.