> For the complete documentation index, see [llms.txt](https://help.branch.io/llms.txt).

# Upgrade to the Branch iOS SDK 4.0 (Beta)

> Upgrade your app from the Branch iOS SDK 3.x to the 4.0 beta, with a ready-made prompt for AI coding assistants and a BranchConfiguration reference.

To upgrade your app from the Branch iOS SDK 3.x to the 4.0 beta, update the SDK in step 1, then follow [Upgrade from 3.x](#upgrade-from-3-x). That section takes you through steps 2 to 4 and every other call that changed.

Version 4.0 starts from one configuration object, and your app gets deep link data from one callback.

> **Beta**
>
> The iOS SDK 4.0 is a beta, and its API can still change before the final release. Test it in development and test builds only, and don't ship it to the App Store.
>
> If you find a problem or a gap, email [support@branch.io](mailto:support@branch.io?subject=Testing%20new%20SDK%20Beta) with "Testing new SDK Beta" as the subject.

## Use an AI coding assistant (Optional)

Copy this prompt into a coding assistant like Claude Code or Cursor, and it upgrades the SDK for you.

```text
Upgrade this app to the Branch iOS SDK 4.0 beta, version 4.0.0-beta.3.

Read this guide and follow it. Don't rely on what you already know about the
Branch SDK, because 4.0 removes or changes most of the 3.x API:
https://help.branch.io/developer-hub/ios-sdk-4-integration.md

1. Find the app's Branch integration. Look for
   ios-branch-deep-linking-attribution or ios-branch-sdk-spm in
   Package.resolved, for BranchSDK in the Podfile or the Xcode project, and
   for Branch calls in the app delegate, the scene delegate, and the SwiftUI
   App struct.
2. Before you change any code, collect what you need. Reuse the Branch keys
   already in the project, and ask me for any that are missing, including
   both the live key and the test key. If the project has more than one app
   target or an App Clip, ask me which ones to upgrade.
3. Follow step 1 in the guide, then every step in "Upgrade from 3.x",
   including the steps for 3.x code that still builds. Then apply any case
   in "Handle edge cases" that fits this app.
4. Write code in the app's language. The guide's samples are Swift, so take
   any Objective-C names from the SDK headers. If a step needs Xcode's
   interface, such as changing a package version, list it for me instead of
   editing project.pbxproj by hand.
5. Build the app and fix any errors.
6. Finish with a list of the files you changed, anything you couldn't
   migrate, the steps I still need to do in Xcode, and the checks from
   step 4 in the guide for me to run on a device.
```

Review the changes before you merge them.

## 1. Update the SDK to the beta version

Change your Branch dependency to version 4.0.0-beta.3 exactly, so your app doesn't pick up a different build. Version 4.0 needs Xcode 15 or later and an app that targets iOS 15 or later. Follow only the steps for the package manager your app uses, either Swift Package Manager or CocoaPods.

### Swift Package Manager

Change the package's version in Xcode. If your app uses the `ios-branch-sdk-spm` package, remove it first, because the 4.0 beta isn't in that package. Then add `https://github.com/BranchMetrics/ios-branch-deep-linking-attribution` in its place.

1. Select your project in the Xcode project navigator.
2. Select the **Package Dependencies** tab.
3. Change the dependency rule of the `ios-branch-deep-linking-attribution` package to **Exact Version**, with 4.0.0-beta.3 as the version.

### CocoaPods

Pin the pod to the beta version in your `Podfile`, then run `pod update BranchSDK` from your project folder.

```ruby
# Podfile
pod 'BranchSDK', '4.0.0-beta.3'
```

## 2. Start the SDK

Create a `BranchConfiguration` with your Branch key, and pass it to `Branch.initialize` when your app finishes launching. Use the same key as your 3.x app, which is usually in the `branch_key` entry of your `Info.plist` file. For a SwiftUI app, skip this sample and use the "SwiftUI app" tab in step 3.

```swift
import UIKit
import BranchSDK

@main
class AppDelegate: UIResponder, UIApplicationDelegate {
    func application(
        _ application: UIApplication,
        didFinishLaunchingWithOptions launchOptions:
            [UIApplication.LaunchOptionsKey: Any]?
    ) -> Bool {
        // Use your test key in debug builds and your live key in release
        // builds, for example with #if DEBUG
        let config = BranchConfiguration(key: "<branch_key>")
        // Automatic opens are on by default. Set this to false to send each
        // open yourself, as in "Send opens yourself"
        config.automaticOpenEvents = true
        // Set any other options on config here, as listed in
        // "BranchConfiguration reference"

        // Call this at launch, or Branch misses the first open
        Branch.initialize(config)
        return true
    }
}
```

Everywhere else, get the SDK from `Branch.sharedInstance()`, which is `nil` until this call succeeds.

## 3. Handle deep links

Register one callback when your app launches, and route the user from it. Branch sends that callback the link data from the launch and from every later link, so your other delegate methods only need to pass each link to Branch.

**App with scenes**

```swift
import UIKit
import BranchSDK

// Your AppDelegate starts the SDK, as in step 2

class SceneDelegate: UIResponder, UIWindowSceneDelegate {
    var window: UIWindow?

    func scene(_ scene: UIScene, willConnectTo session: UISceneSession,
               options connectionOptions: UIScene.ConnectionOptions) {
        // Branch calls this callback for every link, including the link
        // behind a new install
        Branch.sharedInstance()?.requestDeepLinkData(
            sceneOptions: connectionOptions, scene: scene
        ) { params, error in
            self.route(params, error)
        }
    }

    // A Universal Link opens the running app
    func scene(_ scene: UIScene, continue userActivity: NSUserActivity) {
        Branch.sharedInstance()?.requestDeepLinkData(
            scene: scene, userActivity: userActivity)
    }

    // A URI scheme opens the running app
    func scene(_ scene: UIScene,
               openURLContexts URLContexts: Set<UIOpenURLContext>) {
        Branch.sharedInstance()?.requestDeepLinkData(
            scene: scene, openURLContexts: URLContexts)
    }

    // This runs on the main thread
    // The params dictionary holds the link's data, including any keys
    // you added to the link
    private func route(_ params: [AnyHashable: Any]?, _ error: Error?) {
        if error != nil {
            // The request failed, for example because the device is offline
            return
        }
        // Branch sets +clicked_branch_link to true when a Branch Link
        // opened the app
        guard params?["+clicked_branch_link"] as? Bool == true else { return }
        // Show the content the link points to
    }
}
```

**App without scenes**

```swift
import UIKit
import BranchSDK

@main
class AppDelegate: UIResponder, UIApplicationDelegate {
    func application(
        _ application: UIApplication,
        didFinishLaunchingWithOptions launchOptions:
            [UIApplication.LaunchOptionsKey: Any]?
    ) -> Bool {
        // Start the SDK, as in step 2
        let config = BranchConfiguration(key: "<branch_key>")
        Branch.initialize(config)

        // Branch calls this callback for every link, including the link
        // behind a new install
        Branch.sharedInstance()?.requestDeepLinkData(
            launchOptions: launchOptions
        ) { params, error in
            self.route(params, error)
        }
        return true
    }

    // A Universal Link opens the app
    func application(
        _ application: UIApplication,
        continue userActivity: NSUserActivity,
        restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void
    ) -> Bool {
        Branch.sharedInstance()?.requestDeepLinkData(userActivity: userActivity)
        return true
    }

    // A URI scheme opens the app
    func application(
        _ app: UIApplication,
        open url: URL,
        options: [UIApplication.OpenURLOptionsKey: Any] = [:]
    ) -> Bool {
        Branch.sharedInstance()?.requestDeepLinkData(openURL: url)
        return true
    }

    // This runs on the main thread
    // The params dictionary holds the link's data, including any keys
    // you added to the link
    private func route(_ params: [AnyHashable: Any]?, _ error: Error?) {
        if error != nil {
            // The request failed, for example because the device is offline
            return
        }
        // Branch sets +clicked_branch_link to true when a Branch Link
        // opened the app
        guard params?["+clicked_branch_link"] as? Bool == true else { return }
        // Show the content the link points to
    }
}
```

**SwiftUI app**

```swift
import SwiftUI
import UIKit
import BranchSDK

class AppDelegate: NSObject, UIApplicationDelegate {
    func application(
        _ application: UIApplication,
        didFinishLaunchingWithOptions launchOptions:
            [UIApplication.LaunchOptionsKey: Any]?
    ) -> Bool {
        // Start the SDK, as in step 2
        let config = BranchConfiguration(key: "<branch_key>")
        Branch.initialize(config)

        // Branch calls this callback for every link, including the link
        // behind a new install
        Branch.sharedInstance()?.requestDeepLinkData(
            launchOptions: launchOptions
        ) { params, error in
            DeepLinkRouter.route(params, error)
        }
        return true
    }
}

@main
struct MyApp: App {
    @UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate

    var body: some Scene {
        WindowGroup {
            ContentView()
                // A Universal Link or URI scheme opens the app
                .onOpenURL { url in
                    Branch.sharedInstance()?.requestDeepLinkData(openURL: url)
                }
        }
    }
}

enum DeepLinkRouter {
    // This runs on the main thread
    // The params dictionary holds the link's data, including any keys
    // you added to the link
    static func route(_ params: [AnyHashable: Any]?, _ error: Error?) {
        if error != nil {
            // The request failed, for example because the device is offline
            return
        }
        // Branch sets +clicked_branch_link to true when a Branch Link
        // opened the app
        guard params?["+clicked_branch_link"] as? Bool == true else { return }
        // Show the content the link points to
    }
}
```

Read link data in this callback. When your app moves to the background, the SDK can clear the data that `getLatestReferringParams()` returns.

## 4. Check your integration

After you finish [Upgrade from 3.x](#upgrade-from-3-x), run a debug build with logging on, open a Branch Link, and confirm that the link data arrives.

1. Set `config.logLevel = .debug` before you start the SDK, in debug builds only.
2. Run the app on a device.
3. Type `BranchSDK` in the filter field of the Xcode console.
4. Open a Branch Link for your app from the Notes app. Use a link from the same Branch app as your key, such as a `test-app.link` link for a test key.
5. Check that the `requestDeepLinkData completed with params` line shows `+clicked_branch_link` as 1.
6. Check that your release build uses your live key, without the debug log level.

> **If a check fails**
>
> - If the link opens in Safari instead of your app, iOS didn't match the link to your app. Check the Associated Domains entries in Xcode, and the Bundle ID and Apple App Prefix in Branch.
> - If your callback gets an error, the `requestDeepLinkData failed with error` line in the log says why.
> - If `Branch.sharedInstance()` is `nil`, the log says why. Either a call ran before `Branch.initialize`, or `Branch.initialize` rejected a setting and the log names it. The rules are under [Fix a nil shared instance](#fix-a-nil-shared-instance).

## Handle edge cases

Most integrations don't run into these cases. Check them when your app does one of these things, or when something doesn't work as expected.

### Fix a nil shared instance

If a setting isn't valid, `Branch.initialize` returns `nil` and the SDK doesn't start, so every Branch call does nothing. Set a log level to see which setting the SDK rejected. These are the rules:

- The Branch key starts with `key_live_` or `key_test_`.
- The network timeout is more than 0 and at most 60 seconds.
- The third-party wait time is more than 0 and at most 10 seconds.
- The retry count and retry interval are 0 or more.
- A custom API URL uses `https://` or `http://` as its scheme.
- A custom network class set with `remoteInterface` conforms to the `BNCNetworkServiceProtocol` protocol.

### Expect the same link again after a short interruption

Branch also calls your launch callback when your app becomes active again. For example, after the App Tracking Transparency prompt or Control Center closes, the callback gets the last link's data again, because neither one sends your app to the background. Make sure your routing doesn't show the same content twice.

### Wait for the first open

On a new install, calls such as logging events and creating short links fail until Branch finishes its first open. That open can finish after your first deep link callback. To wait for it, observe `BranchDidStartSessionNotification`, which Branch posts when an open finishes. In Swift, its name is `.BranchDidStartSession`.

### Use async/await

Don't `await` the launch calls, `requestDeepLinkData(launchOptions:)` and `requestDeepLinkData(sceneOptions:scene:)`. Branch keeps their callback and calls it again for later links, which an `await` can't handle.

For a link your app already has, you can call `requestDeepLinkData(branchLink:)` with `await`. It throws when the request fails, so mark the call with `try` and handle the error. If a newer link call cancels the request, or you set the attribution level to `none`, the `await` never returns.

### Handle links that open a web page

If Branch opens the link's web page instead of your app, your callback doesn't run.

## Upgrade from 3.x

Most of the upgrade is in how the SDK starts and how your app gets deep link data. Version 4.0 removes the `initSession` calls and their deep link handler. Your app starts the SDK with `Branch.initialize`, and one `requestDeepLinkData` callback receives every link.

1. Replace your `initSession` call by following [2. Start the SDK](#_2-start-the-sdk) and [3. Handle deep links](#_3-handle-deep-links).
2. If your app delayed `initSession` until after the App Tracking Transparency (ATT) prompt, call `Branch.initialize` at launch instead. After the user answers the prompt, Branch sends another open with the new status.
3. Replace the 3.x link calls that still build in 4.0 with the calls in [3. Handle deep links](#_3-handle-deep-links). The old calls still send the link to your launch callback, so if you keep them next to the new calls, Branch resolves each link twice. These are the calls:
   - The `continue(_:)`, `handleDeepLink(_:)`, and `handleDeepLink(withNewSession:)` methods
   - Branch's `application(_:open:options:)` and `application(_:open:sourceApplication:annotation:)` methods
   - The `BranchScene.shared().scene(_:continue:)` and `scene(_:openURLContexts:)` methods
4. Replace each `Branch.getInstance()` call with a `Branch.sharedInstance()` call. In Swift, `sharedInstance()` returns an optional, so call its methods with `?.`, as the samples on this page do.
5. Move every call that ran before `initSession` onto the configuration, or to just after `Branch.initialize` runs. Before then, `Branch.sharedInstance()` is `nil`, so those calls do nothing, and Branch drops any event you log.
6. If you used `Branch.setTrackingDisabled(true)`, save each user's opt-out in your app, because version 4.0 doesn't read the opt-out that 3.x saved.
7. For each user who opted out, set `attributionLevel` to `BranchAttributionLevel.none` on the configuration at every launch. Until you set it, Branch tracks those users again.
8. (Optional) Remove the `branch_key` entry from your `Info.plist` file.
9. Build the app.
10. Fix each build error by using the following table.
11. Change the 3.x calls in the table that still build. These are `try await event.logEvent()` and `BranchWillStartSessionNotification`.
12. Run the checks in [4. Check your integration](#_4-check-your-integration).

This table maps 3.x calls to their 4.0 replacements. In this table, a call that starts with `Branch.` is a class method.

| 3.x | 4.0 |
| --- | --- |
| `Branch.getInstance()` | `Branch.sharedInstance()` |
| `Branch.getInstance("<key>")`, `Branch.setBranchKey(_:)`, `Branch.getTestInstance()`, `Branch.setUseTestBranchKey(_:)`, or `branch_key` in `Info.plist` | `BranchConfiguration(key:)`, with your live or test key |
| `initSession(launchOptions:andRegisterDeepLinkHandler:)` and the other `initSession` calls | `Branch.initialize(_:)`, then `requestDeepLinkData(launchOptions:callback:)` |
| `initSession(launchOptions:andRegisterDeepLinkHandlerUsingBranchUniversalObject:)` | `requestDeepLinkData(launchOptions:callback:)`, then `BranchUniversalObject(dictionary: params)` and `BranchLinkProperties.getFrom(params)` in the callback |
| `BranchScene.shared().initSession(launchOptions:registerDeepLinkHandler:)` | `requestDeepLinkData(sceneOptions:scene:callback:)` |
| `continue(_:)`, `application(_:open:options:)`, `handleDeepLink(_:)`, and the `BranchScene.shared().scene` methods | `requestDeepLinkData(userActivity:)`, `requestDeepLinkData(openURL:)`, or their scene forms, which send the link to your launch callback |
| `handlePushNotification(_:)` | `requestDeepLinkData(userInfo:)`, which sends the link to your launch callback |
| `registerDeepLinkController(_:forKey:withPresentation:)` | Route the user in your `requestDeepLinkData` callback |
| `Branch.disableNextForeground()`, `Branch.disableNextForeground(forTimeInterval:)`, and `Branch.resumeSession()` | `automaticOpenEvents = false` on the configuration, then `sendOpen()` when your app is ready |
| `setIdentity(_:)` | `setUserAlias(_:completion:)` |
| `Branch.setTrackingDisabled(true)` | `attributionLevel = BranchAttributionLevel.none` on the configuration, for users who opted out |
| `Branch.trackingDisabled()` | `Branch.attributionLevelNone()` |
| `setConsumerProtectionAttributionLevel(_:)` before `initSession` | `attributionLevel` on the configuration |
| `setRequestMetadataKey(_:value:)` before `initSession` | `addMetadata(withKey:value:)` on the configuration |
| `disableAdNetworkCallouts(_:)` | Removed, with no replacement |
| `useEUEndpoints()` | `euEndpoint` on the configuration |
| `setNetworkTimeout(_:)`, `setMaxRetries(_:)`, and `setRetryInterval(_:)` | `networkTimeout`, `retryCount`, and `retryInterval` on the configuration |
| `Branch.setAPIUrl(_:)` and `Branch.setSafetrackAPIURL(_:)` | `apiUrl` and `safeTrackAPIUrl` on the configuration |
| `Branch.setSDKWaitTimeForThirdPartyAPIs(_:)` | `thirdPartyAPIsWaitTime` on the configuration |
| `Branch.setDMAParamsForEEA(_:adPersonalizationConsent:adUserDataUsageConsent:)` | `dmaParameters` on the configuration. To change consent after start-up, use `Branch.update(_:)`. |
| `checkPasteboardOnInstall()` | `checkPasteboardOnInstall` on the configuration |
| `setAllowedSchemes(_:)` and `addAllowedScheme(_:)` | `allowedSchemes`, or `addAllowedScheme(_:)` on the configuration |
| `setUrlPatternsToIgnore(_:)` | `urlPatternsToIgnore` on the configuration |
| `setAppClipAppGroup(_:)` | `appClipAppGroup` on the configuration, in both the app and the App Clip |
| `setDeepLinkDebugMode(_:)` | `deepLinkDebugParams` on the configuration |
| `Branch.setNetworkServiceClass(_:)` | `remoteInterface` on the configuration |
| `Branch.setCallbackForTracingRequests(_:)` | `requestTracingCallback` on the configuration |
| `getShortURL(withParams:)`, `getLongURL(withParams:)`, and their variants on `Branch` | `BranchLinkBuilder` with a `BranchLinkProperties`. Put the old params in the properties' `controlParams`. |
| `getSpotlightUrl(withParams:callback:)` | `getSpotlightURL(withParams:callback:)` on `BranchLinkBuilder` |
| `try await event.logEvent()` | `try await event.logEventAsync()` |
| `try await lastAttributedTouchData(withAttributionWindow:)` | `try await lastAttributedTouchData(attributionWindow:)` |
| `BranchWillStartSessionNotification` | Your `requestDeepLinkData` callback, because version 4.0 doesn't post this notification |
| `resetUserSession()` | Removed, with no replacement |

The `BranchUniversalObject` and `BranchLinkProperties` APIs stay the same. The `BranchEvent` API stays the same apart from the async name in the table.

## BranchConfiguration reference

Version 4.0 moves the SDK's settings into one `BranchConfiguration` object, which you pass to `Branch.initialize`. This sample sets the main options to their defaults or example values, with a note on what each one does.

```swift
// The key_live_ or key_test_ prefix picks the environment
let config = BranchConfiguration(key: "<branch_key>")
// On by default. With false, Branch holds each open until you call
// sendOpen() or the app goes to the background
config.automaticOpenEvents = true

// The least severe level that Branch logs. Logging stays off until you
// set this or loggingCallback.
config.logLevel = .error
// Your logger instead of the console
config.loggingCallback = { message, level, error in print(message) }

// Seconds to wait for a response; more than 0, at most 60
config.networkTimeout = 5.5
// Retries after a server error
config.retryCount = 3
// Seconds between retries
config.retryInterval = 1
// Seconds to wait for Google ODM info and the Apple attribution token;
// more than 0, at most 10
config.thirdPartyAPIsWaitTime = 0.5

// FULL is the default. Branch applies this level at every launch and
// replaces the level it saved, so if users can change their consent,
// pass their saved choice instead, or leave this line out.
config.attributionLevel = BranchAttributionLevel.full
// EEA consent for Google. nil keeps the values saved earlier, if any.
config.dmaParameters = BranchDMAParameters.eeaRegion(
    true, adPersonalizationConsent: false, adUserDataUsageConsent: true)

// Sent with every request
config.requestMetadata = ["$braze_install_id": "abc123"]
// true: check the clipboard for a Branch Link on install, which can
// show the paste prompt
config.checkPasteboardOnInstall = false
// App Group shared by your app and its App Clip
config.appClipAppGroup = nil
// Fixed params added to every deep link result, for debugging only
config.deepLinkDebugParams = nil

Branch.initialize(config)
```

Three presets set a starting point for you. The `BranchConfiguration.debug(_:)` preset turns on verbose logging, `BranchConfiguration.production(_:)` logs warnings, and `BranchConfiguration.compliance(_:)` sets the minimal attribution level.

### Send opens yourself

With `automaticOpenEvents` set to `false`, Branch still resolves each link and calls your `requestDeepLinkData` callback, but it holds the open until you call `sendOpen()`. Use this when the open needs data that arrives later, such as Adobe's Experience Cloud ID (ECID). Set the data with `setRequestMetadataKey(_:value:)`, then call `sendOpen()`.

```swift
// Call Branch.sharedInstance()?.sendOpen() after
// requestDeepLinkData(launchOptions:) or
// requestDeepLinkData(sceneOptions:scene:) from step 3
// This example uses Adobe Identity, which returns the ECID asynchronously
Identity.getExperienceCloudId { ecid, _ in
    if let ecid {
        Branch.sharedInstance()?.setRequestMetadataKey(
            "$marketing_cloud_visitor_id", value: ecid)
    }
    Branch.sharedInstance()?.sendOpen()
}
```

- If the launch link is still resolving when you call `sendOpen()`, Branch waits for it and sends the open with the link's data.
- Branch sends at most one open each time your app becomes active, unless a new link arrives.
- If your app goes to the background first, Branch sends the open then. At the `none` attribution level, Branch waits until the user opts in.
- On a new install, events and other requests that need an open fail if your app makes them before it calls `sendOpen()`.

### Change settings while the app runs

To change a setting after start-up, pass `Branch.update(_:)` a configuration that sets only the changed values, as in this consent example. In Objective-C, call `updateConfiguration:` instead.

```swift
// In your consent screen's handler, after Branch.initialize
func onConsentChosen(level: BranchAttributionLevel,
                     adPersonalization: Bool, adUserData: Bool) {
    // Save the choice in your own storage, and pass it to the
    // configuration at the next launch
    saveConsent(level, adPersonalization, adUserData)
    // Same key as at start-up
    let changes = BranchConfiguration(key: "<branch_key>")
    changes.attributionLevel = level
    changes.dmaParameters = BranchDMAParameters.eeaRegion(
        true,
        adPersonalizationConsent: adPersonalization,
        adUserDataUsageConsent: adUserData)
    Branch.update(changes)
}
```

The changes apply to the next request Branch sends, and settings you don't set keep their current values. Branch saves the attribution level and DMA values on the device, so they carry over to the next launch, unless your start-up configuration sets new ones.

An update doesn't start a new session, except when the attribution level changes from `none` to another level. Then Branch sends the open it held, or a new open if `automaticOpenEvents` is on. To start a new session after any other change, call `sendOpen()`, as in [Send opens yourself](#send-opens-yourself).

The key, `appClipAppGroup`, and `checkPasteboardOnInstall` only apply at start-up. To set the `none` level in code, write `BranchAttributionLevel.none` in full, because a bare `.none` is `nil` in Swift.
