When implementing deeplinks for an iOS app recently, I wondered if Swift’s pattern matching could make a viable alternative to a URL routing library. This is an account of the different approaches I tried, and the one I finally settled on.

The majority of URL routing libraries use a pattern syntax that can match each element of a URL’s path in one of three ways:
 

  • Equality: Path element must match the pattern expression
  • Value-binding: Path element can be anything at all, and will be made available in a parameters dictionary
  • Wildcard: Path element can be anything at all, and will be discarded

For example, it might look something like this:

1
2
3
4
Router.register("user/:userId/*") { parameters in
let userId = parameters["userId"]!
// Show user's profile...
}

This would match a URL such as scheme://host/user/john_morgan/profilev2, invoking the closure with a userId of ‘john_morgan’. There are a few reasons I don’t much like this approach:
 

  • The pattern matching is abstracted away using special syntax.
  • The parameter name userId is repeated and stringly typed, so it’s susceptible to typos.
  • parameters["userId"] should never be nil, but the compiler doesn’t know that, so we must force unwrap or add a guard statement.

As it happens Swift’s built-in pattern matching can be used for each of the three pattern types. Here’s an example of all three:

1
2
3
4
5
let example = ("expression-pattern", "value-binding-pattern", "wildcard-pattern")

if case let ("expression-pattern", value, _) = example {
// use value
}

In fact, the expression pattern is more powerful than a simple equality test, as we can define our own matching logic using the pattern matching operator (more of which later). The wildcard pattern is really a special case of the value-binding pattern, so I will refer to them collectively as value-binding patterns from here on in.

Swift’s pattern-matching would seem a natural fit for matching URLs, and Swift’s switch statement would suit the purpose too, so I decided to investigate a URL routing approach based on the two.

NSURL exposes a pathComponents as an Array<String>, e.g., https://myhost.com/user/14253/profilev2 would give ["/", "user", "14253", "profilev2"]. Let’s assume we remove the initial backslash and call the resulting array pathElements. In pseudo-Swift, I’d like to be able to switch on the array a bit like this:

1
2
3
4
5
6
switch pathElements {
case ["user", let userId, _]:
// Go to profile for userId
default:
break
}

However, there is no built-in pattern matching for Arrays and their elements in Swift, so we need to add it somehow…

Approach 1: Pattern Matching Operator

My first thought was to use the pattern-matching operator (~=) to match Arrays of equatable elements based on all elements being equal:

1
2
3
4
func ~=<T: Equatable>(pattern: [T], value: [T]) -> Bool {

return pattern == value
}

This would allow us to match simple patterns in a switch statement:

1
2
3
4
5
6
switch pathElements {
case ["lobby", "main"]:
// Go to lobby
default:
break
}

However, the pattern-matching operator can only be used for expression patterns. It cannot be used for adding custom value-binding patterns, so this is a dead end. We need to convert the array into another type that already supports value-binding patterns for its elements.

Approach 2: Tuples

This led me to think about tuples, as tuples support value-binding patterns for their elements. To convert the pathElements array into a tuple with the same number of elements, perhaps a decompose() method could be overloaded for element counts up to some sensible limit:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
extension Array {

func decompose() -> Element? {

guard count == 1 else { return nil }
return self[0]
}

func decompose() -> (Element, Element)? {

guard count == 2 else { return nil }
return (self[0], self[1])
}

func decompose() -> (Element, Element, Element)? {

guard count == 3 else { return nil }
return (self[0], self[1], self[2])
}
}

This would enable pattern-matching like so:

1
2
3
if case ("user", let userId, _)?: (String, String, String)? = pathElements.decompose() {
// Open profile for userId
}

Unfortunately the compiler can’t infer which decompose() method to invoke, which necessitates the explicit typing after the colon above. Abandoning the overloaded decompose() in favour of unique method names decompose1(), decompose2(), decompose3() etc. helps to clean things up:

1
2
3
4
5
if case ("user", let userId, _)? = pathElements.decompose3() {
// Open profile for userId
} else if case ("lobby", "home")? = pathElements.decompose2() {
// Open lobby
}

However, this serves to highlight a limitation: we can’t use this approach to match multiple patterns within a single switch statement, unless those patterns happen to have the same element count. In the example above, what would we switch on - decompose2() or decompose3()? Instead, we need a structure that can represent different element counts within the same type…

Approach 3: Linked List

This led me to try using an enum type, as enums also support value-binding patterns for their associated values. A linked list (here’s a nice implementation by Airspeed Velocity) seemed promising because it’s built out of enums and can represent an arbitrary number of elements. Here’s what it would look like:

1
2
3
if case .Node("user", .Node(let userId, .Node(_, .End))) = List(pathElements) {
// Open profile for userId
}

Unlike the previous approach, it can also be used to pattern-match lists of any size within a single switch statement:

1
2
3
4
5
6
7
8
9
10
11
12
13
switch List(pathElements) {

case .End, .Node("", .End):
// Open home
case .Node("lobby", .Node("main", .End)):
// Open lobby
case .Node("user", .Node(let userId, .Node(_, .End))):
// Open profile for userId
case .Node("user", .Node(_, .Node("login", .End))):
// Open login
default:
break
}

The trouble is, well, it’s ugly. All those parentheses and repeated .Nodes make it very difficult to read. .Node could be shortened to a single character but nesting multiple enums still generates a confusing amount of parentheses.

Approach 4: Counted

My final approach was a compromise between approaches 2 and 3. What was needed was an enum that could represent arbitrary numbers of elements without needing too many layers of nesting. Enter Counted:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
enum Counted<E> {

case N0
case N1(E)
case N2(E, E)
case N3(E, E, E)
case N4(E, E, E, E)
case N5(E, E, E, E, E)
case N6(E, E, E, E, E, E)
case N7(E, E, E, E, E, E, E)
case N8(E, E, E, E, E, E, E, E)
case N9(E, E, E, E, E, E, E, E, E)

indirect case N10(E, E, E, E, E, E, E, E, E, E, plus: Counted<E>)

init(_ elements: [E]) {

let e = elements

switch e.count {
case 0:
self = .N0
case 1:
self = .N1(e[0])
case 2:
self = .N2(e[0], e[1])
case 3:
self = .N3(e[0], e[1], e[2])
case 4:
self = .N4(e[0], e[1], e[2], e[3])
case 5:
self = .N5(e[0], e[1], e[2], e[3], e[4])
case 6:
self = .N6(e[0], e[1], e[2], e[3], e[4], e[5])
case 7:
self = .N7(e[0], e[1], e[2], e[3], e[4], e[5], e[6])
case 8:
self = .N8(e[0], e[1], e[2], e[3], e[4], e[5], e[6], e[7])
case 9:
self = .N9(e[0], e[1], e[2], e[3], e[4], e[5], e[6], e[7], e[8])
default:
self = .N10(e[0], e[1], e[2], e[3], e[4], e[5], e[6], e[7], e[8], e[9], plus: Counted(Array(e[10..<elements.endIndex])))
}
}
}

Counted is an enum where each case has a different number of elements as associated values. It can be initialized with an Array, and just like List, there’s an indirect case that enables arbitrarily large arrays to be represented via nested Counted enums. Unlike List, a layer of nesting is only required for every 10 elements, which makes things easier to read. Counted enables us to pattern-match paths with any number of elements, and supports expression and value-binding patterns for its associated values. It can also be used in switch statements:

1
2
3
4
5
6
7
8
9
10
11
12
switch Counted(pathElements) {
case .N0, .N1(""):
// Open home
case .N2("lobby", "main"):
// Open lobby
case .N3("user", let userId, "profile"):
// Open profile for userId
case .N3("user", _, "login"):
// Open login
default:
break
}

This can be extended for even greater flexibility. I mentioned that the expression pattern can be used to match based on more than simple equality. For example, I created a Regex struct that can match Strings based on a regular expression, and implemented the pattern-matching operator like so:

1
2
3
4
public func ~=(regex: Regex, string: String) -> Bool {

return regex.matches(string)
}

As a result we can use Regex to match individual path elements within Counted. For example, the following case would match both /pages/contact-us_gbr and /pages/contact-us_usa:

1
2
case .N2("pages", Regex("contact_.*")):
// Open contact page

I added structs Begins(...) and Ends(...), which use the pattern-matching operator to match Counted instances based purely on a slice of the path elements. I also added extensions to NSURL and NSURLComponents to make a Counted list of path elements and a Dictionary of query arguments easily available. The code is available here: URLPatterns.

Deep-linking

Now that I can do more idiomatic Swift pattern-matching for URL path elements, here’s how I use it for deep-linking. I define my app’s deep-link destinations as an enum:

1
2
3
4
5
6
enum DeepLink {

case Home, History, Settings, Terms, News, Contact
case Chat(room: String)
case Profile(userId: String)
}

I then add a failable initializer to DeepLink, which takes an NSURL. This is where the pattern-matching happens:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
extension DeepLink {

init?(url: NSURL) {

guard url.scheme == "myscheme" else { return nil }
guard url.host == "myhost" else { return nil }

switch url.countedPathComponents() {

case .N0, .N1(""): self = .Home
case .N1("history"): self = .History
case .N2(_, "settings"): self = .Settings
case .N2("chat", let room): self = .Chat(room: room)
case .N3("users", let userId, "profile"): self = .Profile(userId: userId)
case Begins("news", "latest"): self = .News
case Ends("terms"): self = .Terms
case .N2("pages", Regex("contact-us.*")) self = .Contact

default: return nil
}
}
}

Once the URL has been converted into a DeepLink, it can be passed to a DeepLinker for routing:

1
2
3
4
5
6
struct DeepLinker {

static func open(link: DeepLink, animated: Bool = true) -> Bool {
// switch on link, selecting tabs, pushing and presenting view controllers as appropriate
}
}

With that set up, opening a deeplink looks like this:

1
2
3
if let link = DeepLink(url: url) {
DeepLinker.open(link)
}

I prefer this approach to the approach taken by most URL routing libraries for a few reasons:

  • It’s simple to bypass URLs and open a deeplink directly, e.g. by calling DeepLinker.open(.Home).

  • The pattern-matching code is no longer in a third-party library, which makes it easier to debug.

  • The pattern-matching code leverages Swift’s built-in pattern-matching, which means it can be customized and extended.

  • The pattern-matching and routing processes are separated into two steps. This provides an override point if needed, e.g.:

1
2
3
if let link = DeepLink(url: url) where !link.requiresLogin {
DeepLinker.open(link)
}

What do you think? Do you like the ‘swiftier’ approach (damn, I nearly managed to avoid that word), or am I misrepresenting URL routing libraries?