Skip to content

Swift examples

Snippets target Swift 5.7+ on iOS 15 / macOS 12 or newer, where URLSession has native async/await. The same code runs on Linux with the swift-corelibs-foundation URLSession or via AsyncHTTPClient (Swift on Server).

import Foundation
enum PhotoPick {
static let api = URL(string: "https://api.photopick.cz/api/v1")!
static let token = ProcessInfo.processInfo.environment["PHOTOPICK_API_KEY"]!
// For iOS apps load the token from Keychain instead of env vars.
}
extension URLRequest {
static func photoPick(_ path: String, method: String = "GET") -> URLRequest {
var req = URLRequest(url: PhotoPick.api.appendingPathComponent(path))
req.httpMethod = method
req.setValue("Bearer \(PhotoPick.token)", forHTTPHeaderField: "Authorization")
req.setValue("application/json", forHTTPHeaderField: "Accept")
return req
}
}
struct RateLimit: Decodable {
let limit: Int
let remaining: Int
let reset: Int
}
struct Me: Decodable {
let apiKeyId: Int
let customerId: Int
let name: String
let scopes: [String]
let rateLimit: RateLimit
}
func whoami() async throws -> Me {
let (data, response) = try await URLSession.shared.data(for: .photoPick("me"))
guard let http = response as? HTTPURLResponse, (200..<300).contains(http.statusCode) else {
throw URLError(.badServerResponse)
}
return try JSONDecoder().decode(Me.self, from: data)
}
// Usage
let me = try await whoami()
print(me.scopes, me.rateLimit.remaining)

Required scope: customer:read.

AsyncSequence that yields every photo across pages — constant memory, lazy.

struct Photo: Decodable {
let id: Int
let filename: String
// ... add the fields you need
}
struct Pagination: Decodable {
let nextCursor: String?
let hasMore: Bool
}
struct PhotoPage: Decodable {
let data: [Photo]
let pagination: Pagination
}
struct PhotoStream: AsyncSequence {
typealias Element = Photo
let pageSize: Int
struct Iterator: AsyncIteratorProtocol {
let pageSize: Int
var buffer: [Photo] = []
var cursor: String? = nil
var done = false
mutating func next() async throws -> Photo? {
if buffer.isEmpty {
if done { return nil }
var components = URLComponents(url: PhotoPick.api.appendingPathComponent("photos"),
resolvingAgainstBaseURL: false)!
components.queryItems = [URLQueryItem(name: "limit", value: String(pageSize))]
+ (cursor.map { [URLQueryItem(name: "cursor", value: $0)] } ?? [])
var req = URLRequest(url: components.url!)
req.setValue("Bearer \(PhotoPick.token)", forHTTPHeaderField: "Authorization")
let (data, _) = try await URLSession.shared.data(for: req)
let page = try JSONDecoder().decode(PhotoPage.self, from: data)
buffer = page.data
cursor = page.pagination.nextCursor
done = !page.pagination.hasMore
}
return buffer.isEmpty ? nil : buffer.removeFirst()
}
}
func makeAsyncIterator() -> Iterator { Iterator(pageSize: pageSize) }
}
// Usage
for try await photo in PhotoStream(pageSize: 100) {
print(photo.id, photo.filename)
}

Required scope: photos:read.

URLSession.download streams to disk without holding the full file in memory.

func downloadPhoto(id: Int, to destination: URL) async throws -> Photo {
// Metadata
let (metaData, _) = try await URLSession.shared.data(for: .photoPick("photos/\(id)"))
let meta = try JSONDecoder().decode(Photo.self, from: metaData)
// Bytes — follows redirects to the signed URL automatically
let (tempURL, response) = try await URLSession.shared.download(for: .photoPick("photos/\(id)/download"))
guard let http = response as? HTTPURLResponse, http.statusCode == 200 else {
throw URLError(.badServerResponse)
}
try? FileManager.default.removeItem(at: destination)
try FileManager.default.moveItem(at: tempURL, to: destination)
return meta
}
let meta = try await downloadPhoto(
id: 1234,
to: URL(fileURLWithPath: "photo-1234.jpg")
)
print("Saved \(meta.filename)")

Required scope: photos:read.

struct Tag: Codable {
let id: Int?
let name: String
let color: String?
}
struct TagAssignment: Codable {
let tagIds: [Int]
}
func send<T: Decodable>(_ req: URLRequest, body: Encodable? = nil) async throws -> T {
var req = req
req.setValue("application/json", forHTTPHeaderField: "Content-Type")
if let body { req.httpBody = try JSONEncoder().encode(body) }
let (data, response) = try await URLSession.shared.data(for: req)
guard let http = response as? HTTPURLResponse, (200..<300).contains(http.statusCode) else {
throw URLError(.badServerResponse)
}
return try JSONDecoder().decode(T.self, from: data)
}
// Create
let created: Tag = try await send(
.photoPick("tags", method: "POST"),
body: Tag(id: nil, name: "Selects", color: "#e94b60")
)
// Attach (full replace)
let _: TagAssignment = try await send(
.photoPick("photos/1234/tags", method: "PUT"),
body: TagAssignment(tagIds: [created.id!])
)
// Rename
let renamed: Tag = try await send(
.photoPick("tags/\(created.id!)", method: "PATCH"),
body: Tag(id: nil, name: "Final selects", color: nil)
)
// Delete
let (_, delResponse) = try await URLSession.shared.data(
for: .photoPick("tags/\(created.id!)", method: "DELETE")
)
print((delResponse as! HTTPURLResponse).statusCode) // 204

Required scopes: tags:write (create/rename/delete), photos:write (attach).

func withRetry<T>(maxAttempts: Int = 5, _ block: () async throws -> (data: Data, response: URLResponse)) async throws -> Data {
var lastResponse: URLResponse?
for attempt in 1...maxAttempts {
let (data, response) = try await block()
lastResponse = response
let http = response as! HTTPURLResponse
if (200..<300).contains(http.statusCode) { return data }
if http.statusCode < 500 && http.statusCode != 429 {
throw URLError(.badServerResponse) // 4xx — don't retry
}
let retryAfter = Int(http.value(forHTTPHeaderField: "Retry-After") ?? "") ?? attempt
let backoff = min(retryAfter, Int(pow(2.0, Double(attempt))))
try await Task.sleep(nanoseconds: UInt64(backoff) * 1_000_000_000)
}
throw URLError(.timedOut)
}
let data = try await withRetry {
try await URLSession.shared.data(for: .photoPick("photos"))
}

See Rate limits and Errors for the full vocabulary.