# Swift (iOS)

A Swift Package with no dependencies, for iOS 15+ and macOS 12+. SwiftUI and UIKit.

## Install

Xcode › File › Add Package Dependencies… › https://github.com/AhmedAshour/apparent-swift (Up to Next Major Version from 0.2.1, product `Apparent`).

In a `Package.swift`:

```swift Package.swift
.package(url: "https://github.com/AhmedAshour/apparent-swift", from: "0.2.1"),
// and on your target:
.product(name: "Apparent", package: "apparent-swift"),
```

## Start it

In the App struct's init, or application(_:didFinishLaunchingWithOptions:):

```swift
import Apparent

Apparent.start(app: "<app id>", key: "<write key>")
```

For development builds that should send nothing: `Apparent.start(app: "<app id>", key: "<write key>", disabled: true)`. You usually don't need it: simulators and TestFlight phones are left out of the numbers by themselves.

## Screens

SwiftUI, on each screen's view:

```swift
HomeView().apparentScreen("home")
```

UIKit: `Apparent.trackUIKitScreens()` once at launch (a screen per view controller), or `Apparent.screen("settings")` yourself.

## Events and users

```swift
Apparent.track("paywall_view", properties: ["source": "onboarding", "price": 4.99])
Apparent.setProperties(["plan": "free"])        // on every later event; nil removes one
Apparent.identify(userID: user.id)              // after sign-in
Apparent.logout()                               // on sign-out
```

More in [Events and users](https://apparent.fyi/docs/events).

## Purchases

With RevenueCat:

```swift
Apparent.linkRevenueCat { Purchases.shared.attribution.setAttributes($0) }
```

Without RevenueCat, StoreKit 2:

```swift
Apparent.observeStoreKitTransactions()                 // at launch: renewals, refunds, Ask to Buy
Apparent.trackPurchase(try await product.purchase())   // where you sell
```

Each transaction is sent once, as `purchase`, `subscription_renewed` or `refund`.

## Links and notifications

```swift
WindowGroup { RootView().apparentDeepLinks() }        // deep_link_opened for links that open the app

// UNUserNotificationCenterDelegate
func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse,
                            withCompletionHandler done: @escaping () -> Void) {
    Apparent.handleNotificationResponse(response)     // push_opened
    done()
}
```

- `deep_link_opened` carries the link with secrets in it (`token`, `code`, `password`, `key`, `session`… parameters) redacted, and its `utm_source`, `utm_medium` and `utm_campaign`. Handle links yourself? Call `Apparent.handleDeepLink(url)` there instead of the modifier. A campaign link that opens the app on its first day counts as where that person came from.
- `push_opened` carries only the `campaign` (or `utm_campaign`) and `title` you put in the notification's data, never what it said. One per tap.
- Turn either off with `trackDeepLinks: false` / `trackPushOpens: false`.

## Errors and opting out

- Crashes (Swift runtime errors, bad memory access, uncaught exceptions) are saved as the app goes down and sent on the next launch. Other crash reporters keep working.
- Errors you catch: `Apparent.captureError(error)`.
- `Apparent.optOut()` / `Apparent.optIn()`, remembered across launches.
- Consent first: `optOutByDefault: true` collects nothing until `Apparent.optIn()`, also on later launches until then.
- Nothing showing up? In a debug build the SDK prints `Apparent: app id or key is wrong — events are not being saved` once when the app id or key doesn't match. Copy both again from Connect.
- Simulators and emulators say so themselves (`simulator` in each batch), so they're never counted, even in a release build.

## Privacy

The package ships a `PrivacyInfo.xcprivacy`: no tracking, no tracking domains. No IDFA, no IDFV, no tracking prompt. The App Privacy answers are on [Privacy answers](https://apparent.fyi/docs/privacy).

## Options

Everything else goes in `Apparent.start(ApparentConfiguration(app:key:…))`:

| Option | Default | Does |
|---|---|---|
| `disabled` | `false` | Store and send nothing |
| `userID` | `nil` | The signed-in person, if known at launch |
| `anonymousID` | `nil` | An id you already have (RevenueCat's app user id), asked for on the first launch only |
| `debug` | `false` | Print what is tracked and sent |
| `sessionTimeout` | 30 min | Time in the background before a visit ends |
| `captureErrors` | `true` | Report crashes on the next launch |
| `trackDeepLinks` | `true` | `deep_link_opened` from `.apparentDeepLinks()` / `handleDeepLink` |
| `trackPushOpens` | `true` | `push_opened` from `handleNotificationResponse` |
| `optOutByDefault` | `false` | Nothing until `optIn()` |
| `beforeSend` | `nil` | `{ event in … }`: change `event.name` / `event.properties`, or return `nil` to drop it. Sees every event, automatic ones too. If it throws, the event is kept |

A `start` without an app id or a key is ignored, so a later correct one still works.
