Skip to content
Open
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
6 changes: 4 additions & 2 deletions OBAKit/Orchestration/AppConfig.swift
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,8 @@ public class AppConfig: CoreAppConfig {
locationService: LocationService,
bundledRegionsFilePath: String,
regionsAPIPath: String?,
dataLoader: URLDataLoader
dataLoader: URLDataLoader,
defaultArrivalDepartureFilter: ArrivalDepartureFilter = .all
) {
self.analytics = analytics
super.init(
Expand All @@ -73,7 +74,8 @@ public class AppConfig: CoreAppConfig {
locationService: locationService,
bundledRegionsFilePath: bundledRegionsFilePath,
regionsAPIPath: regionsAPIPath,
dataLoader: dataLoader
dataLoader: dataLoader,
defaultArrivalDepartureFilter: defaultArrivalDepartureFilter
)
}
}
50 changes: 50 additions & 0 deletions OBAKit/Settings/SettingsViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ class SettingsViewController: FormViewController {

form
+++ mapSection
+++ arrivalDisplaySection
+++ alertsSection
+++ accessibilitySection
+++ surveySection
Expand Down Expand Up @@ -114,8 +115,57 @@ class SettingsViewController: FormViewController {
} else {
application.userDefaults.set(false, forKey: RegionsService.alwaysRefreshRegionsOnLaunchUserDefaultsKey)
}

if let selectedFilterTitle = values[arrivalFilterTag] as? String {
let filter = Self.arrivalFilterFromDisplayTitle(selectedFilterTitle)
application.userDefaults.set(filter.rawValue, forKey: CoreAppConfig.arrivalDepartureFilterUserDefaultsKey)
}
}

private static func arrivalFilterFromDisplayTitle(_ title: String) -> ArrivalDepartureFilter {
for filter in ArrivalDepartureFilter.allCases where arrivalFilterDisplayTitle(for: filter) == title {
return filter
}
return .all
}

// MARK: - Arrival Display Section

private let arrivalFilterTag = "arrivalDepartureFilter"

private func currentArrivalFilterDisplayTitle() -> String {
let saved = application.userDefaults.string(forKey: CoreAppConfig.arrivalDepartureFilterUserDefaultsKey)
let filter = ArrivalDepartureFilter(rawValue: saved ?? "") ?? .all
return Self.arrivalFilterDisplayTitle(for: filter)
}

private static func arrivalFilterDisplayTitle(for filter: ArrivalDepartureFilter) -> String {
switch filter {
case .all:
return OBALoc("settings_controller.arrival_filter.all", value: "All Departures", comment: "Settings option to show all departures")
case .estimatedOnly:
return OBALoc("settings_controller.arrival_filter.real_time", value: "Real-Time Only", comment: "Settings option to show only real-time departures")
case .scheduledOnly:
return OBALoc("settings_controller.arrival_filter.scheduled", value: "Scheduled Only", comment: "Settings option to show only scheduled departures")
}
}

private lazy var arrivalDisplaySection: Section = {
let section = Section(
OBALoc("settings_controller.arrival_display_section.title", value: "Arrival Display", comment: "Settings section title for controlling which arrivals/departures are shown")
)

section <<< AlertRow<String> {
$0.tag = arrivalFilterTag
$0.title = OBALoc("settings_controller.arrival_filter.title", value: "Show Departures", comment: "Title for the departure filter setting row")
$0.selectorTitle = OBALoc("settings_controller.arrival_filter.selector_title", value: "Show Departures", comment: "Title for the departure filter selection alert")
$0.options = ArrivalDepartureFilter.allCases.map { Self.arrivalFilterDisplayTitle(for: $0) }
$0.value = self.currentArrivalFilterDisplayTitle()
}

return section
}()

// MARK: - Map Section

private let mapSectionShowsScale = "mapSectionShowsScale"
Expand Down
48 changes: 36 additions & 12 deletions OBAKit/Stops/StopViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import SafariServices
/// arrivals and departures at this stop, along with the ability to create push
/// notification 'alarms' and bookmarks, view information about the location of a
/// particular vehicle, and report problems with a trip.
public class StopViewController: UIViewController,
public class StopViewController: UIViewController, // swiftlint:disable:this type_body_length
AlarmBuilderDelegate,
AgencyAlertListViewConverters,
AppContext,
Expand Down Expand Up @@ -370,7 +370,7 @@ public class StopViewController: UIViewController,
}

fileprivate func pulldownMenu() -> UIMenu {
return UIMenu(children: [fileMenu(), locationMenu(), sortMenu(), helpMenu()])
return UIMenu(children: [fileMenu(), locationMenu(), sortMenu(), arrivalDepartureFilterMenu(), helpMenu()])
}

func filterMenu() -> UIMenu {
Expand Down Expand Up @@ -499,6 +499,36 @@ public class StopViewController: UIViewController,
return sortMenu
}

private var activeArrivalDepartureFilter: ArrivalDepartureFilter {
if let saved = application.userDefaults.string(forKey: CoreAppConfig.arrivalDepartureFilterUserDefaultsKey),
let filter = ArrivalDepartureFilter(rawValue: saved) {
return filter
}
return .all
}

fileprivate func arrivalDepartureFilterMenu() -> UIMenu {
let currentFilter = activeArrivalDepartureFilter
let filters: [(ArrivalDepartureFilter, String)] = [
(.all, OBALoc("stop_controller.arrival_filter.all_departures", value: "All Departures", comment: "Filter option to show all departures regardless of real-time data availability")),
(.estimatedOnly, OBALoc("stop_controller.arrival_filter.estimated_only", value: "Real-Time Only", comment: "Filter option to show only departures with real-time estimated data")),
(.scheduledOnly, OBALoc("stop_controller.arrival_filter.scheduled_only", value: "Scheduled Only", comment: "Filter option to show only departures with scheduled data and no real-time information"))
]

let actions = filters.map { filter, title -> UIAction in
let action = UIAction(title: title) { [weak self] _ in
guard let self else { return }
self.application.userDefaults.set(filter.rawValue, forKey: CoreAppConfig.arrivalDepartureFilterUserDefaultsKey)
self.dataDidReload()
}
if filter == currentFilter { action.image = UIImage(systemName: "checkmark") }
return action
}

let menuTitle = OBALoc("stop_controller.arrival_filter.menu_title", value: "Departure Type", comment: "Title for the menu that filters departures by data type")
return UIMenu(title: menuTitle, image: UIImage(systemName: "antenna.radiowaves.left.and.right"), children: actions)
}

fileprivate func helpMenu() -> UIMenu {
let reportButton = UIAction(title: OBALoc("stops_controller.report_problem", value: "Report a Problem", comment: "Button that launches the 'Report Problem' UI."), image: UIImage(systemName: "exclamationmark.bubble")) { [unowned self] _ in
self.showReportProblem()
Expand Down Expand Up @@ -880,15 +910,9 @@ public class StopViewController: UIViewController,
var sections: [OBAListViewSection] = []

if stopPreferences.sortType == .time {
let arrDeps: [ArrivalDeparture]
if isListFiltered {
arrDeps = stopArrivals.arrivalsAndDepartures
.filter(preferences: stopPreferences)
.filteringTerminalDuplicates()
} else {
arrDeps = stopArrivals.arrivalsAndDepartures
.filteringTerminalDuplicates()
}
var arrDeps = stopArrivals.arrivalsAndDepartures
if isListFiltered { arrDeps = arrDeps.filter(preferences: stopPreferences) }
arrDeps = arrDeps.filter(by: activeArrivalDepartureFilter).filteringTerminalDuplicates()

let pastDeps = arrDeps.filter { $0.arrivalDepartureMinutes < 0 }
let upcomingDeps = arrDeps.filter { $0.arrivalDepartureMinutes >= 0 }
Expand All @@ -901,6 +925,7 @@ public class StopViewController: UIViewController,

} else {
let groups = stopArrivals.arrivalsAndDepartures
.filter(by: activeArrivalDepartureFilter)
.group(preferences: stopPreferences, filter: isListFiltered)
.localizedStandardCompare()

Expand All @@ -916,7 +941,6 @@ public class StopViewController: UIViewController,

// Always append upcoming section to keep the main route header visible
groupSections.append(sectionForGroup(groupRoute: group.route, arrDeps: upcomingDeps))

return groupSections
}
}
Expand Down
13 changes: 13 additions & 0 deletions OBAKitCore/Models/REST/ArrivalDeparture.swift
Original file line number Diff line number Diff line change
Expand Up @@ -471,6 +471,19 @@ public extension Sequence where Element == ArrivalDeparture {
filter { !preferences.isRouteIDHidden($0.routeID) }
}

/// Filters arrivals/departures based on real-time data availability.
/// - Parameter arrivalDepartureFilter: Controls whether to show all, only estimated, or only scheduled arrivals.
func filter(by arrivalDepartureFilter: ArrivalDepartureFilter) -> [ArrivalDeparture] {
switch arrivalDepartureFilter {
case .all:
return Array(self)
case .estimatedOnly:
return filter { $0.predicted }
case .scheduledOnly:
return filter { !$0.predicted }
}
}

/// Filters out `Route`s that are marked as hidden by `preferences`, and then groups the remaining `ArrivalDeparture`s by `Route`.
/// - Parameter preferences: The `StopPreferences` object that will be used to hide `ArrivalDeparture`s.
/// - Parameter filter: Whether the groups should also be filtered (i.e. have `Route`s hidden).
Expand Down
7 changes: 7 additions & 0 deletions OBAKitCore/Models/UserData/StopPreferences.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,13 @@ public enum StopSort: String, Codable {
case time, route
}

/// Controls which arrivals/departures are displayed based on real-time data availability.
public enum ArrivalDepartureFilter: String, Codable, CaseIterable {
case all
case estimatedOnly
case scheduledOnly
}

/// A model that represents the user's preferences for a particular `Stop`. These preferences are for things like sort order and hidden routes.
public struct StopPreferences: Codable {
public var sortType: StopSort
Expand Down
11 changes: 9 additions & 2 deletions OBAKitCore/Orchestration/CoreAppConfig.swift
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,10 @@ open class CoreAppConfig: NSObject {
public let locationService: LocationService
public let bundledRegionsFilePath: String
public let dataLoader: URLDataLoader
public let defaultArrivalDepartureFilter: ArrivalDepartureFilter

/// UserDefaults key for the user's global arrival/departure filter preference.
public static let arrivalDepartureFilterUserDefaultsKey = "CoreAppConfig.arrivalDepartureFilter"

/// Convenience initializer that pulls from the host application's main `Bundle`.
/// - Parameter appBundle: The application `Bundle` from which initialization properties will be extracted.
Expand All @@ -47,7 +51,8 @@ open class CoreAppConfig: NSObject {
locationService: LocationService(userDefaults: userDefaults, locationManager: CLLocationManager()),
bundledRegionsFilePath: bundledRegionsFilePath,
regionsAPIPath: appBundle.regionsServerAPIPath!,
dataLoader: URLSession.shared
dataLoader: URLSession.shared,
defaultArrivalDepartureFilter: .all
)
}

Expand All @@ -69,7 +74,8 @@ open class CoreAppConfig: NSObject {
locationService: LocationService,
bundledRegionsFilePath: String,
regionsAPIPath: String?,
dataLoader: URLDataLoader
dataLoader: URLDataLoader,
defaultArrivalDepartureFilter: ArrivalDepartureFilter = .all
) {
self.regionsBaseURL = regionsBaseURL
self.apiKey = apiKey
Expand All @@ -80,5 +86,6 @@ open class CoreAppConfig: NSObject {
self.bundledRegionsFilePath = bundledRegionsFilePath
self.regionsAPIPath = regionsAPIPath
self.dataLoader = dataLoader
self.defaultArrivalDepartureFilter = defaultArrivalDepartureFilter
}
}
Loading
Loading