URLMatcher is a flexible url matching library intended to handle the problem of what a URL is for without specifying how to handle it.
Similar libraries assume you'll be binding data to a view controller, then displaying it. This makes them hard to test well and awkward to use if your behavior is different. Maybe you need to select a specific tab, push a navigation controller, then show an alert or do different data binding depending on your local state. URLMatcher leaves this up to you while making it easy to test that you're matching and parsing all the URLs you expect to handle.
It also makes it easy to share app behavior between URL handling and push notifications or other linking methods.
A quick example:
import URLMatcher
enum URLAction {
case login, signup, invite(key: String)
}
var matcher = URLMatcher<URLAction>()
try matcher.add(path: "user/login") { .login }
try matcher.add(path: "user/signup") { .signup }
try matcher.add(path: "user/invite/<key: String>") {(_, values) in
.invite(key: values["key"] as! String)
}
// Grab your url from your web view or app delegate method
let url = URL(string:"http://mydomain.com/user/invite/y78gyug76g")!
if let action = matcher.matchURL(url) {
switch action {
case .login, .signup:
print("Show welcome form")
case .invite(let key):
// Show loading overlay
// Look up invite from the server
// Dismiss the loading overlay
// If available, show the accept invite flow
// If not, show an error alert
print("Load invite: \(key)")
}
} else {
// We don't know what to do with this URL, fallback on the system
}The URLMatcher will find the path that matches the URL and then execute the provided closure to create a response. It returns nil if there is no match.
NOTE: the code samples here are based on the Examples/Readme/main.swift code if you'd like the try it out.
URLMatcher uses the path components of the URL to see if it has a handler for the URL. A path entry like "user/login" will match any URL that has the path /user/login with any scheme, host or query components. It will not match a path that has more path components.
You can also specify data types for matches. For example, the path user/<Int64>/edit will match the paths /user/15/edit and /user/1768768/edit, but not /user/my.email@example.com/edit. You can also label the value like /user/<userId:Int64>/edit in which case the matcher will add the result of converting the value to Int64 in the second dictionary in the handler with the key userId:
struct UserAction {
let userId: Int64?
let action: String
}
var userMatcher = URLMatcher<UserAction>()
try userMatcher.add(path: "user/<userId:Int64>/edit") { (_, values: [String : Any]) -> UserAction in
return UserAction(userId: values["userId"] as! Int64, action: "edit")
}
let action = userMatcher.matchURL(URL(string:"user/6578/edit")!)
// Perform the actionaction will contain the Int64 6578 and edit.
Often we want to create default behaviors for URLs, then enhance that behavior when a more specific match is available. To extend our user example, lets add a fallback for when we don't recognize the action, but still know it's a user match.
try userMatcher.add(path: "user/<userId:Int64>/<*>") { (query: [String: String], values: [String : Any]) -> UserAction in
if query["force_update"] == "true" {
return UserAction(userId: values["userId"] as? Int64, action: "update app")
} else {
return UserAction(userId: values["userId"] as? Int64, action: "show")
}
}
let url = /* grab url */
if let action = userMatcher.matchURL(url) {
if action.action == "show" {
//Show the user
} else if action.action == "edit" {
//Show the user edit screen
} else {
//Show update alert w/ link to app store
}
}The <*> token is a special matcher that matches 0 or more path components and may only be used at the end of a path. You may not label it to get a value either. In this case, if we passed in the url /user/876897/verify/email, you would get the show action response. /user/678689/?force_update=true would return the update app response.
The <*> token will only match if there is not a more specific match available.
In addition to <*>, there is also a special matcher <+>. This token may be used anywhere in the path and matches 1 component. Like the <*> matcher, it will only match if there is a not a more specific match available. To continue our example further, this match handles cases where the component following user is not an Int64.
try userMatcher.add(path: "user/<+>/<*>") {
return UserAction(userId: nil, action: "unknown")
}This match will match any URL that has at least one component after user. As we also match paths that have an Int64 and any number of components after user, we will only hit this case if there if there is not an Int64 value. If we pass in the URL user/an.email@example.com/verify/email, we'll now get an unknown response while still keeping all of the previously added matchers at a higher priority.
In cases with multiple wildcard matchers, the system prefers matches with more specific matches at the beginning of the path. A quick example:
var wildcardMatcher = URLMatcher<String>()
try wildcardMatcher.add(path: "a/<+>") { "a" }
try wildcardMatcher.add(path: "<+>/b") { "b" }
// Will be "a"
wildcardMatcher.matchURL(URL(string:"a/b")!)
// Will be "b"
wildcard = wildcardMatcher.matchURL(URL(string: "c/b")!)Often you need to me a little more specific about the matching behavior. Let's imagine you have a short url format for SMS and a longer one for general use. Like /c/7h1uh89 and /commit/7h1uh8927e39434ae8da5fuy6c1de98c56c09410. You'd like to match these to the same output without duplicating code.
struct Commit : ComponentMatchable {
func matches(urlComponent: String) -> Bool {
return "c" == urlComponent || "commit" == urlComponent
}
}
struct HashId: ComponentParseable {
let label: String
func matches(urlComponent: String) -> Bool {
return valueOf(urlComponent: urlComponent) != nil
}
func valueOf(urlComponent: String) -> Any? {
//Should actually validate more completely, not great string handling
if 7 == urlComponent.characters.count || 40 == urlComponent.characters.count {
return String(urlComponent.characters.prefix(7))
} else {
return nil
}
}
}
var commitMatcher = URLMatcher<String>()
commitMatcher.registerType(name: "Commit") { (_) in Commit() }
commitMatcher.registerType(name: "HashId") { HashId(label: $0) }
try commitMatcher.add(path: "<Commit>/<commitId:HashId>") { (_, values: [String : Any]) -> String in
return values["commitId"] as! String
}
//Both of these return the same result
commitMatcher.matchURL(URL(string:"/c/7h1uh89")!)
commitMatcher.matchURL(URL(string:"/commit/7h1uh8927e39434ae8da5fuy6c1de98c56c09410")!)While this isn't a great example of string handling or working with commit hashes, it should serve as a good example of building your own custom component checks. ComponentMatchable is a ComponentParseable that doesn't set a value even if you label it.
If you need some more complex behavior, you can implement your own PathMatchable and use the add(matcher:) method. If you do wind up needing this, please open an issue with your use case. I've tried to make the core matching flexible and I'd love to try to make it work for you.
Marshall Weir
Copyright (c) 2026 Splitwise, Inc.
Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.