Skip to content

Commit

Permalink
add trip planner tab view and notification
Browse files Browse the repository at this point in the history
  • Loading branch information
hilmyveradin committed Sep 14, 2024
1 parent 7a22390 commit 935900d
Show file tree
Hide file tree
Showing 10 changed files with 195 additions and 24 deletions.
83 changes: 65 additions & 18 deletions OBAKit/ClassicUI/ClassicApplicationRootController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,40 +16,87 @@ public class ClassicApplicationRootController: UITabBarController {
case map = 0
case recentStops
case bookmarks
case tripPlanner
case more
}

private let application: Application

@objc public init(application: Application) {
self.application = application
super.init(nibName: nil, bundle: nil)
self.application.viewRouter.rootController = self
setupControllers()
observeTripPlannerServiceChanges()
}

required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}

self.mapController = MapViewController(application: application)
self.recentStopsController = RecentStopsViewController(application: application)
self.bookmarksController = BookmarksViewController(application: application)
self.moreController = MoreViewController(application: application)
deinit {
NotificationCenter.default.removeObserver(self)
}

super.init(nibName: nil, bundle: nil)
private lazy var mapController: MapViewController = {
MapViewController(application: application)
}()

self.application.viewRouter.rootController = self
private lazy var recentStopsController: RecentStopsViewController = {
RecentStopsViewController(application: application)
}()

private lazy var bookmarksController: BookmarksViewController = {
BookmarksViewController(application: application)
}()

private lazy var moreController: MoreViewController = {
MoreViewController(application: application)
}()

let mapNav = application.viewRouter.buildNavigation(controller: self.mapController, prefersLargeTitles: false)
let recentStopsNav = application.viewRouter.buildNavigation(controller: self.recentStopsController)
let bookmarksNav = application.viewRouter.buildNavigation(controller: self.bookmarksController)
let moreNav = application.viewRouter.buildNavigation(controller: self.moreController)
private var currentViewControllers: [UIViewController] {
let mapNav = application.viewRouter.buildNavigation(controller: mapController, prefersLargeTitles: false)
let recentStopsNav = application.viewRouter.buildNavigation(controller: recentStopsController)
let bookmarksNav = application.viewRouter.buildNavigation(controller: bookmarksController)
let moreNav = application.viewRouter.buildNavigation(controller: moreController)

viewControllers = [mapNav, recentStopsNav, bookmarksNav, moreNav]
var controllers = [mapNav, recentStopsNav, bookmarksNav, moreNav]

selectedIndex = application.userDataStore.lastSelectedView.rawValue
if let tripPlannerService = application.tripPlannerService {
let tripPlannerController = TripPlannerHostingController(tripPlannerService: tripPlannerService)
let tripPlannerNav = application.viewRouter.buildNavigation(controller: tripPlannerController)
controllers.insert(tripPlannerNav, at: 3) // Insert before 'more'
}

return controllers
}

let mapController: MapViewController
let recentStopsController: RecentStopsViewController
let bookmarksController: BookmarksViewController
let moreController: MoreViewController
private func setupControllers() {
updateViewControllers()
}

required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
private func observeTripPlannerServiceChanges() {
NotificationCenter.default.addObserver(
self,
selector: #selector(updateViewControllers),
name: .tripPlannerServiceDidChange,
object: application
)
}

@objc private func updateViewControllers() {
let newViewControllers = currentViewControllers

if newViewControllers != viewControllers {
viewControllers = newViewControllers

// Ensure the selected index is still valid
if selectedIndex >= newViewControllers.count {
selectedIndex = 0
}

application.userDataStore.lastSelectedView = SelectedTab(rawValue: selectedIndex) ?? .map
}
}

public override func tabBar(_ tabBar: UITabBar, didSelect item: UITabBarItem) {
Expand Down
3 changes: 1 addition & 2 deletions OBAKit/Mapping/MapViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -632,8 +632,7 @@ class MapViewController: UIViewController,
promptUserOnRegionMismatch = false
if
let regionMismatchBulletin = RegionMismatchBulletin(application: application),
let uiApp = application.delegate?.uiApplication
{
let uiApp = application.delegate?.uiApplication {
self.regionMismatchBulletin = regionMismatchBulletin
self.regionMismatchBulletin?.show(in: uiApp)
}
Expand Down
29 changes: 29 additions & 0 deletions OBAKit/Orchestration/Application.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import OBAKitCore
import SafariServices
import MapKit
import SwiftUI
import OTPKit
#if canImport(Stripe)
import StripeApplePay
#endif
Expand Down Expand Up @@ -89,6 +90,8 @@ public class Application: CoreApplication, PushServiceDelegate {

lazy var searchManager = SearchManager(application: self)

public private(set) var tripPlannerService: TripPlannerService?

@objc lazy var userActivityBuilder = UserActivityBuilder(application: self)

/// Handles all deep-linking into the app.
Expand Down Expand Up @@ -147,6 +150,7 @@ public class Application: CoreApplication, PushServiceDelegate {
super.init(config: config)

configureAppearanceProxies()
refreshServices()
}

// MARK: - Onboarding/Data Migration
Expand Down Expand Up @@ -541,6 +545,31 @@ public class Application: CoreApplication, PushServiceDelegate {
}
}

public override func regionsService(_ service: RegionsService, willUpdateToRegion region: Region) {
refreshServices()
}

func refreshServices() {
/// Recreates the `restAPIService` from the current region. This is
/// called when the app launches and when the current region changes.
guard let region = regionsService.currentRegion, let url = region.openTripPlannerURL else {
tripPlannerService = nil
NotificationCenter.default.post(name: .tripPlannerServiceDidChange, object: self)
return
}

tripPlannerService = TripPlannerService(
apiClient: RestAPI(baseURL: url),
locationManager: CLLocationManager(),
searchCompleter: MKLocalSearchCompleter()
)

tripPlannerService?.changeMapCamera(to: region.coordinate)

print("trip planner service: ", tripPlannerService)
NotificationCenter.default.post(name: .tripPlannerServiceDidChange, object: self)
}

// MARK: - Analytics

@objc public private(set) var analytics: Analytics?
Expand Down
2 changes: 1 addition & 1 deletion OBAKit/Stops/NearbyStopsViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,7 @@ class NearbyStopsViewController: UIViewController,
directions[stop.direction] = list
}

let tapHandler = { [unowned self] (vm: StopViewModel) -> Void in
let tapHandler = { [unowned self] (vm: StopViewModel) in
self.application.viewRouter.navigateTo(stopID: vm.stopID, from: self)
}

Expand Down
10 changes: 10 additions & 0 deletions OBAKit/Theme/Icons.swift
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,16 @@ class Icons: NSObject {
configureForTabIcon(systemImage(named: "bookmark.fill"))
}

/// The Map tab icon, for apps using a tab bar UI metaphor.
public class var tripPlannerTabIcon: UIImage {
configureForTabIcon(systemImage(named: "bus"))
}

/// The Map tab selected icon, for apps using a tab bar UI metaphor.
public class var tripPlannerSelectedTabIcon: UIImage {
configureForTabIcon(systemImage(named: "bus.fill"))
}

/// A More tab icon, for apps using a tab bar UI metaphor.
public class var moreTabIcon: UIImage {
configureForTabIcon(systemImage(named: "ellipsis.circle"))
Expand Down
12 changes: 12 additions & 0 deletions OBAKit/TripPlanner/TripPlanner+Extension.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
//
// TripPlanner+Extension.swift
// OBAKit
//
// Created by Hilmy Veradin on 14/09/24.
//

import Foundation

extension Notification.Name {
static let tripPlannerServiceDidChange = Notification.Name("tripPlannerServiceDidChange")
}
36 changes: 36 additions & 0 deletions OBAKit/TripPlanner/TripPlannerHostingController.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
//
// TripPlannerHostingView.swift
// OBAKit
//
// Created by Hilmy Veradin on 14/09/24.
//

import UIKit
import SwiftUI
import OBAKitCore
import OTPKit

class TripPlannerHostingController: UIHostingController<AnyView> {

init(tripPlannerService: TripPlannerService) {
let rootView = AnyView(
TripPlannerView()
.environment(tripPlannerService)
.environment(OriginDestinationSheetEnvironment())
)

super.init(rootView: rootView)

title = Strings.tripPlanner
tabBarItem.image = Icons.tripPlannerTabIcon
tabBarItem.selectedImage = Icons.tripPlannerSelectedTabIcon

let appearance = UITabBarAppearance()
appearance.configureWithDefaultBackground()
tabBarItem.scrollEdgeAppearance = appearance
}

@MainActor required dynamic init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
37 changes: 37 additions & 0 deletions OBAKit/TripPlanner/TripPlannerView.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
//
// TripPlannerView.swift
// OBAKit
//
// Created by Hilmy Veradin on 14/09/24.
//

import MapKit
import OTPKit
import SwiftUI

struct TripPlannerView: View {
@Environment(TripPlannerService.self) private var tripPlanner

var body: some View {
ZStack {
TripPlannerExtensionView {
Map(position: tripPlanner.currentCameraPositionBinding, interactionModes: .all) {
tripPlanner.generateMarkers()
tripPlanner.generateMapPolyline()
.stroke(.blue, lineWidth: 5)
}
.mapControls {
if !tripPlanner.isMapMarkingMode {
MapUserLocationButton()
MapPitchToggle()
}
}
}
}

}
}

#Preview {
TripPlannerView()
}
5 changes: 2 additions & 3 deletions OBAKitCore/Models/UserData/UserDataStore.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
import Foundation

@objc(OBASelectedTab) public enum SelectedTab: Int {
case map, recentStops, bookmarks, settings
case map, recentStops, bookmarks, tripPlanner, settings
}

/// `UserDataStore` is a repository for the user's data, such as bookmarks, and recent stops.
Expand Down Expand Up @@ -565,8 +565,7 @@ public class UserDefaultsStore: NSObject, UserDataStore, StopPreferencesStore {
private func upsert(bookmark: Bookmark) {
if
let existing = findBookmark(id: bookmark.id),
let index = bookmarks.firstIndex(of: existing)
{
let index = bookmarks.firstIndex(of: existing) {
bookmarks.remove(at: index)
bookmarks.insert(bookmark, at: index)
}
Expand Down
2 changes: 2 additions & 0 deletions OBAKitCore/Strings/Strings.swift
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,8 @@ public class Strings: NSObject {

public static let map = OBALoc("common.map", value: "Map", comment: "The noun for a map.")

public static let tripPlanner = OBALoc("common.trip_planner", value: "Trip Planner", comment: "The noun for a trip planner.")

public static let migrateData = OBALoc("common.migrate_data", value: "Migrate Data", comment: "A title or command for upgrading data from older versions of OBA.")

public static let migrateDataDescription = OBALoc("common.migrate_data_description", value: "Upgrade your recent stops and bookmarks to work with the latest version of the app. (Requires an internet connection.)", comment: "An explanation about what the Migrate Data feature does.")
Expand Down

0 comments on commit 935900d

Please sign in to comment.