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.
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.
- Select your project in the Xcode project navigator.
- Select the Package Dependencies tab.
- Change the dependency rule of the
ios-branch-deep-linking-attributionpackage 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.
# 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.
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.
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
}
}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
}
}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.
- Set
config.logLevel = .debugbefore you start the SDK, in debug builds only. - Run the app on a device.
- Type
BranchSDKin the filter field of the Xcode console. - 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.linklink for a test key. - Check that the
requestDeepLinkData completed with paramsline shows+clicked_branch_linkas 1. - 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 errorline in the log says why. - If
Branch.sharedInstance()isnil, the log says why. Either a call ran beforeBranch.initialize, orBranch.initializerejected 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_orkey_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://orhttp://as its scheme. - A custom network class set with
remoteInterfaceconforms to theBNCNetworkServiceProtocolprotocol.
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.
- Replace your
initSessioncall by following 2. Start the SDK and 3. Handle deep links. - If your app delayed
initSessionuntil after the App Tracking Transparency (ATT) prompt, callBranch.initializeat launch instead. After the user answers the prompt, Branch sends another open with the new status. - 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(_:), andhandleDeepLink(withNewSession:)methods - Branch's
application(_:open:options:)andapplication(_:open:sourceApplication:annotation:)methods - The
BranchScene.shared().scene(_:continue:)andscene(_:openURLContexts:)methods
- The
- Replace each
Branch.getInstance()call with aBranch.sharedInstance()call. In Swift,sharedInstance()returns an optional, so call its methods with?., as the samples on this page do. - Move every call that ran before
initSessiononto the configuration, or to just afterBranch.initializeruns. Before then,Branch.sharedInstance()isnil, so those calls do nothing, and Branch drops any event you log. - 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. - For each user who opted out, set
attributionLeveltoBranchAttributionLevel.noneon the configuration at every launch. Until you set it, Branch tracks those users again. - (Optional) Remove the
branch_keyentry from yourInfo.plistfile. - Build the app.
- Fix each build error by using the following table.
- Change the 3.x calls in the table that still build. These are
try await event.logEvent()andBranchWillStartSessionNotification. - 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.x | 4.0 |
|---|---|
Branch. | Branch. |
Branch., Branch., Branch., Branch., or branch_key in Info. | BranchConfiguration(, with your live or test key |
initSession( and the other initSession calls | Branch., then requestDeepLinkData( |
initSession( | requestDeepLinkData(, then BranchUniversalObject( and BranchLinkProperties. in the callback |
BranchScene. | requestDeepLinkData( |
continue(, application(, handleDeepLink(, and the BranchScene. methods | requestDeepLinkData(, requestDeepLinkData(, or their scene forms, which send the link to your launch callback |
handlePushNotification( | requestDeepLinkData(, which sends the link to your launch callback |
registerDeepLinkController( | Route the user in your requestDeepLinkData callback |
Branch., Branch., and Branch. | automaticOpenEvents = false on the configuration, then sendOpen() when your app is ready |
setIdentity( | setUserAlias( |
Branch. | attributionLevel = BranchAttributionLevel. on the configuration, for users who opted out |
Branch. | Branch. |
setConsumerProtectionAttributionLevel( before initSession | attributionLevel on the configuration |
setRequestMetadataKey( before initSession | addMetadata( 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. and Branch. | apiUrl and safeTrackAPIUrl on the configuration |
Branch. | thirdPartyAPIsWaitTime on the configuration |
Branch. | dmaParameters on the configuration. To change consent after start-up, use Branch.. |
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. | remoteInterface on the configuration |
Branch. | requestTracingCallback on the configuration |
getShortURL(, getLongURL(, and their variants on Branch | BranchLinkBuilder with a BranchLinkProperties. Put the old params in the properties' controlParams. |
getSpotlightUrl( | getSpotlightURL( on BranchLinkBuilder |
try await event. | try await event. |
try await lastAttributedTouchData( | try await lastAttributedTouchData( |
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.
// 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().
// 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
noneattribution 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.
// 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.
