Skip to content
Get Help

Upgrade to the Branch iOS SDK 4.0Beta

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. 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 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.

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.

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
    }
}
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
    }
}
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, 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.

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.

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.

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 and 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. 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.

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.x4.0
Branch.getInstance()Branch.sharedInstance()
Branch.getInstance("<key>"), Branch.setBranchKey(_:), Branch.getTestInstance(), Branch.setUseTestBranchKey(_:), or branch_key in Info.plistBranchConfiguration(key:), with your live or test key
initSession(launchOptions:andRegisterDeepLinkHandler:) and the other initSession callsBranch.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 methodsrequestDeepLinkData(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 initSessionattributionLevel on the configuration
setRequestMetadataKey(_:value:) before initSessionaddMetadata(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 BranchBranchLinkBuilder 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:)
BranchWillStartSessionNotificationYour 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.

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.