iOS TripWrapper — Migration Guide - scopetech/Mobile-Public GitHub Wiki

iOS TripWrapper — Migration Guide

Audience: Teams who have an existing SCPSmartDrive integration and are adopting TripWrapper. Companion reading: iOS TripWrapper SDK — reference documentation for protocols, data models, and the full API surface. Applies to: ConsolidatedSPM-iOS 1.1.2 (TripWrapperSPM-iOS 1.0.10 / SCPSmartDriveSPM-iOS «SPM package version»). For earlier packages, see the 1.1.1 guide.


What changed in 1.1.2

If you are already integrated against 1.1.1, the only integration-facing change is for beacon / mHub apps:

  • TripConfigProvider now carries onBeaconTripStart. Beacon-mode host apps must return a closure that snapshots the in-range beacon at trip start (see 3.1 and the FAQ). A default no-op is provided, so smart-mode integrations need no changes.

Everything else in this guide is unchanged from 1.1.1.


Table of Contents

  1. Why TripWrapper?
  2. Package Installation
  3. New Files to Add
  4. AppDelegate Changes
  5. Code to Delete
  6. Step-by-Step Checklist
  7. Architecture After Migration
  8. FAQ

1. Why TripWrapper?

In the old architecture your app implemented SCPSmartDriveManagerDelegate in multiple places — TripManager, TripNotificationManager, and sometimes view controllers — each one touching raw trip objects and calling send logic independently. This caused:

  • Duplicated delegate registrations that led to double-send bugs.
  • Trip sending logic scattered across TripManager, TripNotificationManager, and TripsViewController.
  • Manual retry logic (resendTrips / autosendFailedTrips) that was error-prone.
  • Race conditions when trip end was detected while the app was in the background.

TripWrapper centralises all of this. Your app registers a single set of lightweight protocol implementations, and TripWrapper owns the delegate, the send loop, and retry logic.

Before vs After

Before (manual) After (TripWrapper)
TripManager: SCPSmartDriveManagerDelegate TripWrapper owns the delegate internally
TripNotificationManager: SCPSmartDriveManagerDelegate TripNotificationManager: TripNotificationDelegate only
sendTrips() / sendTrip() / sendEvents() Automatic — no app code needed
resendTrips() + autosendFailedTrips() TripWrapper.retryFailedTrips() — called once on foreground
SCPTrip managed objects passed around Value-type TripRecord (thread-safe)
TripsViewController loads local trips via SCPSmartDriveManager TripsViewModel → TripManager → TripWrapper

2. Package Installation

TripWrapper is distributed through the Scope Consolidated Swift Package. Full instructions are in iOS Consolidated SPM Documentation.

  1. In Xcode open File → Add Package Dependencies.
  2. Paste the Consolidated SPM repository URL and select ConsolidatedSPM-iOS 1.1.2.
  3. Select TripWrapper_iOS from the product list and add it to your app target.

Note: If you already consume SCPSmartDrive and SDApiManager from the Consolidated SPM, TripWrapper_iOS is added from the same repository — no new URL needed. ConsolidatedSPM 1.1.2 pins TripWrapperSPM-iOS 1.0.10 and its matched SCPSmartDrive package; update and commit Package.resolved.


3. New Files to Add

Copy the three files below into your project. They are largely client-agnostic; the only parts you need to adapt are the API bridge and — for beacon/mHub apps — the beacon snapshot, both inside TripCoordinator.

3.1 TripCoordinator.swift

TripCoordinator is the bridge between TripWrapper and your backend. It implements four protocols:

Protocol Responsibility
APIManagerProtocol Posts event batches to your server
DeviceIDProvider Returns the unit serial number used to tag trips
TripConfigProvider Answers configuration questions (beacons enabled? auto-send?) and, for beacon/mHub apps, snapshots the in-range beacon at trip start
LoggingProtocol Routes TripWrapper log events into your logging system

Adapt (1) — APIManagerProtocol. Replace SDApiManager with your own backend wrapper:

// ADAPT THIS — replace SDApiManager with your backend wrapper
extension TripCoordinator: APIManagerProtocol {

    func postTripEventBatch(_ batchData: [String: Any],
                            completion: @escaping (APIResponse) -> Void) {
        YourAPIManager.postTripEventBatch(batchData) { response in
            completion(APIResponse(
                success:     response.success,
                error:       response.error,
                rawResponse: response.rawResponse
            ))
        }
    }

    func unitSerialNumber() -> String? { YourAPIManager.unitSerialNumber() }
    func getUsername()     -> String? { YourAPIManager.username() }
}

Adapt (2) — TripConfigProvider.onBeaconTripStart (beacon / mHub apps only, new in 1.1.2). When beacons are enabled, TripWrapper invokes this closure at trip start so the host can snapshot the in-range beacon. That snapshot is what lets a beacon/mHub trip resolve a device ID and be sent — without it, beacon-only trips record but fail to upload. Return a closure that calls your beacon manager's trip-start hook:

extension TripCoordinator: TripConfigProvider {

    func isPendingTripsManualSendingEnabled() -> Bool { /* your config */ }
    func areBeaconsEnabled() -> Bool                  { /* your config */ }
    func tripAutoSendingEnabled() -> Bool             { /* your config */ }

    // NEW in 1.1.2 — beacon / mHub apps must snapshot the in-range beacon here.
    // Smart-mode apps can omit this entirely; the protocol supplies a default no-op.
    var onBeaconTripStart: (() -> Void)? {
        return { SCPBeaconManager.shared.onTripStart() }
    }
}

Import isolation: if bringing SCPBeaconManager into TripCoordinator clashes with another module's type (e.g. two modules each declaring APIResponse), route the call through your existing BLE bridge (e.g. BLEFrameworkProxy.shared.onBeaconTripStart()) instead of importing BLEFramework here. See the beacon FAQ.

Smart mode: if areBeaconsEnabled() returns false, onBeaconTripStart is never invoked. You may leave it unimplemented — the protocol's default no-op applies.

Everything else (DeviceIDProvider, LoggingProtocol) can be used as-is, or with minor naming changes to match your app's LogGroup enum.


3.2 TripNotificationManager.swift

TripNotificationManager now implements only TripNotificationDelegate — not SCPSmartDriveManagerDelegate. TripWrapper calls the three methods below at the correct moments:

Method When called
showTripStartNotification() After didDetectTripAutomotive()
scheduleTripEndNotification() After didDetectVehicleStop()
cancelTripEndNotification() After didResetVehicleStop()

⚠️ Remove SCPSmartDriveManager.sharedInstance()?.addNewDelegateObject(self) from init(). TripWrapper calls your notification delegate directly. Re-registering as an SDK delegate causes duplicate trip handling.

Background-only rule: all three methods check UIApplication.shared.applicationState and skip the notification when the app is active, because the UI updates itself via TripWrapperDelegate callbacks.


3.3 TripManager.swift

TripManager is now a thin facade. It no longer implements SCPSmartDriveManagerDelegate, no longer sends trips, and no longer calls resendTrips().

What it still does:

  • Exposes getLocalTrips(), getTargetTrips(), getCurrentTrip() — each delegates to TripWrapper.
  • Provides convertToTrip(from:) for code that still expects the app's Trip value type.
  • Calls TripWrapper.retryFailedTrips() when the app becomes active.
// New — simple facade
class TripManager: NSObject {
    static let shared = TripManager()

    private override init() {
        super.init()
        // Do NOT register as SCPSmartDriveManager delegate
        NotificationCenter.default.addObserver(
            self,
            selector: #selector(appDidBecomeActive),
            name: UIApplication.didBecomeActiveNotification,
            object: nil
        )
    }

    @objc private func appDidBecomeActive() {
        TripWrapper.shared.retryFailedTrips()   // replaces resendTrips()
    }
}

4. AppDelegate Changes

4.1 Import TripWrapper_iOS

import TripWrapper_iOS   // add this
import SCPSmartDrive
import SDApiManager

4.2 Add initializeTripWrapperSDK()

Add this private method and call it from initializeFeatures(), after configureSmartDrive() returns:

private func initializeTripWrapperSDK() {
    TripWrapper.shared.initialize(
        apiManager:           TripCoordinator.shared,
        logger:               TripCoordinator.shared,
        enableRealtimeSending: isRealtimeTripSendingEnabled,
        deviceIDProvider:     TripCoordinator.shared,
        configProvider:       TripCoordinator.shared,
        notificationDelegate: TripNotificationManager.shared
    )
    TripWrapper.shared.delegate = self
}

In initializeFeatures():

func initializeFeatures(completion: @escaping (String?) -> Void) {
    // ...
    configureSmartDrive { error in ... dispatchGroup.leave() }

    _ = TripManager.shared           // initialise facade
    // REMOVED: _ = TripNotificationManager.shared  ← no longer needed here
    //          (passed directly as notificationDelegate above)

    initializeTripWrapperSDK()       // ← ADD THIS

    // ...
}

Note: Do not touch the SCPSmartDrive configuration block (askLocationPermission, setTripAutostartEnabled, configureMProfiler, etc.). TripWrapper sits on top of SCPSmartDrive — it does not replace it. Everything in configureSmartDrive() stays exactly as it is.

4.3 Implement TripWrapperDelegate

Use these callbacks for UI updates and analytics only. Never send trips or call SDK methods from here.

extension AppDelegate: TripWrapperDelegate {

    func tripStarted(trip: TripRecord) {
        NotificationCenter.default.post(name: NSNotification.Name("RefreshTripUI"), object: trip)
    }

    func tripEnded(trip: TripRecord) {
        NotificationCenter.default.post(name: NSNotification.Name("RefreshTripUI"), object: trip)
    }

    // Called up to once per second during an active trip — keep this lightweight
    func tripUpdated(distance: Double, duration: TimeInterval, speed: Double) {
        NotificationCenter.default.post(
            name: NSNotification.Name("TripComponentsUpdated"),
            object: nil,
            userInfo: ["distance": distance, "duration": duration, "speed": speed]
        )
    }
}

5. Code to Delete

⚠️ Leaving old code alongside TripWrapper causes double-sends, duplicate notifications, and confusing log output.

TripManager (old)

  • Delete the entire SCPSmartDriveManagerDelegate extension (tripSendStatusChanged).
  • Delete sendTrips(_:onTripSend:) and its private helpers (sendTrip, prepareEventBatchData, sendEvents).
  • Delete autosendFailedTrips() and resendTrips().
  • Delete the NSNotification.Name.tripAutosend observer and tripAutocompleted().
  • Delete the NSNotification.Name.newEventRegistration observer and didReceiveNewEvent().
  • Delete smartDrive.addNewDelegateObject(self) in init().

Rule of thumb: if TripManager has any reference to SCPSmartDriveManager other than reading getCurrentSpeed() for display, that code must go.

TripNotificationManager (old)

  • Delete the SCPSmartDriveManagerDelegate extension (didDetectTripStart, didDetectTripEnd, didDetectTripAutomotive, didDetectVehicleStop, didResetVehicleStop).
  • Delete SCPSmartDriveManager.sharedInstance()?.addNewDelegateObject(self) from init().
  • Delete any call to TripManager.shared.sendTrips() inside didDetectTripEnd.

AppDelegate (old)

  • Remove _ = TripNotificationManager.shared from initializeFeatures() if its only purpose was to trigger delegate registration.
  • Remove any SCPSmartDriveManagerDelegate extension on AppDelegate that forwarded trip events.

TripsViewController / TripsViewModel (old)

  • Remove direct calls to SCPSmartDriveManager.sharedInstance()?.getLocalTrips() — use TripManager.shared.getLocalTrips() instead.
  • Remove any SCPSmartDriveManagerDelegate registration in viewWillAppear that was used for trip lifecycle. Keeping it for didReceiveLog (to display logs) is fine.

6. Step-by-Step Checklist

Work through these in order. Each step has a clear done-state so you can build and test before continuing.

  • Add package — Add TripWrapper_iOS via ConsolidatedSPM 1.1.2. Build succeeds with no missing-module errors.
  • Add TripCoordinator.swift — Adapt APIManagerProtocol to call your backend wrapper. Build succeeds.
  • Beacon / mHub apps only — Implement TripConfigProvider.onBeaconTripStart to snapshot the in-range beacon. (Smart-mode apps skip this — the default no-op applies.)
  • Replace TripNotificationManager.swift — Remove the old SCPSmartDriveManagerDelegate extension. New file implements TripNotificationDelegate only.
  • Replace TripManager.swift — Facade version only. All send and retry methods deleted.
  • Update AppDelegate — Add import TripWrapper_iOS, initializeTripWrapperSDK(), call it in initializeFeatures(), add TripWrapperDelegate extension.
  • Delete old code (Section 5) — Build succeeds with zero references to sendTrip, autosendFailedTrips, or resendTrips remaining.
  • Real-device trip test — Start a drive. Verify a trip-start notification appears in the background. End the drive. Verify a trip-end notification appears and a trip record is uploaded.
  • Beacon / mHub apps — Confirm the log shows the beacon snapshot at trip start (e.g. TRIP START - Beacon: …), a device ID resolved at trip end, and a successful upload (no missing tripDeviceID).
  • Crash-recovery test — Force-kill the app mid-trip. Relaunch. Verify the incomplete trip is retried automatically.
  • Duplicate-send check — Confirm in server logs that no trip is sent twice (no duplicate batch_id values).

7. Architecture After Migration

┌─────────────────────────────────────────────────┐
│               SCPSmartDrive SDK                 │
│       (location, motion, trip detection)        │
└────────────────────┬────────────────────────────┘
                     │ SCPSmartDriveManagerDelegate
                     ▼
┌─────────────────────────────────────────────────┐
│                TripWrapper SDK                  │
│  • Owns SCPSmartDriveManagerDelegate            │
│  • Manages trip lifecycle (start/end/retry)     │
│  • Sends events via APIManagerProtocol          │
└───┬───────────────┬──────────────┬──────────────┘
    │               │              │
    ▼               ▼              ▼
TripCoordinator  TripWrapper    TripNotification
(APIManager,     Delegate       Manager
 Logger,         (AppDelegate)  (TripNotification
 DeviceID,        tripStarted()  Delegate)
 Config +         tripEnded()    background
 onBeaconTripStart) tripUpdated() notifications only
    │
    ▼
Your Backend API

TripManager and TripsViewModel sit entirely outside this flow — they read trip data from TripWrapper but never write to it or interact with SCPSmartDrive directly.


8. FAQ

Do I still need to configure SCPSmartDriveManager? Yes. TripWrapper works on top of SCPSmartDrive. Everything in configureSmartDrive() — permissions, autostart, MProfiler, beacon settings — stays exactly as before. TripWrapper subscribes to events from SCPSmartDrive internally.

Can I still read SCPSmartDriveManager for UI values? Yes. Reading getCurrentSpeed(), getRecorderState(), and getCurrentTripDistance() for display is fine. What you must remove is writing trip state (confirmTripStart, confirmTripStop, setDeviceIDforCurrentTrip) and sending batches. TripWrapper owns all of those.

What replaces SCPTrip? TripWrapper exposes TripRecord, a value type that mirrors the fields your UI already uses: tripId, startDate, endDate, distance, duration, startLocation, endLocation, events, maxSpeed, and finishedTrip. TripManager.convertToTrip(from:) bridges TripRecord to your existing Trip struct for any call sites that still expect that type.

What if my backend API signature differs from SDApiManager? Adapt only the three methods in TripCoordinator's APIManagerProtocol extension: postTripEventBatch, unitSerialNumber, and getUsername. The method signatures are fixed by the protocol; your implementation can call any networking layer underneath.

Will TripWrapper send a trip if the app is killed mid-trip? TripWrapper persists pending trip data to disk. On the next launch, TripWrapper.retryFailedTrips() (called by TripManager on didBecomeActive) picks up where it left off. No additional code changes are needed beyond what the checklist describes.

Is isRealtimeTripSendingEnabled still relevant? Yes. Pass it directly into TripWrapper.initialize(enableRealtimeSending:). When true, TripWrapper streams events during the trip. When false, it batches and sends at trip end. The MProfiler configuration in configureSmartDrive() works in parallel with TripWrapper's realtime mode — both are needed when realtime is on.

How do beacon / mHub trips get sent? (new in 1.1.2) A beacon/mHub trip is tagged with the beacon's device ID, and that ID is only available if the in-range beacon is snapshotted at trip start. TripWrapper triggers that snapshot by invoking TripConfigProvider.onBeaconTripStart when a trip starts and beacons are enabled. Implement it to call your beacon manager's trip-start hook (SCPBeaconManager.shared.onTripStart(), or route it through your BLE proxy). If it is not implemented for a beacon-mode app, the trip records but has no device ID and is rejected on upload. Smart-mode apps do not need it — the protocol supplies a default no-op, and the callback is never invoked when areBeaconsEnabled() is false.

My host and a framework both declare APIResponse — importing BLEFramework into TripCoordinator won't compile. Don't import BLEFramework into TripCoordinator. Route onBeaconTripStart through a bridge that already imports it (for example a BLEFrameworkProxy type), so the beacon call is made from the one file that owns the BLEFramework import and the type clash never arises.


Questions? Contact the Scope Mobile SDK team.