diff --git a/OBAKit/ContextMenus/Previewable.swift b/OBAKit/ContextMenus/Previewable.swift new file mode 100644 index 000000000..017c4a2ab --- /dev/null +++ b/OBAKit/ContextMenus/Previewable.swift @@ -0,0 +1,13 @@ +// +// Previewable.swift +// OBAKit +// +// Created by Aaron Brethorst on 4/12/20. +// + +import UIKit + +protocol Previewable: NSObjectProtocol { + func enterPreviewMode() + func exitPreviewMode() +} diff --git a/OBAKit/Controls/ListKit/CollectionController.swift b/OBAKit/Controls/ListKit/CollectionController.swift index 7e365619c..ba42328ae 100644 --- a/OBAKit/Controls/ListKit/CollectionController.swift +++ b/OBAKit/Controls/ListKit/CollectionController.swift @@ -16,7 +16,7 @@ public enum TableCollectionStyle { } /// Meant to be used as a child view controller. It hosts a `UICollectionView` plus all of the logic for using `IGListKit`. -public class CollectionController: UIViewController { +public class CollectionController: UIViewController, UICollectionViewDelegate { private let application: Application public let listAdapter: ListAdapter public let style: TableCollectionStyle @@ -35,6 +35,7 @@ public class CollectionController: UIViewController { self.listAdapter.collectionView = collectionView self.listAdapter.dataSource = dataSource + self.listAdapter.collectionViewDelegate = self } required init?(coder aDecoder: NSCoder) { @@ -120,4 +121,31 @@ public class CollectionController: UIViewController { collectionView.contentInset = UIEdgeInsets(top: collectionView.contentInset.top, left: 0, bottom: 0, right: 0) collectionView.scrollIndicatorInsets = collectionView.contentInset } + + // MARK: - UICollectionViewDelegate + + @available(iOS 13.0, *) + public func collectionView(_ collectionView: UICollectionView, contextMenuConfigurationForItemAt indexPath: IndexPath, point: CGPoint) -> UIContextMenuConfiguration? { + guard let provider = listAdapter.sectionController(forSection: indexPath.section) as? ContextMenuProvider else { + return nil + } + + return provider.contextMenuConfiguration(forItemAt: indexPath) + } + + @available(iOS 13.0, *) + public func collectionView(_ collectionView: UICollectionView, willPerformPreviewActionForMenuWith configuration: UIContextMenuConfiguration, animator: UIContextMenuInteractionCommitAnimating) { + guard + let viewController = animator.previewViewController, + let parent = parent + else { return } + + animator.addCompletion { + if let previewable = viewController as? Previewable { + previewable.exitPreviewMode() + } + + self.application.viewRouter.navigate(to: viewController, from: parent, animated: false) + } + } } diff --git a/OBAKit/Controls/ListKit/OBAListSectionController.swift b/OBAKit/Controls/ListKit/OBAListSectionController.swift index c9c9db6ab..66c0b8b60 100644 --- a/OBAKit/Controls/ListKit/OBAListSectionController.swift +++ b/OBAKit/Controls/ListKit/OBAListSectionController.swift @@ -9,10 +9,21 @@ import Foundation import IGListKit import OBAKitCore +// MARK: - ContextMenuProvider + +@available(iOS 13.0, *) +protocol ContextMenuProvider { + func contextMenuConfiguration(forItemAt indexPath: IndexPath) -> UIContextMenuConfiguration? +} + +// MARK: - OBAListSectionControllerInitializer + protocol OBAListSectionControllerInitializer { init(formatters: Formatters, style: TableCollectionStyle, hasVisualEffectBackground: Bool) } +// MARK: - OBAListSectionController + /// An OBAKit-specific subclass of `ListSectionController` meant to be overriden instead of `ListSectionController`. /// /// Provides easy access to the application-wide `formatters` object, along with the current view controller's table collection style. diff --git a/OBAKit/Stops/StopArrivalListItem.swift b/OBAKit/Stops/StopArrivalListItem.swift index a8621cafc..2dc41bbf4 100644 --- a/OBAKit/Stops/StopArrivalListItem.swift +++ b/OBAKit/Stops/StopArrivalListItem.swift @@ -16,8 +16,13 @@ final class ArrivalDepartureSectionData: NSObject, ListDiffable { let arrivalDeparture: ArrivalDeparture let isAlarmAvailable: Bool let selected: VoidBlock + var onCreateAlarm: VoidBlock? var onShowOptions: VoidBlock? + var onAddBookmark: VoidBlock? + var onShareTrip: VoidBlock? + + var previewDestination: (() -> UIViewController?)? init(arrivalDeparture: ArrivalDeparture, isAlarmAvailable: Bool = false, selected: @escaping VoidBlock) { self.arrivalDeparture = arrivalDeparture @@ -37,7 +42,9 @@ final class ArrivalDepartureSectionData: NSObject, ListDiffable { // MARK: - Controller -final class StopArrivalSectionController: OBAListSectionController, SwipeCollectionViewCellDelegate { +final class StopArrivalSectionController: OBAListSectionController, + ContextMenuProvider, +SwipeCollectionViewCellDelegate { override func cellForItem(at index: Int) -> UICollectionViewCell { guard let object = sectionData else { fatalError() } @@ -84,6 +91,49 @@ final class StopArrivalSectionController: OBAListSectionController UIContextMenuConfiguration? { + let previewProvider = { [weak self] () -> UIViewController? in + guard + let self = self, + let sectionData = self.sectionData + else { return nil } + + return sectionData.previewDestination?() + } + + return UIContextMenuConfiguration(identifier: nil, previewProvider: previewProvider) { [weak self] _ in + guard + let self = self, + let sectionData = self.sectionData + else { return nil } + + var actions = [UIAction]() + + if sectionData.isAlarmAvailable { + let alarm = UIAction(title: Strings.addAlarm, image: Icons.addAlarm) { _ in + sectionData.onCreateAlarm?() + } + actions.append(alarm) + } + + let addBookmark = UIAction(title: Strings.addBookmark, image: Icons.bookmark) { _ in + sectionData.onAddBookmark?() + } + actions.append(addBookmark) + + let shareTrip = UIAction(title: Strings.shareTrip, image: UIImage(systemName: "square.and.arrow.up")) { _ in + sectionData.onShareTrip?() + } + actions.append(shareTrip) + + // Create and return a UIMenu with all of the actions as children + return UIMenu(title: "", children: actions) + } + } } // MARK: - View diff --git a/OBAKit/Stops/StopViewController.swift b/OBAKit/Stops/StopViewController.swift index f3a9567ed..0feb479a8 100644 --- a/OBAKit/Stops/StopViewController.swift +++ b/OBAKit/Stops/StopViewController.swift @@ -20,9 +20,9 @@ public class StopViewController: UIViewController, AlarmBuilderDelegate, AppContext, BookmarkEditorDelegate, + Idleable, ListAdapterDataSource, ModalDelegate, - Idleable, StopPreferencesDelegate { public let application: Application @@ -457,18 +457,33 @@ public class StopViewController: UIViewController, private func buildArrivalDepartureSectionData(arrivalDeparture: ArrivalDeparture) -> ArrivalDepartureSectionData { let alarmAvailable = canCreateAlarm(for: arrivalDeparture) - let data = ArrivalDepartureSectionData( - arrivalDeparture: arrivalDeparture, - isAlarmAvailable: alarmAvailable) { [weak self] in + let data = ArrivalDepartureSectionData(arrivalDeparture: arrivalDeparture, isAlarmAvailable: alarmAvailable) { [weak self] in guard let self = self else { return } self.application.viewRouter.navigateTo(arrivalDeparture: arrivalDeparture, from: self) } + data.previewDestination = { [weak self] in + guard let self = self else { return nil } + let controller = TripViewController(application: self.application, arrivalDeparture: arrivalDeparture) + controller.enterPreviewMode() + return controller + } + data.onCreateAlarm = { [weak self] in guard let self = self else { return } self.addAlarm(arrivalDeparture: arrivalDeparture) } + data.onAddBookmark = { [weak self] in + guard let self = self else { return } + self.addBookmark(arrivalDeparture: arrivalDeparture) + } + + data.onShareTrip = { [weak self] in + guard let self = self else { return } + self.shareTripStatus(arrivalDeparture: arrivalDeparture) + } + data.onShowOptions = { [weak self] in guard let self = self else { return } self.showMoreOptions(arrivalDeparture: arrivalDeparture) @@ -645,13 +660,13 @@ public class StopViewController: UIViewController, public func showMoreOptions(arrivalDeparture: ArrivalDeparture) { let actionSheet = UIAlertController(title: nil, message: nil, preferredStyle: .actionSheet) - actionSheet.addAction(title: OBALoc("stop_controller.add_bookmark", value: "Add Bookmark", comment: "Action sheet button title for adding a bookmark")) { [weak self] _ in + actionSheet.addAction(title: Strings.addBookmark) { [weak self] _ in guard let self = self else { return } self.addBookmark(arrivalDeparture: arrivalDeparture) } if application.features.deepLinking == .running { - actionSheet.addAction(title: OBALoc("stop_controller.share_trip", value: "Share Trip", comment: "Action sheet button title for sharing the status of your trip (i.e. location, arrival time, etc.)")) { [weak self] _ in + actionSheet.addAction(title: Strings.shareTrip) { [weak self] _ in guard let self = self else { return } self.shareTripStatus(arrivalDeparture: arrivalDeparture) } diff --git a/OBAKit/Trip/TripViewController.swift b/OBAKit/Trip/TripViewController.swift index fa2d0ce8e..306d8b49c 100644 --- a/OBAKit/Trip/TripViewController.swift +++ b/OBAKit/Trip/TripViewController.swift @@ -11,34 +11,16 @@ import FloatingPanel import OBAKitCore class TripViewController: UIViewController, + AppContext, FloatingPanelControllerDelegate, Idleable, MKMapViewDelegate, - AppContext { + Previewable { public let application: Application private let tripConvertible: TripConvertible - public var selectedStopTime: TripStopTime? { - didSet { - var animated = true - if isFirstStopTimeLoad { - animated = false - isFirstStopTimeLoad.toggle() - } - self.mapView.deselectAnnotation(oldValue, animated: animated) - - guard - oldValue != self.selectedStopTime, - let selectedStopTime = self.selectedStopTime - else { return } - - self.mapView.selectAnnotation(selectedStopTime, animated: animated) - } - } - private var isFirstStopTimeLoad = true - init(application: Application, tripConvertible: TripConvertible) { self.application = application self.tripConvertible = tripConvertible @@ -82,7 +64,9 @@ class TripViewController: UIViewController, loadData() - floatingPanel.addPanel(toParent: self) + if !isBeingPreviewed { + floatingPanel.addPanel(toParent: self) + } } override func viewWillAppear(_ animated: Bool) { @@ -111,6 +95,24 @@ class TripViewController: UIViewController, self.userActivity = activity } + // MARK: Previewable + + /// Set this to `true` before `viewDidLoad` to present the UI in a stripped-down 'preview mode' + /// suitable for display in a context menu. + var isBeingPreviewed = false + + func enterPreviewMode() { + isBeingPreviewed = true + } + + func exitPreviewMode() { + isBeingPreviewed = false + + if isViewLoaded, floatingPanel.parent == nil { + floatingPanel.addPanel(toParent: self) + } + } + // MARK: - Idle Timer public var idleTimerFailsafe: Timer? @@ -355,4 +357,25 @@ class TripViewController: UIViewController, default: return nil } } + + public var selectedStopTime: TripStopTime? { + didSet { + guard !isBeingPreviewed else { return } + + var animated = true + if isFirstStopTimeLoad { + animated = false + isFirstStopTimeLoad.toggle() + } + self.mapView.deselectAnnotation(oldValue, animated: animated) + + guard + oldValue != self.selectedStopTime, + let selectedStopTime = self.selectedStopTime + else { return } + + self.mapView.selectAnnotation(selectedStopTime, animated: animated) + } + } + private var isFirstStopTimeLoad = true } diff --git a/OBAKit/ViewRouting/Router.swift b/OBAKit/ViewRouting/Router.swift index 811d1b748..7b309c9c9 100644 --- a/OBAKit/ViewRouting/Router.swift +++ b/OBAKit/ViewRouting/Router.swift @@ -53,10 +53,11 @@ public class ViewRouter: NSObject, UINavigationControllerDelegate { /// - Parameters: /// - viewController: The 'to' view controller. /// - fromController: The 'from' view controller. - public func navigate(to viewController: UIViewController, from fromController: UIViewController) { + /// - animated: Is the transition animated or not. + public func navigate(to viewController: UIViewController, from fromController: UIViewController, animated: Bool = true) { assert(fromController.navigationController != nil) viewController.hidesBottomBarWhenPushed = true - fromController.navigationController?.pushViewController(viewController, animated: true) + fromController.navigationController?.pushViewController(viewController, animated: animated) } public func navigateTo(stop: Stop, from fromController: UIViewController, bookmark: Bookmark? = nil) {