Návod na filmové API ve Swiftu se SwiftUI
Tento návod vytvoří aplikaci pro iOS 16 ve Swiftu 5.9, která ukazuje filmy, jež jsou tento týden v trendech, v seznamu SwiftUI s plakáty, roky a skóre. Používá URLSession s async/await a AsyncImage, ošetřuje 401 a 429 a vysvětluje, proč má produkční aplikace volat amodb přes váš vlastní server.
Co vytvoříte
Vytvoříte Trending, aplikaci pro iOS s jednou obrazovkou: seznam filmů, které jsou tento týden v trendech, s plakátem, názvem, rokem a skóre. Potřebujete Xcode se Swiftem 5.9 nebo novějším, cílovou verzi iOS 16 a klíč amodb. Aplikace používá URLSession s async/await, modely Decodable a AsyncImage, bez balíčků. Počítejte s asi 20 minutami.
Klíč uvnitř aplikace lze získat
Každé tajemství, které se dodává v aplikaci, berte jako veřejné. Někdo může číst řetězce z binárního souboru, zkoumat síťový provoz nebo číst úložiště aplikace. V tomto návodu pochází klíč z proměnné schématu Xcode, což je na vašem vlastním zařízení v pořádku. Pro vydávanou aplikaci spusťte malý server, který klíč drží, přepošle volání do amodb a vrátí JSON. Nastavte baseURL v klientovi níže na tento server a uživatele ověřujte tam. Limit požadavků platí na klíč, takže server také umožňuje cachovat jednu odpověď pro všechny vaše uživatele.
Krok 1: Získejte API klíč
Porovnejte tarify na stránce ceník. Každý tarif vrací všechna data, volba se tedy týká požadavků za minutu. Vytvořte klíč v panelu účtu. Než napíšete jakýkoli kód ve Swiftu, ověřte ho z terminálu:
200 znamená, že klíč funguje. 401 znamená, že je nesprávný. Stránka autentizace vysvětluje oba způsoby odeslání klíče.
export AMODB_KEY=YOUR_API_KEY
curl -s -o /dev/null -w "%{http_code}\n" \
"https://api.amodb.dev/3/trending/movie/week?api_key=$AMODB_KEY"Krok 2: Vytvořte projekt v Xcode
Vytvořte projekt aplikace pro iOS s názvem Trending s rozhraním SwiftUI a nastavte minimální cílovou verzi na iOS 16. Smažte vygenerovaný ContentView.swift. Ve svém schématu otevřete Edit Scheme, zvolte Run, pak Arguments a přidejte proměnnou prostředí s názvem AMODB_KEY a svým klíčem. Xcode ji předá aplikaci jen tehdy, když ji spouštíte z Xcode.
Krok 3: Odešlete první požadavek
Přečtěte klíč z prostředí a pošlete ho jako hlavičku Bearer. Nastavte 10sekundový timeout, aby se zaseknuté připojení ukončilo.
let key = ProcessInfo.processInfo.environment["AMODB_KEY"] ?? ""
var request = URLRequest(url: URL(string: "https://api.amodb.dev/3/trending/movie/week?language=en-US")!)
request.timeoutInterval = 10
request.setValue("Bearer \(key)", forHTTPHeaderField: "Authorization")
let (data, response) = try await URLSession.shared.data(for: request)
print((response as? HTTPURLResponse)?.statusCode ?? 0, data.count)Krok 4: Dekódujte odpověď a ukažte plakáty
Odpověď je stránka s polem results s 20 položkami. Každý film má id, title, release_date, poster_path a vote_average, plus media_type nastavené na movie. Nastavte keyDecodingStrategy na .convertFromSnakeCase a deklarujte vlastnosti v camel case. Pole, která nedeklarujete, se ignorují, což se hodí pro pole, jež se objeví později. poster_path může být null a release_date může chybět nebo být prázdný řetězec, proto model deklaruje obě jako volitelná.
poster_path začíná lomítkem, takže řetězec výše správně spojí velikost a název souboru. Plakáty pocházejí z img.amodb.dev, který nepotřebuje klíč a cachuje obrázky 30 dní. Velikosti uvádí stránka obrázky a endpoint popisuje stránka trendy a seznamy.
struct Movie: Decodable, Identifiable {
let id: Int
let title: String
let releaseDate: String?
let posterPath: String?
let voteAverage: Double
var posterURL: URL? {
guard let posterPath else { return nil }
return URL(string: "https://img.amodb.dev/t/p/w185\(posterPath)")
}
}Krok 5: Ošetřete chyby a limity požadavků
Namapujte každý stav na chybu, kterou může obrazovka zobrazit.
| Stav | Význam | Co dělá aplikace |
|---|---|---|
| 401 | Klíč chybí, je nesprávný nebo pozastavený | Zobrazí zprávu, bez opakování. |
| 402 | Platnost tarifu skončila | Zobrazí zprávu. Obnovte ho na stránce ceník. |
| 429 | Překročili jste požadavky za minutu | Počká Retry-After sekund, nejvýše 30, opakuje nejvýše dvakrát. |
| 500 nebo 504 | Selhání na straně serveru | Zobrazí stav a nabídne obnovení tažením dolů. |
URLError | Offline, timeout, ztracené připojení | Zobrazí localizedDescription. |
Všechny kódy najdete na stránkách chyby a limity požadavků.
switch status {
case 200..<300:
return try Self.decoder.decode(T.self, from: data)
case 401:
throw AmodbError.invalidKey
case 429 where attempt < 2:
let header = (response as? HTTPURLResponse)?.value(forHTTPHeaderField: "Retry-After")
try await Task.sleep(for: .seconds(min(Int(header ?? "") ?? 5, 30)))
attempt += 1
case 429:
throw AmodbError.rateLimited
default:
throw AmodbError.server(status)
}Krok 6: Cachujte seznam
Edge cachuje seznamy trendů 30 minut, takže dřívější nový dotaz vrátí stejné filmy. Limit požadavků volání stále počítá. Držte poslední výsledek ve view modelu a načtěte znovu, jen když je starší než 30 minut nebo uživatel obnoví tažením dolů.
Krok 7: Kompletní aplikace
Tento jediný soubor obsahuje modely, klienta, view model a pohledy. Vložte ho přes vygenerovaný soubor aplikace.
Spusťte aplikaci v simulátoru. Seznam se po prvním volání naplní a tažení dolů vynutí opětovné načtení. Skóre je souhrnné vote_average ze seznamu a titul, který zatím nemá žádné hlasy, ukazuje 0.0. Pro skóre z IMDb nebo Kinopoisk vyžádejte film s append_. Objekt ratings obsahuje imdb, kinopoisk, metacritic, rotten_tomatoes a tmdb, každý buď null, nebo objekt s value a votes, jak ukazuje stránka hodnocení.
import SwiftUI
// MARK: Models
struct TrendingPage: Decodable {
let results: [Movie]
}
struct Movie: Decodable, Identifiable {
let id: Int
let title: String
let releaseDate: String?
let posterPath: String?
let voteAverage: Double
var year: String { String((releaseDate ?? "").prefix(4)) }
var posterURL: URL? {
guard let posterPath else { return nil }
return URL(string: "https://img.amodb.dev/t/p/w185\(posterPath)")
}
}
// MARK: Client
enum AmodbError: LocalizedError {
case missingKey
case invalidKey
case rateLimited
case server(Int)
var errorDescription: String? {
switch self {
case .missingKey: return "Set AMODB_KEY in the Xcode scheme."
case .invalidKey: return "The API key was rejected (401)."
case .rateLimited: return "Too many requests. Try again in a minute."
case .server(let status): return "The service answered with status \(status)."
}
}
}
struct AmodbClient {
var baseURL = URL(string: "https://api.amodb.dev/3")!
let apiKey: String
private static let decoder: JSONDecoder = {
let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase
return decoder
}()
func get<T: Decodable>(_ path: String, query: [URLQueryItem] = []) async throws -> T {
guard !apiKey.isEmpty else { throw AmodbError.missingKey }
var components = URLComponents(url: baseURL.appendingPathComponent(path), resolvingAgainstBaseURL: false)!
components.queryItems = query
var request = URLRequest(url: components.url!, timeoutInterval: 10)
request.setValue("Bearer \(apiKey)", forHTTPHeaderField: "Authorization")
var attempt = 0
while true {
let (data, response) = try await URLSession.shared.data(for: request)
let status = (response as? HTTPURLResponse)?.statusCode ?? 0
switch status {
case 200..<300:
return try Self.decoder.decode(T.self, from: data)
case 401:
throw AmodbError.invalidKey
case 429 where attempt < 2:
let header = (response as? HTTPURLResponse)?.value(forHTTPHeaderField: "Retry-After")
try await Task.sleep(for: .seconds(min(Int(header ?? "") ?? 5, 30)))
attempt += 1
case 429:
throw AmodbError.rateLimited
default:
throw AmodbError.server(status)
}
}
}
}
// MARK: View model
@MainActor
final class TrendingViewModel: ObservableObject {
@Published private(set) var movies: [Movie] = []
@Published private(set) var errorMessage: String?
@Published private(set) var isLoading = false
private let client: AmodbClient
private var loadedAt: Date?
init(client: AmodbClient) {
self.client = client
}
func load(force: Bool = false) async {
if !force, let last = loadedAt, !movies.isEmpty, Date().timeIntervalSince(last) < 30 * 60 {
return
}
isLoading = true
defer { isLoading = false }
do {
let page: TrendingPage = try await client.get(
"trending/movie/week",
query: [URLQueryItem(name: "language", value: "en-US")]
)
movies = page.results
loadedAt = Date()
errorMessage = nil
} catch {
errorMessage = error.localizedDescription
}
}
}
// MARK: Views
struct MovieRow: View {
let movie: Movie
var body: some View {
HStack(spacing: 12) {
AsyncImage(url: movie.posterURL) { phase in
switch phase {
case .success(let image):
image.resizable().scaledToFill()
case .failure:
Color.gray.opacity(0.2)
default:
ProgressView()
}
}
.frame(width: 60, height: 90)
.clipShape(RoundedRectangle(cornerRadius: 6))
VStack(alignment: .leading, spacing: 4) {
Text(movie.title).font(.headline)
Text(movie.year).font(.subheadline).foregroundStyle(.secondary)
Label(String(format: "%.1f", movie.voteAverage), systemImage: "star.fill")
.font(.caption)
}
}
}
}
struct ContentView: View {
@StateObject private var model: TrendingViewModel
init(client: AmodbClient) {
_model = StateObject(wrappedValue: TrendingViewModel(client: client))
}
var body: some View {
NavigationStack {
List(model.movies) { movie in
MovieRow(movie: movie)
}
.overlay {
if model.isLoading && model.movies.isEmpty {
ProgressView()
} else if let message = model.errorMessage, model.movies.isEmpty {
Text(message).multilineTextAlignment(.center).padding()
}
}
.navigationTitle("Trending")
.refreshable { await model.load(force: true) }
.task { await model.load() }
}
}
}
@main
struct TrendingApp: App {
private let client = AmodbClient(apiKey: ProcessInfo.processInfo.environment["AMODB_KEY"] ?? "")
var body: some Scene {
WindowGroup {
ContentView(client: client)
}
}
}Další kroky
- Přidejte obrazovku detailu, která načte
/movie/{id}sappend_, a pole najdete na stránce filmy.to_ response= credits, ratings - Přepněte položku dotazu
languagena jazyk zařízení s využitím 14 základních jazyků na stránce jazyky. - Přesuňte klíč na svůj server a před zveřejněním aplikace na něj nasměrujte
baseURL. - Vytvořte stejný seznam pro web pomocí Next.js.