Skip to content

Commit af972b5

Browse files
committed
Control insertion order of custom modes with after: String?
1 parent 17fcab0 commit af972b5

2 files changed

Lines changed: 84 additions & 17 deletions

File tree

Sources/TripKitUI/cards/TKUIRoutingResultsCard+Configuration.swift

Lines changed: 53 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -42,12 +42,18 @@ public extension TKUIRoutingResultsCard {
4242
public let identifier: String
4343
public let title: String
4444
public let subtitle: String?
45+
/// Insert this custom mode after the mode with the matching identifier.
46+
///
47+
/// If `nil`, the mode is inserted at the beginning of the picker. If the
48+
/// identifier cannot be found, the mode is appended at the end.
49+
public let after: String?
4550
public let icon: TKImage
4651

47-
public init(identifier: String, title: String, subtitle: String? = nil, icon: TKImage) {
52+
public init(identifier: String, title: String, subtitle: String? = nil, icon: TKImage, after: String? = nil) {
4853
self.identifier = identifier
4954
self.title = title
5055
self.subtitle = subtitle
56+
self.after = after
5157
self.icon = icon
5258
}
5359

@@ -71,6 +77,11 @@ public extension TKUIRoutingResultsCard {
7177
public var limitToModes: Set<String>? = nil
7278

7379
/// Additional routing modes to inject into the runtime mode picker.
80+
///
81+
/// Custom modes with `after == nil` are inserted at the beginning in the
82+
/// order they are listed. Custom modes with `after` set are inserted after
83+
/// the matching mode identifier when present, or appended if that anchor
84+
/// does not exist.
7485
public var customModes: [CustomMode] = []
7586

7687
/// Adjust the grouped backend routing requests derived from the current
@@ -190,14 +201,47 @@ extension TKUIRoutingResultsCard.Configuration {
190201
}
191202

192203
func routingModes(in regions: [TKRegion]) -> [TKRegion.RoutingMode] {
193-
let regionModes = TKRegionManager.sortedModes(in: regions)
194-
guard !customModes.isEmpty else { return regionModes }
195-
196-
var seen = Set(regionModes.map(\.identifier))
197-
let injected = customModes
198-
.map(\.routingMode)
199-
.filter { seen.insert($0.identifier).inserted }
200-
return regionModes + injected
204+
mergeCustomModes(into: TKRegionManager.sortedModes(in: regions))
205+
}
206+
207+
func mergeCustomModes(into routingModes: [TKRegion.RoutingMode]) -> [TKRegion.RoutingMode] {
208+
var routingModes = routingModes
209+
guard !customModes.isEmpty else { return routingModes }
210+
211+
var seen = Set(routingModes.map(\.identifier))
212+
let injectedModes = customModes.filter { seen.insert($0.identifier).inserted }
213+
214+
var insertionIndexAtStart = 0
215+
for mode in injectedModes where mode.after == nil {
216+
routingModes.insert(mode.routingMode, at: insertionIndexAtStart)
217+
insertionIndexAtStart += 1
218+
}
219+
220+
var pendingAnchoredModes = injectedModes.filter { $0.after != nil }
221+
while !pendingAnchoredModes.isEmpty {
222+
var nextPending: [CustomMode] = []
223+
var insertedAny = false
224+
225+
for mode in pendingAnchoredModes {
226+
guard let after = mode.after else { continue }
227+
228+
if let anchorIndex = routingModes.lastIndex(where: { $0.identifier == after }) {
229+
routingModes.insert(mode.routingMode, at: routingModes.index(after: anchorIndex))
230+
insertedAny = true
231+
} else {
232+
nextPending.append(mode)
233+
}
234+
}
235+
236+
if !insertedAny {
237+
routingModes.append(contentsOf: nextPending.map(\.routingMode))
238+
break
239+
}
240+
241+
pendingAnchoredModes = nextPending
242+
}
243+
244+
return routingModes
201245
}
202246

203247
func routingModeIdentifiers(for selectedModeIdentifiers: Set<String>) -> Set<String> {

Tests/TripKitUITests/TKUIRoutingResultsViewModelTest.swift

Lines changed: 31 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -116,23 +116,46 @@ final class TKUIRoutingResultsViewModelTest: XCTestCase {
116116
.init(identifier: "custom_park_ride", title: "Park & Ride", subtitle: "Drive + public transport", icon: .badgeHeart)
117117
]
118118

119-
let region = TKRegion(
120-
forTestingWithCode: "test",
121-
modes: [
119+
let available = TKUIRoutingResultsCard.config.mergeCustomModes(
120+
into: [
121+
.buildForTesting(TKTransportMode.publicTransport.modeIdentifier),
122+
.buildForTesting(TKTransportMode.car.modeIdentifier)
123+
]
124+
)
125+
126+
XCTAssertEqual(
127+
available.map(\.identifier),
128+
[
129+
"custom_park_ride",
122130
TKTransportMode.publicTransport.modeIdentifier,
123131
TKTransportMode.car.modeIdentifier
124-
],
125-
cities: []
132+
]
126133
)
134+
}
135+
136+
func testCustomModeCanBeInsertedAfterExistingMode() {
137+
TKUIRoutingResultsCard.config.customModes = [
138+
.init(
139+
identifier: "custom_park_ride",
140+
title: "Park & Ride",
141+
icon: .badgeHeart,
142+
after: TKTransportMode.publicTransport.modeIdentifier
143+
)
144+
]
127145

128-
let available = TKUIRoutingResultsCard.config.routingModes(in: [region])
146+
let available = TKUIRoutingResultsCard.config.mergeCustomModes(
147+
into: [
148+
.buildForTesting(TKTransportMode.publicTransport.modeIdentifier),
149+
.buildForTesting(TKTransportMode.car.modeIdentifier)
150+
]
151+
)
129152

130153
XCTAssertEqual(
131154
available.map(\.identifier),
132155
[
133156
TKTransportMode.publicTransport.modeIdentifier,
134-
TKTransportMode.car.modeIdentifier,
135-
"custom_park_ride"
157+
"custom_park_ride",
158+
TKTransportMode.car.modeIdentifier
136159
]
137160
)
138161
}

0 commit comments

Comments
 (0)