Skip to content

Commit

Permalink
feat: put nearby transit in sheet
Browse files Browse the repository at this point in the history
  • Loading branch information
Brandon Rodriguez authored and Brandon Rodriguez committed Mar 28, 2024
1 parent 562c0a2 commit c318942
Show file tree
Hide file tree
Showing 6 changed files with 280 additions and 13 deletions.
16 changes: 16 additions & 0 deletions iosApp/iosApp.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,8 @@
9AF88E052B48913C00E08C7C /* Localizable.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = 9AF88E042B48913C00E08C7C /* Localizable.xcstrings */; };
A430D45FE0676C73075AB85B /* Pods_iosAppTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = ED1649D982654BA7A4D2F2DC /* Pods_iosAppTests.framework */; };
A55C5596CDC797ED68F79279 /* Pods_iosApp.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1C6BD892027AC258EE8F408D /* Pods_iosApp.framework */; };
ED3581662BB4706F005DDC34 /* PartialSheetModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = ED3581652BB4706F005DDC34 /* PartialSheetModifier.swift */; };
ED3581682BB470C4005DDC34 /* PartialSheetDetent.swift in Sources */ = {isa = PBXBuildFile; fileRef = ED3581672BB470C4005DDC34 /* PartialSheetDetent.swift */; };
/* End PBXBuildFile section */

/* Begin PBXContainerItemProxy section */
Expand Down Expand Up @@ -177,6 +179,8 @@
9ADB849F2BAD1B84006581CE /* DebouncerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DebouncerTests.swift; sourceTree = "<group>"; };
9AF88E042B48913C00E08C7C /* Localizable.xcstrings */ = {isa = PBXFileReference; lastKnownFileType = text.json.xcstrings; path = Localizable.xcstrings; sourceTree = "<group>"; };
ED1649D982654BA7A4D2F2DC /* Pods_iosAppTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_iosAppTests.framework; sourceTree = BUILT_PRODUCTS_DIR; };
ED3581652BB4706F005DDC34 /* PartialSheetModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PartialSheetModifier.swift; sourceTree = "<group>"; };
ED3581672BB470C4005DDC34 /* PartialSheetDetent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PartialSheetDetent.swift; sourceTree = "<group>"; };
F71252D2B68FF131F8E6BDE2 /* Pods-iosAppTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-iosAppTests.release.xcconfig"; path = "Target Support Files/Pods-iosAppTests/Pods-iosAppTests.release.xcconfig"; sourceTree = "<group>"; };
/* End PBXFileReference section */

Expand Down Expand Up @@ -429,6 +433,7 @@
9AC10BD82B80060E00EA4605 /* Utils */ = {
isa = PBXGroup;
children = (
EDB1F3DC2BB227160017D8F0 /* Backport */,
9AC10BD92B80067400EA4605 /* ColorHexExtension.swift */,
9AB44A122B911E6400E8FFB3 /* DateExtension.swift */,
9A3B09352B967CEC00691427 /* NonNilModifier.swift */,
Expand All @@ -450,6 +455,15 @@
path = Utils;
sourceTree = "<group>";
};
EDB1F3DC2BB227160017D8F0 /* Backport */ = {
isa = PBXGroup;
children = (
ED3581652BB4706F005DDC34 /* PartialSheetModifier.swift */,
ED3581672BB470C4005DDC34 /* PartialSheetDetent.swift */,
);
path = Backport;
sourceTree = "<group>";
};
/* End PBXGroup section */

/* Begin PBXNativeTarget section */
Expand Down Expand Up @@ -771,13 +785,15 @@
9A2005C92B97B65900F562E1 /* NearbyStopRoutePatternView.swift in Sources */,
9A4E8E592B7EC4B90066B936 /* RoutePill.swift in Sources */,
9A9E05F42B6D6DEA0086B437 /* SearchResultFetcher.swift in Sources */,
ED3581682BB470C4005DDC34 /* PartialSheetDetent.swift in Sources */,
9A3B09362B967CEC00691427 /* NonNilModifier.swift in Sources */,
2152FB042600AC8F00CF470E /* IOSApp.swift in Sources */,
8CD1F8CD2B7164C100F419D4 /* PredictionsFetcher.swift in Sources */,
9A887D572B683103006F5B80 /* SearchResultView.swift in Sources */,
8CEA10252BA4B179001C6EB9 /* AlertsFetcher.swift in Sources */,
6E35D4D02B72C7B700A2BF95 /* HomeMapView.swift in Sources */,
8CC1BB402B59D1F6005386FE /* LocationDataManager.swift in Sources */,
ED3581662BB4706F005DDC34 /* PartialSheetModifier.swift in Sources */,
8C7FA8732B5F36D6009B699D /* Backend.swift in Sources */,
9A2005C72B97B63300F562E1 /* NearbyStopView.swift in Sources */,
9AC4FDF12BACE216004479BF /* NearbyTransitPageView.swift in Sources */,
Expand Down
56 changes: 47 additions & 9 deletions iosApp/iosApp/ContentView.swift
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import CoreLocation
import shared
import SwiftPhoenixClient
import SwiftUI
Expand All @@ -17,6 +18,16 @@ struct ContentView: View {
@EnvironmentObject var searchResultFetcher: SearchResultFetcher
@EnvironmentObject var socketProvider: SocketProvider
@EnvironmentObject var viewportProvider: ViewportProvider
@State private var currentDetent: PartialSheetDetent = .medium
@State private var sheetHeight: CGFloat = .zero

private var sheetDetents: Set<PartialSheetDetent> {
if #available(iOS 16, *) {
[.small, .medium, .large]
} else {
[.medium, .large]
}
}

var body: some View {
NavigationView {
Expand All @@ -41,17 +52,25 @@ struct ContentView: View {
globalFetcher: globalFetcher,
nearbyFetcher: nearbyFetcher,
railRouteShapeFetcher: railRouteShapeFetcher,
viewportProvider: viewportProvider
)
Spacer()
NearbyTransitPageView(
currentLocation: locationDataManager.currentLocation?.coordinate,
nearbyFetcher: nearbyFetcher,
scheduleFetcher: scheduleFetcher,
predictionsFetcher: predictionsFetcher,
viewportProvider: viewportProvider,
alertsFetcher: alertsFetcher
sheetHeight: $sheetHeight
)
.ignoresSafeArea(edges: .bottom)
.sheet(isPresented: .constant(true)) {
/**
NavigationView is only necessary as a workaround for the sheet automatically expanding
https://www.hackingwithswift.com/forums/swiftui/bottom-sheet-resizing-bug/24155/24169
**/
NavigationView {
nearbyTransit
.navigationBarHidden(true)
}
.partialSheetDetents(
sheetDetents,
currentDetent: $currentDetent,
largestUndimmedDetent: .medium
)
}
}
}
.searchable(
Expand All @@ -68,6 +87,25 @@ struct ContentView: View {
}
}.task { alertsFetcher.run() }
}

private var nearbyTransit: some View {
GeometryReader { proxy in
NearbyTransitPageView(
currentLocation: locationDataManager.currentLocation?.coordinate,
nearbyFetcher: nearbyFetcher,
scheduleFetcher: scheduleFetcher,
predictionsFetcher: predictionsFetcher,
viewportProvider: viewportProvider,
alertsFetcher: alertsFetcher
)
.onChange(of: proxy.size.height) { newValue in
// Not actually restricted to iOS 16, this just behaves terribly on iOS 15
if #available(iOS 16, *) {
sheetHeight = newValue
}
}
}
}
}

struct ContentView_Previews: PreviewProvider {
Expand Down
6 changes: 5 additions & 1 deletion iosApp/iosApp/Pages/Map/HomeMapView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -30,18 +30,21 @@ struct HomeMapView: View {

@StateObject private var locationDataManager: LocationDataManager
@State var recenterButton: ViewAnnotation?
@Binding var sheetHeight: CGFloat

init(
globalFetcher: GlobalFetcher,
nearbyFetcher: NearbyFetcher,
railRouteShapeFetcher: RailRouteShapeFetcher,
locationDataManager: LocationDataManager = .init(distanceFilter: 1),
viewportProvider: ViewportProvider
viewportProvider: ViewportProvider,
sheetHeight: Binding<CGFloat>
) {
self.globalFetcher = globalFetcher
self.nearbyFetcher = nearbyFetcher
self.railRouteShapeFetcher = railRouteShapeFetcher
self.viewportProvider = viewportProvider
_sheetHeight = sheetHeight
_locationDataManager = StateObject(wrappedValue: locationDataManager)
}

Expand All @@ -68,6 +71,7 @@ struct HomeMapView: View {
// print(feature.feature.identifier)
true
}
.additionalSafeAreaInsets(.bottom, sheetHeight)
.accessibilityIdentifier("transitMap")
.onAppear { handleAppear(location: proxy.location) }
.onChange(of: globalFetcher.stops) { stops in handleGlobalStops(proxy.map, stops) }
Expand Down
50 changes: 50 additions & 0 deletions iosApp/iosApp/Utils/Backport/PartialSheetDetent.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
//
// PartialSheetDetent.swift
// iosApp
//
// Created by Brandon Rodriguez on 3/27/24.
// Copyright © 2024 MBTA. All rights reserved.
//

import UIKit

/// Replaces the use of PresentationDetent from the SwiftUI package as it's only available iOS 16+
public enum PartialSheetDetent: String, Comparable {
@available(iOS 16, *)
case small = "com.mbta.small"
case medium = "com.apple.UIKit.medium"
case large = "com.apple.UIKit.large"

public static func < (lhs: Self, rhs: Self) -> Bool {
lhs.ordinal < rhs.ordinal
}

public var uiKitDetent: UISheetPresentationController.Detent {
switch self {
case .small:
if #available(iOS 16, *) {
let smallDetentIdentifier = UISheetPresentationController.Detent.Identifier(Self.small.rawValue)
return UISheetPresentationController.Detent.custom(identifier: smallDetentIdentifier) { _ in
120
}
} else {
return .medium()
}
case .medium:
return .medium()
case .large:
return .large()
}
}

private var ordinal: Int {
switch self {
case .small:
0
case .medium:
1
case .large:
2
}
}
}
156 changes: 156 additions & 0 deletions iosApp/iosApp/Utils/Backport/PartialSheetModifier.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
//
// PartialSheetModifier.swift
// iosApp
//
// Created by Brandon Rodriguez on 3/25/24.
// Copyright © 2024 MBTA. All rights reserved.
//

import SwiftUI

public extension View {
/**
Replaces the use of the presentationDetents & presentationBackgroundInteraction modifiers
as they're only available in iOS 16+ for SwiftUI
**/
func partialSheetDetents(
_ detents: Set<PartialSheetDetent>,
currentDetent: Binding<PartialSheetDetent>,
largestUndimmedDetent: PartialSheetDetent
) -> some View {
background(
PartialSheetRepresentable(
detents: detents,
currentDetent: currentDetent,
largestUndimmedDetent: largestUndimmedDetent
)
)
}
}

private struct PartialSheetRepresentable: UIViewControllerRepresentable {
let detents: Set<PartialSheetDetent>
let currentDetent: Binding<PartialSheetDetent>?
let largestUndimmedDetent: PartialSheetDetent?

func makeUIViewController(context _: Context) -> Self.Controller {
Controller(
detents: detents,
currentDetent: currentDetent,
largestUndimmedDetent: largestUndimmedDetent
)
}

func updateUIViewController(_ controller: Self.Controller, context _: Context) {
controller.update(
detents: detents,
currentDetent: currentDetent,
largestUndimmedDetent: largestUndimmedDetent
)
}
}

private extension PartialSheetRepresentable {
final class Controller: UIViewController, UISheetPresentationControllerDelegate {
var detents: Set<PartialSheetDetent>
var currentDetent: Binding<PartialSheetDetent>?
var largestUndimmedDetent: PartialSheetDetent?
weak var localDelegate: UISheetPresentationControllerDelegate?

init(
detents: Set<PartialSheetDetent>,
currentDetent: Binding<PartialSheetDetent>?,
largestUndimmedDetent: PartialSheetDetent?
) {
self.detents = detents
self.currentDetent = currentDetent
self.largestUndimmedDetent = largestUndimmedDetent
super.init(nibName: nil, bundle: nil)
}

@available(*, unavailable)
required init?(coder _: NSCoder) {
fatalError("init(coder:) has not been implemented")
}

override func willMove(toParent parent: UIViewController?) {
super.willMove(toParent: parent)
if let controller = parent?.sheetPresentationController,
controller.delegate !== self,
localDelegate == nil
{
localDelegate = controller.delegate
controller.delegate = self
}
update(detents: detents, currentDetent: currentDetent, largestUndimmedDetent: largestUndimmedDetent)
}

override func willTransition(
to newCollection: UITraitCollection,
with coordinator: UIViewControllerTransitionCoordinator
) {
super.willTransition(to: newCollection, with: coordinator)
update(detents: detents, currentDetent: currentDetent, largestUndimmedDetent: largestUndimmedDetent)
}

override func responds(to aSelector: Selector!) -> Bool {
if super.responds(to: aSelector) { return true }
if localDelegate?.responds(to: aSelector) == true { return true }
return false
}

override func forwardingTarget(for aSelector: Selector!) -> Any? {
if super.responds(to: aSelector) { return self }
return localDelegate
}

func update(
detents: Set<PartialSheetDetent>,
currentDetent: Binding<PartialSheetDetent>?,
largestUndimmedDetent: PartialSheetDetent?
) {
self.detents = detents
self.currentDetent = currentDetent

guard let controller = parent?.sheetPresentationController else { return }

var tintAdjustmentMode: UIView.TintAdjustmentMode {
if let undimmedIdentifier = controller.largestUndimmedDetentIdentifier?.rawValue {
let currentDetent = currentDetent?.wrappedValue ?? .large
let undimmed = PartialSheetDetent(rawValue: undimmedIdentifier) ?? .large
return currentDetent > undimmed ? .dimmed : .normal
} else {
return .automatic
}
}

controller.animateChanges {
controller.detents = detents.map(\.uiKitDetent)
controller.prefersScrollingExpandsWhenScrolledToEdge = true

if let currentDetent {
controller.selectedDetentIdentifier = .init(currentDetent.wrappedValue.rawValue)
}

if let largestUndimmedDetent {
controller.largestUndimmedDetentIdentifier = .init(largestUndimmedDetent.rawValue)
}
}

UIView.animate(withDuration: 0.25) {
controller.presentingViewController.view?.tintAdjustmentMode = tintAdjustmentMode
}
}

func sheetPresentationControllerDidChangeSelectedDetentIdentifier(
_ sheetPresentationController: UISheetPresentationController
) {
guard let currentDetent,
let id = sheetPresentationController.selectedDetentIdentifier?.rawValue,
currentDetent.wrappedValue.rawValue != id
else { return }

currentDetent.wrappedValue = .init(rawValue: id) ?? .large
}
}
}
Loading

0 comments on commit c318942

Please sign in to comment.