> ## Documentation Index
> Fetch the complete documentation index at: https://docs.trygravity.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# iOS SDK

> Swift SDK for integrating Gravity contextual ads into iOS, macOS, tvOS, and watchOS apps.

## Installation

Add the package in Xcode via **File → Add Package Dependencies**:

```
https://github.com/Try-Gravity/gravity-swift-sdk
```

Or in your `Package.swift`:

```swift theme={null}
dependencies: [
    .package(url: "https://github.com/Try-Gravity/gravity-swift-sdk", from: "1.0.0"),
]
```

## How the pieces fit together

<Steps>
  <Step title="Initialize the client">
    Create a `Gravity` instance with your API key and `production: true`.
  </Step>

  <Step title="Collect context & fetch ads">
    Call `gravity.getAds(...)` with the conversation messages, session ID, and placements. The SDK auto-collects device signals (UA, OS, screen, timezone). Run it in parallel with your LLM call.
  </Step>

  <Step title="Render">
    Use the built-in `GravityAdView` (SwiftUI) or render the ad data yourself. Impression tracking fires automatically on appear.
  </Step>
</Steps>

## Quick start

```swift theme={null}
import GravitySDK

// 1. Initialize (once, e.g. in your App or ViewModel)
let gravity = Gravity(apiKey: "pub_...", production: true)

// 2. Fetch ads in parallel with your LLM call
let messages = [
    Message(role: "user", content: "What's the best way to deploy a Postgres DB?"),
    Message(role: "assistant", content: "Here are some options...")
]

async let adResult = gravity.getAds(
    messages: messages,
    sessionId: "sess_abc123",
    userId: currentUser.id,
    placements: [.belowResponse()]
)

// Stream your LLM response...
let result = await adResult

// 3. Render
if let ad = result.ads.first {
    GravityAdView(ad: ad, gravity: gravity, sessionId: "sess_abc123")
}
```

## Constructor

```swift theme={null}
Gravity(
    apiKey: String? = nil,       // defaults to GRAVITY_API_KEY env var
    apiUrl: String = "https://server.trygravity.ai/api/v1/ad",
    timeoutSeconds: TimeInterval = 3.0,
    production: Bool = false,    // false = test ads, no billing
    relevancy: Double = 0.2,     // 0.0–1.0 contextual match threshold
    excludedTopics: [String]? = nil
)
```

| Parameter        | Type           | Default                   | Description                           |
| ---------------- | -------------- | ------------------------- | ------------------------------------- |
| `apiKey`         | `String?`      | `GRAVITY_API_KEY` env var | Your Gravity publisher API key        |
| `timeoutSeconds` | `TimeInterval` | `3.0`                     | Abort after this many seconds         |
| `production`     | `Bool`         | `false`                   | `false` returns test ads (no billing) |
| `relevancy`      | `Double`       | `0.2`                     | Minimum relevancy threshold 0.0–1.0   |
| `excludedTopics` | `[String]?`    | `nil`                     | Topics to exclude from matching       |

## `getAds()`

```swift theme={null}
await gravity.getAds(
    messages: [Message],
    sessionId: String,
    userId: String = "anonymous",
    placements: [Placement],
    userIP: String? = nil,
    hashedIdentity: HashedIdentity? = nil,
    extraUserFields: [String: String]? = nil,
    extraDeviceFields: [String: Any]? = nil,
    production: Bool? = nil,
    relevancy: Double? = nil
) -> AdResult
```

The SDK auto-collects device signals (User-Agent, OS, screen size, timezone, locale, device type) and sends them with the request. For mobile apps, this means your dashboard's Device and Geography breakdowns work automatically — no manual `device` forwarding needed.

<Warning>
  `getAds()` never throws. On any failure, it returns `AdResult.empty` (empty ads array). Safe to fire-and-forget.
</Warning>

## Placements

```swift theme={null}
// Convenience constructors
Placement.belowResponse()           // "below_response"
Placement.inlineResponse()          // "inline_response"
Placement.aboveResponse()           // "above_response"

// Custom
Placement(placement: "right_response", placementId: "sidebar")
```

## SwiftUI — render ads

```swift theme={null}
import GravitySDK

struct ChatView: View {
    let response: String
    let ads: [AdResponse]
    let gravity: Gravity
    let sessionId: String

    var body: some View {
        VStack {
            Text(response)
            if let ad = ads.first {
                SpecAdView(
                    ad: ad,
                    gravity: gravity,
                    sessionId: sessionId
                )
            }
        }
    }
}
```

`SpecAdView` automatically handles the Gravity-managed placement design, safe fallback, impression and click tracking, required ad disclosure, and server-requested feedback controls.

| Parameter      | Type         | Description                                                                                                                                                                                                                                                                                                 |
| -------------- | ------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `ad`           | `AdResponse` | The ad object from `getAds()`                                                                                                                                                                                                                                                                               |
| `gravity`      | `Gravity`    | Client instance for tracking                                                                                                                                                                                                                                                                                |
| `sessionId`    | `String`     | Must match the ad request                                                                                                                                                                                                                                                                                   |
| `showFeedback` | `Bool`       | Allow feedback controls. Default `true`. The prompt renders only when this is `true` **and** the server marked the ad with `feedbackPrompt: true` (selective — borderline-relevance / tight-auction ads). One tap submits and swaps to "Thanks for the feedback"; feedback taps never trigger the ad click. |

## Custom rendering

If you render ads with your own UI, fire tracking manually:

```swift theme={null}
// Impression — fire once when the ad becomes visible
gravity.trackImpression(ad)

// Click — always use clickUrl, not url
if let clickUrl = ad.clickUrl, let url = URL(string: clickUrl) {
    UIApplication.shared.open(url)
}
```

## PII hashing

Hash email and phone for attribution without sending raw values:

```swift theme={null}
import GravitySDK

let hashed = hashPII(email: user.email, phone: user.phone)

let result = await gravity.getAds(
    messages: messages,
    sessionId: sessionId,
    userId: user.id,
    placements: [.belowResponse()],
    hashedIdentity: hashed
)
```

## Ad feedback

```swift theme={null}
// Thumbs up
gravity.sendFeedback(
    sessionId: sessionId,
    ad: ad,
    sentiment: .thumbsUp
)

// Thumbs down with reasons
gravity.sendFeedback(
    sessionId: sessionId,
    ad: ad,
    sentiment: .thumbsDown,
    reasons: ["not_interested", "irrelevant"]
)
```

## Full example — parallel with LLM stream

```swift theme={null}
import GravitySDK

class ChatViewModel: ObservableObject {
    let gravity = Gravity(apiKey: "pub_...", production: true)
    @Published var ads: [AdResponse] = []
    @Published var response = ""

    func send(messages: [Message], sessionId: String, userId: String) async {
        // Fire ad request in parallel
        async let adResult = gravity.getAds(
            messages: messages,
            sessionId: sessionId,
            userId: userId,
            placements: [.belowResponse()]
        )

        // Stream LLM response
        for await chunk in streamLLM(messages) {
            await MainActor.run { response += chunk }
        }

        // Append ads
        let result = await adResult
        await MainActor.run { ads = result.ads }
    }
}
```

## FAQ

<AccordionGroup>
  <Accordion title="Does the ad request block my LLM response?">
    No. Use `async let` to fire `getAds()` in parallel with your LLM call. It never throws, so failures are silent.
  </Accordion>

  <Accordion title="Does the SDK collect IDFA?">
    No. The SDK does not access the Advertising Identifier. If you have IDFA consent via ATT, pass it as `extraDeviceFields: ["ifa": idfa]`.
  </Accordion>

  <Accordion title="What iOS versions are supported?">
    iOS 15+, macOS 12+, tvOS 15+, watchOS 8+.
  </Accordion>

  <Accordion title="Does the SDK add any third-party dependencies?">
    No. It uses only Foundation, CryptoKit, and URLSession — all built into the platform.
  </Accordion>
</AccordionGroup>
