Repository navigation
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.
- Why TripWrapper?
- Package Installation
- New Files to Add
- AppDelegate Changes
- Code to Delete
- Step-by-Step Checklist
- Architecture After Migration
- FAQ
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, andTripsViewController. -
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 (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
|
TripWrapper is distributed through the Scope Consolidated Swift Package. Full instructions are in [iOS Consolidated SPM Documentation](iOS-Consolidated-SPM-documentation).
- In Xcode open File → Add Package Dependencies.
- Paste the Consolidated SPM repository URL.
- Select TripWrapper_iOS from the product list and add it to your app target.
Note: If you already consume
SCPSmartDriveandSDApiManagerfrom the Consolidated SPM,TripWrapper_iOSis added from the same repository — no new URL needed.
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.
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.
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()
|
⚠️ RemoveSCPSmartDriveManager.sharedInstance()?.addNewDelegateObject(self)frominit(). 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.
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'sTripvalue 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()
}
}import TripWrapper_iOS // add this
import SCPSmartDrive
import SDApiManagerAdd 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
SCPSmartDriveconfiguration block (askLocationPermission,setTripAutostartEnabled,configureMProfiler, etc.). TripWrapper sits on top ofSCPSmartDrive— it does not replace it. Everything inconfigureSmartDrive()stays exactly as it is.
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]
)
}
}
⚠️ Leaving old code alongside TripWrapper causes double-sends, duplicate notifications, and confusing log output.
- Delete the entire
SCPSmartDriveManagerDelegateextension (tripSendStatusChanged). - Delete
sendTrips(_:onTripSend:)and its private helpers (sendTrip,prepareEventBatchData,sendEvents). - Delete
autosendFailedTrips()andresendTrips(). - Delete the
NSNotification.Name.tripAutosendobserver andtripAutocompleted(). - Delete the
NSNotification.Name.newEventRegistrationobserver anddidReceiveNewEvent(). - Delete
smartDrive.addNewDelegateObject(self)ininit().
Rule of thumb: if
TripManagerhas any reference toSCPSmartDriveManagerother than readinggetCurrentSpeed()for display, that code must go.
- Delete the
SCPSmartDriveManagerDelegateextension (didDetectTripStart,didDetectTripEnd,didDetectTripAutomotive,didDetectVehicleStop,didResetVehicleStop). - Delete
SCPSmartDriveManager.sharedInstance()?.addNewDelegateObject(self)frominit(). - Delete any call to
TripManager.shared.sendTrips()insidedidDetectTripEnd.
- Remove
_ = TripNotificationManager.sharedfrominitializeFeatures()if its only purpose was to trigger delegate registration. - Remove any
SCPSmartDriveManagerDelegateextension onAppDelegatethat forwarded trip events.
- Remove direct calls to
SCPSmartDriveManager.sharedInstance()?.getLocalTrips()— useTripManager.shared.getLocalTrips()instead. - Remove any
SCPSmartDriveManagerDelegateregistration inviewWillAppearthat was used for trip lifecycle. Keeping it fordidReceiveLog(to display logs) is fine.
Work through these in order. Each step has a clear done-state so you can build and test before continuing.
- Add package — Add
TripWrapper_iOSvia the Consolidated SPM. Build succeeds with no missing-module errors. - Add TripCoordinator.swift — Adapt
APIManagerProtocolto call your backend wrapper. Build succeeds. - Replace TripNotificationManager.swift — Remove the old
SCPSmartDriveManagerDelegateextension. New file implementsTripNotificationDelegateonly. - Replace TripManager.swift — Facade version only. All send and retry methods deleted.
- Update AppDelegate — Add
import TripWrapper_iOS,initializeTripWrapperSDK(), call it ininitializeFeatures(), addTripWrapperDelegateextension. - Delete old code (Section 5) — Build succeeds with zero references to
sendTrip,autosendFailedTrips, orresendTripsremaining. - 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_idvalues).
┌─────────────────────────────────────────────────┐
│ 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.
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.
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