iOS Simplified SDK Integration - scopetech/Mobile-Public GitHub Wiki
iOS Simplified SDK Integration — ScopeSDK & AppScopeHost
For teams integrating the SmartDrive / TripWrapper / SDApiManager SDKs. Companion pages: iOS Session & App Lifecycle Management, Migrating to the Scope SDKs (ConsolidatedSPM 1.1.1) — iOS. Last updated: 24 Jul 2026.
The July 2026 reference apps (SmartDriveDemo) introduce a simplified
integration built from two files. The working reference is the
SmartDriveDemo feature/sdk_simplification branch
| File | Role | Portability |
|---|---|---|
ScopeSDK.swift |
The facade: start / authenticated / stop, trip callbacks, health check. Owns TripWrapper initialization internally. |
Copied verbatim, never edited. Byte-identical in every host app. |
AppScopeHost.swift |
The app's entire integration surface: backend answers (ScopeHost), configuration values (ScopeConfiguration.app), and the logging adapter. |
App-specific — the only file you write. |
One file is retired by this design: the coordinator class that implemented the
TripWrapper protocols directly (TripCoordinator.swift in the demo). Its protocol
conformances now live inside ScopeSDK's internal HostAdapter; delete the coordinator
from your target when adopting the facade. The classic protocol-based integration
remains fully supported (see the TripWrapper README) — this page describes the
recommended style going forward.
1. The principles
Everything else on this page follows from four rules:
ScopeSDK.swiftstays pure. It imports onlyFoundationandTripWrapper_iOS— no app types, noSCPSmartDrive, no API frameworks. That is what lets the file copy between apps unchanged. If you feel the urge to edit it, what you're adding belongs inAppScopeHost.swift. Verification:diff ScopeSDK.swiftbetween any two apps must be empty.AppScopeHost.swiftis the seam. Anything app-specific — backend calls, tenant values, theLogGrouplogging adapter, pointers to your configure/stop routines — lives here. It is expected to differ between apps; the demo's version differs only in backend names (SDApiManagervsAPIManager/CCInterface).- Exactly one initializer per launch. ScopeSDK owns
TripWrapper.shared.initialize(...). Nothing else in the app may call it or setTripWrapper.shared.delegate— the last writer would silently win. If you keep a classic path side by side (e.g. behind a debug switch), select one path once at launch; switching requires an app restart. - Layering: mechanics down, values up. SmartDrive setter sequences and ordering
belong in the SmartDrive SDK (
SCPSmartDriveConfiguration/configure/start/stop); your app supplies values (thresholds, mode, keys) and backend fetches (MProfiler settings, tenant config). TheScopeConfigurationclosures are the bridge between the two — nothing more.
2. Integration, step by step
Step 1 — copy ScopeSDK.swift from the SmartDriveDemo repo into your
project. Do not modify it. (The demo's
AppScopeHost.swift alongside it is the worked example for Step 2.)
Step 2 — write your AppScopeHost.swift. Three sections:
import Foundation
import TripWrapper_iOS
import YourAPIFramework // e.g. SDApiManager
import SCPSmartDrive // LogGroup, for the logging adapter
// 2.1 The four backend answers
struct AppScopeHost: ScopeHost {
func postTripEventBatch(_ batch: [String: Any], completion: @escaping (APIResponse) -> Void) {
YourAPI.postTripEventBatch(batch) { completion(APIResponse(success: $0.success,
error: $0.error,
rawResponse: $0.rawResponse)) }
}
func deviceID() -> String? { YourAPI.unitSerialNumber() }
func username() -> String? { YourAPI.username() }
func isAuthorized() -> Bool { YourAPI.isAuthorized() }
}
// 2.2 The configuration values (one source of truth)
extension ScopeConfiguration {
static var app: ScopeConfiguration {
ScopeConfiguration(
mode: .smart, // or .beacon
realtimeSending: false, // the ONLY place this flag lives
autoSend: true,
configureSmartDrive: { AppDelegate.shared.configureSmartDrive() },
stopSmartDrive: { AppDelegate.shared.stopSmartDrive() }
)
}
}
// 2.3 The logging adapter — lives HERE, not in ScopeSDK.swift,
// because it references the app's LogGroup / logEvent.
extension HostAdapter: LoggingProtocol {
func logTripEvent(_ group: TripLogGroup, description: String) {
let appGroup: LogGroup
switch group {
case .tripStart: appGroup = .TripStart
case .tripEnd: appGroup = .TripEnd
case .trip: appGroup = .Trip
case .beacon: appGroup = .Beacon
case .network: appGroup = .Network
case .other: appGroup = .Other
@unknown default: appGroup = .Other
}
logEvent(appGroup, description: "[TripWrapper] \(description)")
}
}
Your app keeps two routines (in AppDelegate or wherever suits): configureSmartDrive()
— values only: mode-derived flags, thresholds, tenant fetches (MProfiler, MOT keys)
— and stopSmartDrive(). Mark them internal, not private: the closures above live
in another file.
Step 3 — wire the AppDelegate:
// At launch AND after successful login (both may run; the calls are idempotent):
ScopeSDK.shared.onTripStarted = { trip in /* UI notifications, analytics */ }
ScopeSDK.shared.onTripEnded = { trip in /* … */ }
ScopeSDK.shared.start(configuration: .app, host: AppScopeHost())
ScopeSDK.shared.authenticated() // configures + initializes once, post-auth
// On logout (the ONLY place):
ScopeSDK.shared.stop() // runs your stopSmartDrive; re-login re-arms
// In the background refresh task:
ScopeSDK.shared.performBackgroundRefresh()
The notification delegate (TripNotificationManager or your equivalent) is passed by
ScopeSDK internally — implement TripNotificationDelegate as before. Session and
lifecycle rules are unchanged from the
lifecycle guide:
arm in didFinishLaunching guarded by isAuthorized(), logout in exactly one place.
3. What the facade guarantees for you
- The realtime flag cannot desync.
HostAdapter.isRealtimeSendingEnabled()andinitialize(enableRealtimeSending:)both readconfiguration.realtimeSending— agreement by construction (in classic integrations, keeping these two aligned is a documented manual obligation). - Correct ordering — configure, initialize, delegate, retry — is encoded once, in the facade, instead of re-derived per app.
- Health at a glance:
ScopeSDK.shared.isHealthy()wrapsTripWrapper.getDiagnostics(); the SDK's automaticTRIPWRAPPER HEALTH SNAPSHOTlog blocks report missing protocol connections with ⚠️ markers — an integration mistake announces itself in the first screen of the log.
4. Pitfalls (each learned the hard way)
- Don't move the logging adapter into
ScopeSDK.swift— it references the app'sLogGroup; the copy stops compiling in the next app ("Cannot find type 'LogGroup'"). - Don't make
HostAdapterprivate— the logging adapter extends it fromAppScopeHost.swift; it must stayinternal(the class carries a comment saying so). - Don't leave a second
TripWrapper.shared.initializecall anywhere (old coordinator code, sample snippets in comments that got uncommented). One initializer per launch — see principle 3. - Don't skip scheduling trip-end notifications when foreground in your
notification delegate — app state at schedule time says nothing about fire time.
Decide presentation in
userNotificationCenter(_:willPresent:).
5. Flutter
Identical, one level down: copy the same two files into iOS/YOUR_FLUTTER_APPLICATION, wire the
AppDelegate as above inside your FlutterAppDelegate subclass, keep the platform
channel exactly as in the
Flutter lifecycle guide
— Dart never talks to ScopeSDK directly; it talks to your channel, which calls the
facade.
6. Roadmap note
ScopeSDK is designed to graduate into the TripWrapper package in a future release
(its header says so). When that happens, your ScopeSDK.swift copy is deleted and
AppScopeHost.swift — unchanged — becomes your entire integration. Writing the host
file cleanly today is the whole migration for tomorrow.