A bidirectional router with more type safety and less fuss.

Overview

swift-url-routing

A bidirectional URL router with more type safety and less fuss. This library is built with Parsing.


Learn More

This library was discussed in an episode of Point-Free, a video series exploring functional programming and the Swift programming and the Swift language, hosted by Brandon Williams and Stephen Celis.

video poster image

Motivation

URL routing is a ubiquitous problem in both client-side and server-side applications:

  • Clients, such as iOS applications, need to route URLs for deep-linking, which amounts to picking apart a URL in order to figure out where to navigate the user in the app.
  • Servers, such as Vapor applications, also need to pick apart URL requests to figure out what page to serve, but also need to generate valid URLs for linking within the website.

This library provides URL routing function for both client and server applications, and does so in a composable, type-safe manner.

Getting Started

To use the library you first begin with a domain modeling exercise. You model a route enum that represents each URL you want to recognize in your application, and each case of the enum holds the data you want to extract from the URL.

For example, if we had screens in our Books application that represent showing all books, showing a particular book, and searching books, we can model this as an enum:

enum AppRoute {
  case books
  case book(id: Int)
  case searchBooks(query: String, count: Int = 10)
}

Notice that we only encode the data we want to extract from the URL in these cases. There are no details of where this data lives in the URL, such as whether it comes from path parameters, query parameters or POST body data.

Those details are determined by the router, which can be constructed with the tools shipped in this library. Its purpose is to transform an incoming URL into the AppRoute type. For example:

import URLRouting

let appRouter = OneOf {
  // GET /books
  Route(.case(AppRoute.books))) {
    Path { "books" }
  }

  // GET /books/:id
  Route(.case(AppRoute.books(id:))) {
    Path { "books"; Digits() }
  }

  // GET /books/search?query=:query&count=:count
  Route(.case(AppRoute.searchBooks(query:count:))) {
    Path { "books"; "search" }
    Query {
      Field("query")
      Field("count", default: 10) { Digits() }
    }
  }
}

This router describes at a high-level how to pick apart the path components, query parameters, and more from a URL in order to transform it into an AppRoute.

Once this router is defined you can use it to implement deep-linking logic in your application. You can implement a single function that accepts a URL, use the router's match method to transform it into an AppRoute, and then switch on the route to handle each deep link destination:

func handleDeepLink(url: URL) throws {
  switch try appRouter.match(url: url) {
  case .books:
    // navigate to books screen

  case let .book(id: id):
    // navigate to book with id

  case let .searchBooks(query: query, count: count):
    // navigate to search screen with query and count
  }
}

This kind of routing is incredibly useful in client side iOS applications, but it can also be used in server-side applications. Even better, it can automatically transform AppRoute values back into URL's which is handy for linking to various parts of your website:

appRoute.path(for: .searchBooks(query: "Blob Bio"))
// "/books/search?query=Blob%20Bio"
Node.ul(
  books.map { book in
    .li(
      .a(
        .href(appRoute.path(for: .book(id: book.id))),
        book.title
      )
    )
  }
)
<ul>
  <li><a href="/books/1">Blob Autobiography</a></li>
  <li><a href="/books/2">Blobbed around the world</a></li>
  <li><a href="/books/3">Blob's guide to success</a></li>
</ul>

For Vapor bindings to URL Routing, see the Vapor Routing package.

Documentation

The documentation for releases and main are available here:

License

This library is released under the MIT license. See LICENSE for details.

Comments
  • Query Sorting

    Query Sorting

    This addresses an issue i've came across by using a list of query items with the following format:

    ?key[0]=a&key[1]=b&key[2]=c
    

    As soon as the list grows to 10+ elements the result looks like this:

    ?key[0]=a&key[10]=z&key[1]=b
    

    Since certain backends require a specific order of the query items especially when it comes to list indices it's probably a good idea to use a different comparison function for the sorting.

    I've introduced a comparator on URLQueryItem which is basically just an alias for a string comparison function that respects number values. It can be reused across the library to get a consistent sorting behavior.

    opened by fonkadelic 8
  • Adds URLRequestOption

    Adds URLRequestOption

    Allows the framework consumer to attach (on a per request basis) "options" to requests which can be accessed inside the client.

    For example, you might want to build a network client which supports advanced features such as de-duplication, throttling, caching, authentication, retrying. Typically these features would require some kind of configuration, for example, perhaps not all requests should be cached, or the retry strategy might be different for some endpoints. Therefore, we require a mechanism to specify request options to each Route, which we can retrieve inside the client. This is inspired by SwiftUI's EnvironmentValues, and from Dave de Long's blog post on Request Options.

    First, framework consumers would define an option, lets say we want our client to cache response on a per request basis, we could define an option as follow:

    enum CacheOption: URLRequestOption {
      static var defaultValue: Self = .always
      case always, never
    }
    
    enum ThrottleOption: URLRequestOption {
      static var defaultValue: Self = .always
      case always, never
    }
    

    The protocol, URLRequestOption is provided by the URLRouting library. Similar to how we use EnvironmentValues in SwiftUI, we can provide a convenience accessor:

    extension ParserPrinter where Input == URLRequestData {
        func cacheOption(for route: Output) -> CacheOption {
            option(CacheOption.self, for: route)
        }
    
        func throttleOption(for route: Output) -> ThrottleOption {
            option(ThrottleOption.self, for: route)
        }
    }
    

    Inside our router, we can then specify any options, e.g.

    static let router = OneOf {
      Route(.case(.dashboard)) {
        Path { "dashboard" }
        Options {
            CacheOption.never
            ThrottleOption.never
        }
      }
    }
    

    Finally, inside a custom URLClient, we can now query the value of this option for every request.

    extension URLRoutingClient {
        public static func connection<R: ParserPrinter>(
            router: R,
            session: URLSession = .shared,
            decoder: JSONDecoder = .init()
        ) -> Self
        where R.Input == URLRequestData, R.Output == Route {
            Self.init(
                request: { route in
                    // Check request options as part of fetching this route
                    let cacheOption = router.cacheOption(for: route)
    
                    // etc
                },
                decoder: decoder
            )
        }
    }
    

    This PR provides the mechanism to specify user-defined options as part of a Route, and then access them later via the router, or URLRequestData. It does not provide any built in URLRequestOption types, nor does it consume any inside the default URLClient which ships with the framework.

    It is intended that framework consumers would likely define their own Route options for their own purposes, and so would likely have their own network stack/client.

    Very happy to take any feedback or thoughts on this - especially how it works! And of course, the naming. Also, if there is a way for me to achieve this without changing the current framework - that would be great too. I have already tried composing URLRequestData inside my own URLRequest container which handles the options. This does not work very well, because ultimately, we need to specify the options as part of the Route.

    opened by danthorpe 5
  • Runtime errors when testing

    Runtime errors when testing

    Hi,

    I did have these runtime errors when trying to do some testing.

    I will try to find a fix them and update this later.

    Error raise: caught error: "L’opération n’a pas pu s’achever. (URLRouting.URLRoutingDecodingError erreur 1.)"

      func testAppBuildFetch() async throws {
        let api = URLRoutingClient<BitriseRoute>.live
        let output = try await api.request(.apps(.app("33134ce06fcbde63", .builds(.build("7ca6e226-8e30-4921-8afa-e20b397cc338", .fetch)))), as: AppBuildFetchWrappedResponse.self)
      }
    

    Error raise: caught error: "L’opération n’a pas pu s’achever. (Parsing.PrintingError erreur 1.)"

      func testAppsFetch() async throws {
        let route : BitriseRoute = .apps(.fetch())
        let printedRoute = try bitriseRouter.print(route)
        XCTAssertEqual(URLRequest(data: printedRoute)?.url?.absoluteString, "/apps")
      }
    
    Rest of the library code (tap to open)
    import Foundation
    import URLRouting
    
    public let bitriseRequestData = URLRequestData(
      scheme: "https",
      host: "api.bitrise.io",
      path: "v0.1",
      headers: [
        "Authorization": ["SuperSecretToken"],
        "Accept" : ["application/json"]
      ]
    )
    import Foundation
    import URLRouting
    
    extension URLRoutingClient where Route == BitriseRoute {
      @available(macOS 10.15, iOS 13, watchOS 6, tvOS 13, *)
      public static var live: URLRoutingClient<BitriseRoute> = .live(router: bitriseRouter.baseRequestData(bitriseRequestData))
    }
    import Foundation
    
    public struct AppBranchesFetchResponse: Codable {
      public let message: String?
      public let data: [String]?
    }
    import Foundation
    
    public struct AppBuildFetchDataResponse: Codable {
      public let triggeredAt: Date?
      public let startedOnWorkerAt: Date?
      public let environmentPrepareFinishedAt: Date?
      public let finishedAt: Date?
      public let slug: String?
      public let status: Int?
      public let statusText: String?
      public let abortReason: String?
      public let isOnHold: Bool?
      public let isProcessed: Bool?
      public let isStatusSent: Bool?
      public let branch: String?
      public let buildNumber: Int?
      public let commitHash: String?
      public let commitMessage: String?
      public let tag: String?
      public let triggeredWorkflow: String?
      public let triggeredBy: String?
      public let machineTypeID: String?
      public let stackIdentifier: String?
      public let originalBuildParams: OriginalBuildParams?
      public let pullRequestID: Int?
      public let pullRequestTargetBranch: String?
      public let pullRequestViewURL: String?
      public let commitViewURL: String?
      public let creditCost: Int?
      
      public enum CodingKeys: String, CodingKey {
        case triggeredAt = "triggered_at"
        case startedOnWorkerAt = "started_on_worker_at"
        case environmentPrepareFinishedAt = "environment_prepare_finished_at"
        case finishedAt = "finished_at"
        case slug
        case status
        case statusText = "status_text"
        case abortReason = "abort_reason"
        case isOnHold = "is_on_hold"
        case isProcessed = "is_processed"
        case isStatusSent = "is_status_sent"
        case branch
        case buildNumber = "build_number"
        case commitHash = "commit_hash"
        case commitMessage = "commit_message"
        case tag
        case triggeredWorkflow = "triggered_workflow"
        case triggeredBy = "triggered_by"
        case machineTypeID = "machine_type_id"
        case stackIdentifier = "stack_identifier"
        case originalBuildParams = "original_build_params"
        case pullRequestID = "pull_request_id"
        case pullRequestTargetBranch = "pull_request_target_branch"
        case pullRequestViewURL = "pull_request_view_url"
        case commitViewURL = "commit_view_url"
        case creditCost = "credit_cost"
      }
    
      public init(triggeredAt: Date?, startedOnWorkerAt: Date?, environmentPrepareFinishedAt: Date?, finishedAt: Date?, slug: String?, status: Int?, statusText: String?, abortReason: String?, isOnHold: Bool?, isProcessed: Bool?, isStatusSent: Bool?, branch: String?, buildNumber: Int?, commitHash: String?, commitMessage: String?, tag: String?, triggeredWorkflow: String?, triggeredBy: String?, machineTypeID: String?, stackIdentifier: String?, originalBuildParams: OriginalBuildParams?, pullRequestID: Int?, pullRequestTargetBranch: String?, pullRequestViewURL: String?, commitViewURL: String?, creditCost: Int?) {
        self.triggeredAt = triggeredAt
        self.startedOnWorkerAt = startedOnWorkerAt
        self.environmentPrepareFinishedAt = environmentPrepareFinishedAt
        self.finishedAt = finishedAt
        self.slug = slug
        self.status = status
        self.statusText = statusText
        self.abortReason = abortReason
        self.isOnHold = isOnHold
        self.isProcessed = isProcessed
        self.isStatusSent = isStatusSent
        self.branch = branch
        self.buildNumber = buildNumber
        self.commitHash = commitHash
        self.commitMessage = commitMessage
        self.tag = tag
        self.triggeredWorkflow = triggeredWorkflow
        self.triggeredBy = triggeredBy
        self.machineTypeID = machineTypeID
        self.stackIdentifier = stackIdentifier
        self.originalBuildParams = originalBuildParams
        self.pullRequestID = pullRequestID
        self.pullRequestTargetBranch = pullRequestTargetBranch
        self.pullRequestViewURL = pullRequestViewURL
        self.commitViewURL = commitViewURL
        self.creditCost = creditCost
      }  
    }
    import Foundation
    
    // MARK: - AppBuildFetchWrappedResponse
    public struct AppBuildFetchWrappedResponse: Codable {
      public let message: String?
      public let data: AppBuildFetchDataResponse?
      
      public init(message: String?, data: AppBuildFetchDataResponse?) {
        self.message = message
        self.data = data
      }
    }
    import Foundation
    
    public struct AppBuildWorkflowsFetchResponse: Codable {
      public let message: String?
      public let data: [String]?
    }
    import Foundation
    
    // MARK: - AppBuildsFetchResponse
    public struct AppBuildsFetchResponse: Codable {
      public let message: String?
      public let data: [AppBuildFetchDataResponse]?
      public let paging: Paging?
      
      public init(message: String?, data: [AppBuildFetchDataResponse]?, paging: Paging?) {
        self.message = message
        self.data = data
        self.paging = paging
      }
    }
    import Foundation
    
    // MARK: - AppFetchResponse
    public struct AppFetchResponse: Codable {
      public let message: String?
      public let data: AppFetchResponse.AppFetchDataResponse?
      
      public init(message: String?, data: AppFetchResponse.AppFetchDataResponse?) {
        self.message = message
        self.data = data
      }
      
      public struct AppFetchDataResponse: Codable {
        public let slug: String
        public let title: String
        public let projectType: String
        public let provider: String
        public let repoOwner: String
        public let repoURL: String
        public let repoSlug: String
        public let isDisabled: Bool
        public let status: Int
        public let isPublic: String
        public let isGithubChecksEnabled: Bool
        public let owner: Owner
        public let avatarURL: String?
        
        public enum CodingKeys: String, CodingKey {
          case slug, title
          case projectType = "project_type"
          case provider
          case repoOwner = "repo_owner"
          case repoURL = "repo_url"
          case repoSlug = "repo_slug"
          case isDisabled = "is_disabled"
          case status
          case isPublic = "is_public"
          case isGithubChecksEnabled = "is_github_checks_enabled"
          case owner
          case avatarURL = "avatar_url"
        }
        
        public struct Owner: Codable {
          public let accountType: String
          public let name: String
          public let slug: String
          
          public enum CodingKeys: String, CodingKey {
            case accountType = "account_type"
            case name, slug
          }
        }
      }
    }
    import Foundation
    
    // MARK: - AppsFetchResponse
    struct AppsFetchResponse: Codable {
      let message: String?
      let data: [AppFetchResponse.AppFetchDataResponse]?
      let paging: Paging?  
    }
    import Foundation
    
    public struct OriginalBuildParams: Codable {
      public let branch: String?
      public let workflowID: String?
      public let tag: String?
      public let commitMessage: String?
      
      public enum CodingKeys: String, CodingKey {
        case branch
        case workflowID = "workflow_id"
        case tag
        case commitMessage = "commit_message"
      }
      
      public init(branch: String?, workflowID: String?, tag: String?, commitMessage: String?) {
        self.branch = branch
        self.workflowID = workflowID
        self.tag = tag
        self.commitMessage = commitMessage
      }
    }
    import Foundation
    
    // MARK: - Paging
    public struct Paging: Codable {
      public let next: String?
      public let totalItemCount: Int
      public let pageItemLimit: Int
      
      public enum CodingKeys: String, CodingKey {
        case totalItemCount = "total_item_count"
        case pageItemLimit = "page_item_limit"
        case next
      }
    }
    import Foundation
    
    // MARK: - UserResponse
    public struct UserResponse: Codable {
        public let message: String?
        public let data: UserData?
      
      public struct UserData: Codable {
        public let username: String?
        public let slug: String?
        public let email: String?
        public let avatarURL: String?
        public let createdAt: String?
        public let hasUsedOrganizationTrial: Bool?
        public let dataID: Int?
        public let paymentProcessor: String?
        public let unconfirmedEmail: String?
        
        enum CodingKeys: String, CodingKey {
          case username, slug, email
          case avatarURL = "avatar_url"
          case createdAt = "created_at"
          case unconfirmedEmail = "unconfirmed_email"
          case hasUsedOrganizationTrial = "has_used_organization_trial"
          case dataID = "data_id"
          case paymentProcessor = "payment_processor"
        }
      }
    }
    import Foundation
    import URLRouting
    
    // MARK: - AppRoute
    public enum AppRoute: Equatable {
      /// Output is AppFetchResponse
      case fetch
      
      case buildWorkflows(BuildWorkflowsRoute = .fetch)
      case builds(BuildsRoute = .fetch())
      case branches(BranchesRoute = .fetch)
    }
    
    public let appRouter = OneOf {
      Route(.case(AppRoute.fetch))
      
      Route(.case(AppRoute.buildWorkflows)) {
        Path { "build-workflows" }
        buildWorkflowsRouter
      }
    
      Route(.case(AppRoute.builds)) {
        Path { "builds" }
        buildsRouter
      }
    
      Route(.case(AppRoute.branches)) {
        Path { "branches" }
        branchesRouter
      }
    }
    import Foundation
    import URLRouting
    
    // MARK: - AppsRoute
    public enum AppsRoute: Equatable {
      /// Output is AppsFetchResponse
      case fetch(SearchOptions = .init())
      
      case app(String, AppRoute = .fetch)
    
      public struct SearchOptions: Codable, Equatable {
        let sortBy: SortByFetchSearchOptions?
        let next: String?
        let limit: Int?
        
        public init(sortBy: SearchOptions.SortByFetchSearchOptions? = nil, next: String? = nil, limit: Int? = nil) {
          self.sortBy = sortBy
          self.next = next
          self.limit = limit
        }
        
        public enum SortByFetchSearchOptions: String, CaseIterable, Codable {
          case lastBuildAt = "last_build_at"
          case createdAt = "created_at"
        }
      }
    }
    
    public let appsRouter = OneOf {
      Route(.case(AppsRoute.fetch)) {
        Parse(.memberwise(AppsRoute.SearchOptions.init)) {
          Query {
            Field("sort_by", default: nil) { AppsRoute.SearchOptions.SortByFetchSearchOptions.parser() }
            Field("next", .string, default: nil)
            Field("limit",  default: nil) { Digits() }
          }
        }
      }
    
      Route(.case(AppsRoute.app)) {
        Path { Parse(.string) }
        appRouter
      }
    }
    import Foundation
    import URLRouting
    
    // MARK: - ArtifactRoute
    public enum ArtifactRoute: Equatable {
      /// Output is ⚠️
      case fetch
    }
    
    public let artifactRouter = OneOf {
      Route(.case(ArtifactRoute.fetch))
    }
    import Foundation
    import URLRouting
    
    // MARK: - ArtifactsRoute
    public enum ArtifactsRoute: Equatable {
      /// Output is ⚠️
      case fetch
      
      case artifact(String, ArtifactRoute = .fetch)
    }
    
    public let artifactsRouter = OneOf {
      Route(.case(ArtifactsRoute.fetch))
      
      Route(.case(ArtifactsRoute.artifact)) {
        Path { Parse(.string) }
        artifactRouter
      }
    }
    import URLRouting
    import Foundation
    
    // MARK: - BitriseRoute
    public enum BitriseRoute: Equatable {
      case user(UserRoute = .fetch)
      case builds(RootBuildsRoute = .fetch())
      case apps(AppsRoute = .fetch())
    }
    
    public let bitriseRouter = OneOf {
      Route(.case(BitriseRoute.user)) {
        Path { "me" }
        userRouter
      }
      
      Route(.case(BitriseRoute.apps)) {
        Path { "apps" }
        appsRouter
      }
      
      Route(.case(BitriseRoute.builds)) {
        Path { "builds" }
        rootBuildsRouter
      }
    }
    import Foundation
    import URLRouting
    
    // MARK: - BranchesRoute
    public enum BranchesRoute: Equatable {
      /// Output is AppBranchesFetchResponse
      case fetch
    }
    
    public let branchesRouter = OneOf {
      Route(.case(BranchesRoute.fetch))
    }
    import Foundation
    import URLRouting
    
    // MARK: - BuildRoute
    public enum BuildRoute: Equatable {
      /// Output is ⚠️
      case fetch
      case artifacts(ArtifactsRoute = .fetch)
      case log(LogRoute = .fetch)
      case abort(BodyParameters)
      
      public struct BodyParameters: Codable, Equatable {
        var abortReason: String = ""
        var abortWithSuccess: Bool = true
        var skipNotifications: Bool = false
        
        public enum CodingKeys: String, CodingKey {
          case abortReason = "abort_reason"
          case abortWithSuccess = "abort_with_success"
          case skipNotifications = "skip_notifications"
        }
    
        public init(abortReason: String = "", abortWithSuccess: Bool = true, skipNotifications: Bool = false) {
          self.abortReason = abortReason
          self.abortWithSuccess = abortWithSuccess
          self.skipNotifications = skipNotifications
        }
      }
    }
    
    public let buildRouter = OneOf {
      Route(.case(BuildRoute.fetch))
      
      Route(.case(BuildRoute.artifacts)) {
        Path { "artifacts" }
        artifactsRouter
      }
      
      Route(.case(BuildRoute.log)) {
        Path { "log" }
        logRouter
      }
      
      Route(.case(BuildRoute.abort)) {
        Method.post
        Body(.json(BuildRoute.BodyParameters.self))
      }
    }
    import Foundation
    import URLRouting
    
    // MARK: - BuildWorkflowsRoute
    public enum BuildWorkflowsRoute: Equatable {
      /// Output is AppBuildWorkflowsFetchResponse
      case fetch
    }
    
    public let buildWorkflowsRouter = OneOf {
      Route(.case(BuildWorkflowsRoute.fetch))
    }
    import Foundation
    import URLRouting
    
    // MARK: - BuildsRoute
    public enum BuildsRoute: Equatable {
      /// Output is ⚠️
      case trigger(BodyParameters)
    
      // MARK: - BodyParameters
      public struct BodyParameters: Codable, Equatable {
        public var buildParams: BuildParams?
        public var hookInfo: HookInfo?
        
        public init(buildParams: BuildParams?, hookInfo: HookInfo?) {
          self.buildParams = buildParams
          self.hookInfo = hookInfo
        }
        
        public struct BuildParams: Codable, Equatable {
          public var baseRepositoryURL: String?
          public var branch: String?
          public var branchDest: String?
          public var branchDestRepoOwner: String?
          public var branchRepoOwner: String?
          public var buildRequestSlug: String?
          public var commitHash: String?
          public var commitMessage: String?
          public var commitPaths: [CommitPath]?
          public var diffURL: String?
          public var environments: [Environment]?
          public var headRepositoryURL: String?
          public var pullRequestAuthor: String?
          public var pullRequestHeadBranch: String?
          public var pullRequestMergeBranch: String?
          public var pullRequestRepositoryURL: String?
          public var skipGitStatusReport: Bool?
          public var tag: String?
          public var workflowID: String?
          
          public init(baseRepositoryURL: String?, branch: String?, branchDest: String?, branchDestRepoOwner: String?, branchRepoOwner: String?, buildRequestSlug: String?, commitHash: String?, commitMessage: String?, commitPaths: [CommitPath]?, diffURL: String?, environments: [Environment]?, headRepositoryURL: String?, pullRequestAuthor: String?, pullRequestHeadBranch: String?, pullRequestMergeBranch: String?, pullRequestRepositoryURL: String?, skipGitStatusReport: Bool?, tag: String?, workflowID: String?) {
            self.baseRepositoryURL = baseRepositoryURL
            self.branch = branch
            self.branchDest = branchDest
            self.branchDestRepoOwner = branchDestRepoOwner
            self.branchRepoOwner = branchRepoOwner
            self.buildRequestSlug = buildRequestSlug
            self.commitHash = commitHash
            self.commitMessage = commitMessage
            self.commitPaths = commitPaths
            self.diffURL = diffURL
            self.environments = environments
            self.headRepositoryURL = headRepositoryURL
            self.pullRequestAuthor = pullRequestAuthor
            self.pullRequestHeadBranch = pullRequestHeadBranch
            self.pullRequestMergeBranch = pullRequestMergeBranch
            self.pullRequestRepositoryURL = pullRequestRepositoryURL
            self.skipGitStatusReport = skipGitStatusReport
            self.tag = tag
            self.workflowID = workflowID
          }
          
          public struct CommitPath: Codable, Equatable {
            public var added: [String]?
            public var modified: [String]?
            public var removed: [String]?
            
            public init(added: [String]?, modified: [String]?, removed: [String]?) {
              self.added = added
              self.modified = modified
              self.removed = removed
            }
          }
          public struct Environment: Codable, Equatable {
            public var isExpand: Bool?
            public var mappedTo: String?
            public var value: String?
            
            public init(isExpand: Bool?, mappedTo: String?, value: String?) {
              self.isExpand = isExpand
              self.mappedTo = mappedTo
              self.value = value
            }
          }
        }
        
        public struct HookInfo: Codable, Equatable {
          public var type: String?
          
          public init(type: String?) {
            self.type = type
          }
        }
      }
    
      case build(String, BuildRoute = .fetch)
    
      /// Output is AppBuildsFetchResponse
      case fetch(SearchOptions = .init())
      public struct SearchOptions: Codable, Equatable {
        public var sortBy: Sort?
        public var branch: String?
        public var workflow: String?
        public var commitMessage: String?
        public var triggerEventType: String?
        public var pullRequestId: Int?
        public var buildNumber: Int?
        public var after: Date?
        public var before: Date?
        public var status: Status?
        public var next: String?
        public var limit: Int?
        
        public enum Status: Int, Codable, Equatable, CaseIterable {
          case notFinished = 0
          case successful = 1
          case failed = 2
          case abortedWithFailure = 3
          case abortedWithSuccess = 4
        }
        
        public enum Sort: String, Codable, Equatable, CaseIterable {
          case runningFirst = "running_first"
          case createdAt = "created_at"
        }
        
        public init(
          sortBy: BuildsRoute.SearchOptions.Sort? = nil,
          branch: String? = nil,
          workflow: String? = nil,
          commitMessage: String? = nil,
          triggerEventType: String? = nil,
          pullRequestId: Int? = nil//,
    //      buildNumber: Int? = nil,
    //      after: Date? = nil,
    //      before: Date? = nil,
    //      status: BuildsRoute.SearchOptions.Status? = nil,
    //      next: String? = nil,
    //      limit: Int? = nil
        ) {
          self.sortBy = sortBy
          self.branch = branch
          self.workflow = workflow
          self.commitMessage = commitMessage
          self.triggerEventType = triggerEventType
          self.pullRequestId = pullRequestId
          self.buildNumber = nil// buildNumber
          self.after = nil // after
          self.before = nil // before
          self.status = nil // status
          self.next = nil // next
          self.limit = nil // limit
        }
      }
    }
    
    public let buildsRouter = OneOf {
      Route(.case(BuildsRoute.fetch)) {
        Parse(.memberwise(BuildsRoute.SearchOptions.init)) {
          Query {
            Field("sort_by", default: nil) { BuildsRoute.SearchOptions.Sort.parser() }
            Field("branch", .string, default: nil)
            Field("workflow", .string, default: nil)
            Field("commit_message", .string, default: nil)
            Field("trigger_event_type", .string, default: nil)
            Field("pull_request_id", default: nil) { Digits() }
    //        Field("build_number", default: nil) { Digits() }
    //        Field("after", default: nil) { Digits() }
    //        Field("before", default: nil) { Digits() }
    //        Field("status", default: nil) { BuildsRoute.SearchOptions.Status.parser() }
    //        Field("next", .string, default: nil)
    //        Field("limit", default: nil) { Digits() }
          }
        }
      }
      
      Route(.case(BuildsRoute.build)) {
        Path { Parse(.string) }
        buildRouter
      }
      
      Route(.case(BuildsRoute.trigger)) {
        Method.post
        Body(.json(BuildsRoute.BodyParameters.self))
      }
    }
    import Foundation
    import URLRouting
    
    // MARK: - LogRoute
    public enum LogRoute: Equatable {
      /// Output is ⚠️
      case fetch
    }
    
    public let logRouter = OneOf {
      Route(.case(LogRoute.fetch))
    }
    import URLRouting
    import Foundation
    
    // MARK: - RootBuildsRoute
    public enum RootBuildsRoute: Equatable {
      /// Output is ⚠️
      case fetch(SearchOptions = .init())
      
      public struct SearchOptions: Codable, Equatable {
        var isOnHold: Bool? = nil
        var status: Status? = nil
        var next: String? = nil
        var limit: Int? = nil
        
        /// The status of the build: not finished (0), successful (1), failed (2), aborted with failure (3), aborted with success (4)
        public enum Status: Int, Codable, Equatable, CaseIterable {
          case notFinished = 0
          case successful = 1
          case failed = 2
          case abortedWithFailure = 3
          case abortedWithSuccess = 4
        }
        
        public init(isOnHold: Bool? = nil, status: RootBuildsRoute.SearchOptions.Status? = nil, next: String? = nil, limit: Int? = nil) {
          self.isOnHold = isOnHold
          self.status = status
          self.next = next
          self.limit = limit
        }
      }
    }
    
    public let rootBuildsRouter = OneOf {
      Route(.case(RootBuildsRoute.fetch)) {
        Parse(.memberwise(RootBuildsRoute.SearchOptions.init)) {
          Query {
            Field("is_on_hold", default: nil) { Bool.parser() }
            Field("status", default: nil) { RootBuildsRoute.SearchOptions.Status.parser() }
            Field("next", .string, default: nil)
            Field("limit", default: nil) { Digits() }
          }
        }
      }
    }
    import Foundation
    import URLRouting
    
    // MARK: - UserRoute
    public enum UserRoute: Equatable {
      /// Ouput is UserResponse
      case fetch
    }
    
    let userRouter = OneOf {
      Route(.case(UserRoute.fetch))
    }
    
    opened by mackoj 5
  • Add URL Scheme parser and test

    Add URL Scheme parser and test

    💬 Summary of Changes

    • Implements a scheme parser to support routing older style custom schemes on iOS.

    ✅ Checklist

    • [x] 🧐 Performed a self-review of the changes.
    • [x] ✅ Added tests to cover changes.
    • [x] 🏎 Ran Unit Tests before opening PR.
    opened by ryanbooker 5
  • Additional HTTP Headers don't seems to be passe to the request

    Additional HTTP Headers don't seems to be passe to the request

    Hi,

    Thanks for making this great lib as always ! When I try to pass a Authorization header to the session it is not passed to the request.

          let session = URLSession(configuration: .default)
          session.configuration.httpAdditionalHeaders = [
            "Authorization": "some token"
          ]
          let api = URLRoutingClient<TestRoute>.live(router: testRouter.baseURL("https://supersecretserver.com"), session: session)
    

    I think the solution might be to add it to the request after creating it. I will do a PR to explain it better.

    opened by mackoj 5
  • Decoding issue

    Decoding issue

    My playground code which dont have any issue playground code here https://gist.github.com/saroar/a1bab728f4292c54cb76085a633c6a3c my client iOS app dont send any request to server becz its not decoding my query https://github.com/AddaMeSPB/AddaMeIOS/blob/siteRouter/AddameSPM/Sources/EventView/EventsReducer.swift#L112 error coming from decoding from this https://github.com/pointfreeco/swift-url-routing/blob/main/Sources/URLRouting/Client/Client.swift#L53

    HTTPRequest.HRError(
            description: "fetch events get error",
            reason: URLRoutingDecodingError(
              bytes: Data(35 bytes),
              response: NSHTTPURLResponse(),
              underlyingError: DecodingError.dataCorrupted(
                DecodingError.Context(
                  codingPath: [],
                  debugDescription: "The given data was not valid JSON.",
                  underlyingError: NSError(
                    domain: "NSCocoaErrorDomain",
                    code: 3840,
                    userInfo: [
                      "NSDebugDescription": "Invalid value around line 1, column 0.",
                      "NSJSONSerializationErrorIndex": 0
                    ]
                  )
                )
              )
            )
          )
    
    opened by saroar 4
  • Weird compilation error on Route parsing

    Weird compilation error on Route parsing

    Hi, I was trying to model an existing API but I'm encountering a problem.

    Example:

    This is one of the urls: BASE_URL/somepath/SOME_STRING_VAR/ANOTHER_STRING_VAR. The method is POST and it has a String body.

    I was trying to model it like this:

    enum SiteRoute {
        case somepath(var1: String, var2: String, body: String)
    }
    ... 
    let router = OneOf {
        Route(.case(SiteRoute.somepath)) {
            Method.post
            Path { 
                "someroute"
                Parse(.string)
                Parse(.string)
            }
            Body(?) // I can't figure out what to put here.
        }
    }
    

    If I define the model like this, instead:

    enum SiteRoute {
        case somepath(var1: String, var2: String, body: SomeModel)
    }
    ... 
    let router = OneOf {
        Route(.case(SiteRoute.somepath)) {
            Method.post
            Path { 
                "someroute"
                Parse(.string)
                Parse(.string)
            }
            Body(.json(SomeModel.self))
        }
    }
    

    It throws me this compile error. Screenshot 2022-09-13 at 11 11 41

    How can I solve this (theoretically shouldn't be) complicated task? I'm sure there's something I'm missing. Thanks in advance.

    opened by rcasula 3
  • JSONDecoder at Client level

    JSONDecoder at Client level

    In most of APIs, decoding mechanism is the same for all requests for a given client.

    So, this PR introduces a JSONDecoder at the url-routing client level, without loosing the ability to specify a custom Decoder for each request if needed.

    The client declaration is more clear for decoding :

    let apiClient = URLRoutingClient.live(
      router: router.baseURL("https://127.0.0.1:8080"),
      decoder: customDecoder
    )
    

    And you don't have to store neither specify your custom Decoder at each request call.

    opened by jtouzy 3
  • Why my DecodingError.dataCorrupted? I dont see any reason!

    Why my DecodingError.dataCorrupted? I dont see any reason!

    I love this SPM but having also lots of pain after I am using this :(

    my model

    public struct UserGetObject: Codable {
    
        public var id: ObjectId
        public var fullName: String
        public var phoneNumber: String?
        public var email: String?
        public var role: UserRole
        public var language: UserLanguage
    
        public init(
            id: ObjectId,
            fullName: String,
            phoneNumber: String? = nil,
            email: String? = nil,
            role: UserRole,
            language: UserLanguage
        ) {
            self.id = id
            self.fullName = fullName
            self.phoneNumber = phoneNumber
            self.email = email
            self.role = role
            self.language = language
        }
    }
    
    public let usersRouter = OneOf {
        Route(.case(UsersRoute.user)) {
            Path { Parse(.string) }
            userRouter
        }
    
        Route(.case(UsersRoute.update)) {
            Method.put
            Body(.json(UserGetObject.self))
        }
    }
    
    // reducer
    case .nameForm:
            withAnimation { state.selectedPage = .schaduleForm }
    
            var currentUser = environment.currentUser
            currentUser.fullName = state.name
    
            return .task { [cuser = currentUser] in
                return .userUpdate(
                    await TaskResult {
                        try await environment.userClient.update(cuser)
                    }
                )
            }
    
    
    // Clinet
    public struct UserClient {
    
        static public let apiClient: URLRoutingClient<SiteRoute> = .live(
          router: siteRouter.baseRequestData(
              .init(
                  scheme: EnvironmentKeys.rootURL.scheme,
                  host: EnvironmentKeys.rootURL.host,
                  port: 7070,
                  headers: ["Authorization": ["Bearer \(accessTokenTemp)"]]
              )
          )
        )
    
        public typealias UserMeHandler = @Sendable (String) async throws -> UserGetObject
        public typealias UserUpdateHandler = @Sendable (UserGetObject) async throws -> UserGetObject
        public typealias UserDeleteHandler = @Sendable (String) async throws -> Bool
    
        public let userMeHandler: UserMeHandler
        public let update: UserUpdateHandler
        public let delete: UserDeleteHandler
    
        public init(
            userMeHandler: @escaping UserMeHandler,
            update: @escaping UserUpdateHandler,
            delete: @escaping UserDeleteHandler
        ) {
            self.userMeHandler = userMeHandler
            self.update = update
            self.delete = delete
        }
    }
    
    extension UserClient {
        public static var live: UserClient =
        .init(
            userMeHandler: { id in
                return try await UserClient.apiClient.decodedResponse(
                    for: .authEngine(.users(.user(id: id, route: .find))),
                    as: UserGetObject.self,
                    decoder: .iso8601
                ).value
            },
          update: { userInput in
              return try await UserClient.apiClient.decodedResponse(
                for: .authEngine(.users(.update(input: userInput))),
                as: UserGetObject.self,
                decoder: .iso8601
              ).value
          },
          delete: { id in
    
              return try await UserClient.apiClient.data(
                for: .authEngine(.users(.user(id: id, route: .delete)))
              ).response.isResponseOK()
          }
        )
    }
    
    
    received action:
      AppAction.welcome(
        WelcomeAction.userUpdate(
          TaskResult.failure(
            URLRoutingDecodingError(
              bytes: Data(77 bytes),
              response: NSHTTPURLResponse(),
              underlyingError: DecodingError.dataCorrupted(
                DecodingError.Context(
                  codingPath: [],
                  debugDescription: "The given data was not valid JSON.",
                  underlyingError: NSError(
                    domain: "NSCocoaErrorDomain",
                    code: 3840,
                    userInfo: [
                      "NSDebugDescription": "Invalid value around line 1, column 0.",
                      "NSJSONSerializationErrorIndex": 0
                    ]
                  )
                )
              )
            )
          )
        )
      )
      (No state changes)
    

    https://vimeo.com/754632355

    opened by saroar 2
  • Incremental compilation errors

    Incremental compilation errors

    Whenever I make a change to my routes and then build without cleaning first, I get an error like this

    Undefined symbols for architecture arm64:
      "ExampleAPI.SiteRoute.router.unsafeMutableAddressor : Parsing.OneOf<Parsing.OneOfBuilder.OneOf2<URLRouting.Route<Parsing.Parsers.MapConversion<Parsing.Always<URLRouting.URLRequestData, ()>, CasePaths.CasePath<ExampleAPI.SiteRoute, ()>>>, URLRouting.Route<Parsing.Parsers.MapConversion<Parsing.ParserBuilder.ZipVO<URLRouting.Path<URLRouting.PathBuilder.Component<Swift.String>>, Parsing.OneOf<URLRouting.Route<Parsing.Parsers.MapConversion<Parsing.ParserBuilder.ZipOO<URLRouting.Path<URLRouting.PathBuilder.Component<Parsing.From<Parsing.Conversions.SubstringToUTF8View, Parsing.Parsers.IntParser<Swift.Substring.UTF8View, Swift.Int64>>>>, Parsing.OneOf<Parsing.OneOfBuilder.OneOf2<URLRouting.Route<Parsing.Parsers.MapConversion<Parsing.ParserBuilder.ZipVO<URLRouting.Path<URLRouting.PathBuilder.Component<Swift.String>>, Parsing.OneOf<Parsing.OneOfBuilder.OneOf2<URLRouting.Route<Parsing.Parsers.MapConversion<Parsing.Always<URLRouting.URLRequestData, ()>, CasePaths.CasePath<ExampleAPI.ExamplesRoute, ()>>>, URLRouting.Route<Parsing.Parsers.MapConversion<Parsing.ParserBuilder.ZipVO<URLRouting.Method, URLRouting.Body<Parsing.Parsers.MapConversion<Parsing.Parsers.ReplaceError<Parsing.Rest<Foundation.Data>>, Parsing.Conversions.JSON<ExampleAPI.NewExample>>>>, CasePaths.CasePath<ExampleAPI.ExamplesRoute, ExampleAPI.NewExample>>>>>>, CasePaths.CasePath<ExampleAPI.UserRoute, ExampleAPI.ExamplesRoute>>>, URLRouting.Route<Parsing.Parsers.MapConversion<Parsing.ParserBuilder.ZipVO<URLRouting.Path<URLRouting.PathBuilder.Component<Swift.String>>, Parsing.OneOf<Parsing.OneOfBuilder.OneOf3<URLRouting.Route<Parsing.Parsers.MapConversion<Parsing.Always<URLRouting.URLRequestData, ()>, CasePaths.CasePath<ExampleAPI.ThingsRoute, ()>>>, URLRouting.Route<Parsing.Parsers.MapConversion<Parsing.ParserBuilder.ZipVO<URLRouting.Method, URLRouting.Body<Parsing.Parsers.MapConversion<Parsing.Parsers.ReplaceError<Parsing.Rest<Foundation.Data>>, Parsing.Conversions.JSON<ExampleAPI.NewThing>>>>, CasePaths.CasePath<ExampleAPI.ThingsRoute, ExampleAPI.NewThing>>>, URLRouting.Route<Parsing.Parsers.MapConversion<Parsing.ParserBuilder.ZipOO<URLRouting.Path<URLRouting.PathBuilder.Component<Parsing.From<Parsing.Conversions.SubstringToUTF8View, Parsing.Parsers.UUIDParser<Swift.Substring.UTF8View>>>>, Parsing.OneOf<URLRouting.Route<Parsing.Parsers.MapConversion<URLRouting.Method, CasePaths.CasePath<ExampleAPI.ThingRoute, ()>>>>>, CasePaths.CasePath<ExampleAPI.ThingsRoute, (Foundation.UUID, ExampleAPI.ThingRoute)>>>>>>, CasePaths.CasePath<ExampleAPI.UserRoute, ExampleAPI.ThingsRoute>>>>>>, CasePaths.CasePath<ExampleAPI.UsersRoute, (Swift.Int64, ExampleAPI.UserRoute)>>>>>, CasePaths.CasePath<ExampleAPI.SiteRoute, ExampleAPI.UsersRoute>>>>>", referenced from:
          ExampleServer.configure(Vapor.Application) throws -> () in ExampleServer.o
    ld: symbol(s) not found for architecture arm64
    clang: error: linker command failed with exit code 1 (use -v to see invocation)
    

    If I clean shift cmd k and then build, it compiles as expected. That's why I assume this is an incremental compilation issue. This could be an issue specific to my project, but it looks almost entirely related to URLRouting or the underlying Swift Parsing library. I hope someone else has run into this.

    If you've never encountered this issue, then it might be safe to assume it is just my project and close this issue.

    opened by dmzza 2
  • Typo in deprecation

    Typo in deprecation

    https://github.com/pointfreeco/swift-url-routing/blob/380347731cd4c532696f27613fbdb12661ba8949/Sources/URLRouting/Internal/Deprecations.swift#L11

    This line should be: @available(*, deprecated, renamed: "decodedResponse(for:as:decoder:)")

    opened by dmzza 2
  • Fix compilation errors

    Fix compilation errors

    Build fails with "Command SwiftEmitModule failed with a nonzero exit code" error

    1.	Apple Swift version 5.7.1 (swiftlang-5.7.1.135.3 clang-1400.0.29.51)
    2.	Compiling with the current language version
    3.	While evaluating request TypeCheckSourceFileRequest(source_file "/Users/danil/Downloads/swift-url-routing-swift-5-7/Sources/URLRouting/Body.swift")
    4.	While type-checking 'Body' (at /Users/danil/Downloads/swift-url-routing-swift-5-7/Sources/URLRouting/Body.swift:4:8)
    5.	While type-checking 'init(_:)' (at /Users/danil/Downloads/swift-url-routing-swift-5-7/Sources/URLRouting/Body.swift:28:10)
    6.	While evaluating request InterfaceTypeRequest(URLRouting.(file).Body.init(_:)@/Users/danil/Downloads/swift-url-routing-swift-5-7/Sources/URLRouting/Body.swift:28:10)
    7.	While evaluating request GenericSignatureRequest(URLRouting.(file).Body.init(_:)@/Users/danil/Downloads/swift-url-routing-swift-5-7/Sources/URLRouting/Body.swift:28:10)
    8.	While evaluating request InferredGenericSignatureRequest(URLRouting, <Bytes where Bytes : Parser, Bytes.Input == Data>, <C>, URLRouting.(file).Body.init(_:)@/Users/danil/Downloads/swift-url-routing-swift-5-7/Sources/URLRouting/Body.swift:28:10, {}, {(C, C)}, 0)
    9.	While evaluating request InferredGenericSignatureRequestRQM(<Bytes where Bytes : Parser, Bytes.Input == Data>, <C>, URLRouting.(file).Body.init(_:)@/Users/danil/Downloads/swift-url-routing-swift-5-7/Sources/URLRouting/Body.swift:28:10, {}, {(C, C)}, 0)
    
    opened by dankinsoid 6
  • Request header unexpectedly mutated

    Request header unexpectedly mutated

    In this example, I'm supplying an uppercased request header but it's getting lowercased somewhere within the parsing library. I couldn't easily figure out where this was happening to fix with a PR so opening as an issue!

    enum TestRoute {
        case testing(value: String)
    }
    
    let testingRouter = Route(.case(TestRoute.testing)) {
        Path {
            "testing"
        }
        Headers {
            Field("UPPERCASED-HEADER")
        }
    }
    
    func test() async throws {
        let request = try testingRouter.request(
            for: TestRoute.testing(value: "VALUE")
        )
        print(request.allHTTPHeaderFields!)
    }
    

    printed: ["uppercased-header": "VALUE"]

    expected: ["UPPERCASED-HEADER": "VALUE"]

    opened by eappel 4
  • Scoped clients

    Scoped clients

    Adds a new .scoped operator on URLRoutingClient that can be used to create scoped clients.

    This supports a use case where your main client may support a large number of routes and you only want to be able to pass around a client that handles a sub-set of those routes as a dependency.

    For instance, imagine a feature domain within a TCA app - your AppEnvironment may hold onto a URLRoutingClient<AppRoute> client but your account feature may only need to make requests to account-related routes, so you'd like to pass a URLRoutingClient<AccountRoute> client to that domain's environment, limiting its scope and making it easier for you to find the right route at the call-site.

    I debated over whether or not to call this scoped or pullback - I think the shape of this function is a pullback but the term "scope" or "scoped" felt more ergonomic to me in this context.

    Also added some basic tests for the client.

    Suggestion - consider adding some integration tests of the live implementation using a mock server. There's a few available (I've found https://github.com/surpher/PactSwift really easy to use although its use cases extend beyond simply creating a mock server).

    opened by lukeredpath 3
Releases(0.4.0)
  • 0.4.0(Nov 7, 2022)

    What's Changed

    • Fixed: Fields (query, headers, form data, etc.) now preserve order when printed (thanks @fonkadelic, #57). To preserve this order, URL Routing now depends on Apple's Swift Collections package.
    • Fixed: Updated calls to withTaskCancellation to the non-deprecated version (thanks @kgrigsby59, #56).

    New Contributors

    • @kgrigsby59 made their first contribution in https://github.com/pointfreeco/swift-url-routing/pull/56

    Full Changelog: https://github.com/pointfreeco/swift-url-routing/compare/0.3.1...0.4.0

    Source code(tar.gz)
    Source code(zip)
  • 0.3.1(Sep 20, 2022)

  • 0.3.0(Jun 28, 2022)

    • Added: Fragment parser, for parsing a URL fragment (thanks @ryanbooker).
    • Added: Host parser, for parsing a URL host (thanks @ryanbooker).
    • Changed: URL Routing's platform requirements have been bumped to match Parsing's requirements, equivalent to SwiftUI (iOS 13+, macOS 10.15+, tvOS 13+, watchOS 6+). If these minimum requirements don't fit your needs, let us know.
    • Infrastructure: Fixed documentation typos (thanks @fonkadelic, @volkdmitri).
    Source code(tar.gz)
    Source code(zip)
  • 0.2.0(May 20, 2022)

    • Added: URLRoutingClient can now be configured with a JSONDecoder for global response decoding (thanks @jtouzy).
    • Added: a Scheme router (thanks @ryanbooker).
    • Added: URLRoutingClient.data(for:).
    • Updated: URLRoutingClient.request(_:as:decoder:) has been renamed to URLRoutingClient.decodedResponse(for:as:decoder:).
    • Changed: the package name has been changed to swift-url-routing to match its repo name.
    • Optimized: added inlining to the path component router.
    • Infrastructure: documentation updates.
    • Infrastructure: added SPI badges to README (thanks @finestructure).
    Source code(tar.gz)
    Source code(zip)
  • 0.1.0(May 2, 2022)

Owner
Point-Free
A video series exploring Swift and functional programming.
Point-Free
Monarch Router is a Declarative URL- and state-based router written in Swift.

Monarch Router is a declarative routing handler that is capable of managing complex View Controllers hierarchy transitions automatically, decoupling View Controllers from each other via Coordinator and Presenters. It fits right in with Redux style state flow and reactive frameworks.

Eliah Snakin 31 May 19, 2021
Interface-oriented router for discovering modules, and injecting dependencies with protocol in Objective-C and Swift.

ZIKRouter An interface-oriented router for managing modules and injecting dependencies with protocol. The view router can perform all navigation types

Zuik 631 Dec 26, 2022
Crossroad is an URL router focused on handling Custom URL Scheme

Crossroad is an URL router focused on handling Custom URL Scheme. Using this, you can route multiple URL schemes and fetch arguments and parameters easily.

Kohki Miki 331 May 23, 2021
SwiftRouter - A URL Router for iOS, written in Swift

SwiftRouter A URL Router for iOS, written in Swift, inspired by HHRouter and JLRoutes. Installation SwiftRouter Version Swift Version Note Before 1.0.

Chester Liu 259 Apr 16, 2021
A demonstration to the approach of leaving view transition management to a router.

SwiftUI-RouterDemo This is a simplified demonstration to the approach of leaving view transition management to a router.

Elvis Shi 3 May 26, 2021
An experimental navigation router for SwiftUI

SwiftUIRouter ?? An ⚠️ experimental ⚠️ navigation router for SwiftUI Usage ?? Check out ExampleApp for more. Define your routes: import SwiftUIRouter

Orkhan Alikhanov 16 Aug 16, 2022
URLScheme router than supports auto creation of UIViewControllers for associated url parameters to allow creation of navigation stacks

IKRouter What does it do? Once you have made your UIViewControllers conform to Routable you can register them with the parameters that they represent

Ian Keen 94 Feb 28, 2022
An extremely lean implementation on the classic iOS router pattern.

Beeline is a very small library that aims to provide a lean, automatic implementation of the classic iOS router pattern.

Tim Oliver 9 Jul 25, 2022
Helm - A graph-based SwiftUI router

Helm is a declarative, graph-based routing library for SwiftUI. It fully describ

Valentin Radu 99 Dec 5, 2022
Provides a custom presentation modifier that provides more options including full screen presentations. (iOS)

Presentation Also available as a part of my SwiftUI+ Collection – just add it to Xcode 13+ Provides a custom presentation modifier that provides more

SwiftUI+ 15 Dec 3, 2022
📱📲 Navigate between view controllers with ease. 💫 🔜 More stable version (written in Swift 5) coming soon.

CoreNavigation ?? ?? Navigate between view controllers with ease. ?? ?? More stable version (written in Swift 5) coming soon. Getting Started API Refe

Aron Balog 69 Sep 21, 2022
Appz 📱 Launch external apps, and deeplink, with ease using Swift!

Appz ?? Deeplinking to external applications made easy Highlights Web Fallback Support: In case the app can't open the external application, it will f

Kitz 1.1k May 5, 2021
🎯Linker Lightweight way to handle internal and external deeplinks in Swift for iOS

Linker Lightweight way to handle internal and external deeplinks in Swift for iOS. Installation Dependency Managers CocoaPods CocoaPods is a dependenc

Maksim Kurpa 128 May 20, 2021
🍞 [Beta] A view controller that can unwind like presentation and navigation.

FluidPresentation - no more handling presented or pushed in view controller A view controller that supports the interactive dismissal by edge pan gest

Muukii 19 Dec 22, 2021
Easy and maintainable app navigation with path based routing for SwiftUI.

Easy and maintainable app navigation with path based routing for SwiftUI.

Freek Zijlmans 278 Jun 7, 2021
iOS routing done right. Handles both URL recognition and controller displaying with parsed parameters. All in one line, controller stack preserved automatically!

Developed and Maintained by Ipodishima Founder & CTO at Wasappli Inc. (If you need to develop an app, get in touch with our team!) So what is this lib

null 589 Dec 24, 2022
Eugene Kazaev 713 Dec 25, 2022
An open source library for building deep-linkable SwiftUI applications with composition, testing and ergonomics in mind

Composable Navigator An open source library for building deep-linkable SwiftUI applications with composition, testing and ergonomics in mind Vanilla S

Bahn-X 538 Dec 8, 2022
A framework for easily testing Push Notifications and Routing in XCUITests

Mussel ?? ?? A framework for easily testing Push Notifications, Universal Links and Routing in XCUITests. As of Xcode 11.4, users are able to test Pus

Compass 65 Dec 28, 2022