Skip to content

Commit

Permalink
Parse new availabilityInfo on trips and show in routing results (#331)
Browse files Browse the repository at this point in the history
Also allow disabling action buttons on routing results screen
  • Loading branch information
nighthawk authored Feb 8, 2024
1 parent ca4e8b6 commit dfacd10
Show file tree
Hide file tree
Showing 8 changed files with 159 additions and 58 deletions.
1 change: 1 addition & 0 deletions Sources/TripKit/model/API/RoutingAPIModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ extension TKAPI {
public var unsubscribeURL: URL?

@UnknownNil public var availability: TripAvailability?
public var availabilityInfo: String?
}

public enum TripAvailability: String, Codable, Hashable {
Expand Down
7 changes: 7 additions & 0 deletions Sources/TripKit/model/CoreData/Trip+Data.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,13 @@ import Foundation
extension Trip: DataAttachable {}

extension Trip {
/// Additional information when a trip is not available, e.g., due to missing the booking window or
/// it being cancelled. This is localised and meant to be user-facing.
public var availabilityInfo: String? {
get { decodePrimitive(String.self, key: "availabilityInfo") }
set { encodePrimitive(newValue, key: "availabilityInfo") }
}

public var bundleId: String? {
get { decodePrimitive(String.self, key: "bundleId") }
set { encodePrimitive(newValue, key: "bundleId") }
Expand Down
1 change: 1 addition & 0 deletions Sources/TripKit/server/parsing/TKRoutingParser.swift
Original file line number Diff line number Diff line change
Expand Up @@ -294,6 +294,7 @@ public final class TKRoutingParser {
case .canceled: trip.isCanceled = true
case .none: break
}
trip.update(\.availabilityInfo, value: apiTrip.availabilityInfo)

// updated trip isn't strictly speaking new, but we want to process it
// as a successful match.
Expand Down
20 changes: 17 additions & 3 deletions Sources/TripKitUI/cards/TKUICardAction.swift
Original file line number Diff line number Diff line change
Expand Up @@ -67,20 +67,23 @@ open class TKUICardAction<Card, Model>: ObservableObject where Card: TGCard {
/// - icon: Icon to display as the action. Should be a template image.
/// - style: Style for the button.
/// - priority: Priority of action to determine ordering in a list
/// - isEnabled: Set to `false` to show the button but disable it
/// - handler: Handler executed when user taps on the button. Parameters are the action itself, the owning card, the model instance, and the sender. Should return whether the button should be refreshed, by calling the relevant "actions factory" again.
public init(
title: String,
accessibilityLabel: String? = nil,
icon: UIImage,
style: TKUICardActionStyle = .normal,
priority: Int = 0,
isEnabled: Bool = true,
handler: @escaping @MainActor (TKUICardAction<Card, Model>, Card, Model, UIView) -> Bool
) {
self.content = .init(
title: title,
accessibilityLabel: accessibilityLabel,
icon: icon,
style: style
style: style,
isEnabled: isEnabled
)
self.handler = handler
self.priority = priority
Expand All @@ -96,21 +99,24 @@ open class TKUICardAction<Card, Model>: ObservableObject where Card: TGCard {
/// - icon: Provider of icon for the button. Should be a template image.
/// - style: Provider of style for the button.
/// - priority: Priority of action to determine ordering in a list
/// - isEnabled: Set to `false` to show the button but disable it
/// - handler: Handler executed when user taps on the button. Parameters are the owning card, the model instance, and the sender.
public convenience init(
title: @escaping () -> String,
accessibilityLabel: (() -> String)? = nil,
icon: @escaping () -> UIImage,
style: (() -> TKUICardActionStyle)? = nil,
priority: Int = 0,
isEnabled: Bool = true,
handler: @escaping @MainActor (Card, Model, UIView) -> Void
) {
self.init(
title: title(),
accessibilityLabel: accessibilityLabel?(),
icon: icon(),
style: style?() ?? .normal,
priority: priority
priority: priority,
isEnabled: isEnabled
) { action, card, model, view in
handler(card, model, view)
action.content = .init(
Expand Down Expand Up @@ -155,6 +161,11 @@ open class TKUICardAction<Card, Model>: ObservableObject where Card: TGCard {
set { content.isInProgress = newValue }
}

public var isEnabled: Bool {
get { content.isEnabled }
set { content.isEnabled = newValue }
}

/// Priority of the action to determine ordering in a list. Defaults to 0.
///
/// If multiple actions have the same priority, then `.bold` style is
Expand All @@ -171,12 +182,13 @@ open class TKUICardAction<Card, Model>: ObservableObject where Card: TGCard {
}

public struct TKUICardActionContent {
public init(title: String, accessibilityLabel: String? = nil, icon: UIImage, style: TKUICardActionStyle = .normal, isInProgress: Bool = false) {
public init(title: String, accessibilityLabel: String? = nil, icon: UIImage, style: TKUICardActionStyle = .normal, isInProgress: Bool = false, isEnabled: Bool = true) {
self.title = title
self.accessibilityLabel = accessibilityLabel
self.icon = icon
self.style = style
self.isInProgress = isInProgress
self.isEnabled = isEnabled
}

/// Title of the button
Expand All @@ -191,4 +203,6 @@ public struct TKUICardActionContent {
public var style: TKUICardActionStyle

public var isInProgress: Bool

public var isEnabled: Bool
}
65 changes: 28 additions & 37 deletions Sources/TripKitUI/cards/TKUIRoutingResultsCard.swift
Original file line number Diff line number Diff line change
Expand Up @@ -501,61 +501,52 @@ extension TKUIRoutingResultsCard {
}

extension TKUIRoutingResultsCard: UITableViewDelegate {

typealias FooterContent = TKUIResultsSectionFooterView.Content

private func footer(for sectionIndex: Int) -> FooterContent? {
// progress cell does not need a footer
let items = dataSource.sectionModels[sectionIndex].items
guard let firstTrip = items.first?.trip else {
return nil
}

let section = dataSource.sectionModels[sectionIndex]
var content = FooterContent(action: section.action)
if items.count == 1, let info = firstTrip.availabilityInfo {
content.cost = info
content.isWarning = true
} else if controller?.traitCollection.preferredContentSizeCategory.isAccessibilityCategory != true {
content.cost = TKUITripCell.Formatter.costString(costs: section.costs)
content.costAccessibility = TKUITripCell.Formatter.costAccessibilityLabel(costs: section.costs)
}
return content
}

public func tableView(_ tableView: UITableView, viewForFooterInSection section: Int) -> UIView? {
guard let footerView = tableView.dequeueReusableHeaderFooterView(withIdentifier: TKUIResultsSectionFooterView.reuseIdentifier) as? TKUIResultsSectionFooterView else {
assertionFailure()
return nil
}

// progress cell does not need a footer
guard dataSource.sectionModels[section].items.first?.trip != nil else {
return nil
}

let section = dataSource.sectionModels[section]
if controller?.traitCollection.preferredContentSizeCategory.isAccessibilityCategory == true {
footerView.cost = nil
footerView.costLabel.accessibilityLabel = nil
} else {
footerView.cost = TKUITripCell.Formatter.costString(costs: section.costs)
footerView.costLabel.accessibilityLabel = TKUITripCell.Formatter.costAccessibilityLabel(costs: section.costs)
}

if let buttonContent = section.action {
footerView.button.isHidden = false
footerView.button.setTitle(buttonContent.title, for: .normal)
footerView.button.rx.tap
.subscribe(onNext: { [unowned tappedSectionButton] in
tappedSectionButton.onNext(buttonContent.payload)
})
.disposed(by: footerView.disposeBag)

} else {
footerView.button.isHidden = true
guard let content = footer(for: section) else { return nil }
footerView.configure(content) { [unowned tappedSectionButton] action in
tappedSectionButton.onNext(action.payload)
}

return footerView
}

public func tableView(_ tableView: UITableView, heightForFooterInSection section: Int) -> CGFloat {
return getFooterHeight(from: section)
let content = footer(for: section)
return TKUIResultsSectionFooterView.height(for: content, maxWidth: tableView.frame.width)
}

public func tableView(_ tableView: UITableView, estimatedHeightForFooterInSection section: Int) -> CGFloat {
return getFooterHeight(from: section)
let content = footer(for: section)
return TKUIResultsSectionFooterView.height(for: content, maxWidth: tableView.frame.width)
}

private func getFooterHeight(from section: Int) -> CGFloat {
if dataSource.sectionModels[section].items.first?.trip == nil {
return .leastNonzeroMagnitude
} else {
let sizingFooter = TKUIResultsSectionFooterView.forSizing
let size = sizingFooter.systemLayoutSizeFitting(UIView.layoutFittingCompressedSize)
return size.height
}
}

public func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
let section = dataSource.sectionModels[section]
guard let content = section.badge?.footerContent else {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -135,15 +135,16 @@ extension TKUIRoutingResultsViewModel {
let action: SectionAction?
if let primaryAction = TKUIRoutingResultsCard.config.tripGroupActionFactory?(group) {
show = items
action = (
action = .init(
title: primaryAction.title,
accessibilityLabel: primaryAction.accessibilityLabel,
payload: .trigger(primaryAction, group)
payload: .trigger(primaryAction, group),
isEnabled: primaryAction.isEnabled
)

} else if items.count > 2, expand == group {
show = items
action = (title: Loc.Less, accessibilityLabel: nil, payload: .collapse)
action = .init(title: Loc.Less, payload: .collapse)

} else if items.count > 2 {
let good = items
Expand All @@ -154,7 +155,7 @@ extension TKUIRoutingResultsViewModel {
} else {
show = Array(good.prefix(2))
}
action = (title: Loc.More, accessibilityLabel: nil, payload: .expand(group))
action = .init(title: Loc.More, payload: .expand(group))

} else {
show = items
Expand Down Expand Up @@ -246,7 +247,12 @@ extension TKUIRoutingResultsViewModel {
case trigger(TKUIRoutingResultsCard.TripGroupAction, TripGroup)
}

typealias SectionAction = (title: String, accessibilityLabel: String?, payload: ActionPayload)
struct SectionAction {
var title: String
var accessibilityLabel: String? = nil
var payload: ActionPayload
var isEnabled: Bool = true
}

/// A section on the results screen, which consists of various sorted items
struct Section {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -113,11 +113,12 @@ class TKUIResultsAccessoryView: UIView {

override func layoutSubviews() {
// We switch dynamically to vertical layout if the natural size doesn't
// fit horizontally OR if the time button is taller than wide.
// fit horizontally OR if the either button is taller than wide.
let timeSize = timeButton.intrinsicContentSize
let transportSize = transportButton.intrinsicContentSize
let fits = timeSize.width + transportSize.width + 32 < frame.width
&& timeSize.height < timeSize.width * 1.1
&& transportSize.height < transportSize.width * 1.1
stackView.axis = fits ? .horizontal : .vertical

super.layoutSubviews()
Expand Down
Loading

0 comments on commit dfacd10

Please sign in to comment.