Modern URL construction in Swift
These days, most applications need to work with URLs in some form. Perhaps they’re used to make network calls, to read and write files, or to perform various kinds of database operations. In Swift, URLs are by convention (and through the design of Apple’s frameworks) represented using the dedicated URL
type, rather than just using plain strings, which ensures that we’re actually working with valid, properly formatted URLs.
However, that also means that anytime that we have a string that we wish to treat as a URL, we have to perform a conversion that returns an optional — such as in this case:
guard let url = URL(string: "https://swiftbysundell.com") else {
// Hmmm... now what?
return print("Invalid URL")
}
For URLs such as the one above, which are constructed using static string literals, using a conversion that can fail does arguably feel a bit unnecessary. After all, there’s no runtime variance involved here, so there’s really no significant risk that the above kind of conversion will result in nil
, unless we’ve made a typo within our code.
So, when working with such static URLs, it’s very common to simply use force unwrapping to turn the resulting optional URL
into a non-optional one:
let url = URL(string: "https://swiftbysundell.com")!
However, having to do the above kind of force unwrapping manually every time we want to construct a URL is not quite ideal — so let’s see if we can improve things. First, let’s extend URL
with an initializer that accepts a StaticString
(which are Swift string literals without any kind of interpolation or dynamic components), within which we can perform the required unwrapping, but this time we’ll use a custom fatalError
message in case the conversion to a URL
failed:
extension URL {
init(staticString: StaticString) {
guard let url = Self(string: "\(staticString)") else {
fatalError("Invalid static URL string: \(staticString)")
}
self = url
}
}
Using a custom
fatalError
call in situations when we have to force unwrap a value is in general a good practice, since that lets us provide additional context that can be incredibly useful if we ever need to debug a crash caused by anil
value.
With the above in place, we can now easily convert any static string within our code base into a URL
, without having to deal with optionals at every single call site:
let url = URL(staticString: "https://swiftbysundell.com")
Nice! Up until Swift 5.9, the above approach was more or less the best simple way to work with inline, static URLs in a non-optional manner (without requiring any external tools, such as code generation). However, Swift 5.9 introduced a new feature that can be incredibly useful in situations like this — macros.
It’s macro time!
Let’s see if we can write a Swift macro that’ll let us not just convert, but also validate static URL strings at compile time. We’ll start by jumping over to the command line, where we’ll run the following command to create a new macro-based Swift package:
swift package init --type macro --name StaticURL
One thing that’s neat about macro packages, is that they come pre-filled with most of the boilerplate that we’ll need to define and vend our macro to any other targets that wish to use it — and it just so happens that the stringify
macro that’s added as an example is the exact same type of macro that we’re looking to add — a freestanding expression macro.
So let’s go ahead and simply rename the definition of stringify
to staticURL
, and change its input and output types to match the URL
extension we created earlier:
import Foundation
@freestanding(expression)
public macro staticURL(_ value: StaticString) -> URL = #externalMacro(
module: "StaticURLMacros",
type: "StaticURLMacro"
)
Next, let’s rename the StringifyMacro
implementation to StaticURLMacro
(matching the above definition’s type
argument), and replace its previous expansion
code with some logic that first ensures that the passed argument is indeed a string literal (although the Swift type system should already have verified that for us), and then extracts the string and attempts to construct a URL using it.
If all checks pass, then we generate the same kind of force-unwrapping URL
construction code that we manually used to write, which will be the output of our macro. Here’s what all of that looks like:
public struct StaticURLMacro: ExpressionMacro {
public static func expansion(
of node: some FreestandingMacroExpansionSyntax,
in context: some MacroExpansionContext
) throws -> ExprSyntax {
// Verify that a string literal was passed, and extract
// the first segment. We can be sure that only one
// segment exists, since we're only accepting static
// strings (which cannot have any dynamic components):
guard let argument = node.arguments.first?.expression,
let literal = argument.as(StringLiteralExprSyntax.self),
case .stringSegment(let segment) = literal.segments.first
else {
throw StaticURLMacroError.notAStringLiteral
}
// Verify that the passed string is indeed a valid URL:
guard URL(string: segment.content.text) != nil else {
throw StaticURLMacroError.invalidURL
}
// Generate the code required to construct a URL value
// for the passed string at runtime:
return "Foundation.URL(string: \(argument))!"
}
}
Note how we prefix the
URL
type with its parent module (Foundation
) above. That’s to avoid conflicts if our macro is used within a context that has declared its ownURL
type. Applying such prefixes isn’t typically necessary when writing code manually, but is a good practice when writing macros, since we don’t know up-front exactly where our macros will end up being used.
With our macro implementation done, all that remains is to define the StaticURLMacroError
type that’s used above, and to update our CompilerPlugin
to provide the correct macro type:
enum StaticURLMacroError: String, Error, CustomStringConvertible {
case notAStringLiteral = "Argument is not a string literal"
case invalidURL = "Argument is not a valid URL"
public var description: String { rawValue }
}
@main struct StaticURLPlugin: CompilerPlugin {
let providingMacros: [Macro.Type] = [StaticURLMacro.self]
}
With all those pieces in place, if we integrate our new StaticURL
macro package within an application, then we can now easily define static, 100% compile-time validated URLs wherever we’d like:
let url = #staticURL("https://swiftbysundell.com")
Neat! It could definitely be argued that using a macro isn’t really necessary for a use case like this, given that our earlier StaticString
-based extension approach worked just fine (apart from the danger of typos). Like in many cases when working with Swift, this is essentially a trade-off between increased complexity and compile-time safety, and whether or not the additional complexity of a macro will be worth it will likely vary from project to project.
Dynamic components
So far, we’ve been working with URLs that are known at compile time, but what about ones that have to be constructed at runtime? For example, here we’re using string interpolation to define a URL that’ll be used to load User
data from a given web API endpoint:
actor NetworkingService {
private static let baseURL = "https://api.myapp.com"
...
func loadUser(withID id: User.ID) async throws -> User {
guard let url = URL(
string: "\(Self.baseURL)/users/\(id)?refresh=true"
) else {
throw NetworkingError.invalidURL
}
...
}
}
Here we’re facing a very similar problem as when working with static URLs — when reading the above code, we can see that there’s no way that the performed URL
conversion will ever result in nil
, given that our baseURL
and /users/
strings are both static, and if we assume that User.ID
values are always URL-safe.
So would it be possible to convince the compiler that a nil
result can never occur, even when working with dynamic URL components? An initial idea might be to use Foundation’s dedicated URLComponents
builder — which offers a structured way to construct dynamic URLs.
While that approach does have some key advantages over using string interpolation (since we’re now assigning values to explicit parts of the URL we’re building, rather than just working with a loosely formed string) — it’s significantly more verbose in comparison, while still not getting rid of having to unwrap our URL as an optional:
actor NetworkingService {
private static let baseURLComponents = {
var components = URLComponents()
components.scheme = "https"
components.host = "api.myapp.com"
return components
}()
...
func loadUser(withID id: User.ID) async throws -> User {
var urlComponents = Self.baseURLComponents
urlComponents.path = "/users/\(id)"
urlComponents.queryItems = [
URLQueryItem(name: "refresh", value: "true")
]
guard let url = urlComponents.url else {
throw NetworkingError.invalidURL
}
...
}
}
Thankfully, it turns out that there’s a much simpler suite of APIs for dynamic URL construction that were introduced in iOS 16 (and the other 2022 Apple operating system versions) that — when combined with our static URL handling code from before — lets us both completely get rid of optionals, and gives us a really nice syntax for constructing our API call URL.
If we declare our base URL as a static URL
value (rather than a string, or a URLComponents
value), then we can simply call different overloads of the appending
API on that value to construct our dynamic URL in a completely optional-free manner — like this:
actor NetworkingService {
private static let baseURL = #staticURL("https://api.myapp.com")
...
func loadUser(withID id: User.ID) async throws -> User {
let url = Self.baseURL
.appending(components: "users", id)
.appending(queryItems: [
URLQueryItem(name: "refresh", value: "true")
])
...
}
}
Very nice! And the good news is that we’re not limited to just using the above kind of solution when constructing URLs used to perform network calls — we can also use the same suite of APIs when working with file system URLs that we’d previously resolve using FileManager
, such as in this example:
private extension NetworkingService {
func cacheResponseOnDisk(_ response: Response) throws {
guard let cacheFolderURL = FileManager.default.urls(
for: .cachesDirectory,
in: .userDomainMask
).first else {
throw NetworkingError.failedToResolveCacheFolder
}
...
}
}
If we now convert the above code to use the new URL construction APIs, then we’ll end up with a another non-optional solution, just as when constructing our web API URL:
private extension NetworkingService {
func cacheResponseOnDisk(_ response: Response) throws {
let cacheURL = URL
.cachesDirectory
.appending(component: response.cacheID)
...
}
}
URL
now also contains a number of other static properties that can be used to reference common folders on Apple’s platforms, such as the home and temporary directories — all of which hold a predictable, non-optional value:
URL.homeDirectory
URL.documentsDirectory
URL.desktopDirectory
URL.temporaryDirectory
So, as long as we’re targeting the equivalent of iOS 16 or above within a given project, then we’re now able to quite easily construct both web and file system URLs, even when they contain dynamic paths and components, such as query items.

Swift by Sundell is brought to you by the Genius Scan SDK — Add a powerful document scanner to any mobile app, and turn scans into high-quality PDFs with one line of code. Try it today.
Conclusion
Using Foundation’s modern URL construction APIs to be able to avoid optionals when creating URL
values doesn’t just simplify our code, it also reduces the risk of bugs and crashes, and further lets us work with URLs in more structured ways — by replacing things like string interpolation with dedicated APIs for appending path components and query items.
I hope you’ve enjoyed reading this first Swift by Sundell article in over two years, and that you’ll find it useful when working on your Swift projects. If you have any questions, feedback, or comments, then feel free to reach out via either Mastodon or Bluesky.
Thanks for reading — and hey, it’s good to be back!