A Comprehensive Guide to URLs in Swift and SwiftUI

URLs are omnipresent in today’s world, and iOS apps are no exception.

The URLs you use in Swift can identify different resources. The most common examples are web content, local files, and REST API endpoints.

The way you handle a URL in Swift depends on two things. The resource it identifies and how you manage the resource in the architecture of your app.

Architecting SwiftUI apps with MVC and MVVM

GET THE FREE BOOK NOW

Table of contents

What is a URL, and how to use URLs in Swift

Swift programs and iOS apps often need to access different resources to collect data. For example, a file or a program running on a machine on the internet.

These resources are referenced using Uniform Resource Locators, or URLs. URLs specify the location of a resource on a computer network and describe how to access it.

URLs are a subset of Uniform Resource Identifiers, or URIs. URIs can identify real-world objects, such as people, places, and resources.

Since URLs are simple strings, you can represent them in Swift using the String type. However, this approach puts the burden of handling a URL’s details onto the developer using string interpolation.

Instead, you can use the URL, URLComponents, and URLQueryItem types provided by the Foundation framework to handle URLs.

How URLs identify local and web resources

The formal definition of URIs and URLs is quite complex. For our intents and purposes, the structure of a URL can be broken into five simple components.

The structure of a URL

  • The scheme specifies how the components of a URL are to be interpreted.

The most common schemes you will find on the internet are http and https. These are for the Hypertext Transfer Protocol and its secure version.

Another standard scheme in iOS and macOS apps is file for URLs. This points to a file on one’s own device. If you use Universal Links, you can define custom schemes for your apps.

  • The authority identifies who handles a resource.

Often, the authority includes only the host, i.e., the name or the address of a computer on the internet. For example, www.apple.com.

Sometimes the authority can include a userinfo (_username_ and password). It might also include a port on which a program is listening. The well-known port for HTTP servers on the internet is 80, so you usually don’t need to specify it.

  • A path, which identifies the resource we are looking for.

For example, https://api.stackexchange.com/2.3/questions.

  • A query, which contains extra parameters that affect how a resource behaves.

These parameters are usually in the form of key=value pairs. You can use them to specify, for example, the filtering, sorting, or paging of the data you retrieve.

For example, https://api.github.com/repos/octocat/Hello-World/issues?sort=updated&direction=asc.

  • A fragment, which identifies a secondary resource inside the primary resource referenced by the URL.

Fragments are used to identify a specific section on a web page. For example, https://en.wikipedia.org/wiki/J._R._R._Tolkien#Writing.

Managing URL strings and converting them to more convenient types

It’s not recommended practice to rely on strings for URLs. All APIs in Apple’s SDKs only accept values of the URL type.

However, you will occasionally need to hardcode some URLs. This is especially true when your app relies on a specific public or private API.

To do this, use the init(string:) initializer of the URL type.

let url = URL(string: "https://www.matteomanferdini.com/")!
print(url.absoluteString) // https://www.matteomanferdini.com/

The absoluteString property returns the full URL as a String. While you should not pass URL strings around your code, the approach is helpful in some instances. For example, when you want to:

  • Display a URL in the user interface of your app.
  • Store a URL in a database.
  • Encode a URL for network transmission.

The init(string:) initializer returns nil when the URL string you pass is malformed. This is useful when you read URLs from data, but your hardcoded URLs are usually safe.

To remove the forced unwrapping, create a custom initializer that takes a StaticString.

extension URL {
	init(staticString: StaticString) {
		self.init(string: "\(staticString)")!
	}
}

var safeURL = URL(staticString: "https://www.matteomanferdini.com/")

If you find yourself using this initializer too much, it might be a code smell. It’s more common to build all your URLs from a base URL.

let baseURL = URL(string: "https://www.matteomanferdini.com/")!
let mvvmURL = URL(string: "mvvm", relativeTo: baseURL)!
print(mvvmURL.absoluteString) // https://www.matteomanferdini.com/mvvm

Encoding and decoding URLs in JSON data

Many string URLs will come inside the JSON data returned by a REST API.

let json = """
{
    "api_link":"https://api.artic.edu/api/v1/artworks/16568",
    "title":"Water Lilies",
    "artist_display":"Claude Monet\\nFrench, 1840-1926",
    "place_of_origin":"France",
	"medium_display":"Oil on canvas",
}
"""

let data = json.data(using: .utf8)!

Here, the URL type is encodable and decodable. This means you don’t need to convert the string to a URL.

struct Artwork: Decodable {
	let apiLink: URL
	let title: String
	let artistDisplay: String
	let placeOfOrigin: String
	let mediumDisplay: String
}

let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase
let artwork = try decoder.decode(Artwork.self, from: data)

Building a URL and inspecting its individual components

When you work with remote APIs, you rarely inspect the URLs you receive. Mostly, you use them as they are.

However, there are cases where you need to analyze the contents of a URL. That happens when you receive a URL and have to determine which resource it identifies instead of using the URL to retrieve a resource located somewhere else.

One typical example in iOS app development is when Universal Links are used. These allow web pages or other apps to link to content inside your app. That means your app has to analyze the URLs it receives to display the appropriate content.

To let your app receive Universal Links, follow the two steps in this guide from Apple. Once your app gets a Universal Link, it needs to analyze the components of the received URL. Then, it can perform an action or display some content.

How you analyze the URL depends on the structure of your app.

let booksURL = URL(string: "http://gutendex.com/books/?author_year_start=1900&languages=en,fr")!
booksURL.scheme // http
booksURL.host // gutendex.com
booksURL.path // /books
booksURL.query // "author_year_start=1900&languages=en,fr"

Read the parameters in a URL’s query

The APIs of the URL type are limited, especially when building URLs or reading the query parameters.

The Foundation framework, therefore, offers the URLComponents type. This type replicates some of the APIs of the URL type we saw above. According to Apple’s documentation:

This structure parses and constructs URLs according to RFC 3986. Its behavior differs subtly from that of the URL structure, which conforms to older RFCs. However, you can easily obtain a URL value based on the contents of a URLComponents value or vice versa.

Parsing the query of a URL to get its parameters is tedious and error-prone. It’s better to use the URLComponents type.

let hnURL = URL(string: "https://hn.algolia.com/?dateRange=last24h&page=0&prefix=false&query=swift&sort=byPopularity&type=story")!
let components = URLComponents(url: hnURL, resolvingAgainstBaseURL: false)!
print(components.queryItems!)
// [dateRange=last24h, page=0, prefix=false, query=swift, sort=byPopularity, type=story]

The queryItems property of URLComponents returns an array of URLQueryItem values. These have name and value properties.

To get the value of a specific parameter, you must filter the queryItems array by name.

let hnURL = URL(string: "https://hn.algolia.com/?dateRange=last24h&page=0&prefix=false&query=swift&sort=byPopularity&type=story")!
let components = URLComponents(url: hnURL, resolvingAgainstBaseURL: false)!
let dateRange = components.queryItems!.first { queryItem -> Bool in
	queryItem.name == "dateRange"
}!.value
// last24h

Assembling a URL from its individual components

When you deal with websites or REST APIs, you will often need to assemble URLs to point to remote resources.

Again, you could use string interpolation to create the desired URL. However, it’s better to use the URLComponents type to avoid creating malformed URLs.

struct APIResource {
	let path: String
	let site: String
	let tag: String

	var url: URL {
		var components = URLComponents(string: "https://api.stackexchange.com")!
		components.path = "/2.3/\(path)"
		components.queryItems = [
			URLQueryItem(name: "site", value: site),
			URLQueryItem(name: "tagged", value: tag)
		]
		return components.url!
	}
}

let resource = APIResource(path: "questions", site: "crypto", tag: "random-number-generator")
print(resource.url)
// https://api.stackexchange.com/2.3/questions?site=crypto&tagged=random-number-generator

How much abstraction you use to create a URL depends on the REST API you use and the requests your app needs to perform. For more information, read my article on defining the resources of a REST API.

Using file URLs to read and write data on the device’s file system

A web URL is not the only common type of URL you find in iOS apps. The other is file URLs.

You use file URLs to read and write data in a file on the device’s disk. File URLs are like all other URLs, with a few specific distinctions:

  • The scheme of a file URL is file.
  • The path points to a location on the file system. Each path component is a directory, except the last one, which is a file.
  • There is no query.

Using the APIs above, you could create a file URL like any other URL. However, that’s not how you usually generate file URLs in iOS apps. Instead, you get a URL that points to a file through the Bundle and FileManager classes.

Reading the bundled contents of an app

Use the Bundle class to retrieve the resources bundled with your app or Swift package. For the former, use the main static property of the Bundle type. For the latter, use the module static property.

For example, to open a File.json file bundled with your app, use the following code:

let jsonFileURL = Bundle.main.url(forResource: "File", withExtension: "json")!
let jsonData = try! Data(contentsOf: jsonFileURL)

The data is usually decoded using the JSONDecoder class.

Accessing files on the file system

The URL type has some convenient computed properties to get URLs pointing to the file system locations where your iOS apps can read or write files.

The most common location is the document directory. This is where you save the user data generated by your app. The data is generally media files and documents.

let avatarFileURL = URL.documentsDirectory
	.appendingPathComponent("avatar")
	.appendingPathExtension("jpg")

Handling URLs in SwiftUI apps

There are different ways to handle URLs in a SwiftUI app.

  • If the URL points to an image, you can display that image directly inside the user interface of your app.
  • If the URL is a Universal Link, you can trigger the opening of the appropriate app on the user’s device.
  • If the URL points to a web page, you can display the content inside your app or the user’s default browser.

Other kinds of URLs, like calls to a REST API or file URLs, are usually downloaded using the URLSession class. This usually happens in the lower layers of an app’s architecture–for example, controllers or view models.

Loading a remote image using AsyncImage

You can download and display an image in your user interface using SwiftUI’s AsyncImage view.

struct ImageView: View {
	let imageURL = URL(string: "https://randomfox.ca/images/70.jpg")!

	var body: some View {
		AsyncImage(url: imageURL)
			.frame(width: 200, height: 200)
	}
}

You usually know in advance whether a URL points to an image from your app’s structure and the data it displays.

When you don’t know, you can check the file extension in a URL using the pathExtension property of the URL type. However, this approach is not the most reliable.

The only way to be sure is to perform an HTTP request with the URL and check the Content-Type header in the response.

Notice that the AsyncImage view does not give you access to the downloaded data. This is in case you want to store or manipulate it. That might seem annoying, but I think it’s a wise decision on Apple’s part.

AsyncImage is convenient for displaying remote images in your app. But you should not put networking code inside your user interface code.

To perform an HTTP request or download the data of an image, perform an asynchronous call in the lower layers of your app’s architecture.

Opening a URL in the device’s default web browser or another app

To display a tappable URL in your app’s user interface, use the Link view.

struct ContentView: View {
	let twitterURL = URL(string: "https://twitter.com/MatManferdini")!

	var body: some View {
		Link("My Twitter profile", destination: twitterURL)
	}
}

You can customize the visual appearance of a Link view using the buttonStyle(_:) view modifier.

SwiftUI opens a Universal Link in the associated app if possible. Alternatively, it will open the user’s default web browser (usually Safari).

If instead, you want to open a URL when a specific event happens, you need to use the openURL environment value. For example, when a view appears on the screen:

struct ContentView: View {
	let twitterURL = URL(string: "https://twitter.com/MatManferdini")!
	@Environment(\.openURL) private var openURL

	var body: some View {
		Text("Hello, World!")
			.onAppear {
				openURL(twitterURL)
			}
	}
}

If you need to, you can customize how a URL is handled by changing the openURL environment value.

struct ContentView: View {
	let twitterURL = URL(string: "https://twitter.com/MatManferdini")!
	@Environment(\.openURL) private var openURL

	var body: some View {
		Text("Hello, World!")
			.onAppear {
				openURL(twitterURL)
			}
			.environment(\.openURL, OpenURLAction { url in
				// Handle the URL
				return .handled
			})
	}
}

In the example above, I appended the environment(_:_:) view modifier under the onAppear(_:) view modifier. However, in a real SwiftUI app, you would usually place it in a more appropriate place, i.e., higher in the view hierarchy.

For example, place the view modifier at the root of your app hierarchy to set a global URL action that affects the entire app.

Displaying the contents of a web page inside a SwiftUI app

If you want to display a web page inside your app, you have two options: use a web view or the Safari view controller.

Both types do not come from SwiftUI. Instead, they are from other frameworks: WebKit and UIKit, respectively. You must bridge them using the UIViewRepresentable and UIViewControllerRepresentable protocols.

This is how you open a URL in a web view:

import WebKit

struct WebView : UIViewRepresentable {
	let url: URL

	func makeUIView(context: Context) -> WKWebView  {
		return WKWebView()
	}

	func updateUIView(_ uiView: WKWebView, context: Context) {
		let request = URLRequest(url: url)
		uiView.load(request)
	}
}

struct WebContentView: View {
	let webPageURL = URL(string: "https://gamedesignconcepts.wordpress.com")!

	var body: some View {
		WebView(url: url)
	}
}

This is the code to open a URL in a Safari view controller:

import SafariServices

struct SafariView: UIViewControllerRepresentable {
	let url: URL

	func makeUIViewController(
		context: UIViewControllerRepresentableContext) -> SFSafariViewController {
			return SFSafariViewController(url: url)
	}

	func updateUIViewController(
		_ uiViewController: SFSafariViewController,
		context: UIViewControllerRepresentableContext) {
			return
	}
}

struct SafariContentView: View {
	let webPageURL = URL(string: "https://russianmanner.com/church-slavonic-resources/")!
	@State var showSafari = false

	var body: some View {
		Button("Show web page") {
			showSafari = true
		}
		.sheet(isPresented: $showSafari) {
			SafariView(url: webPageURL)
		}
	}
}

Conclusions

URLs can represent all kinds of resources.

How you handle a URL in your app depends on (a) the resource and (b) your app’s objectives and architectural considerations.

Get my free guide below to learn more about structuring your SwiftUI apps.

Architecting SwiftUI apps with MVC and MVVM

It's easy to make an app by throwing some code together. But without best practices and robust architecture, you soon end up with unmanageable spaghetti code. In this guide I'll show you how to properly structure SwiftUI apps.

GET THE FREE BOOK NOW

Leave a Comment