SwiftTypedRouter 0.1.1

SwiftTypedRouter 0.1.1

Maintained by Sam Dean.



  • By
  • deanWombourne

SwiftTypedRouter

Strongly typed routing for SwiftUI

CI Version License Platform

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:

  1. (optional) Create a context type (this can be Void if you want)
struct ProductListAliasContext {
  let category: MyCategoryType
}
  1. Create an Alias - it's identifier can be anything you want, but make it unique
let productListPlusTapAlias = Alias<ProductListAliasContext>("product.list.plus.tap")
  1. 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
  }
}
  1. 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.