Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/21655 trip booking flow #369

Merged
merged 9 commits into from
Sep 2, 2024
4 changes: 2 additions & 2 deletions Examples/CocoaPodsTest/Podfile.lock
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
PODS:
- GeoMonitor (0.1.2)
- Kingfisher (7.11.0)
- Kingfisher (7.12.0)
- RxCocoa (6.7.1):
- RxRelay (= 6.7.1)
- RxSwift (= 6.7.1)
Expand Down Expand Up @@ -43,7 +43,7 @@ EXTERNAL SOURCES:

SPEC CHECKSUMS:
GeoMonitor: 3af6b577d3f55007c3570c05ed20d0b97b18267e
Kingfisher: b9c985d864d43515f404f1ef4a8ce7d802ace3ac
Kingfisher: 53a10ea35051a436b5fb626ca2dd8f3144d755e9
RxCocoa: f5609cb4637587a7faa99c5d5787e3ad582b75a4
RxRelay: 4151ba01152436b08271e08410135e099880eae5
RxSwift: b9a93a26031785159e11abd40d1a55bcb8057e52
Expand Down
21 changes: 21 additions & 0 deletions Sources/TripKit/core/Loc.swift
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,11 @@ public class Loc : NSObject {
return NSLocalizedString("Done", tableName: "Shared", bundle: .tripKit, comment: "Done action")
}

@objc
public static var Select: String {
return NSLocalizedString("Select", tableName: "Shared", bundle: .tripKit, comment: "Select action")
}

@objc
public static var Next: String {
return NSLocalizedString("Next", tableName: "Shared", bundle: .tripKit, comment: "Next action")
Expand Down Expand Up @@ -133,6 +138,18 @@ public class Loc : NSObject {
return NSLocalizedString("Beyond furthest date", tableName: "TripKit", bundle: .tripKit, comment: "Indicator to show if the selected datetime is above the maximum datetime.")
}

public static var PickerDateTitle: String {
return NSLocalizedString("Date", tableName: "TripKit", bundle: .tripKit, comment: "Title for Date Picker")
}

public static var PickerTimeTitle: String {
return NSLocalizedString("Time", tableName: "TripKit", bundle: .tripKit, comment: "Title for Time Picker")
}

public static var SelectReturnDate: String {
return NSLocalizedString("Select Return Date", tableName: "TripKit", bundle: .tripKit, comment: "Header title for Date Picker")
}

public static func LateService(minutes: Int, service: String?) -> String {
if let service = service {
let format = NSLocalizedString("%1$@ late (%2$@ service)", tableName: "TripKit", bundle: .tripKit, comment: "Format for a service's real-time indicator for a service which is late, e.g., '1 min late (1:10 pm service). This means #1 is replaced with something like '1 min' and #2 is replaced with the original time, e.g., '1:10 pm').")
Expand Down Expand Up @@ -317,4 +334,8 @@ public class Loc : NSObject {
return NSLocalizedString("Tap to select your current location as the destination or origin.", tableName: "Shared", bundle: .tripKit, comment: "Accessibility hint for an current location item to provide selection capability.")
}

public static var SelectTime: String {
return NSLocalizedString("Select Time", tableName: "Shared", bundle: .tripKit, comment: "Title for Date Time selection.")
}

}
3 changes: 3 additions & 0 deletions Sources/TripKitUI/cards/TKUIHomeCard.swift
Original file line number Diff line number Diff line change
Expand Up @@ -302,6 +302,9 @@ extension TKUIHomeCard {
self.homeMapManager.zoom(to: city, animated: false)
}

case .enterSearchMode:
enterSearchMode()

case let .handleSelection(selection, component):
switch Self.config.selectionMode {
case .selectOnMap:
Expand Down
31 changes: 11 additions & 20 deletions Sources/TripKitUI/cards/TKUIRoutingResultsCard.swift
Original file line number Diff line number Diff line change
Expand Up @@ -446,6 +446,17 @@ extension TKUIRoutingResultsCard {
tripCell.accessoryType = .disclosureIndicator
#endif
tripCell.accessibilityTraits = .button

tripCell.actionButton.rx.tap
.subscribe(onNext: { [weak self] in
guard
let self,
let primaryAction = TKUITripOverviewCard.config.tripActionsFactory?(trip).first(where: { $0.priority >= TKUITripOverviewCard.DefaultActionPriority.book.rawValue }),
let view = self.controller?.view else { return }
let _ = primaryAction.handler(primaryAction, self, trip, view)
})
.disposed(by: tripCell.disposeBag)

return tripCell

case .customItem(let item):
Expand All @@ -464,26 +475,6 @@ extension TKUITripCell {
}
}

extension TKUITripCell.Model {

init(_ trip: Trip, allowFading: Bool, isArriveBefore: Bool? = nil) {
self.init(
departure: trip.departureTime,
arrival: trip.arrivalTime,
departureTimeZone: trip.departureTimeZone,
arrivalTimeZone: trip.arrivalTimeZone ?? trip.departureTimeZone,
focusOnDuration: !trip.departureTimeIsFixed,
isArriveBefore: isArriveBefore ?? trip.isArriveBefore,
showFaded: allowFading && trip.showFaded,
isCancelled: trip.isCanceled,
hideExactTimes: trip.hideExactTimes,
segments: trip.segments(with: .inSummary),
accessibilityLabel: trip.accessibilityLabel
)
}

}

extension TKMetricClassifier.Classification {

fileprivate var footerContent: (UIImage?, String, UIColor) {
Expand Down
162 changes: 49 additions & 113 deletions Sources/TripKitUI/cards/TKUITripModeByModeCard.swift
Original file line number Diff line number Diff line change
Expand Up @@ -82,16 +82,10 @@ public class TKUITripModeByModeCard: TGPageCard {
private let viewModel: TKUITripModeByModeViewModel

private let segmentCards: [SegmentCardsInfo]
private let headerSegmentIndices: [Int]

private var headerSegmentsView: TKUITripSegmentsView? {
return TKUITripModeByModeCard.findSubview(TKUITripSegmentsView.self, in: headerAccessoryView)
private var headerView: TKUITripModeByModeHeader? {
headerAccessoryView as? TKUITripModeByModeHeader
}
private var headerETALabel: UILabel? {
return TKUITripModeByModeCard.findSubview(UILabel.self, in: headerAccessoryView)
}

private let feedbackGenerator = UISelectionFeedbackGenerator()

private let tripMapManager: TKUITripMapManager

Expand Down Expand Up @@ -134,16 +128,17 @@ public class TKUITripModeByModeCard: TGPageCard {
return (previous.0 + [info], range.upperBound)
}.0

let headerSegments = trip.headerSegments
self.headerSegmentIndices = headerSegments.map { $0.index }

let initialPage = SegmentCardsInfo.cardIndex(ofSegmentAt: segment.index, mode: mode, in: segmentCards) ?? 0

let cards = segmentCards.flatMap { $0.cards.map { $0.0 } }
let actualInitialPage = min(initialPage, cards.count - 1)
super.init(cards: cards, initialPage: actualInitialPage, initialPosition: initialPosition)

self.headerAccessoryView = buildSegmentsView(segments: headerSegments, selecting: segment.index, trip: trip)
let headerView = TKUITripModeByModeHeader.newInstance()
headerView.configure(trip: trip, selecting: segment.index)
headerView.tapHandler = { [weak self] in self?.selectSegment(index: $0) }
headerView.actionHandler = { [weak self] in self?.triggerPrimaryAction() }
self.headerAccessoryView = headerView

// Little hack for starting with selecting the first page on the map, too
didMoveToPage(index: actualInitialPage)
Expand All @@ -167,6 +162,13 @@ public class TKUITripModeByModeCard: TGPageCard {
public override func didBuild(cardView: TGCardView?, headerView: TGHeaderView?) {
super.didBuild(cardView: cardView, headerView: headerView)

if let pageHeader = headerView, let modeByModeHeader = self.headerView {
pageHeader.cornerRadius = 0
let widthConstraint = modeByModeHeader.widthAnchor.constraint(equalTo: pageHeader.widthAnchor, constant: -16)
widthConstraint.priority = .required
widthConstraint.isActive = true
}

tripStartedHandler?(self, viewModel.trip)

viewModel.realTimeUpdate
Expand Down Expand Up @@ -201,9 +203,12 @@ public class TKUITripModeByModeCard: TGPageCard {
public override func didMoveToPage(index: Int) {
super.didMoveToPage(index: index)

guard let cardsInfo = SegmentCardsInfo.cardsInfo(ofCardAtIndex: index, in: segmentCards) else { assertionFailure(); return }
let selectedHeaderIndex = headerSegmentIndices.firstIndex { $0 >= cardsInfo.segmentIndex } // segment on card might not be in header
headerSegmentsView?.selectSegment(atIndex: selectedHeaderIndex ?? 0)
guard
let cardsInfo = SegmentCardsInfo.cardsInfo(ofCardAtIndex: index, in: segmentCards),
let headerView
else { assertionFailure(); return }
let selectedHeaderIndex = headerView.segmentIndices.firstIndex { $0 >= cardsInfo.segmentIndex } // segment on card might not be in header
headerView.segmentsView?.selectSegment(atIndex: selectedHeaderIndex ?? 0)

if let segment = tripMapManager.trip.segments.first(where: { Self.config.builder.cardIdentifier(for: $0) == cardsInfo.segmentIdentifier }) {
let offset = index - cardsInfo.cardsRange.lowerBound
Expand All @@ -212,6 +217,33 @@ public class TKUITripModeByModeCard: TGPageCard {
}
}

private func selectSegment(index: Int) {
guard let cardIndices = SegmentCardsInfo.cardIndices(ofSegmentAt: index, in: segmentCards)
else { assertionFailure(); return }

let target: Int
if cardIndices.contains(currentPageIndex), cardIndices.count > 1 {
target = (currentPageIndex == cardIndices.upperBound - 1)
? cardIndices.lowerBound
: currentPageIndex + 1
} else if !cardIndices.contains(currentPageIndex) {
target = cardIndices.lowerBound
} else {
return // Only a single card which is already selected
}
move(to: target)
}

private func triggerPrimaryAction() {
let trip = viewModel.trip
guard
let primaryAction = TKUITripOverviewCard.config.tripActionsFactory?(trip).first(where: { $0.priority >= TKUITripOverviewCard.DefaultActionPriority.book.rawValue }),
let view = self.controller?.view
else { return }

let _ = primaryAction.handler(primaryAction, self, trip, view)
}

public func offsetToReach(mode: TKUISegmentMode, in segment: TKSegment) -> Int? {
guard
let segmentInfo = segmentCards.first(where: { $0.segmentIndex == segment.index }),
Expand All @@ -234,99 +266,6 @@ public class TKUITripModeByModeCard: TGPageCard {

}

// MARK: - Segments view in header

fileprivate extension Trip {
var headerSegments: [TKSegment] {
return segments(with: .inSummary)
}
}

extension TKUITripModeByModeCard {

private static func findSubview<V: UIView>(_ type: V.Type, in header: UIView?) -> V? {
guard let stack = header as? UIStackView else { return nil }
return stack.arrangedSubviews.compactMap { $0 as? V }.first
}

private static func headerTimeText(for trip: Trip) -> String {
guard !trip.hideExactTimes else { return "" }
let departure = TKStyleManager.timeString(trip.departureTime, for: trip.departureTimeZone)
let arrival = TKStyleManager.timeString(trip.arrivalTime, for: trip.arrivalTimeZone ?? trip.departureTimeZone, relativeTo: trip.departureTimeZone)
return "\(departure) - \(arrival)"
}

private func buildSegmentsView(segments: [TKSegment], selecting index: Int, trip: Trip) -> UIView {
// the segments view
let selectedHeaderIndex = headerSegmentIndices.firstIndex { $0 >= index } // exact segment might not be available!

let segmentsView = TKUITripSegmentsView(frame: .zero)
segmentsView.darkTextColor = .tkLabelPrimary
segmentsView.lightTextColor = .tkLabelSecondary
segmentsView.configure(segments, allowInfoIcons: false)
segmentsView.selectSegment(atIndex: selectedHeaderIndex ?? 0)

let tapper = UITapGestureRecognizer(target: self, action: #selector(segmentTapped))
segmentsView.addGestureRecognizer(tapper)

feedbackGenerator.prepare()

// the label
let label = UILabel()
label.text = TKUITripModeByModeCard.headerTimeText(for: trip)
label.textColor = .tkLabelSecondary
label.font = TKStyleManager.customFont(forTextStyle: .footnote)
label.textAlignment = .center

// this is the placeholder view to create a space between the
// the header (what we are building here) and the botom of
// the view that is going to contain it.
let spacer = UIView()
spacer.backgroundColor = .clear
spacer.heightAnchor.constraint(equalToConstant: 8).isActive = true

// combine them
let stack = UIStackView(arrangedSubviews: [segmentsView, label, spacer])
stack.axis = .vertical
stack.distribution = .fill

if segments.count == 1 {
stack.accessibilityElements = []
}

return stack
}

@objc
private func segmentTapped(_ recognizer: UITapGestureRecognizer) {
guard let segmentsView = self.headerSegmentsView
else { assertionFailure(); return }

let x = recognizer.location(in: segmentsView).x
let headerIndex = segmentsView.segmentIndex(atX: x)

let segmentIndex = headerSegmentIndices[headerIndex]
guard let cardIndices = SegmentCardsInfo.cardIndices(ofSegmentAt: segmentIndex, in: segmentCards)
else { assertionFailure(); return }

let target: Int
if cardIndices.contains(currentPageIndex), cardIndices.count > 1 {
target = (currentPageIndex == cardIndices.upperBound - 1)
? cardIndices.lowerBound
: currentPageIndex + 1
} else if !cardIndices.contains(currentPageIndex) {
target = cardIndices.lowerBound
} else {
return // Only a single card which is already selected
}
move(to: target)

feedbackGenerator.selectionChanged()
feedbackGenerator.prepare()
}

}

// MARK: - Map interaction

extension TKUITripModeByModeCard {
Expand Down Expand Up @@ -368,11 +307,8 @@ extension TKUITripModeByModeCard {
let cardSegments = trip.segments(with: .inDetails)

if segmentsMatch(cardSegments) {
// Update segment view in header
headerSegmentsView?.configure(trip.headerSegments, allowInfoIcons: false)

// Update ETA in header
headerETALabel?.text = TKUITripModeByModeCard.headerTimeText(for: trip)
// Update the header
headerView?.update(trip: trip)

// Important to update the map, too, as template hash codes can change
// an the map uses those for selection handling
Expand Down
1 change: 1 addition & 0 deletions Sources/TripKitUI/cards/TKUITripOverviewCard.swift
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ class TKUITripsPageCard: TGPageCard {
public class TKUITripOverviewCard: TKUITableCard {

public enum DefaultActionPriority: Int {
case book = 20
case go = 15
case notify = 10
case alternatives = 5
Expand Down
6 changes: 6 additions & 0 deletions Sources/TripKitUI/view model/TKUIHomeViewModel+Next.swift
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@ extension TKUIHomeCard {
/// A custom handler that should be triggered, providing the home card view controller
case trigger((UIViewController) -> Void)

case enterSearchMode

case success
}
}
Expand All @@ -64,6 +66,8 @@ extension TKUIHomeViewModel {
/// refresh on the home card.
case handleAction(handler: (UIViewController) -> Single<Bool>)

case enterSearchMode

/// A custom handler that should be triggered, providing the home card view controller
case trigger((UIViewController) -> Void)
}
Expand Down Expand Up @@ -91,6 +95,8 @@ extension TKUIHomeViewModel {
case .hideSection(let identifier):
TKUIHomeCard.hideComponent(id: identifier)
return nil
case .enterSearchMode:
return .enterSearchMode
case .success:
return nil
}
Expand Down
Loading
Loading