1. Важливо: URL складати структурно, а не «склеювати рядок і йти далі»
Якщо ви новачок, то бажання «просто склеїти рядок» абсолютно природне: "\(host)/v1/books?q=\(query)&page=\(page)". На маленькому прикладі це навіть може спрацювати — і саме тому це небезпечно. Це як переходити дорогу «на авось»: інколи щастить, аж доки не перестане.
Проблема в тому, що URL — це не просто рядок. Це структура з правилами. У нього є частини, і кожна має свою роль. Деякі символи не можна писати «як є»: пробіл, амперсанд, знак питання, решітка — навіть окремі символи Юнікоду треба кодувати коректно. У Swift для цього існує стандартний інструмент: URLComponents.
Частини URL та їхні поля в URLComponents
Перш ніж переходити до URLComponents, корисно побачити URL як набір деталей. Коли ви дивитеся на адресу в браузері, мозок сприймає її як один рядок тексту. Для компʼютера це радше конструктор Lego: схема, хост, шлях, параметри… І важливо, що ці деталі мають зʼєднуватися строго за правилами.
Ось проста таблиця, щоб закріпити, як називаються частини URL у Swift:
| Частина URL | Приклад | Що це означає | Поле в URLComponents |
|---|---|---|---|
|
|
«який протокол» | |
|
|
«куди йдемо» (сервер) | |
|
|
«який ресурс» | |
|
|
«параметри запиту» | queryItems (не рядок!) |
|
|
якір, зазвичай для браузера | |
І важливе спостереження: у правильному світі query-частина — це не один рядок, а набір пар name=value. Саме тому й існує URLQueryItem.
2. Чому рядки ламаються: спецсимволи та percent-encoding
Давайте чесно: проблема не в тому, що конкатенація «погана». Проблема в тому, що вона змушує вас вручну памʼятати й дотримуватися всіх правил формату URL. А ручний режим у програмуванні зазвичай закінчується двома речами: помилками та ще більшими помилками.
Найчастіше ламаються символи, які в URL мають спеціальне значення.
Наприклад:
- пробіл у query — його не можна просто так залишити пробілом;
- & розділяє параметри (a=1&b=2), тому якщо & трапляється всередині значення, його потрібно закодувати;
- ? відділяє шлях від query, і якщо ви випадково вставите другий ?, вийде «я художник, я так бачу», але сервер цього не оцінить;
- = відділяє імʼя параметра від значення.
Тема, яка звучить страшно, але насправді проста: percent-encoding. Це коли «небезпечні» символи перетворюються на %XX. Робити це вручну не потрібно. Ба більше, так легко почати кодувати вже закодоване. Цю роботу має виконувати стандартний інструмент.
У Swift саме URLComponents + URLQueryItem беруть на себе це кодування, щоб ви писали «людські» рядки, а на виході отримували «правильний» URL.
3. Складаємо URL через URLComponents
URLComponents є у Foundation, тому майже в усіх прикладах буде import Foundation. І так, технічно URLComponents (як і URLQueryItem) — це value type у Swift. Історично він прийшов зі світу Foundation і повʼязаний із NSURLComponents/NSURLQueryItem.
Для нас це корисно тим, що ми працюємо з ним як зі звичайною Swift-структурою: змінюємо поля, копіюємо, передаємо у функції.
Найпростіший приклад — зібрати URL без query:
import Foundation
var c = URLComponents()
c.scheme = "https"
c.host = "api.example.com"
c.path = "/v1/books"
print(c.url?.absoluteString ?? "Не вдалося зібрати URL")
// https://api.example.com/v1/books
Зверніть увагу: c.url — це URL?, тобто опціонал. Це не примха, а підказка: «збірка може не вдатися». Наприклад, якщо ви забули scheme або host, підсумковий URL може виявитися некоректним.
Трохи практичніший варіант — не просто print, а окрема функція, щоб одразу зрозуміти, де помилка:
import Foundation
func makeBaseURL() -> URL? {
var c = URLComponents()
c.scheme = "https"
c.host = "api.example.com"
c.path = "/v1/books"
return c.url
}
print(makeBaseURL()?.absoluteString ?? "nil")
// https://api.example.com/v1/books
Тут немає жодної магії: замість того щоб «зібрати рядок», ми чесно заповнили поля.
4. Query-параметри через queryItems
А тепер найцікавіше — query-параметри. У типовому API ви дуже часто робите запити на кшталт «пошук», «сортування», «сторінка», «ліміт». І майже завжди там є рядкові значення, де легко зустріти пробіли або «небезпечні» символи.
З URLQueryItem ви не думаєте про амперсанди й пробіли — ви задаєте пари name/value, а Swift робить решту.
import Foundation
var c = URLComponents()
c.scheme = "https"
c.host = "api.example.com"
c.path = "/v1/books"
c.queryItems = [
URLQueryItem(name: "q", value: "swift basics"),
URLQueryItem(name: "sort", value: "title asc")
]
print(c.url?.absoluteString ?? "Не вдалося зібрати URL")
// https://api.example.com/v1/books?q=swift%20basics&sort=title%20asc
Зверніть увагу, як пробіли перетворилися на %20. Це і є «процентне кодування», але нам не довелося робити нічого вручну.
Ще один важливий момент: значення query майже завжди рядкові, тому числа потрібно явно перетворювати:
import Foundation
let page = 2
let limit = 20
var c = URLComponents()
c.scheme = "https"
c.host = "api.example.com"
c.path = "/v1/books"
c.queryItems = [
URLQueryItem(name: "page", value: String(page)),
URLQueryItem(name: "limit", value: String(limit))
]
print(c.url?.absoluteString ?? "Не вдалося зібрати URL")
// https://api.example.com/v1/books?page=2&limit=20
5. Практичні нюанси: опціональні параметри, path і розбір URL
Опціональні параметри: додаємо лише те, що реально задано
У реальному CLI чи застосунку в користувача частина параметрів може бути не задана. Наприклад, пошук q може бути порожнім, а page взагалі не передали. І тут починається класичний біль рядкової збірки: зайві &, порожні значення, випадковий ? наприкінці.
З URLComponents це вирішується акуратним патерном: спочатку збираємо масив items, а потім або ставимо queryItems = nil, або присвоюємо масив.
import Foundation
func makeBooksURL(query: String?, page: Int?) -> URL? {
var c = URLComponents()
c.scheme = "https"
c.host = "api.example.com"
c.path = "/v1/books"
var items: [URLQueryItem] = []
if let query {
items.append(URLQueryItem(name: "q", value: query))
}
if let page {
items.append(URLQueryItem(name: "page", value: String(page)))
}
c.queryItems = items.isEmpty ? nil : items
return c.url
}
print(makeBooksURL(query: nil, page: nil)?.absoluteString ?? "nil") // https://api.example.com/v1/books
print(makeBooksURL(query: "swift", page: 2)?.absoluteString ?? "nil") // https://api.example.com/v1/books?q=swift&page=2
Зверніть увагу на дисципліну: nil, якщо параметрів немає. Це дрібниця, але вона робить URL передбачуваним: без «завислого» знака питання і без дивних артефактів.
path: початковий слеш, який легко забути
Із path є одна класична пастка, яка виглядає як «ну блін, серйозно?» — і так, серйозно. У URLComponents шлях має починатися з /.
Тобто правильно ось так:
import Foundation
var c = URLComponents()
c.scheme = "https"
c.host = "api.example.com"
c.path = "/v1/books"
print(c.url?.absoluteString ?? "Не вдалося")
// https://api.example.com/v1/books
А ось так — потенційна проблема, яка точно стане проблемою для читабельності коду та роботи в команді:
import Foundation
var c = URLComponents()
c.scheme = "https"
c.host = "api.example.com"
c.path = "v1/books" // <-- без початкового слеша
print(c.url?.absoluteString ?? "Не вдалося")
Чому так? Тому що path — це частина URL із чіткою граматикою. І краще одразу домовитися в проєкті: «path завжди починається з /». Тоді ви не ловитимете помилки рівня «чому сервер повертає 404, хоча я ж точно вказав books».
Розбір готового URL та додавання параметрів
Іноді ви не будуєте URL з нуля, а отримуєте його звідкись: наприклад, у вас уже є посилання, і потрібно додати до нього query-параметр (або замінити наявний). Робити це рядковими замінами — майже гарантовано погана ідея: ви ризикуєте зламати кодування або порядок.
URLComponents вміє не тільки складати, а й розбирати.
import Foundation
let url = URL(string: "https://api.example.com/v1/books?q=swift")!
if var c = URLComponents(url: url, resolvingAgainstBaseURL: false) {
var items = c.queryItems ?? []
items.append(URLQueryItem(name: "page", value: "2"))
c.queryItems = items
print(c.url?.absoluteString ?? "Не вдалося")
// https://api.example.com/v1/books?q=swift&page=2
}
Тут ми зробили три важливі речі без ручної метушні:
- коректно розібрали query в масив URLQueryItem;
- додали ще один параметр;
- отримали новий URL із нормальною структурою.
І так, зверніть увагу: URLComponents(url:...) теж може не вдатися, тому й використовуємо if var c = .... А c.url знову є опціоналом.
6. Базові компоненти для API: малий крок до застосунку
Сьогодні ми ще не будуємо повноцінний конструктор кінцевих точок — це буде в наступній лекції. Але можемо зробити дуже корисний малий крок: винести створення «бази URL» в одну функцію. Це вже зменшує копіпасту і знижує ризик забути scheme або переплутати host.
Уявімо, що наш навчальний CLI (умовний LibraryCLI) звертається до API книжок. Поки що нехай це буде api.example.com — нас зараз цікавить техніка складання, а не реальний сервер.
import Foundation
struct APIConfig {
let scheme: String
let host: String
}
func makeComponents(config: APIConfig, path: String) -> URLComponents {
var c = URLComponents()
c.scheme = config.scheme
c.host = config.host
c.path = path
return c
}
А далі використаємо:
import Foundation
let config = APIConfig(scheme: "https", host: "api.example.com")
var c = makeComponents(config: config, path: "/v1/books")
c.queryItems = [
URLQueryItem(name: "q", value: "swift"),
URLQueryItem(name: "limit", value: "10")
]
print(c.url?.absoluteString ?? "nil")
// https://api.example.com/v1/books?q=swift&limit=10
Це ще не «архітектура століття», але це вже добрий тон: базові правила складання URL зберігаються в одному місці, а не розкидаються по всьому проєкту.
Схема: що ми робимо на цьому етапі
Щоб закріпити модель у голові, ось проста блок-схема того, як сьогодні виглядає «правильне» складання URL:
flowchart TD
A["Дані (scheme/host/path + параметри)"] --> B["URLComponents"]
B --> C["URLQueryItem[]"]
C --> B
B --> D["components.url (URL?)"]
D --> E["Далі: URLRequest / URLSession (це вже інші теми)"]
Головна думка така: ми описуємо запит через дані, а не через рядки. Рядки залишаються лише на краях — у введенні користувача та в готовому посиланні для запиту.
7. Типові помилки під час роботи з URLComponents і queryItems
Помилка № 1: складати query вручну через "?q=\(q)&page=\(p)".
Це майже завжди ламається на пробілах, &, = і Юнікоді. Навіть якщо ви спробуєте «закодувати пробіли», ви швидко потрапите в ситуацію подвійного кодування, де % теж починає перетворюватися на %25. Краще сприймати query як структуру і завжди використовувати URLQueryItem.
Помилка № 2: забути, що components.url — це URL?.
Новачки часто думають: «Ну я ж усе заповнив, чому йому бути nil». А потім забувають scheme або помиляються в host, і програма поводиться дивно. Правильний стиль — перевіряти components.url через guard let і явно обробляти ситуацію «URL не зібрався», бо це помилка ще до звернення до мережі.
Помилка № 3: писати path без початкового /.
Це дрібна описка, яка може перетворитися на кілька годин «чому сервер не відповідає так, як треба». Домовтеся в команді або хоча б із собою: path завжди починається з /. Якщо потрібно — додавайте захист у коді (наприклад, нормалізацію "/" + path), але краще привчити себе писати правильно одразу.
Помилка № 4: залишати queryItems = [] замість nil.
Технічно часто все працюватиме, але в реальних проєктах це ускладнює налагодження і іноді призводить до неочікуваних «порожніх query» у логах. Зручніше дотримуватися правила: якщо параметрів немає — query відсутній, отже queryItems = nil. Це робить підсумковий URL акуратнішим і передбачуванішим.
Помилка № 5: намагатися «допомогти» URLComponents і кодувати значення вручну.
Коли ви бачите %20, виникає спокуса скористатися replacingOccurrences(...). Це майже завжди неправильне рішення, бо кодування залежить не лише від пробілів, а ви можете випадково закодувати рядок двічі. URLQueryItem очікує «людське» значення, а кодування має відбуватися автоматично всередині URLComponents.
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ