Sieťovanie v Swift prešlo za posledné roky naozaj dramatickou premenou. Ak si pamätáte časy NSURLConnection a komplikovaných delegátov, viete o čom hovorím. Potom prišiel URLSession s completion handlermi, čo bolo fajn, ale stále to znamenalo vnorené callbacky a pomerne neprehľadný kód. No a dnes? Dnes máme async/await a úprimne, je to ako deň a noc.
V ére Swift 6.x máme k dispozícii nástroje, o ktorých sa nám pred piatimi rokmi ani nesnívalo — natívna konkurencia, striktná kontrola dátových pretekov pri kompilácii a minimálny boilerplate.
V tomto sprievodcovi si krok za krokom postavíme moderného API klienta od základov. Prejdeme si HTTP metódy, ošetrovanie chýb, dekódovanie JSON, autentifikáciu, middleware, stránkovanie aj integráciu so SwiftUI. Všetky príklady sú pripravené na okamžité použitie vo vašom projekte. Tak poďme na to.
Základy URLSession s async/await
Jadrom sieťovej komunikácie v Swift je trieda URLSession. Apple ju predstavilo ešte v iOS 7, no s príchodom Swift 5.5 a natívneho async/await získala úplne novú dimenziu. Zabudnite na vnorené callbacky — moderný kód vyzerá takmer ako synchronný a je oveľa príjemnejšie ho čítať.
Prvý GET požiadavok
Najjednoduchší sieťový požiadavok s async/await vyzerá takto:
import Foundation
// Definícia dátového modelu
struct Pouzivatel: Codable, Sendable {
let id: Int
let meno: String
let email: String
}
// Jednoduchý GET požiadavok
func nacitajPouzivatela(id: Int) async throws -> Pouzivatel {
let url = URL(string: "https://api.example.com/users/\(id)")!
let (data, odpoved) = try await URLSession.shared.data(from: url)
guard let httpOdpoved = odpoved as? HTTPURLResponse,
httpOdpoved.statusCode == 200 else {
throw URLError(.badServerResponse)
}
return try JSONDecoder().decode(Pouzivatel.self, from: data)
}
Všimnite si niekoľko vecí. URLSession.shared je singleton zdieľaný celou aplikáciou — pre jednoduché požiadavky úplne postačuje. Kľúčové slovo await označuje bod pozastavenia, kde sa vykonávanie funkcie zastaví, kým nepríde odpoveď zo servera. Aplikácia pritom zostáva plne responzívna. Metóda data(from:) vracia dvojicu (Data, URLResponse), z ktorej extrahujeme dáta aj HTTP odpoveď.
Rozdiel oproti starému prístupu
Pre porovnanie — takto vyzeral rovnaký požiadavok s completion handlermi:
// Starý prístup s completion handlerom - nepoužívajte
func nacitajPouzivatela(id: Int, completion: @escaping (Result<Pouzivatel, Error>) -> Void) {
let url = URL(string: "https://api.example.com/users/\(id)")!
URLSession.shared.dataTask(with: url) { data, odpoved, chyba in
if let chyba = chyba {
completion(.failure(chyba))
return
}
guard let data = data else {
completion(.failure(URLError(.cannotDecodeContentData)))
return
}
do {
let pouzivatel = try JSONDecoder().decode(Pouzivatel.self, from: data)
completion(.success(pouzivatel))
} catch {
completion(.failure(error))
}
}.resume()
}
Rozdiel je markantný. Async/await verzia je kratšia, čitateľnejšia a chyby sa propagujú automaticky cez throws bez nutnosti manuálneho volania completion(.failure(...)). Osobne som si na starý štýl zvykol, ale keď som prvýkrát prepísal väčší networking layer na async/await, nevedel som sa vrátiť.
Budovanie generického API klienta
Pre reálne aplikácie potrebujeme niečo robustnejšie než priame volanie URLSession.shared. Postavme si generický APIClient založený na protokoloch, ktorý bude znovupoužiteľný naprieč projektami.
Protokol pre API endpoint
import Foundation
// Protokol definujúci endpoint API
protocol APIEndpoint {
associatedtype OdpovedTyp: Decodable
var zakladnaURL: String { get }
var cesta: String { get }
var metoda: HTTPMetoda { get }
var hlavicky: [String: String] { get }
var parametre: [String: String]? { get }
var telo: Encodable? { get }
}
// Predvolené hodnoty
extension APIEndpoint {
var zakladnaURL: String { "https://api.example.com" }
var hlavicky: [String: String] { ["Content-Type": "application/json"] }
var parametre: [String: String]? { nil }
var telo: Encodable? { nil }
}
Všimnite si, ako predvolené hodnoty v extension zjednodušujú definovanie konkrétnych endpointov. Väčšina z nich bude zdieľať rovnakú základnú URL aj hlavičky, takže ich nemusíte zakaždým opakovať.
Trieda APIClient
// Hlavná trieda API klienta
final class APIClient: Sendable {
private let session: URLSession
private let dekoder: JSONDecoder
init(session: URLSession = .shared, dekoder: JSONDecoder = .init()) {
self.session = session
self.dekoder = dekoder
}
// Generická metóda na vykonanie požiadavku
func vykonaj<E: APIEndpoint>(_ endpoint: E) async throws -> E.OdpovedTyp {
let poziadavka = try vytvorPoziadavku(z: endpoint)
let (data, odpoved) = try await session.data(for: poziadavka)
try overOdpoved(odpoved)
return try dekoder.decode(E.OdpovedTyp.self, from: data)
}
// Vytvorenie URLRequest z endpointu
private func vytvorPoziadavku<E: APIEndpoint>(z endpoint: E) throws -> URLRequest {
var komponenty = URLComponents(string: endpoint.zakladnaURL + endpoint.cesta)!
if let parametre = endpoint.parametre {
komponenty.queryItems = parametre.map { URLQueryItem(name: $0.key, value: $0.value) }
}
guard let url = komponenty.url else {
throw APIChyba.nespravnaURL
}
var poziadavka = URLRequest(url: url)
poziadavka.httpMethod = endpoint.metoda.rawValue
poziadavka.allHTTPHeaderFields = endpoint.hlavicky
if let telo = endpoint.telo {
poziadavka.httpBody = try JSONEncoder().encode(telo)
}
return poziadavka
}
// Overenie HTTP odpovede
private func overOdpoved(_ odpoved: URLResponse) throws {
guard let httpOdpoved = odpoved as? HTTPURLResponse else {
throw APIChyba.nespravnaOdpoved
}
switch httpOdpoved.statusCode {
case 200...299:
return // Úspech
case 401:
throw APIChyba.neautorizovany
case 403:
throw APIChyba.zakazanyPristup
case 404:
throw APIChyba.nenajdene
case 500...599:
throw APIChyba.chybaServera(kod: httpOdpoved.statusCode)
default:
throw APIChyba.neocakavanyStatusKod(httpOdpoved.statusCode)
}
}
}
Tento API klient je Sendable, čo znamená, že ho môžete bezpečne používať z viacerých vlákien — kľúčová požiadavka pre Swift 6.x. Vďaka generikám a associatedtype v protokole kompilátor automaticky vie, aký typ dát má dekódovať. Žiadne pretypovávanie, žiadne hádanie — to je proste krásna vec.
HTTP metódy: GET, POST, PUT, DELETE
REST API štandardne využíva štyri základné HTTP metódy (plus PATCH, ktorý sa hodí na čiastočné aktualizácie). Vytvorme si pre ne prehľadný enum a ukážme, ako definovať konkrétne endpointy.
Enum HTTP metód
// Enum pre HTTP metódy
enum HTTPMetoda: String, Sendable {
case get = "GET"
case post = "POST"
case put = "PUT"
case delete = "DELETE"
case patch = "PATCH"
}
Praktické endpointy
// Pomocný wrapper pre kódovanie tela požiadavku
struct AnyEncodable: Encodable {
private let zakoduj: (Encoder) throws -> Void
init<T: Encodable>(_ hodnota: T) {
zakoduj = hodnota.encode
}
func encode(to encoder: Encoder) throws {
try zakoduj(encoder)
}
}
// GET - získanie zoznamu používateľov
struct ZiskajPouzivatelov: APIEndpoint {
typealias OdpovedTyp = [Pouzivatel]
var cesta: String { "/users" }
var metoda: HTTPMetoda { .get }
var parametre: [String: String]?
init(stranka: Int = 1, limit: Int = 20) {
parametre = ["page": "\(stranka)", "limit": "\(limit)"]
}
}
// POST - vytvorenie nového používateľa
struct VytvorPouzivatela: APIEndpoint {
typealias OdpovedTyp = Pouzivatel
var cesta: String { "/users" }
var metoda: HTTPMetoda { .post }
var telo: Encodable?
init(meno: String, email: String) {
telo = AnyEncodable(["meno": meno, "email": email])
}
}
// PUT - aktualizácia existujúceho používateľa
struct AktualizujPouzivatela: APIEndpoint {
typealias OdpovedTyp = Pouzivatel
let pouzivatelID: Int
var cesta: String { "/users/\(pouzivatelID)" }
var metoda: HTTPMetoda { .put }
var telo: Encodable?
init(id: Int, meno: String, email: String) {
self.pouzivatelID = id
telo = AnyEncodable(["meno": meno, "email": email])
}
}
// DELETE - odstránenie používateľa
struct OdstranPouzivatela: APIEndpoint {
typealias OdpovedTyp = PrazdnaOdpoved
let pouzivatelID: Int
var cesta: String { "/users/\(pouzivatelID)" }
var metoda: HTTPMetoda { .delete }
}
// Prázdna odpoveď pre DELETE požiadavky
struct PrazdnaOdpoved: Decodable {}
Použitie je potom maximálne jednoduché:
let klient = APIClient()
// Získanie zoznamu
let pouzivatelia = try await klient.vykonaj(ZiskajPouzivatelov(stranka: 1))
// Vytvorenie nového
let novy = try await klient.vykonaj(VytvorPouzivatela(meno: "Jano", email: "[email protected]"))
// Aktualizácia
let aktualizovany = try await klient.vykonaj(AktualizujPouzivatela(id: 1, meno: "Janko", email: "[email protected]"))
// Odstránenie
_ = try await klient.vykonaj(OdstranPouzivatela(pouzivatelID: 1))
Čitateľné, stručné a typovo bezpečné. Presne takto by mal vyzerať moderný Swift kód.
Ošetrovanie chýb pri sieťovej komunikácii
Spoľahlivé ošetrovanie chýb je základný kameň každého produkčného API klienta. Sieťové požiadavky môžu zlyhať z naozaj mnohých dôvodov — výpadok pripojenia, chyba servera, neplatný token, neočakávaný formát odpovede... A verte mi, v produkcii sa stretne s každým z nich.
Vlastné typy chýb
// Komplexný enum chýb API klienta
enum APIChyba: LocalizedError, Sendable {
case nespravnaURL
case nespravnaOdpoved
case neautorizovany
case zakazanyPristup
case nenajdene
case chybaServera(kod: Int)
case neocakavanyStatusKod(Int)
case chybaDekodovania(Error)
case sietovyProblem(Error)
case prekrocenyCas
case zrusenyPoziadavok
var errorDescription: String? {
switch self {
case .nespravnaURL:
return "Nesprávna URL adresa požiadavku."
case .nespravnaOdpoved:
return "Server vrátil neočakávanú odpoveď."
case .neautorizovany:
return "Prístup odmietnutý. Skontrolujte prihlasovacie údaje."
case .zakazanyPristup:
return "Na túto operáciu nemáte oprávnenie."
case .nenajdene:
return "Požadovaný zdroj nebol nájdený."
case .chybaServera(let kod):
return "Chyba servera (kód \(kod)). Skúste to prosím neskôr."
case .neocakavanyStatusKod(let kod):
return "Neočakávaný status kód: \(kod)."
case .chybaDekodovania(let chyba):
return "Chyba pri spracovaní dát: \(chyba.localizedDescription)"
case .sietovyProblem(let chyba):
return "Problém so sieťou: \(chyba.localizedDescription)"
case .prekrocenyCas:
return "Požiadavok trval príliš dlho. Skontrolujte pripojenie."
case .zrusenyPoziadavok:
return "Požiadavok bol zrušený."
}
}
}
Bezpečné vykonanie požiadavku s ošetrením chýb
// Rozšírenie APIClienta o bezpečné vykonanie
extension APIClient {
func bezpecneVykonaj<E: APIEndpoint>(_ endpoint: E) async -> Result<E.OdpovedTyp, APIChyba> {
do {
let vysledok = try await vykonaj(endpoint)
return .success(vysledok)
} catch let chyba as APIChyba {
return .failure(chyba)
} catch let chyba as DecodingError {
return .failure(.chybaDekodovania(chyba))
} catch let chyba as URLError where chyba.code == .timedOut {
return .failure(.prekrocenyCas)
} catch let chyba as URLError where chyba.code == .cancelled {
return .failure(.zrusenyPoziadavok)
} catch {
return .failure(.sietovyProblem(error))
}
}
}
Vďaka tomuto rozšíreniu môžete na volajúcej strane elegantne spracovať všetky možné chybové stavy pomocou switch nad Result typom. Každá chyba má lokalizovaný popis v slovenčine, čo sa hodí aj pre zobrazovanie hlášok priamo používateľovi.
Dekódovanie JSON s Codable
Protokol Codable je základ pre prácu s JSON v Swift. Lenže reálne API často vyžadujú špeciálnu konfiguráciu dekodéra — iný formát dátumov, konverziu medzi snake_case a camelCase, alebo vlastné dekódovacie stratégie. Pozrime sa, ako to riešiť elegantne.
Konfigurácia JSONDecoder
// Továreň pre konfiguráciu dekodéra
enum DekoderTovaren {
// Štandardný dekodér pre väčšinu API
static func standardny() -> JSONDecoder {
let dekoder = JSONDecoder()
dekoder.keyDecodingStrategy = .convertFromSnakeCase
dekoder.dateDecodingStrategy = .iso8601
return dekoder
}
// Dekodér s vlastným formátom dátumu
static func sVlastnymDatumom(format: String) -> JSONDecoder {
let dekoder = JSONDecoder()
dekoder.keyDecodingStrategy = .convertFromSnakeCase
let formatter = DateFormatter()
formatter.dateFormat = format
formatter.locale = Locale(identifier: "sk_SK")
formatter.timeZone = TimeZone(identifier: "Europe/Bratislava")
dekoder.dateDecodingStrategy = .formatted(formatter)
return dekoder
}
// Dekodér s viacerými formátmi dátumu
static func flexibilny() -> JSONDecoder {
let dekoder = JSONDecoder()
dekoder.keyDecodingStrategy = .convertFromSnakeCase
dekoder.dateDecodingStrategy = .custom { dekoder in
let kontajner = try dekoder.singleValueContainer()
let retazec = try kontajner.decode(String.self)
// Skúsime ISO 8601
let iso8601Formatter = ISO8601DateFormatter()
iso8601Formatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
if let datum = iso8601Formatter.date(from: retazec) {
return datum
}
// Skúsime klasický formát
let datumFormatter = DateFormatter()
datumFormatter.dateFormat = "yyyy-MM-dd"
if let datum = datumFormatter.date(from: retazec) {
return datum
}
throw DecodingError.dataCorruptedError(
in: kontajner,
debugDescription: "Nepodarilo sa dekódovať dátum: \(retazec)"
)
}
return dekoder
}
}
Tá flexibilná verzia dekodéra sa vám hodí vtedy, keď API nie je úplne konzistentné v tom, ako posiela dátumy (a to sa stáva častejšie, než by ste čakali).
Zložitejšie modely s vlastným dekódovaním
// Model s vlastnou dekódovacou logikou
struct Clanok: Codable, Sendable {
let id: Int
let nazov: String
let obsah: String
let autor: Autor
let datumVytvorenia: Date
let tagy: [String]
let pocetZobrazeni: Int
struct Autor: Codable, Sendable {
let id: Int
let menoAPriezvisko: String
let avatar: URL?
}
}
// Model API odpovede s metadátami
struct APIOdpoved<T: Decodable>: Decodable {
let data: T
let metadata: Metadata
struct Metadata: Decodable {
let celkovyPocet: Int
let aktualnaStranka: Int
let pocetStranok: Int
let masiDalsiu: Bool
}
}
Konverzia snake_case na camelCase cez .convertFromSnakeCase automaticky premapuje JSON kľúče ako datum_vytvorenia na Swift property datumVytvorenia. Pre zložitejšie prípady si môžete definovať vlastný CodingKeys enum.
Autentifikácia a zabezpečenie
Väčšina moderných API vyžaduje autentifikáciu. Najčastejšie sú to Bearer tokeny (JWT) alebo API kľúče. Implementujme si vzor interceptora, ktorý automaticky pridá autentifikačné hlavičky ku každému požiadavku — takže sa o to nemusíte starať na každom jednom mieste volania.
Protokol autentifikátora
// Protokol pre rôzne typy autentifikácie
protocol Autentifikator: Sendable {
func autentifikuj(_ poziadavka: inout URLRequest) async throws
}
// Bearer token autentifikácia
final class BearerAutentifikator: Autentifikator {
private let ziskajToken: @Sendable () async throws -> String
init(ziskajToken: @escaping @Sendable () async throws -> String) {
self.ziskajToken = ziskajToken
}
func autentifikuj(_ poziadavka: inout URLRequest) async throws {
let token = try await ziskajToken()
poziadavka.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
}
}
// API kľúč autentifikácia
final class APIKlucAutentifikator: Autentifikator {
private let kluc: String
private let nazovHlavicky: String
init(kluc: String, nazovHlavicky: String = "X-API-Key") {
self.kluc = kluc
self.nazovHlavicky = nazovHlavicky
}
func autentifikuj(_ poziadavka: inout URLRequest) async throws {
poziadavka.setValue(kluc, forHTTPHeaderField: nazovHlavicky)
}
}
Rozšírenie API klienta o autentifikáciu
// API klient s podporou autentifikácie
final class AutentifikovanyAPIClient: Sendable {
private let klient: APIClient
private let autentifikator: Autentifikator
init(klient: APIClient = APIClient(), autentifikator: Autentifikator) {
self.klient = klient
self.autentifikator = autentifikator
}
func vykonaj<E: APIEndpoint>(_ endpoint: E) async throws -> E.OdpovedTyp {
var poziadavka = try vytvorZakladnuPoziadavku(z: endpoint)
try await autentifikator.autentifikuj(&poziadavka)
let (data, odpoved) = try await klient.session.data(for: poziadavka)
try klient.overOdpoved(odpoved)
return try klient.dekoder.decode(E.OdpovedTyp.self, from: data)
}
}
// Použitie s Bearer tokenom
let autentKlient = AutentifikovanyAPIClient(
autentifikator: BearerAutentifikator {
// Tu získame token z Keychain alebo z pamäte
return TokenManager.shared.aktualnyToken
}
)
let profil = try await autentKlient.vykonaj(ZiskajProfil())
Vzor interceptora oddeľuje logiku autentifikácie od samotného API klienta. Vďaka protokolu Autentifikator jednoducho vymeníte typ autentifikácie bez zmeny zvyšku kódu. A keď príde požiadavka na OAuth2? Stačí pridať novú implementáciu protokolu.
Middleware: logovanie a opakovanie požiadavkov
Middleware vzor umožňuje pridávať funkcionalitu do reťazca spracovávania požiadavkov bez toho, aby ste museli meniť jadro API klienta. Dva najčastejšie prípady použitia sú logovanie (neoceniteľné pri debugovaní) a automatické opakovanie pri zlyhaniach.
Protokol middleware
// Protokol pre middleware
protocol APIMiddleware: Sendable {
func spracujPoziadavku(_ poziadavka: URLRequest) async throws -> URLRequest
func spracujOdpoved(_ data: Data, _ odpoved: URLResponse) async throws -> (Data, URLResponse)
}
// Logovací middleware
struct LogovaciMiddleware: APIMiddleware {
func spracujPoziadavku(_ poziadavka: URLRequest) async throws -> URLRequest {
print("➡️ \(poziadavka.httpMethod ?? "?") \(poziadavka.url?.absoluteString ?? "")")
if let telo = poziadavka.httpBody,
let text = String(data: telo, encoding: .utf8) {
print(" Telo: \(text)")
}
return poziadavka
}
func spracujOdpoved(_ data: Data, _ odpoved: URLResponse) async throws -> (Data, URLResponse) {
if let http = odpoved as? HTTPURLResponse {
print("⬅️ Status: \(http.statusCode) | Veľkosť: \(data.count) bajtov")
}
return (data, odpoved)
}
}
// Middleware pre automatické opakovanie požiadavkov
struct OpakovacMiddleware: APIMiddleware {
let maxPocetPokusov: Int
let zakladneCakanie: TimeInterval
init(maxPocetPokusov: Int = 3, zakladneCakanie: TimeInterval = 1.0) {
self.maxPocetPokusov = maxPocetPokusov
self.zakladneCakanie = zakladneCakanie
}
func spracujPoziadavku(_ poziadavka: URLRequest) async throws -> URLRequest {
return poziadavka // Bez zmeny
}
func spracujOdpoved(_ data: Data, _ odpoved: URLResponse) async throws -> (Data, URLResponse) {
return (data, odpoved) // Logika opakovania je v klientovi
}
}
// API klient s podporou middleware a opakovania
final class MiddlewareAPIClient: Sendable {
private let session: URLSession
private let dekoder: JSONDecoder
private let middleware: [APIMiddleware]
private let maxPocetPokusov: Int
init(
session: URLSession = .shared,
dekoder: JSONDecoder = DekoderTovaren.standardny(),
middleware: [APIMiddleware] = [],
maxPocetPokusov: Int = 1
) {
self.session = session
self.dekoder = dekoder
self.middleware = middleware
self.maxPocetPokusov = maxPocetPokusov
}
func vykonaj<E: APIEndpoint>(_ endpoint: E) async throws -> E.OdpovedTyp {
var poziadavka = try vytvorPoziadavku(z: endpoint)
// Aplikovanie middleware na požiadavku
for mw in middleware {
poziadavka = try await mw.spracujPoziadavku(poziadavka)
}
// Opakovanie s exponenciálnym čakaním
var poslednaChyba: Error?
for pokus in 0..<maxPocetPokusov {
do {
var (data, odpoved) = try await session.data(for: poziadavka)
// Aplikovanie middleware na odpoveď
for mw in middleware {
(data, odpoved) = try await mw.spracujOdpoved(data, odpoved)
}
try overOdpoved(odpoved)
return try dekoder.decode(E.OdpovedTyp.self, from: data)
} catch {
poslednaChyba = error
if pokus < maxPocetPokusov - 1 {
let cakanie = pow(2.0, Double(pokus)) * 1.0
try await Task.sleep(for: .seconds(cakanie))
}
}
}
throw poslednaChyba ?? APIChyba.nespravnaOdpoved
}
}
Exponenciálne čakanie (exponential backoff) postupne predlžuje intervaly medzi pokusmi — 1 sekunda, 2 sekundy, 4 sekundy. Tým sa vyhnete tomu, že zahltíte server pri dočasných výpadkoch. Je to jednoduchý trik, ale v praxi funguje skvele.
Stránkovanie výsledkov
Väčšina REST API vracia výsledky po stránkach. Existujú dva hlavné prístupy: offset-based (klasické stránkovanie s číslom stránky) a cursor-based (kurzorové stránkovanie). Implementujme si oba, pretože oba majú svoje miesto.
Offset-based stránkovanie
// Odpoveď s informáciami o stránkovaní
struct StrankovanaOdpoved<T: Decodable>: Decodable {
let polozky: [T]
let celkovyPocet: Int
let stranka: Int
let pocetNaStranku: Int
var masiDalsiuStranku: Bool {
stranka * pocetNaStranku < celkovyPocet
}
}
// Správca stránkovania
actor SpravcoStrankovania<T: Decodable & Sendable> {
private var aktualnaStranka = 1
private var nacitavaSa = false
private var vsetkyNacitane = false
private(set) var polozky: [T] = []
private let nacitajStranku: @Sendable (Int) async throws -> StrankovanaOdpoved<T>
init(nacitajStranku: @escaping @Sendable (Int) async throws -> StrankovanaOdpoved<T>) {
self.nacitajStranku = nacitajStranku
}
func nacitajDalsiu() async throws {
guard !nacitavaSa, !vsetkyNacitane else { return }
nacitavaSa = true
defer { nacitavaSa = false }
let odpoved = try await nacitajStranku(aktualnaStranka)
polozky.append(contentsOf: odpoved.polozky)
vsetkyNacitane = !odpoved.masiDalsiuStranku
aktualnaStranka += 1
}
func resetuj() {
aktualnaStranka = 1
nacitavaSa = false
vsetkyNacitane = false
polozky = []
}
}
Použitie actor tu nie je náhodné — zabezpečuje, že stav stránkovania je vždy konzistentný, aj keď by sa náhodou zavolalo načítanie z viacerých miest súčasne.
Cursor-based stránkovanie
// Kurzorové stránkovanie - ideálne pre nekonečné posúvanie
struct KurzorovaOdpoved<T: Decodable>: Decodable {
let polozky: [T]
let dalsiKurzor: String?
var masiDalsie: Bool { dalsiKurzor != nil }
}
// AsyncSequence pre kurzorové stránkovanie
struct StrankovanaSekvencia<T: Decodable & Sendable>: AsyncSequence {
typealias Element = [T]
let nacitajStranku: @Sendable (String?) async throws -> KurzorovaOdpoved<T>
func makeAsyncIterator() -> AsyncIterator {
AsyncIterator(nacitajStranku: nacitajStranku)
}
struct AsyncIterator: AsyncIteratorProtocol {
let nacitajStranku: @Sendable (String?) async throws -> KurzorovaOdpoved<T>
var aktualnyKurzor: String? = nil
var skoncil = false
mutating func next() async throws -> [T]? {
guard !skoncil else { return nil }
let odpoved = try await nacitajStranku(aktualnyKurzor)
aktualnyKurzor = odpoved.dalsiKurzor
skoncil = !odpoved.masiDalsie
return odpoved.polozky.isEmpty ? nil : odpoved.polozky
}
}
}
// Použitie s for-await-in
func nacitajVsetkyPrispevky() async throws -> [Prispevok] {
var vsetky: [Prispevok] = []
let sekvencia = StrankovanaSekvencia<Prispevok> { kurzor in
// Zavolanie API s kurzorom
try await apiKlient.nacitajPrispevky(kurzor: kurzor)
}
for try await stranka in sekvencia {
vsetky.append(contentsOf: stranka)
}
return vsetky
}
Použitie AsyncSequence pre stránkovanie je podľa mňa jedno z najelegantnejších riešení, aké Swift ponúka. Jednoduchý for try await cyklus automaticky načítava ďalšie stránky, kým sú k dispozícii. Žiadny manuálny tracking stavu, žiadne komplikované callbacky.
Integrácia so SwiftUI pomocou @Observable
V modernom SwiftUI (iOS 17+) používame makro @Observable namiesto staršieho ObservableObject. Vytvorme si kompletný ViewModel pre správu sieťových požiadavkov s načítavacím stavom a ošetrením chýb.
import SwiftUI
// Enum pre stav načítavania
enum StavNacitavania<T: Sendable>: Sendable {
case necinna
case nacitavanie
case uspech(T)
case chyba(String)
}
// ViewModel pre zoznam používateľov
@Observable
@MainActor
final class PouzivateliaViewModel {
var stav: StavNacitavania<[Pouzivatel]> = .necinna
var vybranyPouzivatel: Pouzivatel?
var zobrazChybu = false
var textChyby = ""
private let klient: APIClient
init(klient: APIClient = APIClient()) {
self.klient = klient
}
func nacitajPouzivatelov() async {
stav = .nacitavanie
let vysledok = await klient.bezpecneVykonaj(ZiskajPouzivatelov())
switch vysledok {
case .success(let pouzivatelia):
stav = .uspech(pouzivatelia)
case .failure(let chyba):
textChyby = chyba.localizedDescription
zobrazChybu = true
stav = .chyba(chyba.localizedDescription)
}
}
func obnovData() async {
await nacitajPouzivatelov()
}
}
// SwiftUI View
struct PouzivateliaView: View {
@State private var viewModel = PouzivateliaViewModel()
var body: some View {
NavigationStack {
Group {
switch viewModel.stav {
case .necinna:
ContentUnavailableView(
"Žiadne dáta",
systemImage: "person.3",
description: Text("Potiahnite nadol pre načítanie")
)
case .nacitavanie:
ProgressView("Načítavam používateľov...")
case .uspech(let pouzivatelia):
List(pouzivatelia, id: \.id) { pouzivatel in
VStack(alignment: .leading) {
Text(pouzivatel.meno)
.font(.headline)
Text(pouzivatel.email)
.font(.subheadline)
.foregroundStyle(.secondary)
}
}
.refreshable {
await viewModel.obnovData()
}
case .chyba(let sprava):
ContentUnavailableView(
"Nastala chyba",
systemImage: "exclamationmark.triangle",
description: Text(sprava)
)
}
}
.navigationTitle("Používatelia")
.task {
await viewModel.nacitajPouzivatelov()
}
.alert("Chyba", isPresented: $viewModel.zobrazChybu) {
Button("OK") {}
Button("Skúsiť znova") {
Task { await viewModel.nacitajPouzivatelov() }
}
} message: {
Text(viewModel.textChyby)
}
}
}
}
Modifikátor .task automaticky spustí asynchrónnu úlohu pri zobrazení view a zruší ju pri zmiznutí — o lifecycle sa nemusíte starať. .refreshable pridáva natívnu funkciu potiahnutia nadol. A vďaka @Observable sa view automaticky prekreslí pri každej zmene stavu. Jednoduché a funkčné.
Unit testovanie API klienta
Testovanie sieťového kódu je kriticky dôležité. Nechceme ale, aby testy záviseli od reálneho servera (to by bola nočná mora na CI). Využijeme URLProtocol na mockovanie sieťových odpovedí a dependency injection na vkladanie testovacích závislostí.
Mockovanie pomocou URLProtocol
import Foundation
// Mockovací URL protokol pre testy
final class MockURLProtocol: URLProtocol, @unchecked Sendable {
// Handler, ktorý definuje odpoveď pre každý požiadavok
nonisolated(unsafe) static var obsluznyHandler:
((URLRequest) throws -> (Data, HTTPURLResponse))?
override class func canInit(with request: URLRequest) -> Bool {
return true // Zachytíme všetky požiadavky
}
override class func canonicalRequest(for request: URLRequest) -> URLRequest {
return request
}
override func startLoading() {
guard let handler = MockURLProtocol.obsluznyHandler else {
client?.urlProtocolDidFinishLoading(self)
return
}
do {
let (data, odpoved) = try handler(request)
client?.urlProtocol(self, didReceive: odpoved, cacheStoragePolicy: .notAllowed)
client?.urlProtocol(self, didLoad: data)
client?.urlProtocolDidFinishLoading(self)
} catch {
client?.urlProtocol(self, didFailWithError: error)
}
}
override func stopLoading() {
// Prázdna implementácia - požadovaná podtriedou
}
}
Testovacie prípady
import Testing
@testable import MojaAplikacia
@Suite("Testy API klienta")
struct APIClientTesty {
let klient: APIClient
init() {
// Konfigurácia s mock protokolom
let konfiguracia = URLSessionConfiguration.ephemeral
konfiguracia.protocolClasses = [MockURLProtocol.self]
let session = URLSession(configuration: konfiguracia)
klient = APIClient(session: session)
}
@Test("Úspešné načítanie zoznamu používateľov")
func nacitaniePouzivatelov() async throws {
// Príprava mock dát
let mockPouzivatelia = [
Pouzivatel(id: 1, meno: "Jano", email: "[email protected]"),
Pouzivatel(id: 2, meno: "Mária", email: "[email protected]")
]
let mockData = try JSONEncoder().encode(mockPouzivatelia)
MockURLProtocol.obsluznyHandler = { poziadavka in
let odpoved = HTTPURLResponse(
url: poziadavka.url!,
statusCode: 200,
httpVersion: nil,
headerFields: nil
)!
return (mockData, odpoved)
}
// Vykonanie
let vysledok = try await klient.vykonaj(ZiskajPouzivatelov())
// Overenie
#expect(vysledok.count == 2)
#expect(vysledok[0].meno == "Jano")
#expect(vysledok[1].email == "[email protected]")
}
@Test("Správne ošetrenie 404 chyby")
func osetrenie404() async {
MockURLProtocol.obsluznyHandler = { poziadavka in
let odpoved = HTTPURLResponse(
url: poziadavka.url!,
statusCode: 404,
httpVersion: nil,
headerFields: nil
)!
return (Data(), odpoved)
}
do {
_ = try await klient.vykonaj(ZiskajPouzivatelov())
Issue.record("Očakávali sme chybu, ale požiadavok prešiel")
} catch let chyba as APIChyba {
#expect(chyba == .nenajdene)
} catch {
Issue.record("Neočakávaný typ chyby: \(error)")
}
}
@Test("POST požiadavok obsahuje správne telo")
func postPoziadavka() async throws {
MockURLProtocol.obsluznyHandler = { poziadavka in
// Overíme, že požiadavka obsahuje správne telo
#expect(poziadavka.httpMethod == "POST")
#expect(poziadavka.value(forHTTPHeaderField: "Content-Type") == "application/json")
let mockOdpoved = Pouzivatel(id: 3, meno: "Peter", email: "[email protected]")
let data = try JSONEncoder().encode(mockOdpoved)
let odpoved = HTTPURLResponse(
url: poziadavka.url!,
statusCode: 201,
httpVersion: nil,
headerFields: nil
)!
return (data, odpoved)
}
let novy = try await klient.vykonaj(
VytvorPouzivatela(meno: "Peter", email: "[email protected]")
)
#expect(novy.meno == "Peter")
}
}
Používame nový framework Swift Testing s makrami @Test a #expect namiesto staršieho XCTest. Je to výrazne čitateľnejšie a každý test beží izolovane s vlastnou konfiguráciou, čo zabraňuje vzájomným interakciám.
Osvedčené postupy a optimalizácia výkonu
Na záver si prejdime súbor praktických odporúčaní, ktoré z vášho API klienta urobia produkčne pripravený komponent. Niektoré z nich sa môžu zdať samozrejmé, ale z vlastnej skúsenosti viem, že práve na tieto detaily sa v praxi často zabúda.
Konfigurácia URLSession
// Optimalizovaná konfigurácia pre produkčné prostredie
func vytvorProdukciuSession() -> URLSession {
let konfiguracia = URLSessionConfiguration.default
// Časové limity
konfiguracia.timeoutIntervalForRequest = 30 // 30 sekúnd na požiadavok
konfiguracia.timeoutIntervalForResource = 300 // 5 minút na celkové stiahnutie
// Limity pripojení
konfiguracia.httpMaximumConnectionsPerHost = 6
// Cachovanie
let cache = URLCache(
memoryCapacity: 50 * 1024 * 1024, // 50 MB v pamäti
diskCapacity: 200 * 1024 * 1024 // 200 MB na disku
)
konfiguracia.urlCache = cache
konfiguracia.requestCachePolicy = .returnCacheDataElseLoad
// Sieťové nastavenia
konfiguracia.waitsForConnectivity = true
konfiguracia.multipathServiceType = .handover
return URLSession(configuration: konfiguracia)
}
// Konfigurácia pre sťahovanie na pozadí
func vytvorPozadioveSession(identifikator: String) -> URLSession {
let konfiguracia = URLSessionConfiguration.background(
withIdentifier: identifikator
)
konfiguracia.isDiscretionary = true
konfiguracia.sessionSendsLaunchEvents = true
return URLSession(configuration: konfiguracia)
}
Zrušenie požiadavkov
// Správne rušenie požiadavkov s Task
@Observable
@MainActor
final class VyhladavanieViewModel {
var vysledky: [Vysledok] = []
var hladanyText = "" {
didSet { vyhladajSoCakanim() }
}
private var aktivnaUloha: Task<Void, Never>?
private let klient = APIClient()
private func vyhladajSoCakanim() {
// Zrušenie predchádzajúceho požiadavku
aktivnaUloha?.cancel()
aktivnaUloha = Task {
// Debounce - počkáme 300ms pred odoslaním
try? await Task.sleep(for: .milliseconds(300))
// Kontrola, či úloha nebola zrušená
guard !Task.isCancelled else { return }
do {
let vysledky = try await klient.vyhladaj(text: hladanyText)
if !Task.isCancelled {
self.vysledky = vysledky
}
} catch {
// Pri zrušení ignorujeme chybu
if !Task.isCancelled {
print("Chyba vyhľadávania: \(error)")
}
}
}
}
}
Debounce s 300ms oneskorením je tu zásadný — bez neho by ste pri rýchlom písaní posielali požiadavok na každý stlačený kláves, čo by zbytočne zaťažilo server aj sieť.
Prehľad osvedčených postupov
- Používajte jednu inštanciu URLSession — nevytvárajte novú session pre každý požiadavok. URLSession efektívne spravuje pool pripojení a ich znovupoužitie.
- Označujte typy ako Sendable — Swift 6 vyžaduje explicitnú bezpečnosť pri konkurencii. Všetky dátové modely a API klient by mali byť
Sendable. - Vždy rušte nepotrebné požiadavky — pri odchode z obrazovky alebo pri novom vyhľadávaní zrušte predchádzajúce požiadavky cez
Task.cancel(). - Implementujte cachovanie — správne nastavený
URLCachedramaticky zlepšuje odozvu aj offline skúsenosť. Rešpektujte HTTP hlavičkyCache-ControlaETag. - Používajte ephemeral sessions v testoch —
URLSessionConfiguration.ephemeralneukladá dáta na disk, čo zabraňuje interferencii medzi testami. - Centralizujte konfiguráciu — základná URL, hlavičky a timeout by mali byť na jednom mieste, nie roztrúsené po celom projekte.
- Logovanie vypnite v produkcii — podrobné logovanie sieťových požiadavkov je užitočné pri vývoji, ale v produkčnej verzii môže spomaliť aplikáciu a prezradiť citlivé údaje.
- Využívajte waitsForConnectivity — nastavenie
waitsForConnectivity = truespôsobí, že URLSession automaticky počká na obnovenie pripojenia namiesto okamžitého zlyhania.
Často kladené otázky o sieťovaní v Swift
Aký je rozdiel medzi URLSession.shared a vlastnou konfiguráciou URLSession?
URLSession.shared je singleton s predvolenou konfiguráciou — vhodný pre jednoduché požiadavky, ale neumožňuje prispôsobiť cachovanie, timeouty, certifikátovú validáciu ani sieťové politiky. Pre produkčné aplikácie odporúčame vytvoriť vlastnú inštanciu URLSession s URLSessionConfiguration.default, kde si nastavíte presné časové limity, veľkosť cache a maximálny počet súbežných pripojení. Vlastná konfigurácia tiež umožňuje použiť ephemeral session pre citlivé dáta alebo background session pre sťahovanie veľkých súborov.
Ako správne ošetriť výpadok internetového pripojenia v Swift aplikácii?
Najrobustnejší prístup kombinuje viacero techník. Na úrovni URLSession nastavte waitsForConnectivity = true, aby sa požiadavky automaticky odložili namiesto okamžitého zlyhania. Pre aktívne monitorovanie stavu siete použite NWPathMonitor z Network frameworku, ktorý vás upozorní na zmeny pripojenia v reálnom čase. Na úrovni UI implementujte automatické opakovanie s exponenciálnym čakaním a zobrazujte používateľovi jasné informácie o stave. Zvážte aj offline mód cez lokálne cachovanie dát pomocou URLCache alebo perzistentnú databázu ako SwiftData.
Je bezpečné volať URLSession z hlavného vlákna?
Áno, volanie async metód URLSession z hlavného vlákna je úplne bezpečné. Na rozdiel od synchronného sieťovania, kde by sa hlavné vlákno zablokovalo, mechanizmus async/await zabezpečí, že vlákno sa uvoľní počas čakania na odpoveď servera. UI zostáva plne responzívne. Dôležité je však spracovávať výsledok na hlavnom vlákne (pomocou @MainActor), ak aktualizujete UI prvky. Modifier .task vo SwiftUI toto automaticky zabezpečuje a navyše ruší úlohu pri zmiznutí view.
Ako implementovať obnovu JWT tokenu bez opakovania požiadavku manuálne?
Najelegantnejšie riešenie je implementácia interceptora v API klientovi. Keď server vráti status 401, interceptor automaticky zavolá endpoint na obnovenie tokenu s refresh tokenom, uloží nový access token a zopakuje pôvodný požiadavok — to všetko transparentne pre volajúci kód. Dôležité je synchronizovať obnovenie tokenu pomocou actora, aby sa pri viacerých súbežných 401 odpovediach token obnovoval iba raz. Ostatné požiadavky čakajú na dokončenie obnovy a potom sa automaticky zopakujú s novým tokenom.
Kedy použiť Alamofire alebo inú knižnicu tretej strany namiesto čistého URLSession?
S príchodom async/await a moderných API v Swift 6.x sa potreba knižníc tretích strán výrazne znížila. Čistý URLSession dnes pokrýva väčšinu prípadov použitia vrátane JSON kódovania, upload/download s progresom, certifikátového pinningu aj pozadového sťahovania. Alamofire stále dáva zmysel v komplexných projektoch, kde využijete pokročilé funkcie ako automatická serializácia parametrov, validačné reťazce či multipart upload. Pre väčšinu stredne veľkých aplikácií je však vlastný API klient postavený na URLSession dostatočne výkonný, ľahší a bez externých závislostí.