A modern OpenTripPlanner client for iOS, written in Swift and SwiftUI. OTPKit gives your app a complete, production-quality trip planning experience — search, itineraries, turn-by-turn directions, live trip progress — rendered on top of a map view that you own. It powers trip planning in the OneBusAway iOS app and is a project of the Open Transit Software Foundation.
Jump to what you're here for:
- Complete UI, not just a networking layer. Origin/destination search, transport mode selection, itinerary comparison, step-by-step directions, and live progress along an active trip — all included, all SwiftUI, localized into 13 languages.
- Bring your own map. OTPKit draws routes and annotations through a small
OTPMapProviderprotocol. Use the bundledMKMapViewAdapterfor MapKit, or implement the protocol to keep your existing map stack. - Works with both OTP generations. OTP 1.x (REST) and OTP 2.x (GTFS GraphQL) behind a single
APIServiceprotocol — pick the implementation that matches your server and the rest of your code doesn't change. - Shared-mobility aware. On OTP 2.x servers, OTPKit can fetch bike/scooter share stations and free-floating vehicles and plan rental-aware trips ("get me there using this bike").
- Proven in production. This is the trip planner inside OneBusAway iOS, exercised daily by real riders.
| iOS | 18.0+ |
| Swift | 6.0 toolchain (package builds in Swift 5 language mode) |
| Server | OpenTripPlanner 1.5.x+ (REST) or 2.x (GTFS GraphQL) |
Add OTPKit with Swift Package Manager:
dependencies: [
.package(url: "https://github.com/OneBusAway/OTPKit.git", from: "0.16.0")
]OTPKit is pre-1.0: releases are tagged and safe to pin, but the API may still change between minor versions.
Three pieces wire together: a map provider (adapting the map view you own), an API service (matching your OTP server's generation), and a TripPlanner (which builds the UI). Here's the complete setup in a UIKit view controller, matching what the demo app does:
import MapKit
import OTPKit
import SwiftUI
import UIKit
class ViewController: UIViewController {
private var tripPlanner: TripPlanner?
override func viewDidLoad() {
super.viewDidLoad()
// 1. You own the map view; give OTPKit an adapter for it.
let mapView = MKMapView(frame: view.bounds)
view.addSubview(mapView)
let mapProvider = MKMapViewAdapter(mapView: mapView)
// 2. Point OTPKit at your server, and scope location search
// to your service area.
let config = OTPConfiguration(
otpServerURL: URL(string: "https://otp.example.com/otp/")!,
searchRegion: MKCoordinateRegion(
center: CLLocationCoordinate2D(latitude: 47.6062, longitude: -122.3321),
latitudinalMeters: 50000,
longitudinalMeters: 50000
)
)
// 3. Pick the service that matches your server:
// RestAPIService for OTP 1.x, GraphQLAPIService for OTP 2.x.
let apiService = RestAPIService(baseURL: config.otpServerURL)
// 4. Create the planner and present its UI as a bottom sheet.
let planner = TripPlanner(
otpConfig: config,
apiService: apiService,
mapProvider: mapProvider
)
let plannerView = planner.createTripPlannerView { [weak self] in
self?.dismiss(animated: true)
}
present(PanelHostingController(rootView: plannerView, sourceView: view), animated: true)
tripPlanner = planner
}
}createTripPlannerView returns a plain SwiftUI view, so you're not locked into the bottom-sheet presentation — host it however your app's navigation works. It also accepts optional prefill parameters (origin, destination, viaPoint, transportMode) for deep-linking straight into a planned trip.
| Your OTP server | Use | Notes |
|---|---|---|
OTP 1.x (REST /plan endpoint) |
RestAPIService |
|
| OTP 2.x (GTFS GraphQL API) | GraphQLAPIService |
Required for bike/scooter rental features |
Both are Swift actors conforming to APIService; you can also implement APIService yourself to add authentication, caching, or a custom backend.
- Transport modes: pass
enabledTransportModestoOTPConfiguration(defaults to transit, walk, bike, car). Rental modes like.bikeRentalare opt-in and need an OTP 2.x server with rental data. - Theme: pass an
OTPThemeConfigurationto adjust colors. - Map behavior: implement
OTPMapProviderto control exactly how routes and stops render on your map.
OTPKit ships translations for Arabic, English, Filipino, French, Italian, Korean, Polish, Portuguese (Brazil), Russian, Simplified Chinese, Spanish, Traditional Chinese, and Vietnamese.
Your app must declare which of these languages it supports, or OTPKit will render in English. iOS resolves an app's language from the main bundle, so a host app that ships no localizations pins the whole process to English regardless of the device language. If your app is already localized into the languages you care about, there's nothing to do. Otherwise, declare them in your app target's Info.plist:
<key>CFBundleLocalizations</key>
<array>
<string>en</string>
<string>ar</string>
<string>es</string>
<string>fil</string>
<string>fr</string>
<string>it</string>
<string>ko</string>
<string>pl</string>
<string>pt-BR</string>
<string>ru</string>
<string>vi</string>
<string>zh-Hans</string>
<string>zh-Hant</string>
</array>OTPKit resolves strings from its own resource bundle, so redefining the same keys in your app's Localizable.strings will not override them. To change or add wording, edit OTPKit/Sources/OTPKit/Resources/en.lproj/Localizable.strings and its sibling locales.
No configuration needed — the demo ships with working public OTP servers:
git clone https://github.com/OneBusAway/otpkit.git
cd otpkit
open Demo/OTPKitDemo.xcodeprojRun the OTPKitDemo scheme on an iOS simulator, then pick a region on first launch:
- San Diego — OTP 1.x REST
- Seattle — OTP 1.x REST
- Seattle (OTP 2.x GraphQL) — the GraphQL API, including bike share
Tip: set the simulator's location (Features → Location) to somewhere inside the region you picked so "current location" trip planning works.
Issues and pull requests are welcome. The library lives in OTPKit/ (sources and tests) with Package.swift at the repo root; the demo app lives in Demo/.
The package uses iOS-only frameworks, so build and test through Xcode (not swift test):
# Run the test suite
xcodebuild test -scheme OTPKit -destination 'platform=iOS Simulator,name=iPhone 17 Pro'CI runs SwiftLint in strict mode and fails fast, so run it before pushing:
brew install swiftlint
swiftlint --strictpre-commit can run SwiftLint and the test suite automatically before every push:
brew install pre-commit
pre-commit install --hook-type pre-pushOTPKit began as a Google Summer of Code project and continues to grow with each cohort:
- GSoC 2025: built by Manu R with mentorship from Aaron Brethorst — read the final report
- GSoC 2024: Hilmy Veradin
Licensed under the Apache License, Version 2.0. See LICENSE for details.
