How do you build, change and resolve URL paths safely?

2.3.3 · L2 · Interview

Predict first

What does this print?

let base = URL(string: "https://x.com/a/b/")!
print(URL(string: "../c", relativeTo: base)!.absoluteString)
  • https://x.com/a/c
  • https://x.com/a/b/c
  • https://x.com/c

Predict first

What does this print?

print(URL(string: "https://x.com/a/b")!.deletingLastPathComponent().absoluteString)
  • https://x.com/a
  • https://x.com/a/

Mental model. Path components are steps along a corridor. The trailing slash tells you whether the last step is a room or a door.

Edit paths with the URL APIs, not string slicing: appendingPathComponent(_:) (or appending(path:)) adds a segment and percent-encodes it, deletingLastPathComponent() removes one, and pathExtension / deletingPathExtension() handle the suffix. standardized collapses . and ...

A URL can also be relative to a base: URL(string: "../c", relativeTo: base). Resolution follows the browser rules, so a trailing slash on the base matters: /a/b/ is a folder, /a/b is a file named b, and the same relative link lands in different places. Call absoluteURL before storing a relative URL.

.. climbs out of the folder b/, so the link resolves next to it.

The result keeps a trailing slash, marking it as a folder.

Common traps

  • Concatenating "/" by hand. You get double slashes or missing ones, and no encoding.
  • Storing a relative URL. It loses its base on encoding in some paths. Convert with absoluteURL first.

They might follow up with

  • How do you swap a file extension? url.deletingPathExtension().appendingPathExtension("png").
  • Why does appendingPathComponent add a slash for folders? It asks the disk, for file URLs, unless you pass isDirectory:.

Related questions

  • 2.3.2 What is the difference between a file URL and a web URL, and URL(fileURLWithPath:) and URL(string:)?
  • 2.3.4 Why use URLComponents and URLQueryItem, and what do they still not encode for you?

← What is the difference between a file URL and a web URL, and…   Why use URLComponents and URLQueryItem, and what do they still not… →