O que significa “consumir uma API” no iOS
Consumir uma API é fazer requisições HTTP (como GET e POST) para um servidor, receber uma resposta (geralmente JSON) e transformar esses dados em modelos Swift para exibir na interface. No iOS moderno, o caminho mais direto é usar URLSession com async/await, decodificar com Codable e tratar estados como carregando, sucesso e erro.
Arquitetura simples e boas práticas (View vs Serviço)
Para manter o código organizado, separe responsabilidades:
- Serviço de rede: monta requisições, chama
URLSession, valida resposta e decodifica JSON. - Camada de API: define endpoints e métodos (ex.:
fetchPosts(),createPost(...)). - ViewModel: chama a API, controla estados (loading, erro, dados) e expõe propriedades para a View.
- View: apenas renderiza estados e dispara ações (ex.: “recarregar”).
Passo a passo: criando um módulo de rede com URLSession + async/await
1) Defina erros de rede
Erros claros facilitam mensagens para o usuário e debug. Crie um enum com casos comuns.
import Foundation
enum NetworkError: Error, LocalizedError {
case invalidURL
case invalidResponse
case httpStatus(Int)
case emptyData
case decoding(Error)
case transport(Error)
var errorDescription: String? {
switch self {
case .invalidURL:
return "URL inválida."
case .invalidResponse:
return "Resposta inválida do servidor."
case .httpStatus(let code):
return "Falha na requisição (HTTP \(code))."
case .emptyData:
return "Resposta vazia."
case .decoding:
return "Não foi possível interpretar os dados recebidos."
case .transport:
return "Erro de conexão. Verifique sua internet."
}
}
}2) Crie um cliente HTTP reutilizável
Esse cliente centraliza: execução da requisição, validação do status code, decodificação JSON e configuração de headers.
import Foundation
final class HTTPClient {
private let session: URLSession
private let decoder: JSONDecoder
init(session: URLSession = .shared) {
self.session = session
self.decoder = JSONDecoder()
// Ajuste conforme sua API (snake_case, datas etc.)
self.decoder.keyDecodingStrategy = .useDefaultKeys
}
func get<T: Decodable>(
url: URL,
headers: [String: String] = [:]
) async throws -> T {
var request = URLRequest(url: url)
request.httpMethod = "GET"
headers.forEach { request.setValue($0.value, forHTTPHeaderField: $0.key) }
return try await send(request)
}
func post<T: Decodable, Body: Encodable>(
url: URL,
body: Body,
headers: [String: String] = [:]
) async throws -> T {
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
headers.forEach { request.setValue($0.value, forHTTPHeaderField: $0.key) }
do {
request.httpBody = try JSONEncoder().encode(body)
} catch {
throw NetworkError.transport(error)
}
return try await send(request)
}
private func send<T: Decodable>(_ request: URLRequest) async throws -> T {
do {
let (data, response) = try await session.data(for: request)
guard let http = response as? HTTPURLResponse else {
throw NetworkError.invalidResponse
}
guard (200...299).contains(http.statusCode) else {
throw NetworkError.httpStatus(http.statusCode)
}
guard !data.isEmpty else {
throw NetworkError.emptyData
}
do {
return try decoder.decode(T.self, from: data)
} catch {
throw NetworkError.decoding(error)
}
} catch {
throw NetworkError.transport(error)
}
}
}Modelos Codable: criando e decodificando JSON
Vamos consumir uma API pública bem conhecida para testes: https://jsonplaceholder.typicode.com. Ela retorna posts e comentários em JSON.
- Ouça o áudio com a tela desligada
- Ganhe Certificado após a conclusão
- + de 5000 cursos para você explorar!
Baixar o aplicativo
Modelo para GET (Post)
import Foundation
struct Post: Identifiable, Codable {
let userId: Int
let id: Int
let title: String
let body: String
}Como as chaves do JSON são iguais às propriedades (userId, id, title, body), não precisamos de CodingKeys. Se sua API usar snake_case (ex.: user_id), você pode configurar decoder.keyDecodingStrategy = .convertFromSnakeCase.
Modelo para POST (criar Post)
Em um POST, normalmente você envia um corpo com os campos necessários. Aqui vamos enviar title, body e userId.
import Foundation
struct CreatePostBody: Encodable {
let title: String
let body: String
let userId: Int
}Camada de API: endpoints e métodos
Agora crie uma camada específica para a API, usando o HTTPClient. Assim, a ViewModel não precisa saber montar URLs ou requests.
import Foundation
final class PostsAPI {
private let client: HTTPClient
private let baseURL = URL(string: "https://jsonplaceholder.typicode.com")!
init(client: HTTPClient = HTTPClient()) {
self.client = client
}
func fetchPosts() async throws -> [Post] {
let url = baseURL.appending(path: "posts")
return try await client.get(url: url)
}
func createPost(title: String, body: String, userId: Int) async throws -> Post {
let url = baseURL.appending(path: "posts")
let payload = CreatePostBody(title: title, body: body, userId: userId)
// JSONPlaceholder aceita e retorna um objeto (simulado)
return try await client.post(url: url, body: payload)
}
}Se você precisar de headers (por exemplo, Authorization), passe no parâmetro headers do cliente:
let headers = ["Authorization": "Bearer SEU_TOKEN"]
let posts: [Post] = try await client.get(url: url, headers: headers)Cache básico em memória (sem persistência)
Um cache simples evita refazer a mesma requisição ao voltar para a tela. Aqui vamos guardar a lista em memória e definir uma “validade” curta.
import Foundation
actor PostsCache {
private var cachedPosts: [Post]?
private var timestamp: Date?
private let ttl: TimeInterval
init(ttl: TimeInterval = 60) {
self.ttl = ttl
}
func get() -> [Post]? {
guard let cachedPosts, let timestamp else { return nil }
if Date().timeIntervalSince(timestamp) <= ttl {
return cachedPosts
}
return nil
}
func set(_ posts: [Post]) {
self.cachedPosts = posts
self.timestamp = Date()
}
func clear() {
self.cachedPosts = nil
self.timestamp = nil
}
}Usamos actor para garantir segurança de concorrência ao acessar o cache.
ViewModel: estados de carregamento, erro e dados
Uma forma prática de modelar estado é com um enum. Isso evita múltiplas flags soltas.
import Foundation
@MainActor
final class PostsViewModel: ObservableObject {
enum State {
case idle
case loading
case loaded([Post])
case failed(String)
}
@Published private(set) var state: State = .idle
private let api: PostsAPI
private let cache: PostsCache
init(api: PostsAPI = PostsAPI(), cache: PostsCache = PostsCache()) {
self.api = api
self.cache = cache
}
func loadPosts(forceRefresh: Bool = false) async {
if !forceRefresh, let cached = await cache.get() {
state = .loaded(cached)
return
}
state = .loading
do {
let posts = try await api.fetchPosts()
await cache.set(posts)
state = .loaded(posts)
} catch {
let message = (error as? LocalizedError)?.errorDescription ?? error.localizedDescription
state = .failed(message)
}
}
func createSamplePost() async {
state = .loading
do {
_ = try await api.createPost(title: "Novo post", body: "Conteúdo", userId: 1)
// Após criar, recarregue (ou insira localmente na lista)
await loadPosts(forceRefresh: true)
} catch {
let message = (error as? LocalizedError)?.errorDescription ?? error.localizedDescription
state = .failed(message)
}
}
}Por que @MainActor? Porque atualizamos propriedades observadas pela UI. Assim garantimos que alterações em @Published ocorram na thread principal.
Tela: consumindo API pública e exibindo em lista
A View vai reagir ao estado: mostra progresso, lista ou erro com botão de tentar novamente.
import SwiftUI
struct PostsListView: View {
@StateObject private var vm = PostsViewModel()
var body: some View {
Group {
switch vm.state {
case .idle, .loading:
VStack(spacing: 12) {
ProgressView()
Text("Carregando...")
.foregroundStyle(.secondary)
}
case .failed(let message):
VStack(spacing: 12) {
Text("Não foi possível carregar")
.font(.headline)
Text(message)
.font(.subheadline)
.foregroundStyle(.secondary)
.multilineTextAlignment(.center)
Button("Tentar novamente") {
Task { await vm.loadPosts(forceRefresh: true) }
}
}
.padding()
case .loaded(let posts):
List(posts) { post in
NavigationLink(value: post) {
VStack(alignment: .leading, spacing: 6) {
Text(post.title)
.font(.headline)
.lineLimit(2)
Text(post.body)
.font(.subheadline)
.foregroundStyle(.secondary)
.lineLimit(2)
}
}
}
.refreshable {
await vm.loadPosts(forceRefresh: true)
}
}
}
.navigationDestination(for: Post.self) { post in
PostDetailView(post: post)
}
.navigationTitle("Posts")
.toolbar {
ToolbarItem(placement: .topBarTrailing) {
Button("POST") {
Task { await vm.createSamplePost() }
}
}
}
.task {
await vm.loadPosts()
}
}
}Tela de detalhes
import SwiftUI
struct PostDetailView: View {
let post: Post
var body: some View {
ScrollView {
VStack(alignment: .leading, spacing: 12) {
Text(post.title)
.font(.title2)
.bold()
Text(post.body)
.font(.body)
Divider()
Text("User ID: \(post.userId)")
.font(.footnote)
.foregroundStyle(.secondary)
}
.padding()
}
.navigationTitle("Detalhes")
.navigationBarTitleDisplayMode(.inline)
}
}Tratamento de erros: o que checar em cada etapa
| Etapa | Problema comum | Como tratamos |
|---|---|---|
| Montar URL | String inválida | NetworkError.invalidURL (quando aplicável) |
| Conexão | Sem internet, timeout | NetworkError.transport |
| Resposta HTTP | Status 401/404/500 | NetworkError.httpStatus(code) |
| Dados | Body vazio | NetworkError.emptyData |
| Decodificação | Modelo não bate com JSON | NetworkError.decoding(error) |
Introdução prática a POST: headers e envio de JSON
Para POST, os pontos essenciais são:
- Definir
httpMethod = "POST" - Adicionar header
Content-Type: application/json - Codificar o corpo com
JSONEncodere atribuir emhttpBody - Decodificar a resposta (muitas APIs retornam o recurso criado)
No exemplo, o botão “POST” chama createSamplePost(). Em uma API real, você normalmente coletaria os campos em uma tela de formulário e enviaria o payload.
Dicas rápidas para evitar bugs comuns
- Modelos e JSON não batem: verifique nomes de chaves, tipos (String vs Int) e campos opcionais. Se um campo pode vir ausente, declare como opcional (
String?). - Datas: se a API retorna datas em string, configure
decoder.dateDecodingStrategy(ex.: ISO8601) e useDateno modelo. - Atualização de UI fora da main thread: mantenha a ViewModel em
@MainActor. - Requisições repetidas: use cache em memória (como mostrado) e
.refreshablepara atualização manual. - Testabilidade: injete dependências (
HTTPClient,PostsAPI) via init para facilitar mocks em testes.