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:

  1. ScopeSDK.swift stays pure. It imports only Foundation and TripWrapper_iOS — no app types, no SCPSmartDrive, 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 in AppScopeHost.swift. Verification: diff ScopeSDK.swift between any two apps must be empty.
  2. AppScopeHost.swift is the seam. Anything app-specific — backend calls, tenant values, the LogGroup logging 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 (SDApiManager vs APIManager/CCInterface).
  3. Exactly one initializer per launch. ScopeSDK owns TripWrapper.shared.initialize(...). Nothing else in the app may call it or set TripWrapper.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.
  4. 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). The ScopeConfiguration closures 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() and initialize(enableRealtimeSending:) both read configuration.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() wraps TripWrapper.getDiagnostics(); the SDK's automatic TRIPWRAPPER HEALTH SNAPSHOT log 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's LogGroup; the copy stops compiling in the next app ("Cannot find type 'LogGroup'").
  • Don't make HostAdapter private — the logging adapter extends it from AppScopeHost.swift; it must stay internal (the class carries a comment saying so).
  • Don't leave a second TripWrapper.shared.initialize call 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.