JavaRush /Курси /Swift SELF /URLComponents і

URLComponents і queryItems — безпечне складання URL

Swift SELF
Рівень 63 , Лекція 4
Відкрита

1. Важливо: URL складати структурно, а не «склеювати рядок і йти далі»

Якщо ви новачок, то бажання «просто склеїти рядок» абсолютно природне: "\(host)/v1/books?q=\(query)&page=\(page)". На маленькому прикладі це навіть може спрацювати — і саме тому це небезпечно. Це як переходити дорогу «на авось»: інколи щастить, аж доки не перестане.

Проблема в тому, що URL — це не просто рядок. Це структура з правилами. У нього є частини, і кожна має свою роль. Деякі символи не можна писати «як є»: пробіл, амперсанд, знак питання, решітка — навіть окремі символи Юнікоду треба кодувати коректно. У Swift для цього існує стандартний інструмент: URLComponents.

Частини URL та їхні поля в URLComponents

Перш ніж переходити до URLComponents, корисно побачити URL як набір деталей. Коли ви дивитеся на адресу в браузері, мозок сприймає її як один рядок тексту. Для компʼютера це радше конструктор Lego: схема, хост, шлях, параметри… І важливо, що ці деталі мають зʼєднуватися строго за правилами.

Ось проста таблиця, щоб закріпити, як називаються частини URL у Swift:

Частина URL Приклад Що це означає Поле в URLComponents
scheme
https
«який протокол»
scheme
host
api.example.com
«куди йдемо» (сервер)
host
path
/v1/books
«який ресурс»
path
query
?q=swift&page=1
«параметри запиту» queryItems (не рядок!)
fragment
#section1
якір, зазвичай для браузера
fragment

І важливе спостереження: у правильному світі 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.

1
Опитування
Мережа Swift, рівень 63, лекція 4
Недоступний
Мережа Swift
Основи роботи з мережею у Swift
Коментарі
ЩОБ ПОДИВИТИСЯ ВСІ КОМЕНТАРІ АБО ЗАЛИШИТИ КОМЕНТАР,
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ