SwiftTypedRouter
Strongly typed routing for SwiftUI
Goals of this library
- Swift is strongly typed. How strongly typed can we make a routing library?
- Single location to customise routing
- Routing should be customisable even when the code running is in another library (i.e. if we have some shared code we should be able to configure it's flow)
Installation
It's a cocoapod. Add pod 'SwiftTypedRouter'
to your podfile. See https://cocoapods.org for more information. Check out the example app in this repo to see this in action as a pod.
You can also just check it out and copy the files into your app if you like.
Carthage / SPM support are issues - feel free to grab them and make a PR ;)
Basic use
Setup
On startup (probably in your SceneDelegate
) register routes with a Router
let router = Router()
router.add("home") { HomeView() }
router.add("product/list/:cat/:num") { (category: String, page: Int) in
ProductListView(category, page)
}
router.add("product/details/:id") { (id: String) in
ProductDetailsView(id: id)
}
And make the router accessible to your views
yourFirstView
.withRouter(router)
Usage
Given a path, a router will return a View (or it's own 404 view if it can't find a matching one)
NavigationLink(destination: router.view("product/list/hats/0")) {
Text("See a list of Hats")
}
You can also check ahead of time whether the router will match a path or not. NB: This is faster than checking view(_:) != nil
if router.canMatch("product/list/hats/0") {
NavigationLink(destination: router.view("product/list/hats/0")) {
Text("See a list of Hats")
}
} else {
Text("No Hats For You")
}
Advanced Usage
The basic usage passes around a lot of strings. That's fine, but you can use the router with a more strongly typed set of values.
Template
Calling router.add("product/details/:id")
is, behind the scenes, making a Template
type using the path passed in and the parameters of the action block. You can make a Template
yourself using a TemplateFactory
.
extension Template {
static let productDetails = TemplateFactory.start().path("product", "details").placeholder("id", String.self).template()
}
And you can use it when adding an action to the router
// Old
// router.add("product/details/:id") { (id: String) in
// With template
router.add(Template.productDetails) { id in
ProductDetailsView(id: id)
}
So far, there isn't that much benefit.
You can also use the template to create paths to pass into the router - these will then be type-safe.
extension Path {
static func productDetails(id: String) -> Path { Template.productDetails.path(id) }
}
Now, when you use the router to find a view you would do this:
// Old
// NavigationLink(destination: router.view("product/details/123456")) {
NavigationLink(destination: router.view(.productDetails(id: "123456"))) {
Text("See Product Details")
}
It's a bit more typing to set up, but as you're only ever typing in the path once the compiler does all the sanity error checking for you.
Aliases
Sometimes, you want to configure your app on a more granular level i.e. you want product/details/123 to go to a product details screen, but you want to override the destination of specific ui elements.
If you have access to the code of the source view then this is trivial - just edit the file :) However, if your view is in a library you might want to configure multiple apps to have different behaviours. Aliases allow you to do that.
An alias redirects a path to another path, with a richer context.
For example, if we have a + button on a screen containing a list of products, we would do this:
- (optional) Create a context type (this can be
Void
if you want)
struct ProductListAliasContext {
let category: MyCategoryType
}
- Create an Alias - it's identifier can be anything you want, but make it unique
let productListPlusTapAlias = Alias<ProductListAliasContext>("product.list.plus.tap")
- Configure your router to redirect that alias to the correct path
router.alias(productListPlusTapAlias) { context in
if context.category == "hats" {
return Path.specialHatHandlingView
} else {
return Path.normalProductAddView
}
}
- Use the alias instead of the path directly in the view
var context: ProductListAliasContext { ProductListAliasContext(category: self.categpory) }
...
NavigationLink(destination: router.view(productListPlusTapAlias, context: context))) {
Text("+")
}
NB If you don't want a context, define your alias as Alias<Void>("some.identifier")
and you don't need to pass a context parameter around.