Skip to content

coodly/UntappdAPI

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

20 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

UntappdAPI

A modern Swift package for interacting with the Untappd API v4. Built with Swift 6 concurrency features including async/await, actors, and Sendable types for complete thread safety.

Features

  • âś… Modern async/await API - No completion handlers, just clean async code
  • âś… Swift 6 Concurrency - Full Sendable conformance and actor isolation
  • âś… Type-safe - Strongly typed models with Codable support
  • âś… Pluggable networking - Bring your own URLSession or custom implementation
  • âś… Comprehensive error handling - Typed errors with detailed information
  • âś… Compact mode support - Reduce response payload size when needed

Requirements

  • Swift 6.2+
  • macOS 13.0+
  • Untappd API credentials (client ID and client secret)

Installation

Swift Package Manager

Add the following to your Package.swift file:

dependencies: [
    .package(url: "https://github.com/coodly/swift-untappd-api.git", from: "1.0.0")
]

Or add it through Xcode:

  1. File → Add Package Dependencies
  2. Enter the repository URL
  3. Select version requirements

Getting Started

1. Get API Credentials

Register your application at Untappd API to obtain your client ID and client secret.

2. Implement NetworkFetch

The library uses a protocol-based approach for networking, allowing you to use URLSession or any custom implementation:

import Foundation
import UntappdAPI

struct URLSessionFetch: NetworkFetch {
    func fetch(request: URLRequest) async throws -> (Data, URLResponse) {
        try await URLSession.shared.data(for: request)
    }
}

3. Initialize the Client

let fetcher = URLSessionFetch()
let untappd = await Untappd.anonymous(
    clientID: "YOUR_CLIENT_ID",
    clientSecret: "YOUR_CLIENT_SECRET",
    fetch: fetcher
)

Usage

Search for Beers

Search the Untappd database for beers by name:

do {
    let beers = try await untappd.search(beer: "Pliny the Elder")

    for beer in beers {
        print("\(beer.beer.beerName) by \(beer.brewery.breweryName)")
        print("Style: \(beer.beer.beerStyle)")
        print("ABV: \(beer.beer.beerAbv)%")
        print("---")
    }
} catch {
    print("Search failed: \(error)")
}

Get Beer Details

Retrieve detailed information about a specific beer:

do {
    let beerId = 4691
    let details = try await untappd.info(for: beerId)

    print("Name: \(details.beerName)")
    print("Style: \(details.beerStyle)")
    print("ABV: \(details.beerAbv)%")
    print("Rating: \(details.ratingScore)")
    print("Description: \(details.beerDescription)")
    print("Brewery: \(details.brewery.breweryName)")

    // Access photos if available
    if let media = details.media {
        print("Photos: \(media.items.count)")
        for photo in media.items {
            print("Photo URL: \(photo.photo.photoImgLg)")
        }
    }
} catch {
    print("Failed to fetch beer info: \(error)")
}

Compact Mode

Use compact mode to reduce the response payload size:

let details = try await untappd.info(for: beerId, compact: true)

Get Info from BeerOverview

If you have a BeerOverview from search results, you can fetch full details directly:

let searchResults = try await untappd.search(beer: "IPA")
if let firstBeer = searchResults.first {
    let details = try await untappd.info(for: firstBeer.beer)
    print("Full details: \(details)")
}

Data Models

BeerItem

Returned from search results:

struct BeerItem: Codable, Sendable {
    let beer: BeerOverview
    let brewery: Brewery
}

BeerOverview

Lightweight beer information:

struct BeerOverview: Codable, Sendable {
    let bid: Int                // Beer ID
    let beerName: String        // Beer name
    let beerStyle: String       // Style (e.g., "IPA - American")
    let beerAbv: Double         // Alcohol by volume
}

BeerDetails

Complete beer information:

struct BeerDetails: Codable, Sendable {
    let bid: Int
    let beerName: String
    let beerStyle: String
    let beerDescription: String
    let beerAbv: Double
    let ratingScore: Double
    let brewery: Brewery
    let media: MediaContainer?   // Optional photos
}

Brewery

Brewery information:

struct Brewery: Codable, Sendable {
    let breweryId: Int
    let breweryName: String
}

Error Handling

The library throws typed UntappdError for different failure scenarios:

enum UntappdError: Error {
    case noData                                            // No data received
    case network(Error)                                    // Network error (wraps underlying error)
    case server(String)                                    // Server error (HTTP status != 200)
    case invalidJSON                                       // JSON decoding failed
    case unknown                                           // Unknown error
    case rateLimitExceeded(limit: Int, remaining: Int, resetsAt: Date?)  // Rate limit hit
}

All errors conform to LocalizedError, so you can get user-friendly descriptions:

do {
    let beers = try await untappd.search(beer: "IPA")
    // Process beers...
} catch {
    // Simple error display using localized description
    print(error.localizedDescription)
}

For more granular error handling:

do {
    let beers = try await untappd.search(beer: "IPA")
    // Process beers...
} catch let error as UntappdError {
    switch error {
    case .noData:
        print("No data received from server")
    case .network(let underlying):
        print("Network error: \(underlying.localizedDescription)")
    case .server(let message):
        print("Server error: \(message)")
    case .invalidJSON:
        print("Failed to decode response")
    case .rateLimitExceeded(let limit, let remaining, let resetsAt):
        print("Rate limit hit: \(remaining)/\(limit) requests remaining")
        if let resetDate = resetsAt {
            print("Resets at: \(resetDate)")
        }
    case .unknown:
        print("Unknown error occurred")
    }
} catch {
    print("Unexpected error: \(error)")
}

Optional Logging

Enable logging for debugging purposes:

import UntappdAPI

// Implement the Logger protocol
struct ConsoleLogger: Logger {
    func log<T>(_ object: T, file: String, function: String, line: Int) {
        print("[\(file):\(line)] \(function) - \(object)")
    }
}

// Set the logger
Logging.set(logger: ConsoleLogger())

Advanced Usage

Custom NetworkFetch Implementation

You can provide a custom networking implementation for testing or advanced use cases:

actor MockNetworkFetch: NetworkFetch {
    private let mockData: Data

    init(mockData: Data) {
        self.mockData = mockData
    }

    func fetch(request: URLRequest) async throws -> (Data, URLResponse) {
        let response = HTTPURLResponse(
            url: request.url!,
            statusCode: 200,
            httpVersion: nil,
            headerFields: nil
        )!
        return (mockData, response)
    }
}

// Use in tests
let mockFetch = MockNetworkFetch(mockData: testJSON)
let untappd = await Untappd.anonymous(
    clientID: "test",
    clientSecret: "test",
    fetch: mockFetch
)

Concurrency-Safe Design

The library is built with Swift 6 concurrency in mind:

  • All data models conform to Sendable
  • Internal state is protected by actors
  • No data races possible at compile time
  • Safe to use across multiple tasks and actors
// Safe to call from multiple tasks concurrently
await withTaskGroup(of: [BeerItem].self) { group in
    group.addTask {
        try await untappd.search(beer: "IPA")
    }
    group.addTask {
        try await untappd.search(beer: "Stout")
    }

    for try await results in group {
        print("Found \(results.count) beers")
    }
}

API Rate Limits

Untappd enforces API rate limits (typically 100 requests per hour for anonymous access). When you hit the rate limit, the library will throw a UntappdError.rateLimitExceeded error that includes:

  • The total rate limit (limit)
  • Remaining requests (remaining)
  • When the limit resets (resetsAt)

The library detects rate limit errors but does not implement automatic throttling or retry logic. You should handle rate limiting in your application code, for example:

do {
    let beers = try await untappd.search(beer: "IPA")
    // Process beers...
} catch UntappdError.rateLimitExceeded(let limit, let remaining, let resetsAt) {
    if let resetDate = resetsAt {
        let waitTime = resetDate.timeIntervalSinceNow
        print("Rate limit exceeded. Waiting \(waitTime) seconds...")
        try await Task.sleep(nanoseconds: UInt64(waitTime * 1_000_000_000))
        // Retry the request
    }
}

## License

Licensed under the Apache License, Version 2.0. See LICENSE file for details.

Copyright 2019 Coodly LLC

## Contributing

Contributions are welcome! Please feel free to submit a Pull Request.

## Resources

- [Untappd API Documentation](https://untappd.com/api/docs)
- [Swift Concurrency](https://docs.swift.org/swift-book/documentation/the-swift-programming-language/concurrency/)
- [Swift Package Manager](https://swift.org/package-manager/)

About

No description, website, or topics provided.

Resources

License

Stars

Watchers

Forks

Packages

No packages published