Send current user location (#1272)

* add new asset for location, add logic for sharing user current location

* work on default zoom and auth for location

* add attribution button

* fix attribution button position, fix isSharingUserLocation

* Add NSLocationWhenInUseUsageDescription back

* Fix initial animation

* change map logo and attribution position

* Add annotation just in the setup

* add errors and alert for map vies

* location unit test

* add unit test for location sharing

* add changelog

* Update ElementX/Sources/Other/MapLibre/MapLibreMapView.swift

Co-authored-by: Alfonso Grillo <alfogrillo@gmail.com>

* add comments and minor parameters refactor

* Update ElementX/Sources/Screens/LocationSharing/StaticLocationScreenCoordinator.swift

Co-authored-by: Mauro <34335419+Velin92@users.noreply.github.com>

* Update ElementX/Sources/Screens/LocationSharing/StaticLocationScreenViewModel.swift

Co-authored-by: Mauro <34335419+Velin92@users.noreply.github.com>

* Sort assets

* Cleanup

* Cleanup

* Fix error localizations

* Fix tests

---------

Co-authored-by: Alfonso Grillo <alfogrillo@gmail.com>
Co-authored-by: Mauro <34335419+Velin92@users.noreply.github.com>
This commit is contained in:
Flescio
2023-07-11 12:42:59 +02:00
committed by GitHub
parent 5e97a4a0fa
commit 153149d93f
23 changed files with 1139 additions and 129 deletions
@@ -18,12 +18,13 @@ import CoreLocation
import Foundation
enum LocationSharingViewError: Error, Hashable {
case failedSharingLocation
case missingAuthorization
case mapError(MapLibreError)
}
enum StaticLocationScreenViewModelAction {
case close
case openSystemSettings
case sendLocation(GeoURI, isUserLocation: Bool)
}
@@ -33,47 +34,36 @@ enum StaticLocationInteractionMode: Hashable {
}
struct StaticLocationScreenViewState: BindableState {
init(interactionMode: StaticLocationInteractionMode, isSharingUserLocation: Bool = false, showsUserLocationMode: ShowUserLocationMode = .hide) {
init(interactionMode: StaticLocationInteractionMode) {
self.interactionMode = interactionMode
self.isSharingUserLocation = isSharingUserLocation
self.showsUserLocationMode = showsUserLocationMode
switch interactionMode {
case .picker:
bindings = .init()
case .viewOnly(let geoURI, _):
bindings = .init(mapCenterLocation: .init(latitude: geoURI.latitude, longitude: geoURI.longitude))
bindings.showsUserLocationMode = .showAndFollow
case .viewOnly:
bindings.showsUserLocationMode = .show
}
}
let interactionMode: StaticLocationInteractionMode
/// Indicates whether the user is sharing his current location
var isSharingUserLocation: Bool
/// Behavior mode of the current user's location, can be hidden, only shown and shown following the user
var showsUserLocationMode: ShowUserLocationMode
var bindings = StaticLocationScreenBindings()
var showBottomToolbar: Bool {
interactionMode == .picker
var isSharingUserLocation: Bool {
bindings.isLocationAuthorized == true && bindings.showsUserLocationMode == .showAndFollow
}
var mapAnnotationCoordinate: CLLocationCoordinate2D? {
var bindings = StaticLocationScreenBindings(showsUserLocationMode: .hide)
var initialMapCenter: CLLocationCoordinate2D {
switch interactionMode {
case .picker:
return nil
// middle point in Europe, to be used if the users location is not yet known
return .init(latitude: 49.843, longitude: 9.902056)
case .viewOnly(let geoURI, _):
return .init(latitude: geoURI.latitude, longitude: geoURI.longitude)
}
}
var isLocationPickerMode: Bool {
switch interactionMode {
case .picker:
return true
case .viewOnly:
return false
}
interactionMode == .picker
}
var navigationTitle: String {
@@ -95,9 +85,13 @@ struct StaticLocationScreenViewState: BindableState {
}
var zoomLevel: Double {
15.0
}
var initialZoomLevel: Double {
switch interactionMode {
case .picker:
return 5.0
return 2.7
case .viewOnly:
return 15.0
}
@@ -115,6 +109,10 @@ struct StaticLocationScreenViewState: BindableState {
struct StaticLocationScreenBindings {
var mapCenterLocation: CLLocationCoordinate2D?
var showsUserLocationMode: ShowUserLocationMode
var isLocationAuthorized: Bool?
/// Information describing the currently displayed alert.
var mapError: MapLibreError? {
@@ -125,7 +123,7 @@ struct StaticLocationScreenBindings {
return nil
}
set {
alertInfo = newValue.map { AlertInfo(id: .mapError($0)) }
alertInfo = newValue.map { AlertInfo(locationSharingViewError: .mapError($0)) }
}
}
@@ -138,5 +136,33 @@ struct StaticLocationScreenBindings {
enum StaticLocationScreenViewAction {
case close
case selectLocation
case centerToUser
case userDidPan
}
extension AlertInfo where T == LocationSharingViewError {
init(locationSharingViewError error: LocationSharingViewError,
primaryButton: AlertButton = AlertButton(title: L10n.actionOk, action: nil),
secondaryButton: AlertButton? = nil) {
switch error {
case .missingAuthorization:
self.init(id: error,
title: "",
message: L10n.errorMissingLocationAuth(InfoPlistReader.main.bundleDisplayName),
primaryButton: primaryButton,
secondaryButton: secondaryButton)
case .mapError(.failedLoadingMap):
self.init(id: error,
title: "",
message: L10n.errorFailedLoadingMap(InfoPlistReader.main.bundleDisplayName),
primaryButton: primaryButton,
secondaryButton: secondaryButton)
case .mapError(.failedLocatingUser):
self.init(id: error,
title: "",
message: L10n.errorFailedLocatingUser(InfoPlistReader.main.bundleDisplayName),
primaryButton: primaryButton,
secondaryButton: secondaryButton)
}
}
}
@@ -51,6 +51,12 @@ final class StaticLocationScreenCoordinator: CoordinatorProtocol {
switch action {
case .close:
actionsSubject.send(.close)
case .openSystemSettings:
guard let url = URL(string: UIApplication.openSettingsURLString),
UIApplication.shared.canOpenURL(url) else {
return
}
UIApplication.shared.open(url)
case .sendLocation(let geoURI, let isUserLocation):
actionsSubject.send(.selectedLocation(geoURI, isUserLocation: isUserLocation))
}
@@ -38,8 +38,17 @@ class StaticLocationScreenViewModel: StaticLocationScreenViewModelType, StaticLo
guard let coordinate = state.bindings.mapCenterLocation else { return }
actionsSubject.send(.sendLocation(.init(coordinate: coordinate), isUserLocation: state.isSharingUserLocation))
case .userDidPan:
state.showsUserLocationMode = .hide
state.isSharingUserLocation = false
state.bindings.showsUserLocationMode = .show
case .centerToUser:
switch state.bindings.isLocationAuthorized {
case .some(true), .none:
state.bindings.showsUserLocationMode = .showAndFollow
case .some(false):
let action: () -> Void = { [weak self] in self?.actionsSubject.send(.openSystemSettings) }
state.bindings.alertInfo = .init(locationSharingViewError: .missingAuthorization,
primaryButton: .init(title: L10n.actionNotNow, role: .cancel, action: nil),
secondaryButton: .init(title: L10n.commonSettings, action: action))
}
}
}
}
@@ -44,17 +44,20 @@ struct StaticLocationScreen: View {
ZStack(alignment: .center) {
MapLibreMapView(builder: builder,
options: mapOptions,
showsUserLocationMode: .hide,
showsUserLocationMode: $context.showsUserLocationMode,
error: $context.mapError,
mapCenterCoordinate: $context.mapCenterLocation,
userDidPan: {
context.send(viewAction: .userDidPan)
})
isLocationAuthorized: $context.isLocationAuthorized) {
context.send(viewAction: .userDidPan)
}
.ignoresSafeArea(.all, edges: mapSafeAreaEdges)
if context.viewState.isLocationPickerMode {
LocationMarkerView()
}
}
.ignoresSafeArea(.all, edges: mapSafeAreaEdges)
.overlay(alignment: .bottomTrailing) {
centerToUserLocationButton
}
}
// MARK: - Private
@@ -72,7 +75,7 @@ struct StaticLocationScreen: View {
}
}
if context.viewState.showBottomToolbar {
if context.viewState.isLocationPickerMode {
ToolbarItemGroup(placement: .bottomBar) {
selectLocationButton
Spacer()
@@ -81,19 +84,22 @@ struct StaticLocationScreen: View {
}
private var mapOptions: MapLibreMapView.Options {
guard let coordinate = context.viewState.mapAnnotationCoordinate else {
return .init(zoomLevel: context.viewState.zoomLevel)
var annotations: [LocationAnnotation] = []
if context.viewState.isLocationPickerMode == false {
let annotation = LocationAnnotation(coordinate: context.viewState.initialMapCenter, anchorPoint: .bottomCenter) {
LocationMarkerView()
}
annotations.append(annotation)
}
return .init(zoomLevel: context.viewState.zoomLevel,
mapCenter: coordinate,
annotations: [LocationAnnotation(coordinate: coordinate, anchorPoint: .bottomCenter) {
LocationMarkerView()
}])
initialZoomLevel: context.viewState.initialZoomLevel,
mapCenter: context.viewState.initialMapCenter,
annotations: annotations)
}
private var mapSafeAreaEdges: Edge.Set {
context.viewState.showBottomToolbar ? .horizontal : [.horizontal, .bottom]
context.viewState.isLocationPickerMode ? .horizontal : [.horizontal, .bottom]
}
@ScaledMetric private var shareMarkerSize: CGFloat = 28
@@ -111,6 +117,15 @@ struct StaticLocationScreen: View {
}
}
private var centerToUserLocationButton: some View {
Button {
context.send(viewAction: .centerToUser)
} label: {
Image(asset: context.viewState.isSharingUserLocation ? Asset.Images.locationPointerFull : Asset.Images.locationPointer)
}
.padding(16)
}
private var closeButton: some View {
Button(L10n.actionCancel) {
context.send(viewAction: .close)
@@ -127,14 +142,13 @@ struct StaticLocationScreen: View {
@ViewBuilder
private var shareSheet: some View {
if let location = context.viewState.mapAnnotationCoordinate {
let locationDescription = context.viewState.locationDescription
AppActivityView(activityItems: [ShareToMapsAppActivity.MapsAppType.apple.activityURL(for: location, locationDescription: locationDescription)],
applicationActivities: ShareToMapsAppActivity.MapsAppType.allCases.map { ShareToMapsAppActivity(type: $0, location: location, locationDescription: locationDescription) })
.edgesIgnoringSafeArea(.bottom)
.presentationDetents([.medium, .large])
.presentationDragIndicator(.hidden)
}
let location = context.viewState.initialMapCenter
let locationDescription = context.viewState.locationDescription
AppActivityView(activityItems: [ShareToMapsAppActivity.MapsAppType.apple.activityURL(for: location, locationDescription: locationDescription)],
applicationActivities: ShareToMapsAppActivity.MapsAppType.allCases.map { ShareToMapsAppActivity(type: $0, location: location, locationDescription: locationDescription) })
.edgesIgnoringSafeArea(.bottom)
.presentationDetents([.medium, .large])
.presentationDragIndicator(.hidden)
}
}