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:
.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:):
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:
HomeView().apparentScreen("home")UIKit: Apparent.trackUIKitScreens() once at launch (a screen per view controller), or Apparent.screen("settings") yourself.
Events and users
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-outMore in Events and users.
Purchases
With RevenueCat:
Apparent.linkRevenueCat { Purchases.shared.attribution.setAttributes($0) }Without RevenueCat, StoreKit 2:
Apparent.observeStoreKitTransactions() // at launch: renewals, refunds, Ask to Buy
Apparent.trackPurchase(try await product.purchase()) // where you sellEach transaction is sent once, as purchase, subscription_renewed or refund.
Links and notifications
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_openedcarries the link with secrets in it (token,code,password,key,session… parameters) redacted, and itsutm_source,utm_mediumandutm_campaign. Handle links yourself? CallApparent.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_openedcarries only thecampaign(orutm_campaign) andtitleyou 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: truecollects nothing untilApparent.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 savedonce when the app id or key doesn't match. Copy both again from Connect. - Simulators and emulators say so themselves (
simulatorin 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.
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.