mirror of
https://github.com/Novaloop-AG/element-x-ios.git
synced 2026-11-03 06:55:59 +00:00
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:
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user