Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 6 additions & 3 deletions .github/workflows/test.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,11 @@ jobs:
${{ runner.os }}-spm-

- name: Switch Xcode
run: sudo xcode-select -switch /Applications/Xcode_26.0.app
run: sudo xcode-select -switch /Applications/Xcode_26.2.app

- name: List Available Simulators
run: |
xcrun simctl list devices

# Build
- name: Build OTPKit
Expand All @@ -54,13 +58,12 @@ jobs:
-scheme OTPKit
-destination 'platform=iOS Simulator,name=iPhone 17 Pro'
-resultBundlePath OTPKitTests.xcresult
-quiet

# Upload results
- uses: kishikawakatsumi/xcresulttool@v1.7.0
continue-on-error: true
with:
show-passed-tests: false # Avoid truncation of annotations by GitHub by omitting succeeding tests.
path: |
OTPKitDemoTests.xcresult
OTPKitTests.xcresult
if: success() || failure()
25 changes: 20 additions & 5 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -156,22 +156,31 @@ func testFetchPlan() async throws {
### Basic Setup
```swift
import OTPKit
import MapKit

// 1. Create your map view (OTPKit doesn't provide one)
let mapView = MKMapView()
let mapProvider = MKMapViewAdapter(mapView: mapView)

// 2. Create configuration
// 2. Define the search region for location suggestions
let searchRegion = MKCoordinateRegion(
center: CLLocationCoordinate2D(latitude: 47.6062, longitude: -122.3321),
latitudinalMeters: 50000,
longitudinalMeters: 50000
)

// 3. Create configuration with required search region
let config = OTPConfiguration(
otpServerURL: URL(string: "https://otp.example.com")!,
enabledTransportModes: [.transit, .walk, .bike],
themeConfiguration: OTPThemeConfiguration(primaryColor: .blue)
themeConfiguration: OTPThemeConfiguration(primaryColor: .blue),
searchRegion: searchRegion
)

// 3. Initialize API service
// 4. Initialize API service
let apiService = RestAPIService(baseURL: config.otpServerURL)

// 4. Create and present OTP view
// 5. Create and present OTP view
let otpView = OTPView(
otpConfig: config,
apiService: apiService,
Expand All @@ -186,9 +195,15 @@ let themeConfig = OTPThemeConfiguration(
secondaryColor: .green,
backgroundColor: .systemBackground
)
let searchRegion = MKCoordinateRegion(
center: CLLocationCoordinate2D(latitude: 47.6062, longitude: -122.3321),
latitudinalMeters: 50000,
longitudinalMeters: 50000
)
let config = OTPConfiguration(
otpServerURL: url,
themeConfiguration: themeConfig
themeConfiguration: themeConfig,
searchRegion: searchRegion
)
```

Expand Down
12 changes: 11 additions & 1 deletion Demo/OTPKitDemo/OTPDemoViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -141,9 +141,19 @@ class OTPDemoViewController: UIViewController {
return
}

// Create search region from the selected region info
let searchRegion = MKCoordinateRegion(
center: regionInfo.center,
latitudinalMeters: 50000,
longitudinalMeters: 50000
)

// Create bottom sheet and present it
let tripPlanner = TripPlanner(
otpConfig: OTPConfiguration(otpServerURL: serverURL),
otpConfig: OTPConfiguration(
otpServerURL: serverURL,
searchRegion: searchRegion
),
apiService: apiService,
mapProvider: provider,
notificationCenter: NotificationCenter.default
Expand Down
7 changes: 7 additions & 0 deletions Demo/OTPKitDemo/OnboardingViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,13 @@ class OnboardingViewController: UIViewController {
private var continueButton: UIButton!

private let regions = [
OTPRegionInfo(
name: "San Diego",
description: "San Diego",
icon: "building.2.fill",
url: URL(string: "https://realtime.sdmts.com:9091/otp/")!,
center: CLLocationCoordinate2D(latitude: 32.8850078, longitude: -117.2393175)
),
OTPRegionInfo(
name: "Seattle",
description: "Puget Sound region",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,12 @@ class SearchManager: NSObject, MKLocalSearchCompleterDelegate {

private let searchCompleter = MKLocalSearchCompleter()

override init() {
init(region: MKCoordinateRegion) {
super.init()
searchCompleter.delegate = self
searchCompleter.resultTypes = [.address, .pointOfInterest]
searchCompleter.region = MKCoordinateRegion(.world)
searchCompleter.region = region
searchCompleter.regionPriority = .required // iOS 18+ - strictly enforce region
}

func search(query: String) {
Expand Down
26 changes: 25 additions & 1 deletion OTPKit/Sources/OTPKit/Core/OTPConfiguration.swift
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,37 @@ public struct OTPConfiguration {
/// UI theme configuration
public let themeConfiguration: OTPThemeConfiguration

/// Geographic region for location search suggestions.
/// Search results will be strictly limited to this region.
public let searchRegion: MKCoordinateRegion

public init(
otpServerURL: URL,
enabledTransportModes: [TransportMode] = TransportMode.allCases,
themeConfiguration: OTPThemeConfiguration = OTPThemeConfiguration()
themeConfiguration: OTPThemeConfiguration = OTPThemeConfiguration(),
searchRegion: MKCoordinateRegion
) {
self.otpServerURL = otpServerURL
self.enabledTransportModes = enabledTransportModes
self.themeConfiguration = themeConfiguration
self.searchRegion = searchRegion
}
}

// MARK: - Environment Key for Search Region

private struct OTPSearchRegionKey: EnvironmentKey {
// Default to Seattle area for previews (matches existing PreviewHelpers coordinates)
static let defaultValue = MKCoordinateRegion(
center: CLLocationCoordinate2D(latitude: 47.6062, longitude: -122.3321),
latitudinalMeters: 50000,
longitudinalMeters: 50000
)
}

extension EnvironmentValues {
public var otpSearchRegion: MKCoordinateRegion {
get { self[OTPSearchRegionKey.self] }
set { self[OTPSearchRegionKey.self] = newValue }
}
}
13 changes: 11 additions & 2 deletions OTPKit/Sources/OTPKit/Core/Previews/PreviewHelpers.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,16 +11,25 @@ import SwiftUI

class PreviewHelpers {

/// Seattle region for previews (matches common test coordinates)
static let seattleRegion = MKCoordinateRegion(
center: CLLocationCoordinate2D(latitude: 47.6062, longitude: -122.3321),
latitudinalMeters: 50000,
longitudinalMeters: 50000
)

static func mockOTPConfiguration() -> OTPConfiguration {
return OTPConfiguration(
otpServerURL: URL(string: "https://example.com")!
otpServerURL: URL(string: "https://example.com")!,
searchRegion: seattleRegion
)
}

@MainActor
static func mockTripPlannerViewModel(canPlanTrip: Bool = true) -> TripPlannerViewModel {
let config = OTPConfiguration(
otpServerURL: URL(string: "https://example.com")!
otpServerURL: URL(string: "https://example.com")!,
searchRegion: seattleRegion
)
let mapView = MKMapView()
let mapProvider = MKMapViewAdapter(mapView: mapView)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,43 +11,54 @@ import MapKit
/// Integrates with MapKit to fetch and display search suggestions and details.
struct SearchSheetView: View {
@Environment(\.otpTheme) private var theme
@Environment(\.otpSearchRegion) private var searchRegion

let selectedMode: LocationMode
let onLocationSelected: OnLocationSelectedHandler

@State private var searchText = ""
@State private var searchResults: [Location] = []

// MapKit search
@State private var searchManager = SearchManager()
// MapKit search - initialized lazily with region from environment
@State private var searchManager: SearchManager?

var body: some View {
NavigationView {
VStack(spacing: 0) {
SearchBar(searchText: $searchText) { newValue in
guard let manager = searchManager else { return }
if newValue.isEmpty {
searchManager.clear()
manager.clear()
} else {
searchManager.search(query: newValue)
manager.search(query: newValue)
}
}
.padding(.horizontal, 16)
.padding(.top, 16)

// Search Results
if searchManager.isSearching {
buildSearchingView()
} else if searchManager.searchCompletions.isEmpty && !searchText.isEmpty {
buildNoSearchResultsView()
} else if !searchManager.searchCompletions.isEmpty {
buildSearchResultsView()
if let manager = searchManager {
if manager.isSearching {
buildSearchingView()
} else if manager.searchCompletions.isEmpty && !searchText.isEmpty {
buildNoSearchResultsView()
} else if !manager.searchCompletions.isEmpty {
buildSearchResultsView(manager: manager)
} else {
buildDefaultView()
}
} else {
buildDefaultView()
}
}
.navigationTitle(buildNavigationTitle())
.navigationBarTitleDisplayMode(.inline)
}
.onAppear {
if searchManager == nil {
searchManager = SearchManager(region: searchRegion)
}
}
}

@ViewBuilder
Expand Down Expand Up @@ -110,10 +121,10 @@ struct SearchSheetView: View {
}

@ViewBuilder
private func buildSearchResultsView() -> some View {
List(searchManager.searchCompletions, id: \.self) { completion in
private func buildSearchResultsView(manager: SearchManager) -> some View {
List(manager.searchCompletions, id: \.self) { completion in
SearchCompletionRow(completion: completion) {
searchManager.performDetailedSearch(for: completion) { location in
manager.performDetailedSearch(for: completion) { location in
onLocationSelected(location, selectedMode)
}
}
Expand Down
1 change: 1 addition & 0 deletions OTPKit/Sources/OTPKit/Presentation/TripPlanner.swift
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ public class TripPlanner {

return view
.environment(\.otpTheme, viewModel.config.themeConfiguration)
.environment(\.otpSearchRegion, viewModel.config.searchRegion)
.environmentObject(mapCoordinator)
.environmentObject(viewModel)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -169,11 +169,15 @@ private extension TripPlannerView {
}

#Preview {
let otpConfig = OTPConfiguration(otpServerURL: URL(string: "https://example.com")!)
let otpConfig = PreviewHelpers.mockOTPConfiguration()
let mapView = MKMapView()
let mapProvider = MKMapViewAdapter(mapView: mapView)
let mapCoordinator = MapCoordinator(mapProvider: mapProvider)
let viewModel = TripPlannerViewModel(config: otpConfig, apiService: PreviewHelpers.MockAPIService(), mapCoordinator: mapCoordinator)
let viewModel = TripPlannerViewModel(
config: otpConfig,
apiService: PreviewHelpers.MockAPIService(),
mapCoordinator: mapCoordinator
)

TripPlannerView(
viewModel: viewModel,
Expand Down
14 changes: 12 additions & 2 deletions OTPKit/Tests/Helpers/TestFixtures.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

import Foundation
import CoreLocation
import MapKit
@testable import OTPKit

/// Provides test fixtures for OTPKit tests
Expand Down Expand Up @@ -68,15 +69,24 @@ enum TestFixtures {
return Place(name: name, lon: -122, lat: 47, vertexType: "TRANSIT")
}

/// Seattle region for tests (matches common test coordinates)
static let seattleRegion = MKCoordinateRegion(
center: CLLocationCoordinate2D(latitude: 47.6062, longitude: -122.3321),
latitudinalMeters: 50000,
longitudinalMeters: 50000
)

static func makeOTPConfiguration(
serverURL: URL = URL(string: "https://otp.example.com")!,
enabledModes: [TransportMode] = [.transit, .walk],
theme: OTPThemeConfiguration? = nil
theme: OTPThemeConfiguration? = nil,
searchRegion: MKCoordinateRegion? = nil
) -> OTPConfiguration {
OTPConfiguration(
otpServerURL: serverURL,
enabledTransportModes: enabledModes,
themeConfiguration: theme ?? OTPThemeConfiguration()
themeConfiguration: theme ?? OTPThemeConfiguration(),
searchRegion: searchRegion ?? seattleRegion
)
}

Expand Down
5 changes: 1 addition & 4 deletions OTPKit/Tests/TripPlannerViewModelTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -35,10 +35,7 @@ struct TripPlannerViewModelTests {
defaults.removeObject(forKey: "OTPKit.TripOptions.maxWalkingDistance")
defaults.removeObject(forKey: "OTPKit.TripOptions.routePreference")

let config = OTPConfiguration(
otpServerURL: URL(string: "https://otp.example.com")!,
enabledTransportModes: enabledModes
)
let config = TestFixtures.makeOTPConfiguration(enabledModes: enabledModes)

let apiService = mockAPIService ?? TestFixtures.MockAPIService()
let mapProvider = MockMapProvider()
Expand Down
Loading
Loading