Internal handoff · iOS
This document explains how the app starts: Splash → ATT → Adjust → OneSignal tags → In-App Message → Home. Follow it step by step. You do not need deep SDK experience.
| Area | Owner |
|---|---|
Splash screen UI, ATT prompt, Adjust SDK, fetching IDs, sending tags, waiting for OneSignal user, firing app_ready, navigating to Home |
You (app developer) |
OneSignal dashboard: In-App Message HTML, Full Screen layout, trigger app_ready = true, offer URL / iframe |
Already done by the OneSignal owner — do not recreate or change it |
The offer is shown by OneSignal IAM (not a custom in-app browser screen).
| Term | Meaning |
|---|---|
| Splash | The first full-screen image while the app prepares. |
| ATT | App Tracking Transparency — Apple’s “Allow tracking?” popup (iOS 14+). |
| Adjust | Attribution SDK. Gives us adid, and helps with idfa / idfv / campaign. |
| OneSignal | Used here for In-App Messages only (not push notifications in this flow). |
| IAM | In-App Message — a full-screen web message shown by OneSignal over the app. |
| Tag | A key/value saved on the OneSignal user (example: idfa → device ID). |
| Trigger | A condition that tells OneSignal “show this IAM now”. We use app_ready = true. |
| onesignalId | OneSignal’s user id. We wait for it (with timeout) before relying on tags/IAM. |
Work in parallel where possible. Do not wait one-after-another if steps can overlap.
Initialize OneSignal immediately. Keep IAM paused so it does not show too early.
Start the launch coordinator. Show splash image.
(A) Wait for OneSignal user id (max ~12 seconds)
(B) Show ATT → then init Adjust → fetch adid, idfa, idfv in parallel
Also listen for Adjust attribution and send campaign when it arrives (do not block for it).
After Adjust IDs are ready and OneSignal user wait finished (or timed out), call addTags with mmp_id, idfa, idfv.
Set trigger app_ready = true and unpause In-App Messages. Wait until IAM did display (or timeout ~20s).
After IAM is shown (+ short hold) or timeout: switch to Home with animations disabled. IAM can stay on top.
max(ATT+Adjust IDs, OneSignal user) plus IAM display time — not the sum of every step one after another.
Add the OneSignal iOS SDK (5.x) and link these products to your app target:
| Product / framework | Required? | Why |
|---|---|---|
OneSignalFramework |
Yes | Core OneSignal SDK (user, tags, init). |
OneSignalInAppMessages |
Yes | In-App Messages (IAM). Without this, IAM will not show. |
OneSignalLocation |
No | Not needed for this flow. |
OneSignalExtension |
No | Notification Service Extension only — do not use for this IAM-only flow. |
Link AdjustSdk (not Adjust WebBridge). Also add these system frameworks to the app target (usually Optional / Weak):
| Framework | Required? | Why |
|---|---|---|
AdjustSdk (SPM) |
Yes | Attribution, adid, campaign, ID helpers. |
AdSupport.framework |
Yes | IDFA access. |
AppTrackingTransparency.framework |
Yes | ATT permission prompt (iOS 14+). |
AdServices.framework |
Yes (recommended) | Apple Search Ads / AdServices attribution support. |
StoreKit.framework |
Yes (recommended) | SKAdNetwork APIs. |
WebKit.framework |
Yes (recommended) | Used with Adjust / web surfaces; also needed for OneSignal IAM WebViews. |
AdjustWebBridge |
No | Not used in this flow — do not link it. |
In Xcode: select the app target → General → Frameworks, Libraries, and Embedded Content (and/or Package Products).
| Key | Why |
|---|---|
NSUserTrackingUsageDescription |
Required by Apple for the ATT popup text. |
OneSignal_in_app_message_hide_gray_overlay = true |
Cleaner full-screen IAM (less gray chrome). |
OneSignal_in_app_message_hide_drop_shadow = true |
Cleaner full-screen IAM (no drop shadow). |
NSAdvertisingAttributionReportEndpoint |
Send SKAdNetwork postback copies to Adjust: https://adjust-skadnetwork.com |
AttributionCopyEndpoint |
AdAttributionKit postback copies to Adjust: https://adjust-skadnetwork.com |
SKAdNetworkItems |
Network IDs for Meta / Google / TikTok / Snapchat — see next section. |
Add these IDs under SKAdNetworkItems in Info.plist so Apple can attribute installs
when you advertise on those networks (with Adjust). All values must be lowercase.
| Network | SKAdNetworkIdentifier |
|---|---|
| Meta (Facebook) | v9wttpbfk9.skadnetwork |
| Meta (Instagram) | n38lu8286q.skadnetwork |
cstr6suwn9.skadnetwork | |
| TikTok | 22mmun2rn5.skadnetwork |
| TikTok / Pangle | 238da6jt44.skadnetwork |
| TikTok / Pangle | gta9lk7p23.skadnetwork |
| Snapchat | 424m5254lk.skadnetwork |
| Snapchat | 8s468mfl3y.skadnetwork |
<key>SKAdNetworkItems</key>
<array>
<dict><key>SKAdNetworkIdentifier</key><string>v9wttpbfk9.skadnetwork</string></dict>
<dict><key>SKAdNetworkIdentifier</key><string>n38lu8286q.skadnetwork</string></dict>
<dict><key>SKAdNetworkIdentifier</key><string>cstr6suwn9.skadnetwork</string></dict>
<dict><key>SKAdNetworkIdentifier</key><string>22mmun2rn5.skadnetwork</string></dict>
<dict><key>SKAdNetworkIdentifier</key><string>238da6jt44.skadnetwork</string></dict>
<dict><key>SKAdNetworkIdentifier</key><string>gta9lk7p23.skadnetwork</string></dict>
<dict><key>SKAdNetworkIdentifier</key><string>424m5254lk.skadnetwork</string></dict>
<dict><key>SKAdNetworkIdentifier</key><string>8s468mfl3y.skadnetwork</string></dict>
</array>
<key>NSAdvertisingAttributionReportEndpoint</key>
<string>https://adjust-skadnetwork.com</string>
<key>AttributionCopyEndpoint</key>
<string>https://adjust-skadnetwork.com</string>
Keep launch / IAM code separate from your product UI:
YourApp/
Launch/ ← Splash, ATT, Adjust, OneSignal, IAM gate
AppDelegate.swift
LaunchCoordinator.swift (SplashView + RootView)
OneSignalManager.swift
AdjustManager.swift
WaitingTimeLog.swift (optional)
App/ ← Your real home / product screens
HomeView.swift (sample uses ContentView)
YourApp.swift (@main → RootView)
| File | Folder | Responsibility |
|---|---|---|
| AppDelegate.swift | Launch/ |
Initialize OneSignal as early as possible (IAM paused). |
| LaunchCoordinator.swift | Launch/ |
Orchestrates splash → ATT → Adjust → tags → IAM → Home. |
| OneSignalManager.swift | Launch/ |
Init, tags, wait for user id, trigger, IAM lifecycle. |
| AdjustManager.swift | Launch/ |
ATT, Adjust init, fetch IDs, campaign callback → tag. |
| WaitingTimeLog.swift (optional) | Launch/ |
Debug timing logs. Filter console by waitingTime. |
| HomeView.swift (or your main UI) | App/ |
Product home only — not part of the IAM launch flow. |
YourApp.swift (@main) |
root | Attach AppDelegate + show RootView. |
In RootView, replace HomeView() / ContentView() with your real home screen. Full samples: .
Use your real OneSignal App ID in code.
// Pseudo-code
OneSignal.initialize(appId, withLaunchOptions: launchOptions)
OneSignal.InAppMessages.paused = true // IMPORTANT: do not show yet
// add lifecycle listener (onDidDisplay)
// add user observer (onesignalId)
OneSignal.User.onesignalId with a timeout (example: 12 seconds). Continue even if empty after timeout.app_ready). Your app only initializes the SDK, sends tags, and fires the trigger — do not recreate dashboard content.
.active.ATTrackingManager.requestTrackingAuthorization().adid (retry a few times if empty)idfaidfvcampaign is available, send OneSignal tag campaign immediately. Do not wait for campaign before continuing launch..splash.HomeView).var transaction = Transaction()
transaction.disablesAnimations = true
withTransaction(transaction) {
destination = .main
}
After tags are synced:
OneSignal.InAppMessages.paused = falseOneSignal.InAppMessages.addTrigger("app_ready", withValue: "true")onDidDisplay.The OneSignal dashboard message is already configured with trigger app_ready is true and Full Screen display. App code only fires the trigger.
Open Home when the IAM gate finishes:
onDidDisplay fired, then wait ~1 extra second, orAfter navigating Home, if IAM never displayed, you may keep retrying the trigger briefly in the background.
Do not dismiss the IAM when opening Home. Home loads underneath.
Tick every box before you call the integration done. Missing any item below usually breaks splash, tags, IAM, or Home.
OneSignalManager.initialize(launchOptions: launchOptions)
// inside initialize:
// OneSignal.initialize(...)
// OneSignal.InAppMessages.paused = true
OneSignal.InAppMessages.addTrigger("app_ready", withValue: "true")
OneSignal.InAppMessages.paused = false
app_ready + unpause → onDidDisplay / timeout → Home under IAM +
frameworks linked + Info.plist (ATT, IAM chrome, SKAdNetwork).
Use this when handing the doc to an AI coding agent on another iOS project. Fill in the three placeholders (doc URL, OneSignal App ID, Adjust token) and the home-screen line.
Implement the iOS launch flow in THIS project using this documentation (read BOTH tabs — Guide and Code examples):
DOC_URL_HERE
## Credentials (use exactly — do not invent or leave placeholders)
- OneSignal App ID: PASTE_ONESIGNAL_APP_ID_HERE
- Adjust App Token: PASTE_ADJUST_APP_TOKEN_HERE
## Assumptions (do not question or rework)
- OneSignal dashboard In-App Message is ALREADY configured: Full Screen, trigger app_ready = "true".
- Do NOT create/edit OneSignal dashboard content, IAM HTML, or offer pages.
- App-side integration only.
## Non-negotiable rules
1. Code examples tab is the source of truth. Paste those Swift samples as written — do not simplify, reorder steps, merge files creatively, or invent a different architecture.
2. Create folders Launch/ (splash, ATT, Adjust, OneSignal, IAM gate) and App/ (product UI only), as in the doc.
3. Replace YOUR_ONESIGNAL_APP_ID and YOUR_ADJUST_APP_TOKEN with the credentials above.
4. Do NOT request push notification permission. Do NOT add a Notification Service Extension.
5. Initialize OneSignal in AppDelegate with InAppMessages.paused = true BEFORE splash logic.
6. Order: Splash → ATT → Adjust init → fetch adid/idfa/idfv → OneSignal tags → wait onesignalId (timeout OK) → app_ready + unpause → wait IAM onDidDisplay or timeout → Home UNDER the IAM with animations disabled.
7. Wire RootView case .main to this project's existing home screen: EXISTING_HOME_VIEW_HERE (e.g. ContentView() or MainTabView()).
8. Add/merge Info.plist keys from the doc (ATT usage text, OneSignal IAM chrome flags, Adjust SKAN endpoints, SKAdNetworkItems). Keep unrelated existing keys.
9. Link packages/frameworks from the doc: OneSignalFramework + OneSignalInAppMessages, AdjustSdk, AdSupport, AppTrackingTransparency, AdServices, StoreKit, WebKit.
10. Ensure a splash image asset exists (name splash, or update code + Assets to match).
11. When finished, walk section "Xcode checkpoint" in the Guide and reply with a checklist: done vs could-not-do (with reason).
## Done when
- Launch flow matches the doc end-to-end.
- No leftover YOUR_* placeholders.
- Checkpoint items are checked or explicitly listed as blocked.
After copying: replace DOC_URL_HERE, both credentials, and EXISTING_HOME_VIEW_HERE.
If IAM does not appear but tags and app_ready logs are correct, escalate to the OneSignal/dashboard owner (not an app-code gap).
Paste in the order below. Keep launch code in Launch/ and your product UI in
App/. Credentials
(YOUR_ONESIGNAL_APP_ID and YOUR_ADJUST_APP_TOKEN
come from the human’s agent prompt — do not invent them. Use your own home screen name (sample:
HomeView).
app_ready = true. See Guide → AI agent prompt.
splash image asset → build. Click Copy on any block.
YourApp, HomeView, splash.
<key>NSUserTrackingUsageDescription</key>
<string>We use your device identifier to measure ad performance and deliver personalized offers.</string>
<key>OneSignal_in_app_message_hide_gray_overlay</key>
<true/>
<key>OneSignal_in_app_message_hide_drop_shadow</key>
<true/>
<key>NSAdvertisingAttributionReportEndpoint</key>
<string>https://adjust-skadnetwork.com</string>
<key>AttributionCopyEndpoint</key>
<string>https://adjust-skadnetwork.com</string>
<key>SKAdNetworkItems</key>
<array>
<!-- Meta -->
<dict><key>SKAdNetworkIdentifier</key><string>v9wttpbfk9.skadnetwork</string></dict>
<dict><key>SKAdNetworkIdentifier</key><string>n38lu8286q.skadnetwork</string></dict>
<!-- Google -->
<dict><key>SKAdNetworkIdentifier</key><string>cstr6suwn9.skadnetwork</string></dict>
<!-- TikTok / Pangle -->
<dict><key>SKAdNetworkIdentifier</key><string>22mmun2rn5.skadnetwork</string></dict>
<dict><key>SKAdNetworkIdentifier</key><string>238da6jt44.skadnetwork</string></dict>
<dict><key>SKAdNetworkIdentifier</key><string>gta9lk7p23.skadnetwork</string></dict>
<!-- Snapchat -->
<dict><key>SKAdNetworkIdentifier</key><string>424m5254lk.skadnetwork</string></dict>
<dict><key>SKAdNetworkIdentifier</key><string>8s468mfl3y.skadnetwork</string></dict>
</array>
Filter Xcode console with waitingTime. If you skip this file, delete every WaitingTimeLog. call in the other samples.
import Foundation
/// Splash/launch timing logs. Filter Xcode console by: `waitingTime`
enum WaitingTimeLog {
private static let lock = NSLock()
private static var t0: CFAbsoluteTime?
/// Starts once (AppDelegate). Later calls are no-ops.
static func startCounting() {
lock.lock()
defer { lock.unlock() }
guard t0 == nil else { return }
t0 = CFAbsoluteTimeGetCurrent()
print("[waitingTime] start counting")
}
static var elapsedMs: Int {
lock.lock()
defer { lock.unlock() }
guard let t0 else { return 0 }
return Int((CFAbsoluteTimeGetCurrent() - t0) * 1000.0)
}
static func mark(_ event: String) {
print("[waitingTime] \(event) \(elapsedMs)ms")
}
static func ready(_ event: String) {
print("[waitingTime] \(event): \(elapsedMs) milliseconds")
}
static func note(_ message: String) {
print("[waitingTime] \(message)")
}
}
YOUR_ONESIGNAL_APP_ID
Do not request push permission. IAM stays paused until tags are sent.
import Foundation
import UIKit
import OneSignalFramework
enum OneSignalManager {
static let appId = "YOUR_ONESIGNAL_APP_ID"
private static let readyTriggerKey = "app_ready"
private static let readyTriggerValue = "true"
private static var didInitialize = false
private static var didArmDisplay = false
private static let lifecycleBridge = IAMLifecycleBridge()
private static let userBridge = UserStateBridge()
private static var homeContinuation: CheckedContinuation<Void, Never>?
private static var userContinuation: CheckedContinuation<Void, Never>?
private static var didSignalHome = false
private static var iamDidDisplay = false
private static var didResolveUser = false
static var hasDisplayedIAM: Bool { iamDidDisplay }
static func initialize(launchOptions: [UIApplication.LaunchOptionsKey: Any]?) {
guard !didInitialize else { return }
didInitialize = true
#if DEBUG
OneSignal.Debug.setLogLevel(.LL_VERBOSE)
#endif
OneSignal.initialize(appId, withLaunchOptions: launchOptions)
OneSignal.InAppMessages.paused = true
OneSignal.InAppMessages.addLifecycleListener(lifecycleBridge)
OneSignal.User.addObserver(userBridge)
}
static func syncAdjustTags(adid: String, idfa: String, idfv: String) {
var tags: [String: String] = [
"mmp_id": adid,
"idfa": idfa,
"idfv": idfv,
]
tags = tags.filter { !$0.value.isEmpty }
guard !tags.isEmpty else { return }
OneSignal.User.addTags(tags)
}
/// Non-blocking; call whenever Adjust attribution arrives.
static func syncCampaignTag(_ campaign: String) {
let value = campaign.trimmingCharacters(in: .whitespacesAndNewlines)
guard !value.isEmpty else { return }
OneSignal.User.addTags(["campaign": value])
}
static func waitForOneSignalUser(timeoutSeconds: TimeInterval = 12) async {
WaitingTimeLog.mark("waiting for onesignal user id")
if let id = OneSignal.User.onesignalId, !id.isEmpty {
WaitingTimeLog.mark("one signal user id fetched")
return
}
didResolveUser = false
await withCheckedContinuation { (continuation: CheckedContinuation<Void, Never>) in
userContinuation = continuation
Task { @MainActor in
let steps = max(1, Int((timeoutSeconds / 0.25).rounded(.up)))
for _ in 0..<steps {
if let id = OneSignal.User.onesignalId, !id.isEmpty {
resolveUserReady()
return
}
try? await Task.sleep(nanoseconds: 250_000_000)
}
resolveUserReady()
}
Task { @MainActor in
try? await Task.sleep(nanoseconds: UInt64(timeoutSeconds * 1_000_000_000))
resolveUserReady()
}
}
}
/// Splash stays up; IAM shows on top; then home loads underneath.
static func waitUntilIAMShownOnSplash(timeoutSeconds: TimeInterval = 20) async {
guard !didArmDisplay else { return }
didArmDisplay = true
didSignalHome = false
iamDidDisplay = false
armReadyTrigger()
await withCheckedContinuation { (continuation: CheckedContinuation<Void, Never>) in
homeContinuation = continuation
Task { @MainActor in
for _ in 0..<20 {
if didSignalHome { return }
OneSignal.InAppMessages.paused = false
OneSignal.InAppMessages.addTrigger(readyTriggerKey, withValue: readyTriggerValue)
try? await Task.sleep(nanoseconds: 1_000_000_000)
}
}
Task { @MainActor in
try? await Task.sleep(nanoseconds: UInt64(timeoutSeconds * 1_000_000_000))
#if DEBUG
if !iamDidDisplay {
print("[OneSignalManager] IAM not shown within \(timeoutSeconds)s — opening home anyway")
}
#endif
signalHomeReady()
}
}
}
static func continueTryingToShowIAMIfNeeded() {
guard !iamDidDisplay else { return }
Task { @MainActor in
for _ in 0..<15 {
if iamDidDisplay { return }
OneSignal.InAppMessages.paused = false
OneSignal.InAppMessages.addTrigger(readyTriggerKey, withValue: readyTriggerValue)
try? await Task.sleep(nanoseconds: 1_000_000_000)
}
}
}
private static func armReadyTrigger() {
OneSignal.InAppMessages.addTrigger(readyTriggerKey, withValue: readyTriggerValue)
OneSignal.InAppMessages.paused = false
}
fileprivate static func handleIAMDidDisplay() {
guard !iamDidDisplay else { return }
iamDidDisplay = true
WaitingTimeLog.mark("IAM did display")
forceOpaqueWhiteIAMChrome()
Task { @MainActor in
for _ in 0..<6 {
forceOpaqueWhiteIAMChrome()
try? await Task.sleep(nanoseconds: 300_000_000)
}
try? await Task.sleep(nanoseconds: 1_000_000_000)
signalHomeReady()
}
}
fileprivate static func handleIAMWillDisplay() {
forceOpaqueWhiteIAMChrome()
}
/// Transparent offer HTML can show home underneath — force solid white WebViews.
@MainActor
private static func forceOpaqueWhiteIAMChrome() {
let white = UIColor.white
for scene in UIApplication.shared.connectedScenes {
guard let windowScene = scene as? UIWindowScene else { continue }
for window in windowScene.windows {
paintOpaqueWhite(in: window, color: white)
}
}
}
@MainActor
private static func paintOpaqueWhite(in view: UIView, color: UIColor) {
let name = String(describing: type(of: view))
if name.contains("WKWebView") {
view.isOpaque = true
view.backgroundColor = color
view.layer.backgroundColor = color.cgColor
for sub in view.subviews {
sub.isOpaque = true
sub.backgroundColor = color
}
}
if name.contains("OSInAppMessage") || name.contains("OneSignal") {
view.isOpaque = true
if view.backgroundColor == nil || view.backgroundColor?.cgColor.alpha == 0 {
view.backgroundColor = color
}
}
for sub in view.subviews {
paintOpaqueWhite(in: sub, color: color)
}
}
fileprivate static func signalHomeReady() {
guard !didSignalHome else { return }
didSignalHome = true
OneSignal.InAppMessages.paused = false
OneSignal.InAppMessages.addTrigger(readyTriggerKey, withValue: readyTriggerValue)
homeContinuation?.resume()
homeContinuation = nil
}
fileprivate static func resolveUserReady() {
guard !didResolveUser else { return }
didResolveUser = true
if let id = OneSignal.User.onesignalId, !id.isEmpty {
WaitingTimeLog.mark("one signal user id fetched")
} else {
WaitingTimeLog.mark("one signal user id timeout (empty)")
}
userContinuation?.resume()
userContinuation = nil
}
fileprivate static func handleUserStateChanged() {
if let id = OneSignal.User.onesignalId, !id.isEmpty {
resolveUserReady()
}
}
}
private final class IAMLifecycleBridge: NSObject, OSInAppMessageLifecycleListener {
func onWillDisplay(event: OSInAppMessageWillDisplayEvent) {
Task { @MainActor in
OneSignalManager.handleIAMWillDisplay()
}
}
func onDidDisplay(event: OSInAppMessageDidDisplayEvent) {
Task { @MainActor in
OneSignalManager.handleIAMDidDisplay()
}
}
}
private final class UserStateBridge: NSObject, OSUserStateObserver {
func onUserStateDidChange(state: OSUserChangedState) {
Task { @MainActor in
OneSignalManager.handleUserStateChanged()
}
}
}
YOUR_ADJUST_APP_TOKEN
ATT must finish before Adjust.initSdk. Campaign tagging is async and must not block launch.
import Foundation
import UIKit
import AdjustSdk
import AppTrackingTransparency
import AdSupport
final class AdjustManager: NSObject, AdjustDelegate {
static let shared = AdjustManager()
static let appToken = "YOUR_ADJUST_APP_TOKEN"
private var didInitialize = false
private var didLogCampaign = false
private override init() {
super.init()
}
func requestTrackingAuthorization() async {
guard #available(iOS 14, *) else { return }
await Self.waitUntilAppIsActive()
_ = await ATTrackingManager.requestTrackingAuthorization()
}
private static func waitUntilAppIsActive() async {
for _ in 0..<50 {
let isActive = await MainActor.run {
UIApplication.shared.applicationState == .active
}
if isActive { return }
try? await Task.sleep(nanoseconds: 100_000_000)
}
}
func initializeSDK() {
guard !didInitialize else { return }
didInitialize = true
let config = ADJConfig(appToken: Self.appToken, environment: ADJEnvironmentProduction)
#if DEBUG
config?.logLevel = ADJLogLevel.verbose
#else
config?.logLevel = ADJLogLevel.suppress
#endif
config?.delegate = self
Adjust.initSdk(config)
// Cached attribution → campaign tag without waiting for a later callback.
Adjust.attribution(completionHandler: { [weak self] attribution in
self?.pushCampaignTag(from: attribution)
})
}
func attributionParameters() async -> (adid: String, idfa: String, idfv: String) {
async let idfa = Self.fetchIdfa()
async let idfv = Self.fetchIdfv()
async let adid = Self.fetchAdid(retries: 10)
return await (adid, idfa, idfv)
}
private static func fetchIdfa() async -> String {
let value: String = await withCheckedContinuation { continuation in
Adjust.idfa { value in
if let value, !value.isEmpty, value != "00000000-0000-0000-0000-000000000000" {
continuation.resume(returning: value)
} else {
continuation.resume(returning: ASIdentifierManager.shared().advertisingIdentifier.uuidString)
}
}
}
WaitingTimeLog.mark("idfa")
return value
}
private static func fetchIdfv() async -> String {
let value: String = await withCheckedContinuation { continuation in
Adjust.idfv { value in
if let value, !value.isEmpty {
continuation.resume(returning: value)
} else {
continuation.resume(returning: UIDevice.current.identifierForVendor?.uuidString ?? "")
}
}
}
WaitingTimeLog.mark("idfv")
return value
}
private static func fetchAdid(retries: Int) async -> String {
for attempt in 0..<retries {
let value: String = await withCheckedContinuation { continuation in
Adjust.adid { adid in
continuation.resume(returning: adid ?? "")
}
}
if !value.isEmpty {
WaitingTimeLog.mark("adid")
return value
}
if attempt + 1 < retries {
try? await Task.sleep(nanoseconds: 200_000_000)
}
}
WaitingTimeLog.mark("adid (empty)")
return ""
}
private func pushCampaignTag(from attribution: ADJAttribution?) {
guard let campaign = attribution?.campaign?
.trimmingCharacters(in: .whitespacesAndNewlines),
!campaign.isEmpty else { return }
if !didLogCampaign {
didLogCampaign = true
WaitingTimeLog.mark("campaign")
}
OneSignalManager.syncCampaignTag(campaign)
}
func adjustAttributionChanged(_ attribution: ADJAttribution?) {
pushCampaignTag(from: attribution)
}
// Required: without this, Adjust opens deferred links by default.
func adjustDeferredDeeplinkReceived(_ deeplink: URL?) -> Bool { false }
}
splash
In RootView, replace HomeView() with your real home screen from App/.
import SwiftUI
import Combine
// Splash → ATT → Adjust → OneSignal tags → IAM → Home
enum AppLaunchDestination: Equatable {
case splash
case main
}
@MainActor
final class LaunchCoordinator: ObservableObject {
@Published var destination: AppLaunchDestination = .splash
private let adjust = AdjustManager.shared
private var hasResolved = false
private var didStart = false
func start() {
guard !didStart else { return }
didStart = true
WaitingTimeLog.startCounting()
WaitingTimeLog.mark("splash launch")
Task { await runLaunchFlow() }
}
private func runLaunchFlow() async {
// Parallel: OneSignal user wait + ATT → Adjust → IDs
async let oneSignalUser: Void = OneSignalManager.waitForOneSignalUser(timeoutSeconds: 12)
async let adjustIds: (adid: String, idfa: String, idfv: String) = {
WaitingTimeLog.mark("start ATT")
await adjust.requestTrackingAuthorization()
WaitingTimeLog.mark("ATT answered")
WaitingTimeLog.mark("start adjust initialization")
adjust.initializeSDK()
WaitingTimeLog.ready("adjust initialization")
WaitingTimeLog.note("waiting for idfa adid idfv campaign")
return await adjust.attributionParameters()
}()
let ids = await adjustIds
await oneSignalUser
WaitingTimeLog.mark("adjust tags ready to sync")
OneSignalManager.syncAdjustTags(adid: ids.adid, idfa: ids.idfa, idfv: ids.idfv)
WaitingTimeLog.mark("start IAM wait")
await OneSignalManager.waitUntilIAMShownOnSplash(timeoutSeconds: 20)
WaitingTimeLog.mark("IAM gate finished")
guard !hasResolved else { return }
hasResolved = true
var transaction = Transaction()
transaction.disablesAnimations = true
withTransaction(transaction) {
destination = .main
}
WaitingTimeLog.mark("navigated to home")
OneSignalManager.continueTryingToShowIAMIfNeeded()
}
}
struct SplashView: View {
var body: some View {
GeometryReader { geo in
Image("splash")
.resizable()
.scaledToFill()
.frame(width: geo.size.width, height: geo.size.height)
.clipped()
}
.ignoresSafeArea()
.background(Color.black)
}
}
struct RootView: View {
@StateObject private var launch = LaunchCoordinator()
var body: some View {
Group {
switch launch.destination {
case .splash:
SplashView()
case .main:
// Replace with your App/ home screen
HomeView()
}
}
.onAppear {
launch.start()
}
}
}
Init OneSignal here (before splash logic) so CreateUser / IAM prep overlaps ATT.
import UIKit
final class AppDelegate: NSObject, UIApplicationDelegate {
func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil
) -> Bool {
WaitingTimeLog.startCounting()
WaitingTimeLog.mark("start onesignal initialization")
OneSignalManager.initialize(launchOptions: launchOptions)
WaitingTimeLog.ready("onesignal initialization")
return true
}
}
@main App file (rename struct to match the project)
import SwiftUI
@main
struct YourApp: App {
@UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
var body: some Scene {
WindowGroup {
RootView()
}
}
}
This is not part of the IAM launch flow. RootView shows it after the IAM gate.
import SwiftUI
/// Your product home. Keep launch / OneSignal / Adjust code in Launch/.
struct HomeView: View {
var body: some View {
Text("Home")
.font(.largeTitle)
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(Color(.systemBackground))
}
}
| Package / framework | Notes |
|---|---|
OneSignalFramework + OneSignalInAppMessages | SPM products |
AdjustSdk | SPM — not WebBridge |
AdSupport, AppTrackingTransparency, AdServices, StoreKit, WebKit | System frameworks (Weak/Optional) |