For the complete documentation index, see llms.txt. This page is also available as Markdown.

First request

Build a small SwiftUI app that gets a client token from your token endpoint, searches for a food, fetches its full details, computes a portion, and shows the result.

1. Create the Xcode project

  1. In Xcode, choose File → New → Project → iOS → App.

  2. Name it JanuaryQuickstart, with SwiftUI for Interface, Swift for Language, and iOS 15 or later as the deployment target.

  3. Add the SDK to the JanuaryQuickstart target.

2. Set the scheme's environment variables

Choose Product → Scheme → Edit Scheme → Run → Arguments and add these environment variables:

JANUARY_PARTNER_TOKEN_URL=https://your-backend.example.com/january/client-token
JANUARY_PARTNER_SESSION_TOKEN=your-app-session-token
JANUARY_END_USER_ID=acme-user-8271

Use your token endpoint's URL, a valid session token for it, and the end-user ID your backend mints tokens for.

No backend yet? Start the token relay, set JANUARY_PARTNER_TOKEN_URL=http://localhost:8787/api/january/client-token, and leave out JANUARY_PARTNER_SESSION_TOKEN.

3. Add the source

Add a file named BackendTokenProvider.swift containing the BackendTokenProvider from Authentication. Leave JanuaryQuickstartApp.swift as Xcode created it, and replace ContentView.swift with:

import Foundation
import January
import SwiftUI

struct QuickstartError: LocalizedError {
    let errorDescription: String?
    init(_ message: String) { errorDescription = message }
}

@MainActor
final class QuickstartModel: ObservableObject {
    enum State {
        case loading
        case loaded(name: String, servings: Int, calories: Double)
        case failed(String)
    }

    @Published private(set) var state: State = .loading
    private var client: JanuaryClient?

    func load() async {
        state = .loading
        do {
            let client = try self.client ?? makeClient()
            self.client = client

            let results = try await client.foods.search(
                .init(query: "greek yogurt", category: .branded, limit: 10)
            )
            guard let match = results.items.first else {
                state = .failed("January returned no matching foods.")
                return
            }

            let food = try await client.foods.get(id: match.id)
            let portion = try food.portion() // One primary serving
            state = .loaded(
                name: food.name ?? "Unnamed food",
                servings: food.servings.count,
                calories: portion.nutrition.calories?.value ?? 0
            )
        } catch is CancellationError {
            return
        } catch {
            state = .failed(error.localizedDescription)
        }
    }

    private func makeClient() throws -> JanuaryClient {
        let environment = ProcessInfo.processInfo.environment
        guard let rawURL = environment["JANUARY_PARTNER_TOKEN_URL"],
              let tokenEndpoint = URL(string: rawURL) else {
            throw QuickstartError("Set JANUARY_PARTNER_TOKEN_URL in the Xcode scheme.")
        }
        guard let endUserID = environment["JANUARY_END_USER_ID"] else {
            throw QuickstartError("Set JANUARY_END_USER_ID in the Xcode scheme.")
        }
        let sessionToken = environment["JANUARY_PARTNER_SESSION_TOKEN"] ?? ""

        return try JanuaryClient(
            endUserID: endUserID,
            timezone: TimeZone.current,
            clientTokenProvider: BackendTokenProvider(
                tokenEndpoint: tokenEndpoint,
                appSessionToken: { sessionToken }
            )
        )
    }
}

struct ContentView: View {
    @StateObject private var model = QuickstartModel()

    var body: some View {
        VStack(spacing: 16) {
            Text("January SDK")
                .font(.title.bold())

            switch model.state {
            case .loading:
                ProgressView("Connecting…")
            case .loaded(let name, let servings, let calories):
                Text(name).font(.headline)
                Text("\(servings) serving options")
                Text("\(calories, specifier: "%.1f") calories")
            case .failed(let message):
                Text(message).foregroundStyle(.red)
                Button("Try Again") {
                    Task { await model.load() }
                }
            }
        }
        .padding()
        .task {
            await model.load()
        }
    }
}

The model creates the client once and reuses it on Try Again, as a real app should (Client lifecycle).

4. Run it

Choose an iOS Simulator and press Run. The screen shows Connecting…, then:

On failure, it shows the error message and a Try Again button. An error from your token endpoint shows the provider's message, such as "Your token endpoint returned HTTP 401." If the endpoint can't be reached, the SDK retries for about 47 seconds before showing the error. See Errors and Troubleshooting.

Next: Example app, or skip to Core concepts.

Last updated

Was this helpful?