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
Swift Package Manager (Recommended)
// Package.swift
dependencies: [
.package(url: "https://github.com/bookovia/ios-sdk.git", from: "2.1.0")
]
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
- iOS 13.0+
- macOS 10.15+
- Xcode 14+
- Swift 5.7+
Setup
Info.plist Configuration
Add required permissions to yourInfo.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
- GitHub: github.com/bookovia/ios-sdk
- Swift Package Index: swiftpackageindex.com/bookovia/ios-sdk
- CocoaPods: cocoapods.org/pods/BookoviaSDK
- Issues: GitHub Issues
- Email: ios-support@bookovia.com
Ready to build iOS telematics apps? Check out our quickstart guide and API reference for more examples.