Repository navigation
iOS Session and App Lifecycle Management
Companion to the iOS SmartDrive framework and iOS SDApiManager Framework pages. Last updated: 13 Jul, 2026.
This page explains how to keep a user logged in across screen-off, backgrounding, and app termination, and how to keep trip recording armed without forcing the user to log in again. If your users are being sent back to the login screen after locking the phone or reopening the app, this page is for you.
Important
The SDK already persists the session for you. SDApiManager stores the auth token internally after login, and SDApiManager.isAuthorized() returns true on the next launch without logging in again. You do not need to store tokens yourself, and you must not clear them on launch. A forced re-login after a screen-off is almost always caused by the app doing something explicit — iOS alone does not require it.
- 1. The one principle
- 2. Two managers: session vs recording
- 3. Screen-off is not app-kill
- 4. Lifecycle contract
- 5. Step-by-step implementation
- 6. Common mistakes that force a re-login
- 7. How to verify
- 8. Reference implementation
Three rules cover almost every case:
-
At launch, check the session — don't re-create it. If
isAuthorized()istrue, skip the login screen and go straight into the app. -
Arm the SDK from the app launch entry point (
didFinishLaunchingWithOptions), guarded byisAuthorized(), so recording re-arms itself after a real termination — with no login. -
Call
logout()in exactly one place: when the user taps Logout (and, if you wish, immediately before a fresh login to clear a stale session). Never on launch, foreground, or background.
If those three hold, screen-off and app-kill both "just work".
| Concern | Manager | Key calls |
|---|---|---|
| Session / auth (who is logged in) | SDApiManager |
isAuthorized(), login(...), logout()
|
| Trip recording (arming / stopping) | SCPSmartDriveManager |
see SmartDrive framework §2–3 |
Session persistence is handled by SDApiManager. Arming recording is handled by SCPSmartDriveManager. The rest of this page ties the two together in the correct order.
Read this first — it is the single most common misunderstanding.
-
Screen off (power button): the app is suspended, then resumes via
applicationDidBecomeActive. There is no relaunch and no login — nothing session-related should happen. -
Terminated (swiped away, memory pressure, long background, or a background-location relaunch): the next open runs
didFinishLaunchingWithOptionsagain. Because the session was persisted,isAuthorized()is stilltrue, so the app re-arms recording and skips the login screen.
Warning
If the app forces a login after a mere screen-off, the app is doing something explicit — almost certainly one of the mistakes in §5. iOS does not require a re-login on resume.
What to do at each lifecycle event:
| Event | Method | Do | Do NOT |
|---|---|---|---|
| App launch (every start, incl. relaunch after kill) | didFinishLaunchingWithOptions |
Configure API, then arm the SDK guarded by isAuthorized()
|
Re-create or reset the session |
| Deciding login vs skip | login screen viewDidLoad
|
Check the existing session first; skip login if authorized | Always show the login form |
| After successful login | login success callback | login → fetch account → arm the SDK | — |
| Return to foreground (screen back on) | applicationDidBecomeActive |
Refresh only, guarded by isAuthorized()
|
Log out or re-login |
| Go to background (screen off) | applicationDidEnterBackground |
Nothing session-related (e.g. schedule background fetch) | Log out or tear down the session |
| After termination | next didFinishLaunchingWithOptions
|
Same launch path — session is reused automatically | Add anything extra |
| User taps Logout | Logout action only | SDApiManager.logout() |
Call logout() anywhere else |
didFinishLaunchingWithOptions is the only method guaranteed to run on every process start — including when iOS relaunches a terminated app in the background. Configure the API and arm the SDK here.
// AppDelegate.swift
func application(_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: ...) -> Bool {
UIApplication.shared.isIdleTimerDisabled = true
UIDevice.current.isBatteryMonitoringEnabled = true
configureAPI() // establishes the base URL (once per launch)
registerNotifications()
registerBackgroundFetchHandler()
// Arms SmartDrive using the PERSISTED session.
// Self-guards on isAuthorized(), so it is a safe no-op before first login.
initializeFeatures { error in
if let error = error {
logEvent(.AppStart, description: "initializeFeatures error: \(error)")
}
}
return true
}The arming step guards itself on the persisted session — this is what makes it safe to call on every launch:
// AppDelegate.swift
func initializeFeatures(completion: @escaping (String?) -> Void) {
// 1. Check authorization
guard SDApiManager.isAuthorized() else {
completion("User is not authorized")
return
}
// 2. Create a group to track asynchronous operations
let dispatchGroup = DispatchGroup()
var configurationError: String?
// 3. Managers initialization (synchronously)
_ = TripManager.shared
// 4+5. SmartDrive configuration + TripWrapper setup via the ScopeSDK facade
wireScopeSDKCallbacks()
ScopeSDK.shared.start(configuration: .app, host: AppScopeHost())
ScopeSDK.shared.authenticated()
if appType == .beacon {
_ = BLEFrameworkProxy.shared
configureBLEFramework()
_ = SCPBeaconManager.shared
_ = SBeaconManager.shared
SBeaconManager.shared.resetScan()
SBeaconManager.shared.scanForBeacons()
}
// 6. Wellness Setup (asynchronously)
dispatchGroup.enter()
SDApiManager.getMobileSettings { settings in
defer { dispatchGroup.leave() }
guard let wellnessLicenseKey = settings.wellnessLicenseKey else {
configurationError = configurationError ?? "Wellness license key is missing"
return
}
WellnessHealth.shared.configure(licenseKey: wellnessLicenseKey)
}
// 7. Completion (when all operations are completed)
dispatchGroup.notify(queue: .main) {
if let error = configurationError {
logEvent(.AppStart, description: "Configuration completed with error: \(error)")
} else {
logEvent(.AppStart, description: "All managers initialized successfully")
}
completion(configurationError)
}
}Tip
After the app is killed and iOS relaunches it, this method runs again, sees the stored session, and re-arms recording with no login screen. If the app instead rebuilds a fresh session here, everything is lost.
The login screen must first ask "is there already a valid session?" and, if so, skip straight through.
// LoginViewController.swift
override func viewDidLoad() {
super.viewDidLoad()
setupUI()
bindViewModel()
checkExistingSession() // ← decides whether login is even needed
}
private func checkExistingSession() {
if viewModel.isAuthenticated() { // SDApiManager.isAuthorized()
viewModel.fetchAccountDetails() // already logged in — no credentials needed
}
// else: show the login form
}// LoginViewModel.swift
func isAuthenticated() -> Bool {
return SDApiManager.isAuthorized() // persisted across launches
}Warning
This is the step most likely to be missing. If the app always shows the login form (or always calls login/logout) on launch, the user is forced to re-authenticate even though a valid session already exists.
Order matters: login → fetch account → arm the SDK → main screen.
// LoginViewController.swift — bindViewModel()
viewModel.onLoginSuccess = { [weak self] username in
DispatchQueue.main.async { self?.handleLoginSuccess(username: username) }
}
viewModel.onAccountFetchSuccess = { [weak self] in
DispatchQueue.main.async { self?.navigateToMainScreen() }
}// LoginViewController.swift
private func handleLoginSuccess(username: String) {
viewModel.fetchAccountDetails() // loads account + vehicle + base URL
}
private func navigateToMainScreen() {
AppDelegate.shared.initializeFeatures { error in // arm SDK after first login
// ... present the main screen
}
}Note
Only after fetchAccountDetails() do the USER ID, vehicle, and base URL populate. Arming the SDK before this runs is why authentication/integration data can show up empty in logs.
A suspended app resuming does not re-run launch — it only fires applicationDidBecomeActive. Refresh; never re-login.
// AppDelegate.swift
func applicationDidBecomeActive(_ application: UIApplication) {
onDidBecomeActive()
}
func onDidBecomeActive() {
guard SDApiManager.isAuthorized() else { return } // still logged in? continue
refreshFeatures() // refresh only — no logout, no re-login
}Turning the screen off suspends the app; it does not terminate it and does not end the session. Do nothing destructive here.
// AppDelegate.swift
func applicationDidEnterBackground(_ application: UIApplication) {
onDidEnterBackground() // e.g. schedule background fetch — nothing session-related
}When iOS terminates the app (memory pressure, or a background-location relaunch), the next start runs Step 1 again. Because the session was persisted, isAuthorized() is still true, so the SDK re-arms and the login screen is skipped. There is nothing extra to implement — it is the same launch path, which is exactly why Step 1 must be correct.
// LoginViewModel.swift — login()
SDApiManager.logout() // clears any old session, ONLY as part of an explicit login
SDApiManager.login(withUsername: username, password: password) { ... }SDApiManager.logout() should be called in exactly one place: when the user taps Logout (and, as here, immediately before a fresh login to clear a stale session). It must never be called on launch, on foreground, or on background.
- ❌ Calling
SDApiManager.logout()on launch or foreground (wipes the stored session). - ❌ Re-initializing / resetting the API session on every launch instead of reading
isAuthorized(). - ❌ Always showing the login screen instead of calling the "check existing session" step first.
- ❌ Arming the SDK only from the login screen, so a background relaunch (no UI) never re-arms.
- ✅ Correct: persist nothing manually, check
isAuthorized()at launch, arm the SDK fromdidFinishLaunchingWithOptions, and onlylogout()on explicit user action.
- Log in normally and confirm a trip records.
- Press the power button to turn the screen off, wait, turn it back on → the app should resume without a login screen.
- Swipe the app away (force-quit), reopen → the login screen should be skipped because the session is still valid.
- Drive with the app killed → a trip should still be recorded (background relaunch re-arms via Step 1).
- Tap Logout → the next launch should show the login screen.
If step 2 or 3 shows a login screen, review §5 Step 2 and the §6 checklist — the app is clearing or ignoring the persisted session.
The SmartDrive demo app implements this exact pattern. Its README has a file-by-file map (AppDelegate.swift, LoginViewController.swift, LoginViewModel.swift) showing where each step above lives, so you can compare it directly against your own app.
Consolidated SDK
SmartDrive
PortalApiBase
PortalApiClient
Wellness
SBeacon
- Overview
- Installation
- Initialization
- Beacon Scanning
- Beacon Pairing
- Trip Management
- Driving Behavior
- Calibration
- Event Handling
- Data Models
- API Reference
Consolidated SPM
SDApiManager
CCFrameworkOpen
SmartDrive
SBeacon