Swift provides URL for representing URLs and URLComponents for parsing and building them. These Foundation types offer a type-safe, Swift-native API for URL manipulation on iOS, macOS, and other Apple platforms.
Key Takeaways
- 1URL is for simple URL representation and file paths
- 2URLComponents provides mutable URL parsing and building
- 3URLQueryItem handles query parameter encoding automatically
- 4All URL types are value types (structs) in Swift
- 5Use URLComponents for building URLs to avoid encoding issues
“A URL is a type that identifies the location of a resource, such as an item on a remote server or the path to a local file. You can construct URLs from their component parts or from a string that you provide.”
The URL Type
Swift's URL struct is simple to use for basic URL representation. It returns nil for invalid URLs, so you'll use optional binding (if let) when parsing user input.
import Foundation
// Parse a URL string
if let url = URL(string: "https://user:pass@example.com:8080/path?query=value#fragment") {
print("Scheme: \(url.scheme ?? "")") // https
print("Host: \(url.host ?? "")") // example.com
print("Port: \(url.port ?? 0)") // 8080
print("Path: \(url.path)") // /path
print("Query: \(url.query ?? "")") // query=value
print("Fragment: \(url.fragment ?? "")") // fragment
print("User: \(url.user ?? "")") // user
print("Password: \(url.password ?? "")") // pass
// Path components
print("Path Components: \(url.pathComponents)") // ["/", "path"]
print("Last Component: \(url.lastPathComponent)") // path
print("Extension: \(url.pathExtension)") // ""
}
// URL is optional - invalid URLs return nil
let invalidUrl = URL(string: "not a valid url")
print(invalidUrl == nil) // trueMost properties return optionals because those components might not be present in every URL. The path property always exists (at minimum it's /), so it returns a non-optional String.
URLComponents for Parsing
For detailed URL manipulation, URLComponents is more powerful than URL. It provides mutable access to all components and handles query parameters through URLQueryItem.
import Foundation
let urlString = "https://api.example.com/search?q=swift+programming&page=1"
if var components = URLComponents(string: urlString) {
print("Scheme: \(components.scheme ?? "")") // https
print("Host: \(components.host ?? "")") // api.example.com
print("Path: \(components.path)") // /search
print("Query: \(components.query ?? "")") // q=swift+programming&page=1
// Parse query items
if let queryItems = components.queryItems {
for item in queryItems {
print("\(item.name): \(item.value ?? "")")
}
// q: swift programming
// page: 1
}
// Get specific parameter
let searchQuery = components.queryItems?.first { $0.name == "q" }?.value
print("Search: \(searchQuery ?? "")") // swift programming
// Percent-encoded vs decoded
print("Encoded query: \(components.percentEncodedQuery ?? "")")
// q=swift+programming&page=1
}The queryItems array gives you structured access to parameters. Use first(where:) to find specific parameters: it's more Swift-idiomatic than looping manually. The percentEncodedQuery property shows the raw encoded form.
URL Properties
The table below shows the most useful URL properties. Most return optionals since components might be absent.
| Property | Type | Description |
|---|---|---|
| scheme | String? | Protocol (http, https) |
| host | String? | Domain name |
| port | Int? | Port number (nil for default) |
| path | String | URL path |
| query | String? | Raw query string |
| fragment | String? | Fragment/hash |
| user | String? | Username |
| password | String? | Password |
| pathComponents | [String] | Path as array |
| lastPathComponent | String | Final path segment |
Note that port returns nil when using the default port for the scheme. pathComponents includes / as the first element for absolute paths.
Building URLs
Building URLs in Swift is straightforward with URLComponents. Set properties and query items, then access the url property for the final result.
import Foundation
// Build URL from components
var components = URLComponents()
components.scheme = "https"
components.host = "api.example.com"
components.path = "/v2/users"
components.queryItems = [
URLQueryItem(name: "page", value: "1"),
URLQueryItem(name: "limit", value: "20"),
URLQueryItem(name: "sort", value: "name")
]
if let url = components.url {
print(url)
// https://api.example.com/v2/users?page=1&limit=20&sort=name
}
// Build with special characters (auto-encoded)
var searchComponents = URLComponents()
searchComponents.scheme = "https"
searchComponents.host = "api.example.com"
searchComponents.path = "/search"
searchComponents.queryItems = [
URLQueryItem(name: "q", value: "swift & objective-c"),
URLQueryItem(name: "filter", value: "category:mobile")
]
if let url = searchComponents.url {
print(url)
// https://api.example.com/search?q=swift%20%26%20objective-c&filter=category:mobile
}
// From existing URL
if var components = URLComponents(string: "https://example.com/path") {
components.queryItems = [URLQueryItem(name: "new", value: "param")]
print(components.url!)
// https://example.com/path?new=param
}URLQueryItem handles encoding automatically, so you pass raw values. The components struct validates the URL, so .url returns an optional: it's nil if the combination of components is invalid.
Modifying Query Parameters
These extension methods add convenient parameter manipulation to URL. They follow Swift conventions by returning optional URLs.
import Foundation
extension URL {
func addingQueryItems(_ items: [URLQueryItem]) -> URL? {
guard var components = URLComponents(url: self, resolvingAgainstBaseURL: true) else {
return nil
}
var existingItems = components.queryItems ?? []
existingItems.append(contentsOf: items)
components.queryItems = existingItems
return components.url
}
func settingQueryItems(_ items: [URLQueryItem]) -> URL? {
guard var components = URLComponents(url: self, resolvingAgainstBaseURL: true) else {
return nil
}
var existing = components.queryItems ?? []
for newItem in items {
existing.removeAll { $0.name == newItem.name }
existing.append(newItem)
}
components.queryItems = existing
return components.url
}
func removingQueryItems(named names: [String]) -> URL? {
guard var components = URLComponents(url: self, resolvingAgainstBaseURL: true) else {
return nil
}
components.queryItems = components.queryItems?.filter { !names.contains($0.name) }
if components.queryItems?.isEmpty == true {
components.queryItems = nil
}
return components.url
}
var queryParameters: [String: String] {
guard let components = URLComponents(url: self, resolvingAgainstBaseURL: true),
let queryItems = components.queryItems else {
return [:]
}
return Dictionary(queryItems.map { ($0.name, $0.value ?? "") }) { first, _ in first }
}
}
// Usage
let url = URL(string: "https://api.com/search?q=swift&page=1")!
let updated = url.settingQueryItems([URLQueryItem(name: "page", value: "2")])
print(updated!) // https://api.com/search?q=swift&page=2
let removed = url.removingQueryItems(named: ["page"])
print(removed!) // https://api.com/search?q=swift
let params = url.queryParameters
print(params) // ["q": "swift", "page": "1"]The extensions use removeAll(where:) to filter parameters, which is cleaner than manual iteration. The queryParameters computed property converts query items to a dictionary for convenient access.
Resolving Relative URLs
Swift resolves relative URLs by passing a base URL as the second argument to the URL initializer.
import Foundation
let baseURL = URL(string: "https://example.com/docs/guide/")!
// Resolve relative paths
let relative = URL(string: "../api/reference", relativeTo: baseURL)
print(relative?.absoluteString ?? "")
// https://example.com/docs/api/reference
let samePath = URL(string: "page.html", relativeTo: baseURL)
print(samePath?.absoluteString ?? "")
// https://example.com/docs/guide/page.html
let absolutePath = URL(string: "/about", relativeTo: baseURL)
print(absolutePath?.absoluteString ?? "")
// https://example.com/about
// Check if URL is relative
let relativeURL = URL(string: "page.html", relativeTo: baseURL)!
print("Relative: \(relativeURL.relativeString)") // page.html
print("Absolute: \(relativeURL.absoluteString)") // https://example.com/docs/guide/page.html
print("Base: \(relativeURL.baseURL?.absoluteString ?? "")") // https://example.com/docs/guide/URLs created with a base URL remember their origin: relativeString shows the original relative form, while absoluteString shows the resolved URL. The baseURL property provides access to the base.
Handling Deep Links
Deep linking is a crucial iOS/macOS feature. This example shows how to parse custom URL schemes into structured navigation targets.
import Foundation
struct DeepLinkHandler {
enum DeepLink {
case product(id: String)
case category(name: String, page: Int?)
case search(query: String)
case unknown
}
static func parse(_ url: URL) -> DeepLink {
guard let components = URLComponents(url: url, resolvingAgainstBaseURL: true) else {
return .unknown
}
let pathComponents = url.pathComponents.filter { $0 != "/" }
let queryItems = components.queryItems ?? []
switch pathComponents.first {
case "product":
guard pathComponents.count > 1 else { return .unknown }
return .product(id: pathComponents[1])
case "category":
guard pathComponents.count > 1 else { return .unknown }
let page = queryItems.first { $0.name == "page" }
.flatMap { $0.value }
.flatMap { Int($0) }
return .category(name: pathComponents[1], page: page)
case "search":
guard let query = queryItems.first(where: { $0.name == "q" })?.value else {
return .unknown
}
return .search(query: query)
default:
return .unknown
}
}
}
// Usage
let productURL = URL(string: "myapp://product/12345")!
let result = DeepLinkHandler.parse(productURL)
// .product(id: "12345")
let searchURL = URL(string: "myapp://search?q=swift%20books")!
let searchResult = DeepLinkHandler.parse(searchURL)
// .search(query: "swift books")The enum-based approach is idiomatic Swift: DeepLink represents all possible navigation targets with associated values. Pattern matching with switch ensures you handle all cases. Return .unknown for invalid or unrecognized URLs instead of crashing.
URL Validation
URL validation in Swift leverages optionals and guard statements for clean, safe code.
import Foundation
struct URLValidator {
static func isValidHTTPURL(_ urlString: String) -> Bool {
guard let url = URL(string: urlString),
let scheme = url.scheme?.lowercased(),
["http", "https"].contains(scheme),
url.host != nil else {
return false
}
return true
}
static func isSafeRedirect(_ urlString: String, allowedHosts: [String]) -> Bool {
guard let url = URL(string: urlString),
let scheme = url.scheme?.lowercased(),
["http", "https"].contains(scheme),
let host = url.host?.lowercased() else {
return false
}
return allowedHosts.contains { allowed in
host == allowed || host.hasSuffix(".\(allowed)")
}
}
static func isLocalURL(_ urlString: String) -> Bool {
// Relative URLs without scheme
guard let url = URL(string: urlString) else { return false }
return url.scheme == nil && !urlString.hasPrefix("//")
}
}
// Usage
print(URLValidator.isValidHTTPURL("https://example.com")) // true
print(URLValidator.isValidHTTPURL("javascript:alert(1)")) // false
print(URLValidator.isValidHTTPURL("file:///etc/passwd")) // false
let allowed = ["myapp.com", "api.myapp.com"]
print(URLValidator.isSafeRedirect("https://myapp.com/cb", allowedHosts: allowed)) // true
print(URLValidator.isSafeRedirect("https://evil.com/cb", allowedHosts: allowed)) // falseThe struct-based validators use guard let for early returns, which is cleaner than nested if let statements. The hasSuffix check handles subdomains matching allowed hosts.
With URLSession
URLSession is the standard networking API in Swift. Here's how to build URLs for API calls using async/await.
import Foundation
// Build URL with query parameters for API call
func fetchUsers(page: Int, limit: Int) async throws -> Data {
var components = URLComponents(string: "https://api.example.com/users")!
components.queryItems = [
URLQueryItem(name: "page", value: String(page)),
URLQueryItem(name: "limit", value: String(limit))
]
guard let url = components.url else {
throw URLError(.badURL)
}
let (data, response) = try await URLSession.shared.data(from: url)
guard let httpResponse = response as? HTTPURLResponse,
(200...299).contains(httpResponse.statusCode) else {
throw URLError(.badServerResponse)
}
// Access final URL after redirects
print("Final URL: \(httpResponse.url?.absoluteString ?? "")")
return data
}
// POST with URL-encoded body
func submitForm(data: [String: String]) async throws -> Data {
var components = URLComponents()
components.queryItems = data.map { URLQueryItem(name: $0.key, value: $0.value) }
let bodyString = components.percentEncodedQuery ?? ""
let bodyData = bodyString.data(using: .utf8)!
var request = URLRequest(url: URL(string: "https://api.example.com/submit")!)
request.httpMethod = "POST"
request.httpBody = bodyData
request.setValue("application/x-www-form-urlencoded", forHTTPHeaderField: "Content-Type")
let (data, _) = try await URLSession.shared.data(for: request)
return data
}For GET requests, build the URL with URLComponents and pass it to URLSession.shared.data(from:). For POST with form-encoded body, use percentEncodedQuery to get properly encoded form data. The async/await syntax makes error handling clean with try.