Skip to content

iOS TripWrapper Migration Guide

YuliyaArtsiomchykScope edited this page Jun 5, 2026 · 1 revision

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.


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](iOS-Consolidated-SPM-documentation).

  1. In Xcode open File → Add Package Dependencies.
  2. Paste the Consolidated SPM repository URL.
  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.


3. New Files to Add

Copy the three files below into your project. They are largely client-agnostic; the only part you need to adapt is the API bridge 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?)
LoggingProtocol Routes TripWrapper log events into your logging system

The only section you must adapt is 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() }
}

Everything else (DeviceIDProvider, TripConfigProvider, 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) {
        // Post to NotificationCenter so view controllers can update
        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 the Consolidated SPM. Build succeeds with no missing-module errors.
  • Add TripCoordinator.swift — Adapt APIManagerProtocol to call your backend wrapper. Build succeeds.
  • 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.
  • 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
    │             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.


Questions? Contact the Scope Mobile SDK team.

Clone this wiki locally