Skip to main content

Documentation Index

Fetch the complete documentation index at: https://docs.bookovia.com/llms.txt

Use this file to discover all available pages before exploring further.

Bookovia iOS SDK

The official iOS SDK for the Bookovia Telematics API provides a native Swift implementation optimized for iOS applications with modern async/await syntax, Combine integration, and offline capabilities.

Features

  • Native Swift - Built with Swift 5.7+ and modern concurrency
  • Async/Await Support - Modern asynchronous programming patterns
  • Combine Integration - Reactive programming with Combine framework
  • Core Data Integration - Offline support with Core Data persistence
  • Background Location - Continuous tracking with background app refresh
  • SwiftUI & UIKit - Compatible with both UI frameworks

Installation

// Package.swift
dependencies: [
    .package(url: "https://github.com/bookovia/ios-sdk.git", from: "2.1.0")
]
Or add via Xcode: File → Add Package Dependencies

CocoaPods

# Podfile
pod 'BookoviaSDK', '~> 2.1.0'

# Optional: for offline support
pod 'BookoviaSDK/Offline', '~> 2.1.0'

Carthage

# Cartfile
github "bookovia/ios-sdk" ~> 2.1.0
Requirements:
  • iOS 13.0+
  • macOS 10.15+
  • Xcode 14+
  • Swift 5.7+

Setup

Info.plist Configuration

Add required permissions to your Info.plist:
<key>NSLocationWhenInUseUsageDescription</key>
<string>This app needs location access to track vehicle trips</string>

<key>NSLocationAlwaysAndWhenInUseUsageDescription</key>
<string>This app needs location access to track trips in the background</string>

<key>UIBackgroundModes</key>
<array>
    <string>location</string>
    <string>background-fetch</string>
</array>

SDK Initialization

import BookoviaSDK

@main
struct BookoviaApp: App {
    init() {
        // Configure Bookovia SDK
        BookoviaSDK.configure(
            apiKey: "bkv_live_your_api_key",
            config: BookoviaConfig(
                baseURL: "https://api.bookovia.com/v1",
                enableOfflineMode: true,
                logLevel: .debug
            )
        )
    }
    
    var body: some Scene {
        WindowGroup {
            ContentView()
        }
    }
}

Quick Start

Initialize Client

import BookoviaSDK

class TripManager: ObservableObject {
    private let client: BookoviaClient
    @Published var currentTrip: Trip?
    @Published var isTracking = false
    
    init() {
        self.client = BookoviaClient.shared
    }
}

Start a Trip

func startTrip() async throws -> Trip {
    let request = StartTripRequest(
        vehicleId: "vehicle_123",
        driverId: "driver_456",
        startLocation: Location(
            latitude: 40.7128,
            longitude: -74.0060
        ),
        metadata: [
            "purpose": "delivery",
            "platform": "ios"
        ]
    )
    
    do {
        let trip = try await client.trips.start(request)
        await MainActor.run {
            self.currentTrip = trip
            self.isTracking = true
        }
        
        // Start location tracking
        try await startLocationTracking(for: trip.tripId)
        
        return trip
    } catch {
        print("Failed to start trip: \(error)")
        throw error
    }
}

Location Tracking

import CoreLocation

class LocationTracker: NSObject, ObservableObject {
    private let locationManager = CLLocationManager()
    private let client = BookoviaClient.shared
    private var currentTripId: String?
    
    @Published var authorizationStatus: CLAuthorizationStatus = .notDetermined
    @Published var lastLocation: CLLocation?
    
    override init() {
        super.init()
        locationManager.delegate = self
        locationManager.desiredAccuracy = kCLLocationAccuracyBest
        locationManager.distanceFilter = 10 // meters
    }
    
    func requestPermission() {
        locationManager.requestWhenInUseAuthorization()
    }
    
    func requestAlwaysAuthorization() {
        locationManager.requestAlwaysAuthorization()
    }
    
    func startTracking(for tripId: String) {
        currentTripId = tripId
        
        guard authorizationStatus == .authorizedAlways || 
              authorizationStatus == .authorizedWhenInUse else {
            print("Location permission not granted")
            return
        }
        
        locationManager.startUpdatingLocation()
        
        // Enable background location if authorized
        if authorizationStatus == .authorizedAlways {
            locationManager.allowsBackgroundLocationUpdates = true
            locationManager.pausesLocationUpdatesAutomatically = false
        }
    }
    
    func stopTracking() {
        locationManager.stopUpdatingLocation()
        locationManager.allowsBackgroundLocationUpdates = false
        currentTripId = nil
    }
}

extension LocationTracker: CLLocationManagerDelegate {
    func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
        guard let location = locations.last,
              let tripId = currentTripId else { return }
        
        lastLocation = location
        
        Task {
            do {
                try await client.locations.upload(
                    LocationUploadRequest(
                        tripId: tripId,
                        latitude: location.coordinate.latitude,
                        longitude: location.coordinate.longitude,
                        timestamp: location.timestamp,
                        speed: location.speed,
                        heading: location.course,
                        accuracy: location.horizontalAccuracy
                    )
                )
            } catch {
                print("Failed to upload location: \(error)")
            }
        }
    }
    
    func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) {
        print("Location error: \(error)")
    }
    
    func locationManagerDidChangeAuthorization(_ manager: CLLocationManager) {
        authorizationStatus = manager.authorizationStatus
    }
}

API Reference

Trip Service

// Trip Query Methods (DataSyncManager)
class DataSyncManager {
    // User Trip Queries
    func getUserActiveTrips(userId: String, completion: @escaping (Result<[String: Any], Error>) -> Void)
    func getUserTripHistory(userId: String, status: String?, completion: @escaping (Result<[String: Any], Error>) -> Void)
    
    // Organization Trip Queries
    func getOrgActiveTrips(orgId: String, completion: @escaping (Result<[String: Any], Error>) -> Void)
    func getOrgTripHistory(orgId: String, status: String?, completion: @escaping (Result<[String: Any], Error>) -> Void)
    
    // Vehicle Trip Queries
    func getVehicleActiveTrips(vehicleId: String, completion: @escaping (Result<[String: Any], Error>) -> Void)
    func getVehicleTripHistory(vehicleId: String, status: String?, completion: @escaping (Result<[String: Any], Error>) -> Void)
}

// Usage
let tripService = client.trips

// Start trip
let trip = try await tripService.start(
    StartTripRequest(
        vehicleId: "vehicle_123",
        driverId: "driver_456"
    )
)

// List trips
let trips = try await tripService.list(
    TripFilters(
        status: .active,
        limit: 50
    )
)

Location Service

protocol LocationService {
    func upload(_ request: LocationUploadRequest) async throws
    func batchUpload(_ request: BatchUploadRequest) async throws -> BatchUploadResponse
    func getRoute(tripId: String) async throws -> RouteResponse
    func findNearby(_ request: NearbySearchRequest) async throws -> [NearbyLocation]
}

// Batch upload
let locations = [
    LocationPoint(
        latitude: 40.7128,
        longitude: -74.0060,
        timestamp: Date(),
        speed: 35.0,
        heading: 180.0
    )
]

try await client.locations.batchUpload(
    BatchUploadRequest(
        tripId: "trip_123",
        locations: locations
    )
)

Safety Analytics

protocol SafetyService {
    func getScore(_ request: SafetyScoreRequest) async throws -> SafetyScore
    func analyzeBehavior(_ request: BehaviorAnalysisRequest) async throws -> BehaviorAnalysis
    func getHarshEvents(_ filters: EventFilters) async throws -> EventListResponse
    func getCrashRisk(_ request: CrashRiskRequest) async throws -> CrashRiskAssessment
}

// Get safety score
let score = try await client.safety.getScore(
    SafetyScoreRequest(
        driverId: "driver_456",
        dateRange: DateRange(
            start: Calendar.current.date(byAdding: .day, value: -30, to: Date())!,
            end: Date()
        )
    )
)

print("Safety Score: \(score.score)/100 (Grade: \(score.grade))")

Data Models

Core Models

struct Trip: Codable, Identifiable {
    let id: String
    let tripId: String
    let organizationId: String
    let vehicleId: String
    let driverId: String?
    let status: TripStatus
    let startTime: Date
    let endTime: Date?
    let startLocation: Location?
    let endLocation: Location?
    let analytics: TripAnalytics
    let metadata: [String: Any]?
    let createdAt: Date
    let updatedAt: Date
}

struct Location: Codable {
    let latitude: Double
    let longitude: Double
    let address: String?
}

struct LocationPoint: Codable {
    let latitude: Double
    let longitude: Double
    let timestamp: Date
    let speed: Double
    let heading: Double
    let accuracy: Double?
    let altitude: Double?
}

enum TripStatus: String, Codable, CaseIterable {
    case active = "active"
    case completed = "completed"
    case paused = "paused"
    case cancelled = "cancelled"
}

Request Models

struct StartTripRequest: Codable {
    let vehicleId: String
    let driverId: String?
    let startLocation: Location?
    let metadata: [String: Any]?
    let odometerReading: Int?
    let fuelLevelPercent: Double?
}

struct StopTripRequest: Codable {
    let endLocation: Location?
    let odometerReading: Int?
    let fuelLevelPercent: Double?
    let metadata: [String: Any]?
}

struct TripFilters: Codable {
    let vehicleId: String?
    let driverId: String?
    let status: TripStatus?
    let startDate: Date?
    let endDate: Date?
    let limit: Int?
    let offset: Int?
    
    init(
        vehicleId: String? = nil,
        driverId: String? = nil,
        status: TripStatus? = nil,
        startDate: Date? = nil,
        endDate: Date? = nil,
        limit: Int? = 25,
        offset: Int? = 0
    ) {
        self.vehicleId = vehicleId
        self.driverId = driverId
        self.status = status
        self.startDate = startDate
        self.endDate = endDate
        self.limit = limit
        self.offset = offset
    }
}

SwiftUI Integration

Trip Management View

import SwiftUI
import BookoviaSDK

struct TripView: View {
    @StateObject private var tripManager = TripManager()
    @StateObject private var locationTracker = LocationTracker()
    
    @State private var isStartingTrip = false
    @State private var errorMessage: String?
    
    var body: some View {
        NavigationView {
            VStack(spacing: 20) {
                // Trip Status
                if let trip = tripManager.currentTrip {
                    TripStatusView(trip: trip)
                } else {
                    Text("No active trip")
                        .font(.headline)
                        .foregroundColor(.secondary)
                }
                
                // Control Buttons
                HStack(spacing: 20) {
                    Button(action: startTrip) {
                        Text("Start Trip")
                            .frame(maxWidth: .infinity)
                    }
                    .buttonStyle(.borderedProminent)
                    .disabled(tripManager.currentTrip != nil || isStartingTrip)
                    
                    Button(action: stopTrip) {
                        Text("Stop Trip")
                            .frame(maxWidth: .infinity)
                    }
                    .buttonStyle(.bordered)
                    .disabled(tripManager.currentTrip == nil)
                }
                
                // Location Status
                LocationStatusView(tracker: locationTracker)
                
                Spacer()
            }
            .padding()
            .navigationTitle("Bookovia Trips")
            .alert("Error", isPresented: .constant(errorMessage != nil)) {
                Button("OK") { errorMessage = nil }
            } message: {
                if let error = errorMessage {
                    Text(error)
                }
            }
        }
        .onAppear {
            locationTracker.requestPermission()
        }
    }
    
    private func startTrip() {
        isStartingTrip = true
        
        Task {
            do {
                let trip = try await tripManager.startTrip()
                locationTracker.startTracking(for: trip.tripId)
            } catch {
                await MainActor.run {
                    errorMessage = "Failed to start trip: \(error.localizedDescription)"
                }
            }
            
            await MainActor.run {
                isStartingTrip = false
            }
        }
    }
    
    private func stopTrip() {
        Task {
            do {
                try await tripManager.stopTrip()
                locationTracker.stopTracking()
            } catch {
                await MainActor.run {
                    errorMessage = "Failed to stop trip: \(error.localizedDescription)"
                }
            }
        }
    }
}

struct TripStatusView: View {
    let trip: Trip
    
    var body: some View {
        VStack(spacing: 8) {
            Text("Trip Active")
                .font(.headline)
                .foregroundColor(.green)
            
            Text(trip.tripId)
                .font(.caption)
                .foregroundColor(.secondary)
            
            HStack {
                VStack {
                    Text("\(trip.analytics.distanceKm, specifier: "%.1f")")
                        .font(.title2)
                        .fontWeight(.semibold)
                    Text("km")
                        .font(.caption)
                        .foregroundColor(.secondary)
                }
                
                Divider()
                
                VStack {
                    Text("\(trip.analytics.durationMinutes)")
                        .font(.title2)
                        .fontWeight(.semibold)
                    Text("minutes")
                        .font(.caption)
                        .foregroundColor(.secondary)
                }
                
                Divider()
                
                VStack {
                    Text("\(trip.analytics.safetyScore)")
                        .font(.title2)
                        .fontWeight(.semibold)
                        .foregroundColor(safetyScoreColor(trip.analytics.safetyScore))
                    Text("safety")
                        .font(.caption)
                        .foregroundColor(.secondary)
                }
            }
            .frame(maxWidth: .infinity)
        }
        .padding()
        .background(Color(.systemGray6))
        .cornerRadius(12)
    }
    
    private func safetyScoreColor(_ score: Int) -> Color {
        switch score {
        case 90...100: return .green
        case 70..<90: return .orange
        default: return .red
        }
    }
}

Combine Integration

import Combine
import BookoviaSDK

class TripViewModel: ObservableObject {
    @Published var currentTrip: Trip?
    @Published var isLoading = false
    @Published var error: BookoviaError?
    
    private let client = BookoviaClient.shared
    private var cancellables = Set<AnyCancellable>()
    
    func startTrip(vehicleId: String, driverId: String?) {
        isLoading = true
        error = nil
        
        let request = StartTripRequest(
            vehicleId: vehicleId,
            driverId: driverId
        )
        
        client.trips.startPublisher(request)
            .receive(on: DispatchQueue.main)
            .sink(
                receiveCompletion: { [weak self] completion in
                    self?.isLoading = false
                    
                    if case .failure(let error) = completion {
                        self?.error = error
                    }
                },
                receiveValue: { [weak self] trip in
                    self?.currentTrip = trip
                }
            )
            .store(in: &cancellables)
    }
    
    func stopCurrentTrip() {
        guard let tripId = currentTrip?.tripId else { return }
        
        isLoading = true
        error = nil
        
        client.trips.stopPublisher(tripId: tripId)
            .receive(on: DispatchQueue.main)
            .sink(
                receiveCompletion: { [weak self] completion in
                    self?.isLoading = false
                    
                    if case .failure(let error) = completion {
                        self?.error = error
                    }
                },
                receiveValue: { [weak self] trip in
                    self?.currentTrip = trip.status == .completed ? nil : trip
                }
            )
            .store(in: &cancellables)
    }
}

Core Data Integration

Core Data Model

import CoreData

@objc(TripEntity)
public class TripEntity: NSManagedObject {
    @NSManaged public var tripId: String
    @NSManaged public var vehicleId: String
    @NSManaged public var driverId: String?
    @NSManaged public var status: String
    @NSManaged public var startTime: Date
    @NSManaged public var endTime: Date?
    @NSManaged public var distanceKm: Double
    @NSManaged public var durationMinutes: Int32
    @NSManaged public var safetyScore: Int32
    @NSManaged public var synced: Bool
}

extension TripEntity {
    func toTrip() -> Trip {
        Trip(
            id: tripId,
            tripId: tripId,
            organizationId: "", // Set from context
            vehicleId: vehicleId,
            driverId: driverId,
            status: TripStatus(rawValue: status) ?? .active,
            startTime: startTime,
            endTime: endTime,
            startLocation: nil,
            endLocation: nil,
            analytics: TripAnalytics(
                distanceKm: distanceKm,
                durationMinutes: Int(durationMinutes),
                maxSpeedKmh: 0,
                avgSpeedKmh: 0,
                idleTimeMinutes: 0,
                locationsCount: 0,
                eventsCount: 0,
                safetyScore: Int(safetyScore),
                ecoScore: 100
            ),
            metadata: nil,
            createdAt: startTime,
            updatedAt: Date()
        )
    }
}

Offline Trip Manager

import CoreData

class OfflineTripManager: ObservableObject {
    private let client = BookoviaClient.shared
    private let persistentContainer: NSPersistentContainer
    
    init() {
        persistentContainer = NSPersistentContainer(name: "BookoviaModel")
        persistentContainer.loadPersistentStores { _, error in
            if let error = error {
                print("Core Data error: \(error)")
            }
        }
    }
    
    func startTripOffline(vehicleId: String) -> String {
        let context = persistentContainer.viewContext
        let tripEntity = TripEntity(context: context)
        
        let tripId = "offline_\(Int(Date().timeIntervalSince1970))"
        
        tripEntity.tripId = tripId
        tripEntity.vehicleId = vehicleId
        tripEntity.status = TripStatus.active.rawValue
        tripEntity.startTime = Date()
        tripEntity.distanceKm = 0
        tripEntity.durationMinutes = 0
        tripEntity.safetyScore = 100
        tripEntity.synced = false
        
        do {
            try context.save()
            return tripId
        } catch {
            print("Failed to save offline trip: \(error)")
            return tripId
        }
    }
    
    func syncPendingTrips() async {
        let context = persistentContainer.viewContext
        let request: NSFetchRequest<TripEntity> = TripEntity.fetchRequest()
        request.predicate = NSPredicate(format: "synced == NO")
        
        do {
            let unsyncedTrips = try context.fetch(request)
            
            for tripEntity in unsyncedTrips {
                do {
                    let request = StartTripRequest(
                        vehicleId: tripEntity.vehicleId,
                        driverId: tripEntity.driverId
                    )
                    
                    let syncedTrip = try await client.trips.start(request)
                    
                    // Update with server trip ID
                    tripEntity.tripId = syncedTrip.tripId
                    tripEntity.synced = true
                    
                    try context.save()
                    
                } catch {
                    print("Failed to sync trip \(tripEntity.tripId): \(error)")
                }
            }
        } catch {
            print("Failed to fetch unsynced trips: \(error)")
        }
    }
}

Error Handling

import BookoviaSDK

do {
    let trip = try await client.trips.start(request)
    // Handle success
} catch BookoviaError.authentication(let message) {
    // Handle authentication error
    print("Authentication failed: \(message)")
    
} catch BookoviaError.validation(let errors) {
    // Handle validation errors
    print("Validation errors:")
    for error in errors {
        print("- \(error.field): \(error.message)")
    }
    
} catch BookoviaError.rateLimit(let retryAfter) {
    // Handle rate limiting
    print("Rate limited. Retry after \(retryAfter) seconds")
    
} catch BookoviaError.network(let underlyingError) {
    // Handle network errors
    print("Network error: \(underlyingError.localizedDescription)")
    
} catch BookoviaError.server(let statusCode, let message) {
    // Handle server errors
    print("Server error \(statusCode): \(message)")
    
} catch {
    // Handle other errors
    print("Unexpected error: \(error)")
}

Testing

Unit Tests

import XCTest
@testable import BookoviaSDK

final class TripManagerTests: XCTestCase {
    var tripManager: TripManager!
    var mockClient: MockBookoviaClient!
    
    override func setUp() {
        super.setUp()
        mockClient = MockBookoviaClient()
        tripManager = TripManager(client: mockClient)
    }
    
    func testStartTripSuccess() async throws {
        // Given
        let expectedTrip = createMockTrip()
        mockClient.mockTrip = expectedTrip
        
        // When
        let result = try await tripManager.startTrip()
        
        // Then
        XCTAssertEqual(result.tripId, expectedTrip.tripId)
        XCTAssertEqual(tripManager.currentTrip?.tripId, expectedTrip.tripId)
    }
    
    func testStartTripAuthenticationError() async {
        // Given
        mockClient.shouldThrowAuthError = true
        
        // When & Then
        do {
            _ = try await tripManager.startTrip()
            XCTFail("Expected authentication error")
        } catch BookoviaError.authentication {
            // Expected error
        } catch {
            XCTFail("Unexpected error: \(error)")
        }
    }
}

class MockBookoviaClient: BookoviaClientProtocol {
    var mockTrip: Trip?
    var shouldThrowAuthError = false
    
    var trips: TripService { MockTripService(client: self) }
    var locations: LocationService { MockLocationService() }
    var safety: SafetyService { MockSafetyService() }
    var fleet: FleetService { MockFleetService() }
}

UI Tests

import XCTest

final class TripUITests: XCTestCase {
    var app: XCUIApplication!
    
    override func setUp() {
        super.setUp()
        app = XCUIApplication()
        app.launchArguments = ["--uitesting"]
        app.launch()
    }
    
    func testStartAndStopTrip() {
        let startButton = app.buttons["Start Trip"]
        let stopButton = app.buttons["Stop Trip"]
        
        // Initially stop button should be disabled
        XCTAssertTrue(startButton.isEnabled)
        XCTAssertFalse(stopButton.isEnabled)
        
        // Start trip
        startButton.tap()
        
        // Wait for trip to start
        let tripActiveText = app.staticTexts["Trip Active"]
        XCTAssertTrue(tripActiveText.waitForExistence(timeout: 5))
        
        // Now stop button should be enabled
        XCTAssertFalse(startButton.isEnabled)
        XCTAssertTrue(stopButton.isEnabled)
        
        // Stop trip
        stopButton.tap()
        
        // Wait for trip to stop
        let noActiveTripText = app.staticTexts["No active trip"]
        XCTAssertTrue(noActiveTripText.waitForExistence(timeout: 5))
    }
}

Example Apps

Complete iOS Implementation

// ContentView.swift
import SwiftUI
import BookoviaSDK

struct ContentView: View {
    @StateObject private var tripManager = TripManager()
    @StateObject private var locationTracker = LocationTracker()
    
    var body: some View {
        NavigationView {
            TripView(
                tripManager: tripManager,
                locationTracker: locationTracker
            )
        }
        .onAppear {
            locationTracker.requestPermission()
        }
    }
}

// App.swift
import SwiftUI
import BookoviaSDK

@main
struct BookoviaTelematicsApp: App {
    init() {
        BookoviaSDK.configure(
            apiKey: "bkv_live_your_api_key_here",
            config: BookoviaConfig(
                enableOfflineMode: true,
                logLevel: .debug
            )
        )
    }
    
    var body: some Scene {
        WindowGroup {
            ContentView()
        }
    }
}

Support


Ready to build iOS telematics apps? Check out our quickstart guide and API reference for more examples.