Open map from timeline (#1199)

* Add navigation to expaneded map

* Add MapLibreMapView.Options

* Add AppActivityView

* Add ShareToMapsAppActivity

* Add share sheet presentation

* Add localisations

* Cleanup

* Fix UT build errors

* Revert breaking change

* Fix UIView setup

* Add support for location’s description

* Show popover on iPad

* Restore assets

* More cleanup
This commit is contained in:
Alfonso Grillo
2023-06-29 11:12:42 +02:00
committed by GitHub
parent 67e48e465f
commit 3f6bfbcc14
22 changed files with 489 additions and 150 deletions
@@ -27,13 +27,90 @@ enum StaticLocationScreenViewModelAction {
case sendLocation(GeoURI)
}
enum StaticLocationInteractionMode: Hashable {
case picker
case viewOnly(geoURI: GeoURI, description: String? = nil)
}
struct StaticLocationScreenViewState: BindableState {
init(interactionMode: StaticLocationInteractionMode, isPinDropSharing: Bool = true, showsUserLocationMode: ShowUserLocationMode = .hide) {
self.interactionMode = interactionMode
self.isPinDropSharing = isPinDropSharing
self.showsUserLocationMode = showsUserLocationMode
switch interactionMode {
case .picker:
bindings = .init()
case .viewOnly(let geoURI, _):
bindings = .init(mapCenterLocation: .init(latitude: geoURI.latitude, longitude: geoURI.longitude))
}
}
let interactionMode: StaticLocationInteractionMode
/// Indicates whether the user has moved around the map to drop a pin somewhere other than their current location
var isPinDropSharing = true
/// Behavior mode of the current user's location, can be hidden, only shown and shown following the user
var showsUserLocationMode: ShowUserLocationMode = .hide
var bindings = StaticLocationScreenBindings()
var showBottomToolbar: Bool {
interactionMode == .picker
}
var mapAnnotationCoordinate: CLLocationCoordinate2D? {
switch interactionMode {
case .picker:
return nil
case .viewOnly(let geoURI, _):
return .init(latitude: geoURI.latitude, longitude: geoURI.longitude)
}
}
var showPinInTheCenter: Bool {
switch interactionMode {
case .picker:
return isPinDropSharing
case .viewOnly:
return false
}
}
var navigationTitle: String {
switch interactionMode {
case .picker:
return L10n.screenShareLocationTitle
case .viewOnly:
return L10n.screenViewLocationTitle
}
}
var showShareAction: Bool {
switch interactionMode {
case .picker:
return false
case .viewOnly:
return true
}
}
var zoomLevel: Double {
switch interactionMode {
case .picker:
return 5.0
case .viewOnly:
return 15.0
}
}
var locationDescription: String? {
switch interactionMode {
case .picker:
return nil
case .viewOnly(_, let description):
return description
}
}
}
struct StaticLocationScreenBindings {
@@ -54,6 +131,8 @@ struct StaticLocationScreenBindings {
/// Information describing the currently displayed alert.
var alertInfo: AlertInfo<LocationSharingViewError>?
var showShareSheet = false
}
enum StaticLocationScreenViewAction {
@@ -17,7 +17,9 @@
import Combine
import SwiftUI
struct StaticLocationScreenCoordinatorParameters { }
struct StaticLocationScreenCoordinatorParameters {
let interactionMode: StaticLocationInteractionMode
}
enum StaticLocationScreenCoordinatorAction {
case close
@@ -38,7 +40,7 @@ final class StaticLocationScreenCoordinator: CoordinatorProtocol {
init(parameters: StaticLocationScreenCoordinatorParameters) {
self.parameters = parameters
viewModel = StaticLocationScreenViewModel()
viewModel = StaticLocationScreenViewModel(interactionMode: parameters.interactionMode)
}
// MARK: - Public
@@ -26,8 +26,8 @@ class StaticLocationScreenViewModel: StaticLocationScreenViewModelType, StaticLo
actionsSubject.eraseToAnyPublisher()
}
init() {
super.init(initialViewState: StaticLocationScreenViewState())
init(interactionMode: StaticLocationInteractionMode) {
super.init(initialViewState: .init(interactionMode: interactionMode))
}
override func process(viewAction: StaticLocationScreenViewAction) {
@@ -22,43 +22,81 @@ struct StaticLocationScreen: View {
private let builder = MapTilerStyleBuilder(appSettings: ServiceLocator.shared.settings)
var body: some View {
mapView
.ignoresSafeArea(.all, edges: .horizontal)
.navigationTitle(L10n.screenShareLocationTitle)
.navigationBarTitleDisplayMode(.inline)
.toolbar { toolbar }
.alert(item: $context.alertInfo)
VStack(spacing: 0) {
if let locationDescription = context.viewState.locationDescription {
Text(locationDescription)
.lineLimit(2)
.foregroundColor(Color.compound.textPrimary)
.font(.compound.bodyMD)
.padding(.horizontal, 16)
.padding(.vertical, 8)
}
mapView
}
.navigationTitle(context.viewState.navigationTitle)
.navigationBarTitleDisplayMode(.inline)
.toolbar { toolbar }
.alert(item: $context.alertInfo)
}
private var mapView: some View {
ZStack(alignment: .center) {
MapLibreMapView(builder: builder,
options: mapOptions,
showsUserLocationMode: .hide,
error: $context.mapError,
mapCenterCoordinate: $context.mapCenterLocation,
userDidPan: {
context.send(viewAction: .userDidPan)
})
if context.viewState.isPinDropSharing {
if context.viewState.showPinInTheCenter {
LocationMarkerView()
}
}
.ignoresSafeArea(.all, edges: mapSafeAreaEdges)
}
// MARK: - Private
@ToolbarContentBuilder
private var toolbar: some ToolbarContent {
ToolbarItem(placement: .navigationBarLeading) {
closeButton
}
ToolbarItemGroup(placement: .bottomBar) {
shareLocationButton
Spacer()
if context.viewState.showShareAction {
ToolbarItem(placement: .navigationBarTrailing) {
shareButton
.popover(isPresented: $context.showShareSheet) { shareSheet }
}
}
if context.viewState.showBottomToolbar {
ToolbarItemGroup(placement: .bottomBar) {
selectLocationButton
Spacer()
}
}
}
private var mapOptions: MapLibreMapView.Options {
guard let coordinate = context.viewState.mapAnnotationCoordinate else {
return .init(zoomLevel: context.viewState.zoomLevel)
}
return .init(zoomLevel: context.viewState.zoomLevel,
mapCenter: coordinate,
annotations: [LocationAnnotation(coordinate: coordinate, anchorPoint: .bottomCenter) {
LocationMarkerView()
}])
}
private var mapSafeAreaEdges: Edge.Set {
context.viewState.showBottomToolbar ? .horizontal : [.horizontal, .bottom]
}
@ScaledMetric private var shareMarkerSize: CGFloat = 28
private var shareLocationButton: some View {
private var selectLocationButton: some View {
Button {
context.send(viewAction: .selectLocation)
} label: {
@@ -73,25 +111,52 @@ struct StaticLocationScreen: View {
}
private var closeButton: some View {
Button(L10n.actionCancel, action: close)
Button(L10n.actionCancel) {
context.send(viewAction: .close)
}
}
private func close() {
context.send(viewAction: .close)
private var shareButton: some View {
Button {
context.showShareSheet = true
} label: {
Image(systemName: "square.and.arrow.up")
}
}
@ViewBuilder
private var shareSheet: some View {
if let location = context.viewState.mapAnnotationCoordinate {
AppActivityView(activityItems: [ShareToMapsAppActivity.MapsAppType.apple.activityURL(for: location)],
applicationActivities: ShareToMapsAppActivity.MapsAppType.allCases.map { ShareToMapsAppActivity(type: $0, location: location) })
.edgesIgnoringSafeArea(.bottom)
.presentationDetents([.medium, .large])
.presentationDragIndicator(.hidden)
}
}
}
// MARK: - Previews
struct StaticLocationScreenViewer_Previews: PreviewProvider {
static let viewModel = {
let viewModel = StaticLocationScreenViewModel()
return viewModel
}()
static var previews: some View {
NavigationView {
StaticLocationScreen(context: viewModel.context)
NavigationStack {
StaticLocationScreen(context: StaticLocationScreenViewModel(interactionMode: .picker).context)
}
.previewDisplayName("Picker")
NavigationStack {
StaticLocationScreen(context: StaticLocationScreenViewModel(interactionMode: .viewOnly(geoURI: .init(latitude: 41.9027835, longitude: 12.4963655))).context)
}
.previewDisplayName("View Only")
NavigationStack {
StaticLocationScreen(context: StaticLocationScreenViewModel(interactionMode: .viewOnly(geoURI: .init(latitude: 41.9027835, longitude: 12.4963655), description: "Cool position")).context)
}
.previewDisplayName("View Only (with description)")
}
}
private extension CGPoint {
static let bottomCenter: Self = .init(x: 0.5, y: 1)
}