frzi / swiftui-router

Path-based routing in SwiftUI
MIT License
900 stars 43 forks source link

Type safe routes #50

Closed denizdogan closed 2 years ago

denizdogan commented 2 years ago

Currently, I'm doing this:

enum AppRoutes {
    static let splash = "/"
    static let login = "/login"
    static let feed = "/feed"
}

SwitchRoutes {
    Route(AppRoutes.splash) {
        SplashScreenView()
    }
    Route(AppRoutes.feed) {
        FeedView()
    }
    ...
}

Is there any way to make this properly type safe, or at least safer? It seems rather fragile to rely on strings in this manner. Ideally, I wouldn't want to define my own "route enum" and have navigator.navigate accept only that type, etc.

frzi commented 2 years ago

You could make an enum and extend the Route, NavLink and Navigator to accept this enum.

As an example:

enum AppRoute: String {
    case news
    case settings
    case login
}

extension Route where ValidatedData == RouteInformation {
    init(route: AppRoute, @ViewBuilder content: @escaping (ValidatedData) -> Content) {
        self.init(route.rawValue, content: content)
    }
}

extension Navigator {
    func navigate(_ route: AppRoute) {
        self.navigate(route.rawValue)
    }
}

extension NavLink {
    init(to route: AppRoute, replace: Bool = false, exact: Bool = false, @ViewBuilder content: @escaping (Bool) -> Content) {
        self.init(to: route.rawValue, replace: replace, exact: exact, content: content)
    }
}

navigator.navigate(to: .news)

You can now use type safe routes.

Things become a bit more complicated when you want to work with parameters/placeholders however. But with Swift's enum having support for associated values you could probably come up with some wicked things:

enum AppRoute {
    case news(id: UUID? = nil)
    case settings
    case login

    var path: String {
        switch self {
        case .news(let id): return "/news/\(id?.uuidString ?? "")"
        case .settings: return "/settings"
        case .login: return "/login"
        }
    }
}

navigator.navigate(to: .news(id: someUUID))
denizdogan commented 2 years ago

@frzi Makes sense, thank you!

denizdogan commented 2 years ago

@frzi I can't seem to figure out what the Route init would look like when using enum AppRoute with associated values on some of the cases. Am I right in suspecting that it's not simple?

frzi commented 2 years ago

You'll probably want the paths for Routes to be formed differently. As an example:

enum AppRoute {
    case news(id: UUID? = nil)

    // Read this property in `NavLink` and `Navigator.navigate()`
    var path: String {
        switch self {
        case .news(let id): return "/news/\(id?.uuidString ?? "")"
        }
    }

    // Read this property in `Route`
    var route: String {
        switch self {
        case .news(let id): return "/news/\(id?.uuidString ?? ":id?")"
        }
    }
}

Route(.news()) { /* ... */ } // For `/news/:id?`
Route(.news(someConstantUUID)) { /* ... */ } // For `/news/edc54f97-ff00-463a-8897-f75144e41b7b` or something

With a bit of imagination I'm sure you'll be able to come up with a solution that fits your needs perfectly. 😄