{"id":9738,"date":"2025-03-31T14:00:00","date_gmt":"2025-03-31T11:00:00","guid":{"rendered":"https:\/\/handoli.com\/index.php\/2025\/03\/31\/modern-url-construction-in-swift\/"},"modified":"2025-03-31T14:00:00","modified_gmt":"2025-03-31T11:00:00","slug":"modern-url-construction-in-swift","status":"publish","type":"post","link":"https:\/\/handoli.com\/index.php\/2025\/03\/31\/modern-url-construction-in-swift\/","title":{"rendered":"Modern URL construction in Swift"},"content":{"rendered":"<p>These days, most applications need to work with URLs in some form. Perhaps they\u2019re 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\u2019s frameworks) represented using the dedicated <code>URL<\/code> type, rather than just using plain strings, which ensures that we\u2019re actually working with valid, properly formatted URLs.<\/p>\n<p>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 \u2014 such as in this case:<\/p>\n<pre class=\"splash\"><code><span class=\"s-keyword\">guard let<\/span> url = <span class=\"s-type\">URL<\/span>(string: <span class=\"s-string\">\"https:\/\/swiftbysundell.com\"<\/span>) <span class=\"s-keyword\">else<\/span> {\n    <span class=\"s-comment\">\/\/ Hmmm... now what?<\/span>\n    <span class=\"s-keyword\">return<\/span> <span class=\"s-call\">print<\/span>(<span class=\"s-string\">\"Invalid URL\"<\/span>)\n}<\/code><\/pre>\n<p>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 <em>unnecessary<\/em>. After all, there\u2019s no runtime variance involved here, so there\u2019s really no significant risk that the above kind of conversion will result in <code>nil<\/code>, unless we\u2019ve made a typo within our code.<\/p>\n<p>So, when working with such static URLs, it\u2019s very common to simply use force unwrapping to turn the resulting optional <code>URL<\/code> into a non-optional one:<\/p>\n<pre class=\"splash\"><code><span class=\"s-keyword\">let<\/span> url = <span class=\"s-type\">URL<\/span>(string: <span class=\"s-string\">\"https:\/\/swiftbysundell.com\"<\/span>)!<\/code><\/pre>\n<p>However, having to do the above kind of force unwrapping manually every time we want to construct a URL is not quite ideal \u2014 so let\u2019s see if we can improve things. First, let\u2019s extend <code>URL<\/code> with an initializer that accepts a <code>StaticString<\/code> (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\u2019ll use a custom <code>fatalError<\/code> message in case the conversion to a <code>URL<\/code> failed:<\/p>\n<pre class=\"splash\"><code><span class=\"s-keyword\">extension<\/span> <span class=\"s-type\">URL<\/span> {\n    <span class=\"s-keyword\">init<\/span>(staticString: <span class=\"s-type\">StaticString<\/span>) {\n        <span class=\"s-keyword\">guard let<\/span> url = <span class=\"s-type\">Self<\/span>(string: <span class=\"s-string\">\"<\/span>(staticString)<span class=\"s-string\">\"<\/span>) <span class=\"s-keyword\">else<\/span> {\n            <span class=\"s-call\">fatalError<\/span>(<span class=\"s-string\">\"Invalid static URL string:<\/span> (staticString)<span class=\"s-string\">\"<\/span>)\n        }\n\n        <span class=\"s-keyword\">self<\/span> = url\n    }\n}<\/code><\/pre>\n<blockquote>\n<p>Using a custom <code>fatalError<\/code> call in situations when we <em>have<\/em> 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 a <code>nil<\/code> value.<\/p>\n<\/blockquote>\n<p>With the above in place, we can now easily convert any static string within our code base into a <code>URL<\/code>, without having to deal with optionals at every single call site:<\/p>\n<pre class=\"splash\"><code><span class=\"s-keyword\">let<\/span> url = <span class=\"s-type\">URL<\/span>(staticString: <span class=\"s-string\">\"https:\/\/swiftbysundell.com\"<\/span>)<\/code><\/pre>\n<p>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 \u2014 <em>macros<\/em>.<\/p>\n<h2>It\u2019s macro time!<\/h2>\n<p>Let\u2019s see if we can write a Swift macro that\u2019ll let us not just convert, but also <em>validate<\/em> static URL strings at compile time. We\u2019ll start by jumping over to the command line, where we\u2019ll run the following command to create a new macro-based Swift package:<\/p>\n<pre><code class=\"no-highlight\">swift package init --type macro --name StaticURL<\/code><\/pre>\n<p>One thing that\u2019s neat about macro packages, is that they come pre-filled with most of the boilerplate that we\u2019ll need to define and vend our macro to any other targets that wish to use it \u2014 and it just so happens that the <code>stringify<\/code> macro that\u2019s added as an example is the exact same type of macro that we\u2019re looking to add \u2014 a <em>freestanding expression macro<\/em>.<\/p>\n<p>So let\u2019s go ahead and simply rename the definition of <code>stringify<\/code> to <code>staticURL<\/code>, and change its input and output types to match the <code>URL<\/code> extension we created earlier:<\/p>\n<pre class=\"splash\"><code><span class=\"s-keyword\">import<\/span> Foundation\n\n<span class=\"s-keyword\">@freestanding<\/span>(expression)\n<span class=\"s-keyword\">public macro<\/span> staticURL(<span class=\"s-keyword\">_<\/span> value: <span class=\"s-type\">StaticString<\/span>) -&gt; <span class=\"s-type\">URL<\/span> = <span class=\"s-call\">#externalMacro<\/span>(\n    module: <span class=\"s-string\">\"StaticURLMacros\"<\/span>,\n    type: <span class=\"s-string\">\"StaticURLMacro\"<\/span>\n)<\/code><\/pre>\n<p>Next, let\u2019s rename the <code>StringifyMacro<\/code> implementation to <code>StaticURLMacro<\/code> (matching the above definition\u2019s <code>type<\/code> argument), and replace its previous <code>expansion<\/code> 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.<\/p>\n<p>If all checks pass, then we generate the same kind of force-unwrapping <code>URL<\/code> construction code that we manually used to write, which will be the output of our macro. Here\u2019s what all of that looks like:<\/p>\n<pre class=\"splash\"><code><span class=\"s-keyword\">public struct<\/span> StaticURLMacro: <span class=\"s-type\">ExpressionMacro<\/span> {\n    <span class=\"s-keyword\">public static func<\/span> expansion(\n        of node: <span class=\"s-keyword\">some<\/span> <span class=\"s-type\">FreestandingMacroExpansionSyntax<\/span>,\n        in context: <span class=\"s-keyword\">some<\/span> <span class=\"s-type\">MacroExpansionContext<\/span>\n    ) <span class=\"s-keyword\">throws<\/span> -&gt; <span class=\"s-type\">ExprSyntax<\/span> {\n        <span class=\"s-comment\">\/\/ Verify that a string literal was passed, and extract\n        \/\/ the first segment. We can be sure that only one\n        \/\/ segment exists, since we're only accepting static\n        \/\/ strings (which cannot have any dynamic components):<\/span>\n        <span class=\"s-keyword\">guard let<\/span> argument = node.<span class=\"s-property\">arguments<\/span>.<span class=\"s-property\">first<\/span>?.<span class=\"s-property\">expression<\/span>,\n              <span class=\"s-keyword\">let<\/span> literal = argument.<span class=\"s-call\">as<\/span>(<span class=\"s-type\">StringLiteralExprSyntax<\/span>.<span class=\"s-keyword\">self<\/span>),\n              <span class=\"s-keyword\">case<\/span> .<span class=\"s-dotAccess\">stringSegment<\/span>(<span class=\"s-keyword\">let<\/span> segment) = literal.<span class=\"s-property\">segments<\/span>.<span class=\"s-property\">first<\/span>\n        <span class=\"s-keyword\">else<\/span> {\n            <span class=\"s-keyword\">throw<\/span> <span class=\"s-type\">StaticURLMacroError<\/span>.<span class=\"s-property\">notAStringLiteral<\/span>\n        }\n        \n        <span class=\"s-comment\">\/\/ Verify that the passed string is indeed a valid URL:<\/span>\n        <span class=\"s-keyword\">guard<\/span> <span class=\"s-type\">URL<\/span>(string: segment.<span class=\"s-property\">content<\/span>.<span class=\"s-property\">text<\/span>) != <span class=\"s-keyword\">nil else<\/span> {\n            <span class=\"s-keyword\">throw<\/span> <span class=\"s-type\">StaticURLMacroError<\/span>.<span class=\"s-property\">invalidURL<\/span>\n        }\n\n        <span class=\"s-comment\">\/\/ Generate the code required to construct a URL value\n        \/\/ for the passed string at runtime:<\/span>\n        <span class=\"s-keyword\">return<\/span> <span class=\"s-string\">\"Foundation.URL(string:<\/span> (argument)<span class=\"s-string\">)!\"<\/span>\n    }\n}<\/code><\/pre>\n<blockquote>\n<p>Note how we prefix the <code>URL<\/code> type with its parent module (<code>Foundation<\/code>) above. That\u2019s to avoid conflicts if our macro is used within a context that has declared its own <code>URL<\/code> type. Applying such prefixes isn\u2019t typically necessary when writing code manually, but is a good practice when writing macros, since we don\u2019t know up-front exactly where our macros will end up being used.<\/p>\n<\/blockquote>\n<p>With our macro implementation done, all that remains is to define the <code>StaticURLMacroError<\/code> type that\u2019s used above, and to update our <code>CompilerPlugin<\/code> to provide the correct macro type:<\/p>\n<pre class=\"splash\"><code><span class=\"s-keyword\">enum<\/span> StaticURLMacroError: <span class=\"s-type\">String<\/span>, <span class=\"s-type\">Error<\/span>, <span class=\"s-type\">CustomStringConvertible<\/span> {\n    <span class=\"s-keyword\">case<\/span> notAStringLiteral = <span class=\"s-string\">\"Argument is not a string literal\"<\/span>\n    <span class=\"s-keyword\">case<\/span> invalidURL = <span class=\"s-string\">\"Argument is not a valid URL\"<\/span>\n\n    <span class=\"s-keyword\">public var<\/span> description: <span class=\"s-type\">String<\/span> { rawValue }\n}\n\n<span class=\"s-keyword\">@main struct<\/span> StaticURLPlugin: <span class=\"s-type\">CompilerPlugin<\/span> {\n    <span class=\"s-keyword\">let<\/span> providingMacros: [<span class=\"s-type\">Macro<\/span>.<span class=\"s-type\">Type<\/span>] = [<span class=\"s-type\">StaticURLMacro<\/span>.<span class=\"s-keyword\">self<\/span>]\n}<\/code><\/pre>\n<p>With all those pieces in place, if we integrate our new <code>StaticURL<\/code> macro package within an application, then we can now easily define static, 100% compile-time validated URLs wherever we\u2019d like:<\/p>\n<pre class=\"splash\"><code><span class=\"s-keyword\">let<\/span> url = <span class=\"s-call\">#staticURL<\/span>(<span class=\"s-string\">\"https:\/\/swiftbysundell.com\"<\/span>)<\/code><\/pre>\n<p>Neat! It could definitely be argued that using a macro isn\u2019t really necessary for a use case like this, given that our earlier <code>StaticString<\/code>-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.<\/p>\n<h2>Dynamic components<\/h2>\n<p>So far, we\u2019ve 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\u2019re using string interpolation to define a URL that\u2019ll be used to load <code>User<\/code> data from a given web API endpoint:<\/p>\n<pre class=\"splash\"><code><span class=\"s-keyword\">actor<\/span> NetworkingService {\n    <span class=\"s-keyword\">private static let<\/span> baseURL = <span class=\"s-string\">\"https:\/\/api.myapp.com\"<\/span>\n    ...\n\n    <span class=\"s-keyword\">func<\/span> loadUser(withID id: <span class=\"s-type\">User<\/span>.<span class=\"s-type\">ID<\/span>) <span class=\"s-keyword\">async throws<\/span> -&gt; <span class=\"s-type\">User<\/span> {\n        <span class=\"s-keyword\">guard let<\/span> url = <span class=\"s-type\">URL<\/span>(\n            string: <span class=\"s-string\">\"<\/span>(<span class=\"s-type\">Self<\/span>.<span class=\"s-property\">baseURL<\/span>)<span class=\"s-string\">\/users\/<\/span>(id)<span class=\"s-string\">?refresh=true\"<\/span>\n        ) <span class=\"s-keyword\">else<\/span> {\n            <span class=\"s-keyword\">throw<\/span> <span class=\"s-type\">NetworkingError<\/span>.<span class=\"s-property\">invalidURL<\/span>\n        }\n\n        ...\n    }\n}<\/code><\/pre>\n<p>Here we\u2019re facing a very similar problem as when working with static URLs \u2014 when reading the above code, we can see that there\u2019s no way that the performed <code>URL<\/code> conversion will ever result in <code>nil<\/code>, given that our <code>baseURL<\/code> and <code>\/users\/<\/code> strings are both static, and if we assume that <code>User.ID<\/code> values are always URL-safe.<\/p>\n<p>So would it be possible to convince the compiler that a <code>nil<\/code> result can never occur, even when working with dynamic URL components? An initial idea might be to use Foundation\u2019s dedicated <code>URLComponents<\/code> builder \u2014 which offers a structured way to construct dynamic URLs.<\/p>\n<p>While that approach <em>does<\/em> have some key advantages over using string interpolation (since we\u2019re now assigning values to explicit parts of the URL we\u2019re building, rather than just working with a loosely formed string) \u2014 it\u2019s significantly more verbose in comparison, while still not getting rid of having to unwrap our URL as an optional:<\/p>\n<pre class=\"splash\"><code><span class=\"s-keyword\">actor<\/span> NetworkingService {\n    <span class=\"s-keyword\">private static let<\/span> baseURLComponents = {\n        <span class=\"s-keyword\">var<\/span> components = <span class=\"s-type\">URLComponents<\/span>()\n        components.<span class=\"s-property\">scheme<\/span> = <span class=\"s-string\">\"https\"<\/span>\n        components.<span class=\"s-property\">host<\/span> = <span class=\"s-string\">\"api.myapp.com\"<\/span>\n        <span class=\"s-keyword\">return<\/span> components\n    }()\n    \n    ...\n\n    <span class=\"s-keyword\">func<\/span> loadUser(withID id: <span class=\"s-type\">User<\/span>.<span class=\"s-type\">ID<\/span>) <span class=\"s-keyword\">async throws<\/span> -&gt; <span class=\"s-type\">User<\/span> {\n        <span class=\"s-keyword\">var<\/span> urlComponents = <span class=\"s-type\">Self<\/span>.<span class=\"s-property\">baseURLComponents<\/span>\n        urlComponents.<span class=\"s-property\">path<\/span> = <span class=\"s-string\">\"\/users\/<\/span>(id)<span class=\"s-string\">\"<\/span>\n        urlComponents.<span class=\"s-property\">queryItems<\/span> = [\n            <span class=\"s-type\">URLQueryItem<\/span>(name: <span class=\"s-string\">\"refresh\"<\/span>, value: <span class=\"s-string\">\"true\"<\/span>)\n        ]\n\n        <span class=\"s-keyword\">guard let<\/span> url = urlComponents.<span class=\"s-property\">url<\/span> <span class=\"s-keyword\">else<\/span> {\n            <span class=\"s-keyword\">throw<\/span> <span class=\"s-type\">NetworkingError<\/span>.<span class=\"s-property\">invalidURL<\/span>\n        }\n\n        ...\n    }\n}<\/code><\/pre>\n<p>Thankfully, it turns out that there\u2019s 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 \u2014 when combined with our static URL handling code from before \u2014 lets us both completely get rid of optionals, and gives us a really nice syntax for constructing our API call URL.<\/p>\n<p>If we declare our base URL as a static <code>URL<\/code> value (rather than a string, or a <code>URLComponents<\/code> value), then we can simply call different overloads of the <code>appending<\/code> API on that value to construct our dynamic URL in a completely optional-free manner \u2014 like this:<\/p>\n<pre class=\"splash\"><code><span class=\"s-keyword\">actor<\/span> NetworkingService {\n    <span class=\"s-keyword\">private static let<\/span> baseURL = <span class=\"s-call\">#staticURL<\/span>(<span class=\"s-string\">\"https:\/\/api.myapp.com\"<\/span>)\n    ...\n\n    <span class=\"s-keyword\">func<\/span> loadUser(withID id: <span class=\"s-type\">User<\/span>.<span class=\"s-type\">ID<\/span>) <span class=\"s-keyword\">async throws<\/span> -&gt; <span class=\"s-type\">User<\/span> {\n        <span class=\"s-keyword\">let<\/span> url = <span class=\"s-type\">Self<\/span>.<span class=\"s-property\">baseURL<\/span>\n            .<span class=\"s-call\">appending<\/span>(components: <span class=\"s-string\">\"users\"<\/span>, id)\n            .<span class=\"s-call\">appending<\/span>(queryItems: [\n                <span class=\"s-type\">URLQueryItem<\/span>(name: <span class=\"s-string\">\"refresh\"<\/span>, value: <span class=\"s-string\">\"true\"<\/span>)\n            ])\n            \n        ...\n    }\n}<\/code><\/pre>\n<p>Very nice! And the good news is that we\u2019re not limited to just using the above kind of solution when constructing URLs used to perform network calls \u2014 we can also use the same suite of APIs when working with file system URLs that we\u2019d previously resolve using <code>FileManager<\/code>, such as in this example:<\/p>\n<pre class=\"splash\"><code><span class=\"s-keyword\">private extension<\/span> <span class=\"s-type\">NetworkingService<\/span> {\n    <span class=\"s-keyword\">func<\/span> cacheResponseOnDisk(<span class=\"s-keyword\">_<\/span> response: <span class=\"s-type\">Response<\/span>) <span class=\"s-keyword\">throws<\/span> {\n        <span class=\"s-keyword\">guard let<\/span> cacheFolderURL = <span class=\"s-type\">FileManager<\/span>.<span class=\"s-property\">default<\/span>.<span class=\"s-call\">urls<\/span>(\n            for: .<span class=\"s-dotAccess\">cachesDirectory<\/span>,\n            in: .<span class=\"s-dotAccess\">userDomainMask<\/span>\n        ).<span class=\"s-property\">first<\/span> <span class=\"s-keyword\">else<\/span> {\n            <span class=\"s-keyword\">throw<\/span> <span class=\"s-type\">NetworkingError<\/span>.<span class=\"s-property\">failedToResolveCacheFolder<\/span>\n        }\n\n        ...\n    }\n}<\/code><\/pre>\n<p>If we now convert the above code to use the new URL construction APIs, then we\u2019ll end up with a another non-optional solution, just as when constructing our web API URL:<\/p>\n<pre class=\"splash\"><code><span class=\"s-keyword\">private extension<\/span> <span class=\"s-type\">NetworkingService<\/span> {\n    <span class=\"s-keyword\">func<\/span> cacheResponseOnDisk(<span class=\"s-keyword\">_<\/span> response: <span class=\"s-type\">Response<\/span>) <span class=\"s-keyword\">throws<\/span> {\n        <span class=\"s-keyword\">let<\/span> cacheURL = <span class=\"s-type\">URL<\/span>\n            .<span class=\"s-dotAccess\">cachesDirectory<\/span>\n            .<span class=\"s-call\">appending<\/span>(component: response.<span class=\"s-property\">cacheID<\/span>)\n\n        ...\n    }\n}<\/code><\/pre>\n<p><code>URL<\/code> now also contains a number of other static properties that can be used to reference common folders on Apple\u2019s platforms, such as the home and temporary directories \u2014 all of which hold a predictable, non-optional value:<\/p>\n<pre class=\"splash\"><code><span class=\"s-type\">URL<\/span>.<span class=\"s-property\">homeDirectory<\/span>\n<span class=\"s-type\">URL<\/span>.<span class=\"s-property\">documentsDirectory<\/span>\n<span class=\"s-type\">URL<\/span>.<span class=\"s-property\">desktopDirectory<\/span>\n<span class=\"s-type\">URL<\/span>.<span class=\"s-property\">temporaryDirectory<\/span><\/code><\/pre>\n<p>So, as long as we\u2019re targeting the equivalent of iOS 16 or above within a given project, then we\u2019re now able to quite easily construct both web and file system URLs, even when they contain dynamic paths and components, such as query items.<\/p>\n<h2>Conclusion<\/h2>\n<p>Using Foundation\u2019s modern URL construction APIs to be able to avoid optionals when creating <code>URL<\/code> values doesn\u2019t just simplify our code, it also reduces the risk of bugs and crashes, and further lets us work with URLs in more structured ways \u2014 by replacing things like string interpolation with dedicated APIs for appending path components and query items.<\/p>\n<p>I hope you\u2019ve enjoyed reading this first Swift by Sundell article in over two years, and that you\u2019ll 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 <a href=\"https:\/\/mastodon.social\/@johnsundell\">Mastodon<\/a> or <a href=\"https:\/\/bsky.app\/profile\/johnsundell.bsky.social\">Bluesky<\/a>.<\/p>\n<p>Thanks for reading \u2014 and hey, it\u2019s good to be back!<\/p>","protected":false},"excerpt":{"rendered":"<p>These days, most applications need to work with URLs in some form. Perhaps they\u2019re 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\u2019s frameworks) represented using the dedicated URL type, rather than just using [&hellip;]<\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"rop_custom_images_group":[],"rop_custom_messages_group":[],"rop_publish_now":"initial","rop_publish_now_accounts":[],"rop_publish_now_history":[],"rop_publish_now_status":"pending","footnotes":""},"categories":[1,15],"tags":[],"class_list":["post-9738","post","type-post","status-publish","format-standard","hentry","category-explore","category-world"],"_links":{"self":[{"href":"https:\/\/handoli.com\/index.php\/wp-json\/wp\/v2\/posts\/9738","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/handoli.com\/index.php\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/handoli.com\/index.php\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/handoli.com\/index.php\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/handoli.com\/index.php\/wp-json\/wp\/v2\/comments?post=9738"}],"version-history":[{"count":0,"href":"https:\/\/handoli.com\/index.php\/wp-json\/wp\/v2\/posts\/9738\/revisions"}],"wp:attachment":[{"href":"https:\/\/handoli.com\/index.php\/wp-json\/wp\/v2\/media?parent=9738"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/handoli.com\/index.php\/wp-json\/wp\/v2\/categories?post=9738"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/handoli.com\/index.php\/wp-json\/wp\/v2\/tags?post=9738"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}