mirror of
https://github.com/vector-im/element-ios.git
synced 2024-09-28 23:32:41 +00:00
Merge pull request #6016 from vector-im/maximee/5982_ls_static_share_viewer
[Location Sharing] Add separate screen for viewing static shared location
This commit is contained in:
commit
dcfaadbf97
23 changed files with 699 additions and 162 deletions
|
@ -2139,6 +2139,7 @@ Tap the + to start adding people.";
|
|||
"live_location_sharing_banner_stop" = "Stop";
|
||||
"location_sharing_static_share_title" = "Send my current location";
|
||||
"location_sharing_pin_drop_share_title" = "Send this location";
|
||||
"location_sharing_live_map_callout_title" = "Share location";
|
||||
|
||||
// MARK: - MatrixKit
|
||||
|
||||
|
|
|
@ -2759,6 +2759,10 @@ public class VectorL10n: NSObject {
|
|||
public static var locationSharingInvalidAuthorizationSettings: String {
|
||||
return VectorL10n.tr("Vector", "location_sharing_invalid_authorization_settings")
|
||||
}
|
||||
/// Share location
|
||||
public static var locationSharingLiveMapCalloutTitle: String {
|
||||
return VectorL10n.tr("Vector", "location_sharing_live_map_callout_title")
|
||||
}
|
||||
/// Share live location
|
||||
public static var locationSharingLiveShareTitle: String {
|
||||
return VectorL10n.tr("Vector", "location_sharing_live_share_title")
|
||||
|
|
|
@ -246,8 +246,54 @@ final class RoomCoordinator: NSObject, RoomCoordinatorProtocol {
|
|||
|
||||
completion?()
|
||||
}
|
||||
|
||||
private func showLocationCoordinatorWithEvent(_ event: MXEvent, bubbleData: MXKRoomBubbleCellDataStoring) {
|
||||
guard #available(iOS 14.0, *) else {
|
||||
return
|
||||
}
|
||||
|
||||
guard let navigationRouter = self.navigationRouter,
|
||||
let mediaManager = mxSession?.mediaManager,
|
||||
let locationContent = event.location else {
|
||||
MXLog.error("[RoomCoordinator] Invalid location showing coordinator parameters. Returning.")
|
||||
return
|
||||
}
|
||||
|
||||
let avatarData = AvatarInput(mxContentUri: bubbleData.senderAvatarUrl,
|
||||
matrixItemId: bubbleData.senderId,
|
||||
displayName: bubbleData.senderDisplayName)
|
||||
|
||||
|
||||
let location = CLLocationCoordinate2D(latitude: locationContent.latitude, longitude: locationContent.longitude)
|
||||
let coordinateType = locationContent.assetType
|
||||
|
||||
guard let locationSharingCoordinatetype = coordinateType.locationSharingCoordinateType() else {
|
||||
fatalError("[LocationSharingCoordinator] event asset type is not supported: \(coordinateType)")
|
||||
}
|
||||
|
||||
let parameters = StaticLocationViewingCoordinatorParameters(mediaManager: mediaManager,
|
||||
avatarData: avatarData,
|
||||
location: location,
|
||||
coordinateType: locationSharingCoordinatetype)
|
||||
|
||||
let coordinator = StaticLocationViewingCoordinator(parameters: parameters)
|
||||
|
||||
coordinator.completion = { [weak self, weak coordinator] in
|
||||
guard let self = self, let coordinator = coordinator else {
|
||||
return
|
||||
}
|
||||
|
||||
self.navigationRouter?.dismissModule(animated: true, completion: nil)
|
||||
self.remove(childCoordinator: coordinator)
|
||||
}
|
||||
|
||||
add(childCoordinator: coordinator)
|
||||
|
||||
navigationRouter.present(coordinator, animated: true)
|
||||
coordinator.start()
|
||||
}
|
||||
|
||||
private func startLocationCoordinatorWithEvent(_ event: MXEvent? = nil, bubbleData: MXKRoomBubbleCellDataStoring? = nil) {
|
||||
private func startLocationCoordinator() {
|
||||
guard #available(iOS 14.0, *) else {
|
||||
return
|
||||
}
|
||||
|
@ -259,29 +305,13 @@ final class RoomCoordinator: NSObject, RoomCoordinatorProtocol {
|
|||
return
|
||||
}
|
||||
|
||||
var avatarData: AvatarInputProtocol
|
||||
if event != nil, let bubbleData = bubbleData {
|
||||
avatarData = AvatarInput(mxContentUri: bubbleData.senderAvatarUrl,
|
||||
matrixItemId: bubbleData.senderId,
|
||||
displayName: bubbleData.senderDisplayName)
|
||||
} else {
|
||||
avatarData = AvatarInput(mxContentUri: user.avatarUrl,
|
||||
let avatarData = AvatarInput(mxContentUri: user.avatarUrl,
|
||||
matrixItemId: user.userId,
|
||||
displayName: user.displayname)
|
||||
}
|
||||
|
||||
var location: CLLocationCoordinate2D?
|
||||
var coordinateType: MXEventAssetType = .user
|
||||
if let locationContent = event?.location {
|
||||
location = CLLocationCoordinate2D(latitude: locationContent.latitude, longitude: locationContent.longitude)
|
||||
coordinateType = locationContent.assetType
|
||||
}
|
||||
|
||||
let parameters = LocationSharingCoordinatorParameters(roomDataSource: roomViewController.roomDataSource,
|
||||
mediaManager: mediaManager,
|
||||
avatarData: avatarData,
|
||||
location: location,
|
||||
coordinateType: coordinateType)
|
||||
avatarData: avatarData)
|
||||
|
||||
let coordinator = LocationSharingCoordinator(parameters: parameters)
|
||||
|
||||
|
@ -411,11 +441,11 @@ extension RoomCoordinator: RoomViewControllerDelegate {
|
|||
}
|
||||
|
||||
func roomViewControllerDidRequestLocationSharingFormPresentation(_ roomViewController: RoomViewController) {
|
||||
startLocationCoordinatorWithEvent()
|
||||
startLocationCoordinator()
|
||||
}
|
||||
|
||||
func roomViewController(_ roomViewController: RoomViewController, didRequestLocationPresentationFor event: MXEvent, bubbleData: MXKRoomBubbleCellDataStoring) {
|
||||
startLocationCoordinatorWithEvent(event, bubbleData: bubbleData)
|
||||
showLocationCoordinatorWithEvent(event, bubbleData: bubbleData)
|
||||
}
|
||||
|
||||
func roomViewController(_ roomViewController: RoomViewController, locationShareActivityViewControllerFor event: MXEvent) -> UIActivityViewController? {
|
||||
|
|
|
@ -23,8 +23,6 @@ struct LocationSharingCoordinatorParameters {
|
|||
let roomDataSource: MXKRoomDataSource
|
||||
let mediaManager: MXMediaManager
|
||||
let avatarData: AvatarInputProtocol
|
||||
let location: CLLocationCoordinate2D?
|
||||
let coordinateType: MXEventAssetType
|
||||
}
|
||||
|
||||
// Map between type from MatrixSDK and type from SwiftUI target, as we don't want
|
||||
|
@ -79,16 +77,8 @@ final class LocationSharingCoordinator: Coordinator, Presentable {
|
|||
init(parameters: LocationSharingCoordinatorParameters) {
|
||||
self.parameters = parameters
|
||||
|
||||
// TODO: Make this check before creating LocationSharingCoordinator
|
||||
// Use LocationSharingCoordinateType in parameters
|
||||
guard let locationSharingCoordinatetype = parameters.coordinateType.locationSharingCoordinateType() else {
|
||||
fatalError("[LocationSharingCoordinator] event asset type is not supported: \(parameters.coordinateType)")
|
||||
}
|
||||
|
||||
let viewModel = LocationSharingViewModel(mapStyleURL: BuildSettings.tileServerMapStyleURL,
|
||||
avatarData: parameters.avatarData,
|
||||
location: parameters.location,
|
||||
coordinateType: locationSharingCoordinatetype,
|
||||
isLiveLocationSharingEnabled: BuildSettings.liveLocationSharingEnabled)
|
||||
let view = LocationSharingView(context: viewModel.context)
|
||||
.addDependency(AvatarService.instantiate(mediaManager: parameters.mediaManager))
|
||||
|
@ -111,14 +101,7 @@ final class LocationSharingCoordinator: Coordinator, Presentable {
|
|||
case .cancel:
|
||||
self.completion?()
|
||||
case .share(let latitude, let longitude, let coordinateType):
|
||||
|
||||
// Show share sheet on existing location display
|
||||
if let location = self.parameters.location {
|
||||
self.presentShareLocationActivity(with: location)
|
||||
} else {
|
||||
self.shareStaticLocation(latitude: latitude, longitude: longitude, coordinateType: coordinateType)
|
||||
}
|
||||
|
||||
self.shareStaticLocation(latitude: latitude, longitude: longitude, coordinateType: coordinateType)
|
||||
case .shareLiveLocation(let timeout):
|
||||
self.startLiveLocationSharing(with: timeout)
|
||||
}
|
||||
|
|
|
@ -0,0 +1,29 @@
|
|||
//
|
||||
// Copyright 2022 New Vector Ltd
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import CoreLocation
|
||||
|
||||
/// Build a UIActivityViewController to share a location
|
||||
class ShareLocationActivityControllerBuilder {
|
||||
|
||||
func build(with location: CLLocationCoordinate2D) -> UIActivityViewController {
|
||||
return UIActivityViewController(activityItems: [ShareToMapsAppActivity.urlForMapsAppType(.apple, location: location)],
|
||||
applicationActivities: [ShareToMapsAppActivity(type: .apple, location: location),
|
||||
ShareToMapsAppActivity(type: .google, location: location),
|
||||
ShareToMapsAppActivity(type: .osm, location: location)])
|
||||
}
|
||||
}
|
|
@ -55,9 +55,6 @@ struct LocationSharingViewState: BindableState {
|
|||
/// Current user avatarData
|
||||
let userAvatarData: AvatarInputProtocol
|
||||
|
||||
/// Shared annotation to display existing location
|
||||
let sharedAnnotation: LocationAnnotation?
|
||||
|
||||
/// Map annotations to display on map
|
||||
var annotations: [LocationAnnotation]
|
||||
|
||||
|
@ -77,14 +74,6 @@ struct LocationSharingViewState: BindableState {
|
|||
/// Used to hide live location sharing features until is finished
|
||||
var isLiveLocationSharingEnabled: Bool = false
|
||||
|
||||
var shareButtonVisible: Bool {
|
||||
return self.displayExistingLocation == false
|
||||
}
|
||||
|
||||
var displayExistingLocation: Bool {
|
||||
return sharedAnnotation != nil
|
||||
}
|
||||
|
||||
var shareButtonEnabled: Bool {
|
||||
!showLoadingIndicator
|
||||
}
|
||||
|
|
|
@ -21,7 +21,6 @@ import CoreLocation
|
|||
@available(iOS 14.0, *)
|
||||
enum MockLocationSharingScreenState: MockScreenState, CaseIterable {
|
||||
case shareUserLocation
|
||||
case displayExistingLocation
|
||||
|
||||
var screenType: Any.Type {
|
||||
LocationSharingView.self
|
||||
|
@ -29,16 +28,9 @@ enum MockLocationSharingScreenState: MockScreenState, CaseIterable {
|
|||
|
||||
var screenView: ([Any], AnyView) {
|
||||
|
||||
var location: CLLocationCoordinate2D?
|
||||
if self == .displayExistingLocation {
|
||||
location = CLLocationCoordinate2D(latitude: 51.4932641, longitude: -0.257096)
|
||||
}
|
||||
|
||||
let mapStyleURL = URL(string: "https://api.maptiler.com/maps/streets/style.json?key=fU3vlMsMn4Jb6dnEIFsx")!
|
||||
let viewModel = LocationSharingViewModel(mapStyleURL: mapStyleURL,
|
||||
avatarData: AvatarInput(mxContentUri: "", matrixItemId: "alice:matrix.org", displayName: "Alice"),
|
||||
location: location,
|
||||
coordinateType: .user,
|
||||
isLiveLocationSharingEnabled: true)
|
||||
return ([viewModel],
|
||||
AnyView(LocationSharingView(context: viewModel.context)
|
||||
|
|
|
@ -41,38 +41,12 @@ class LocationSharingViewModel: LocationSharingViewModelType, LocationSharingVie
|
|||
|
||||
// MARK: - Setup
|
||||
|
||||
init(mapStyleURL: URL, avatarData: AvatarInputProtocol, location: CLLocationCoordinate2D? = nil, coordinateType: LocationSharingCoordinateType, isLiveLocationSharingEnabled: Bool = false) {
|
||||
|
||||
var sharedAnnotation: LocationAnnotation?
|
||||
var annotations: [LocationAnnotation] = []
|
||||
var highlightedAnnotation: LocationAnnotation?
|
||||
var showsUserLocation: Bool = false
|
||||
|
||||
// Displaying an existing location
|
||||
if let sharedCoordinate = location {
|
||||
let sharedLocationAnnotation: LocationAnnotation
|
||||
switch coordinateType {
|
||||
case .user:
|
||||
sharedLocationAnnotation = UserLocationAnnotation(avatarData: avatarData, coordinate: sharedCoordinate)
|
||||
case .pin:
|
||||
sharedLocationAnnotation = PinLocationAnnotation(coordinate: sharedCoordinate)
|
||||
}
|
||||
|
||||
annotations.append(sharedLocationAnnotation)
|
||||
highlightedAnnotation = sharedLocationAnnotation
|
||||
|
||||
sharedAnnotation = sharedLocationAnnotation
|
||||
} else {
|
||||
// Share current location
|
||||
showsUserLocation = true
|
||||
}
|
||||
|
||||
init(mapStyleURL: URL, avatarData: AvatarInputProtocol, isLiveLocationSharingEnabled: Bool = false) {
|
||||
let viewState = LocationSharingViewState(mapStyleURL: mapStyleURL,
|
||||
userAvatarData: avatarData,
|
||||
sharedAnnotation: sharedAnnotation,
|
||||
annotations: annotations,
|
||||
highlightedAnnotation: highlightedAnnotation,
|
||||
showsUserLocation: showsUserLocation,
|
||||
annotations: [],
|
||||
highlightedAnnotation: nil,
|
||||
showsUserLocation: true,
|
||||
isLiveLocationSharingEnabled: isLiveLocationSharingEnabled)
|
||||
|
||||
super.init(initialViewState: viewState)
|
||||
|
@ -90,12 +64,6 @@ class LocationSharingViewModel: LocationSharingViewModelType, LocationSharingVie
|
|||
case .cancel:
|
||||
completion?(.cancel)
|
||||
case .share:
|
||||
// Share existing location
|
||||
if let location = state.sharedAnnotation?.coordinate {
|
||||
completion?(.share(latitude: location.latitude, longitude: location.longitude, coordinateType: .user))
|
||||
return
|
||||
}
|
||||
|
||||
// Share current user location
|
||||
guard let location = state.bindings.userLocation else {
|
||||
processError(.failedLocatingUser)
|
||||
|
|
|
@ -0,0 +1,51 @@
|
|||
//
|
||||
// Copyright 2022 New Vector Ltd
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
struct MapViewErrorAlertInfoBuilder {
|
||||
|
||||
func build(with error: LocationSharingViewError, dimissalCallback: (() -> Void)?) -> AlertInfo<LocationSharingAlertType>? {
|
||||
|
||||
let alertInfo: AlertInfo<LocationSharingAlertType>?
|
||||
|
||||
switch error {
|
||||
case .failedLoadingMap:
|
||||
alertInfo = AlertInfo(id: .mapLoadingError,
|
||||
title: VectorL10n.locationSharingLoadingMapErrorTitle(AppInfo.current.displayName),
|
||||
primaryButton: (VectorL10n.ok, dimissalCallback))
|
||||
case .failedLocatingUser:
|
||||
alertInfo = AlertInfo(id: .userLocatingError,
|
||||
title: VectorL10n.locationSharingLocatingUserErrorTitle(AppInfo.current.displayName),
|
||||
primaryButton: (VectorL10n.ok, dimissalCallback))
|
||||
case .invalidLocationAuthorization:
|
||||
alertInfo = AlertInfo(id: .authorizationError,
|
||||
title: VectorL10n.locationSharingInvalidAuthorizationErrorTitle(AppInfo.current.displayName),
|
||||
primaryButton: (VectorL10n.locationSharingInvalidAuthorizationNotNow, dimissalCallback),
|
||||
secondaryButton: (VectorL10n.locationSharingInvalidAuthorizationSettings, {
|
||||
if let applicationSettingsURL = URL(string:UIApplication.openSettingsURLString) {
|
||||
UIApplication.shared.open(applicationSettingsURL)
|
||||
}
|
||||
}))
|
||||
default:
|
||||
alertInfo = nil
|
||||
}
|
||||
|
||||
return alertInfo
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -36,14 +36,6 @@ class LocationSharingUITests: XCTestCase {
|
|||
XCTAssertTrue(app.otherElements["Map"].exists)
|
||||
}
|
||||
|
||||
func testInitialExistingLocation() {
|
||||
goToScreenWithIdentifier(MockLocationSharingScreenState.displayExistingLocation.title)
|
||||
|
||||
XCTAssertTrue(app.buttons["Cancel"].exists)
|
||||
XCTAssertTrue(app.buttons["LocationSharingView.shareButton"].exists)
|
||||
XCTAssertTrue(app.otherElements["Map"].exists)
|
||||
}
|
||||
|
||||
// Need a delay when showing the map otherwise the simulator breaks
|
||||
private func goToScreenWithIdentifier(_ identifier: String) {
|
||||
app.goToScreenWithIdentifier(identifier)
|
||||
|
|
|
@ -26,22 +26,20 @@ class LocationSharingViewModelTests: XCTestCase {
|
|||
var cancellables = Set<AnyCancellable>()
|
||||
|
||||
func testInitialState() {
|
||||
let viewModel = buildViewModel(withLocation: false)
|
||||
let viewModel = buildViewModel()
|
||||
|
||||
XCTAssertTrue(viewModel.context.viewState.shareButtonEnabled)
|
||||
XCTAssertTrue(viewModel.context.viewState.shareButtonVisible)
|
||||
XCTAssertFalse(viewModel.context.viewState.showLoadingIndicator)
|
||||
|
||||
XCTAssertNotNil(viewModel.context.viewState.mapStyleURL)
|
||||
XCTAssertNotNil(viewModel.context.viewState.userAvatarData)
|
||||
|
||||
XCTAssertNil(viewModel.context.viewState.sharedAnnotation)
|
||||
XCTAssertNil(viewModel.context.viewState.bindings.userLocation)
|
||||
XCTAssertNil(viewModel.context.viewState.bindings.alertInfo)
|
||||
}
|
||||
|
||||
func testCancellation() {
|
||||
let viewModel = buildViewModel(withLocation: false)
|
||||
let viewModel = buildViewModel()
|
||||
|
||||
let expectation = self.expectation(description: "Cancellation completion should be invoked")
|
||||
|
||||
|
@ -62,10 +60,9 @@ class LocationSharingViewModelTests: XCTestCase {
|
|||
}
|
||||
|
||||
func testShareNoUserLocation() {
|
||||
let viewModel = buildViewModel(withLocation: false)
|
||||
let viewModel = buildViewModel()
|
||||
|
||||
XCTAssertNil(viewModel.context.viewState.bindings.userLocation)
|
||||
XCTAssertNil(viewModel.context.viewState.sharedAnnotation)
|
||||
|
||||
viewModel.context.send(viewAction: .share)
|
||||
|
||||
|
@ -73,36 +70,8 @@ class LocationSharingViewModelTests: XCTestCase {
|
|||
XCTAssertEqual(viewModel.context.viewState.bindings.alertInfo?.id, .userLocatingError)
|
||||
}
|
||||
|
||||
func testShareExistingLocation() {
|
||||
let viewModel = buildViewModel(withLocation: true)
|
||||
|
||||
let expectation = self.expectation(description: "Share completion should be invoked")
|
||||
|
||||
viewModel.completion = { result in
|
||||
switch result {
|
||||
case .share(let latitude, let longitude, _):
|
||||
XCTAssertEqual(latitude, viewModel.context.viewState.sharedAnnotation?.coordinate.latitude)
|
||||
XCTAssertEqual(longitude, viewModel.context.viewState.sharedAnnotation?.coordinate.longitude)
|
||||
expectation.fulfill()
|
||||
case .cancel:
|
||||
XCTFail()
|
||||
case .shareLiveLocation(timeout: let timeout):
|
||||
XCTFail()
|
||||
}
|
||||
}
|
||||
|
||||
XCTAssertNil(viewModel.context.viewState.bindings.userLocation)
|
||||
XCTAssertNotNil(viewModel.context.viewState.sharedAnnotation)
|
||||
|
||||
viewModel.context.send(viewAction: .share)
|
||||
|
||||
XCTAssertNil(viewModel.context.viewState.bindings.alertInfo)
|
||||
|
||||
waitForExpectations(timeout: 3)
|
||||
}
|
||||
|
||||
func testLoading() {
|
||||
let viewModel = buildViewModel(withLocation: false)
|
||||
let viewModel = buildViewModel()
|
||||
|
||||
viewModel.startLoading()
|
||||
|
||||
|
@ -116,7 +85,7 @@ class LocationSharingViewModelTests: XCTestCase {
|
|||
}
|
||||
|
||||
func testInvalidLocationAuthorization() {
|
||||
let viewModel = buildViewModel(withLocation: false)
|
||||
let viewModel = buildViewModel()
|
||||
|
||||
viewModel.context.viewState.errorSubject.send(.invalidLocationAuthorization)
|
||||
|
||||
|
@ -124,9 +93,8 @@ class LocationSharingViewModelTests: XCTestCase {
|
|||
XCTAssertEqual(viewModel.context.viewState.bindings.alertInfo?.id, .authorizationError)
|
||||
}
|
||||
|
||||
private func buildViewModel(withLocation: Bool) -> LocationSharingViewModel {
|
||||
private func buildViewModel() -> LocationSharingViewModel {
|
||||
LocationSharingViewModel(mapStyleURL: URL(string: "http://empty.com")!,
|
||||
avatarData: AvatarInput(mxContentUri: "", matrixItemId: "", displayName: ""),
|
||||
location: (withLocation ? CLLocationCoordinate2D(latitude: 51.4932641, longitude: -0.257096) : nil), coordinateType: .user)
|
||||
avatarData: AvatarInput(mxContentUri: "", matrixItemId: "", displayName: ""))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
//
|
||||
//
|
||||
// Copyright 2021 New Vector Ltd
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
|
@ -117,12 +117,12 @@ extension LocationSharingMapView {
|
|||
func mapView(_ mapView: MGLMapView, viewFor annotation: MGLAnnotation) -> MGLAnnotationView? {
|
||||
|
||||
if let userLocationAnnotation = annotation as? UserLocationAnnotation {
|
||||
return LocationAnnotatonView(userLocationAnnotation: userLocationAnnotation)
|
||||
return LocationAnnotationView(userLocationAnnotation: userLocationAnnotation)
|
||||
} else if let pinLocationAnnotation = annotation as? PinLocationAnnotation {
|
||||
return LocationAnnotatonView(pinLocationAnnotation: pinLocationAnnotation)
|
||||
return LocationAnnotationView(pinLocationAnnotation: pinLocationAnnotation)
|
||||
} else if annotation is MGLUserLocation && locationSharingMapView.mapCenterCoordinate == nil, let currentUserAvatarData = locationSharingMapView.userAvatarData {
|
||||
// Replace default current location annotation view with a UserLocationAnnotatonView when the map is center on user location
|
||||
return LocationAnnotatonView(avatarData: currentUserAvatarData)
|
||||
return LocationAnnotationView(avatarData: currentUserAvatarData)
|
||||
}
|
||||
|
||||
return nil
|
||||
|
|
|
@ -36,11 +36,9 @@ struct LocationSharingView: View {
|
|||
mapView
|
||||
VStack(spacing: 0) {
|
||||
MapCreditsView()
|
||||
if context.viewState.shareButtonVisible {
|
||||
buttonsView
|
||||
.background(theme.colors.background)
|
||||
.clipShape(RoundedCornerShape(radius: 8, corners: [.topLeft, .topRight]))
|
||||
}
|
||||
buttonsView
|
||||
.background(theme.colors.background)
|
||||
.clipShape(RoundedCornerShape(radius: 8, corners: [.topLeft, .topRight]))
|
||||
}
|
||||
}
|
||||
.toolbar {
|
||||
|
@ -54,17 +52,6 @@ struct LocationSharingView: View {
|
|||
.font(.headline)
|
||||
.foregroundColor(theme.colors.primaryContent)
|
||||
}
|
||||
ToolbarItem(placement: .navigationBarTrailing) {
|
||||
if context.viewState.displayExistingLocation {
|
||||
Button {
|
||||
context.send(viewAction: .share)
|
||||
} label: {
|
||||
Image(uiImage: Asset.Images.locationShareIcon.image)
|
||||
.accessibilityIdentifier("LocationSharingView.shareButton")
|
||||
}
|
||||
.disabled(!context.viewState.shareButtonEnabled)
|
||||
}
|
||||
}
|
||||
}
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.introspectNavigationController { navigationController in
|
||||
|
|
|
@ -19,7 +19,7 @@ import SwiftUI
|
|||
import Mapbox
|
||||
|
||||
@available(iOS 14, *)
|
||||
class LocationAnnotatonView: MGLUserLocationAnnotationView {
|
||||
class LocationAnnotationView: MGLUserLocationAnnotationView {
|
||||
|
||||
// MARK: Private
|
||||
|
|
@ -0,0 +1,88 @@
|
|||
//
|
||||
// Copyright 2021 New Vector Ltd
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import UIKit
|
||||
import SwiftUI
|
||||
import MatrixSDK
|
||||
|
||||
struct StaticLocationViewingCoordinatorParameters {
|
||||
let mediaManager: MXMediaManager
|
||||
let avatarData: AvatarInputProtocol
|
||||
let location: CLLocationCoordinate2D
|
||||
let coordinateType: LocationSharingCoordinateType
|
||||
}
|
||||
|
||||
final class StaticLocationViewingCoordinator: Coordinator, Presentable {
|
||||
|
||||
// MARK: - Properties
|
||||
|
||||
// MARK: Private
|
||||
|
||||
private let parameters: StaticLocationViewingCoordinatorParameters
|
||||
private let staticLocationViewingHostingController: UIViewController
|
||||
private var staticLocationViewingViewModel: StaticLocationViewingViewModelProtocol
|
||||
|
||||
private let shareLocationActivityControllerBuilder = ShareLocationActivityControllerBuilder()
|
||||
|
||||
// MARK: Public
|
||||
|
||||
// Must be used only internally
|
||||
var childCoordinators: [Coordinator] = []
|
||||
var completion: (() -> Void)?
|
||||
|
||||
// MARK: - Setup
|
||||
|
||||
@available(iOS 14.0, *)
|
||||
init(parameters: StaticLocationViewingCoordinatorParameters) {
|
||||
self.parameters = parameters
|
||||
|
||||
let viewModel = StaticLocationViewingViewModel(mapStyleURL: BuildSettings.tileServerMapStyleURL,
|
||||
avatarData: parameters.avatarData,
|
||||
location: parameters.location,
|
||||
coordinateType: parameters.coordinateType)
|
||||
let view = StaticLocationView(viewModel: viewModel.context)
|
||||
.addDependency(AvatarService.instantiate(mediaManager: parameters.mediaManager))
|
||||
staticLocationViewingViewModel = viewModel
|
||||
staticLocationViewingHostingController = VectorHostingController(rootView: view)
|
||||
}
|
||||
|
||||
// MARK: - Public
|
||||
func start() {
|
||||
MXLog.debug("[StaticLocationSharingViewerCoordinator] did start.")
|
||||
staticLocationViewingViewModel.completion = { [weak self] result in
|
||||
guard let self = self else { return }
|
||||
MXLog.debug("[StaticLocationSharingViewerCoordinator] StaticLocationSharingViewerViewModel did complete with result: \(result).")
|
||||
switch result {
|
||||
case .close:
|
||||
self.completion?()
|
||||
case .share(let coordinate):
|
||||
self.presentLocationActivityController(with: coordinate)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func toPresentable() -> UIViewController {
|
||||
return self.staticLocationViewingHostingController
|
||||
}
|
||||
|
||||
func presentLocationActivityController(with coordinate: CLLocationCoordinate2D) {
|
||||
|
||||
let shareActivityController = shareLocationActivityControllerBuilder.build(with: coordinate)
|
||||
|
||||
self.staticLocationViewingHostingController.present(shareActivityController, animated: true)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,57 @@
|
|||
//
|
||||
// Copyright 2021 New Vector Ltd
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import SwiftUI
|
||||
import CoreLocation
|
||||
|
||||
/// Using an enum for the screen allows you define the different state cases with
|
||||
/// the relevant associated data for each case.
|
||||
@available(iOS 14.0, *)
|
||||
enum MockStaticLocationViewingScreenState: MockScreenState, CaseIterable {
|
||||
// A case for each state you want to represent
|
||||
// with specific, minimal associated data that will allow you
|
||||
// mock that screen.
|
||||
case showUserLocation
|
||||
case showPinLocation
|
||||
|
||||
/// The associated screen
|
||||
var screenType: Any.Type {
|
||||
StaticLocationView.self
|
||||
}
|
||||
|
||||
/// A list of screen state definitions
|
||||
static var allCases: [MockStaticLocationViewingScreenState] {
|
||||
return [.showUserLocation, .showPinLocation]
|
||||
}
|
||||
|
||||
/// Generate the view struct for the screen state.
|
||||
var screenView: ([Any], AnyView) {
|
||||
let location = CLLocationCoordinate2D(latitude: 51.4932641, longitude: -0.257096)
|
||||
let coordinateType: LocationSharingCoordinateType = self == .showUserLocation ? .user : .pin
|
||||
|
||||
let mapStyleURL = URL(string: "https://api.maptiler.com/maps/streets/style.json?key=fU3vlMsMn4Jb6dnEIFsx")!
|
||||
let viewModel = StaticLocationViewingViewModel(mapStyleURL: mapStyleURL,
|
||||
avatarData: AvatarInput(mxContentUri: "", matrixItemId: "alice:matrix.org", displayName: "Alice"),
|
||||
location: location,
|
||||
coordinateType: coordinateType)
|
||||
|
||||
return ([viewModel],
|
||||
AnyView(StaticLocationView(viewModel: viewModel.context)
|
||||
.addDependency(MockAvatarService.example))
|
||||
)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,60 @@
|
|||
//
|
||||
// Copyright 2021 New Vector Ltd
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import Combine
|
||||
import CoreLocation
|
||||
|
||||
// MARK: View model
|
||||
|
||||
enum StaticLocationViewingViewAction {
|
||||
case close
|
||||
case share
|
||||
}
|
||||
|
||||
enum StaticLocationViewingViewModelResult {
|
||||
case close
|
||||
case share(_ coordinate: CLLocationCoordinate2D)
|
||||
}
|
||||
|
||||
// MARK: View
|
||||
|
||||
@available(iOS 14, *)
|
||||
struct StaticLocationViewingViewState: BindableState {
|
||||
|
||||
/// Map style URL
|
||||
let mapStyleURL: URL
|
||||
|
||||
/// Current user avatarData
|
||||
let userAvatarData: AvatarInputProtocol
|
||||
|
||||
/// Shared annotation to display existing location
|
||||
let sharedAnnotation: LocationAnnotation
|
||||
|
||||
var showLoadingIndicator: Bool = false
|
||||
|
||||
var shareButtonEnabled: Bool {
|
||||
!showLoadingIndicator
|
||||
}
|
||||
|
||||
let errorSubject = PassthroughSubject<LocationSharingViewError, Never>()
|
||||
|
||||
var bindings = StaticLocationViewingViewBindings()
|
||||
}
|
||||
|
||||
struct StaticLocationViewingViewBindings {
|
||||
var alertInfo: AlertInfo<LocationSharingAlertType>?
|
||||
}
|
|
@ -0,0 +1,86 @@
|
|||
//
|
||||
// Copyright 2021 New Vector Ltd
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import CoreLocation
|
||||
|
||||
@available(iOS 14, *)
|
||||
typealias StaticLocationViewingViewModelType = StateStoreViewModel<StaticLocationViewingViewState,
|
||||
Never,
|
||||
StaticLocationViewingViewAction>
|
||||
@available(iOS 14, *)
|
||||
class StaticLocationViewingViewModel: StaticLocationViewingViewModelType, StaticLocationViewingViewModelProtocol {
|
||||
|
||||
// MARK: - Properties
|
||||
|
||||
// MARK: Private
|
||||
|
||||
private var mapViewErrorAlertInfoBuilder: MapViewErrorAlertInfoBuilder
|
||||
|
||||
// MARK: Public
|
||||
|
||||
var completion: ((StaticLocationViewingViewModelResult) -> Void)?
|
||||
|
||||
// MARK: - Setup
|
||||
|
||||
init(mapStyleURL: URL, avatarData: AvatarInputProtocol, location: CLLocationCoordinate2D, coordinateType: LocationSharingCoordinateType) {
|
||||
let sharedAnnotation: LocationAnnotation
|
||||
switch coordinateType {
|
||||
case .user:
|
||||
sharedAnnotation = UserLocationAnnotation(avatarData: avatarData, coordinate: location)
|
||||
case .pin:
|
||||
sharedAnnotation = PinLocationAnnotation(coordinate: location)
|
||||
}
|
||||
|
||||
let viewState = StaticLocationViewingViewState(mapStyleURL: mapStyleURL,
|
||||
userAvatarData: avatarData,
|
||||
sharedAnnotation: sharedAnnotation)
|
||||
|
||||
mapViewErrorAlertInfoBuilder = MapViewErrorAlertInfoBuilder()
|
||||
|
||||
super.init(initialViewState: viewState)
|
||||
|
||||
state.errorSubject.sink { [weak self] error in
|
||||
guard let self = self else { return }
|
||||
self.processError(error)
|
||||
}.store(in: &cancellables)
|
||||
}
|
||||
|
||||
// MARK: - Public
|
||||
|
||||
override func process(viewAction: StaticLocationViewingViewAction) {
|
||||
switch viewAction {
|
||||
case .close:
|
||||
completion?(.close)
|
||||
case .share:
|
||||
completion?(.share(state.sharedAnnotation.coordinate))
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Private
|
||||
|
||||
private func processError(_ error: LocationSharingViewError) {
|
||||
guard state.bindings.alertInfo == nil else {
|
||||
return
|
||||
}
|
||||
|
||||
let alertInfo = mapViewErrorAlertInfoBuilder.build(with: error) { [weak self] in
|
||||
self?.completion?(.close)
|
||||
}
|
||||
|
||||
state.bindings.alertInfo = alertInfo
|
||||
}
|
||||
}
|
|
@ -0,0 +1,24 @@
|
|||
//
|
||||
// Copyright 2021 New Vector Ltd
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
protocol StaticLocationViewingViewModelProtocol {
|
||||
|
||||
var completion: ((StaticLocationViewingViewModelResult) -> Void)? { get set }
|
||||
@available(iOS 14, *)
|
||||
var context: StaticLocationViewingViewModelType.Context { get }
|
||||
}
|
|
@ -0,0 +1,39 @@
|
|||
//
|
||||
// Copyright 2021 New Vector Ltd
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import XCTest
|
||||
import RiotSwiftUI
|
||||
|
||||
@available(iOS 14.0, *)
|
||||
class StaticLocationViewingUITests: MockScreenTest {
|
||||
|
||||
private var app: XCUIApplication!
|
||||
|
||||
override func setUp() {
|
||||
continueAfterFailure = false
|
||||
|
||||
app = XCUIApplication()
|
||||
app.launch()
|
||||
}
|
||||
|
||||
func testInitialExistingLocation() {
|
||||
goToScreenWithIdentifier(MockStaticLocationViewingScreenState.showUserLocation.title)
|
||||
|
||||
XCTAssertTrue(app.buttons["Cancel"].exists)
|
||||
XCTAssertTrue(app.buttons["StaticLocationView.shareButton"].exists)
|
||||
XCTAssertTrue(app.otherElements["Map"].exists)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,90 @@
|
|||
//
|
||||
// Copyright 2021 New Vector Ltd
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import XCTest
|
||||
import Combine
|
||||
import CoreLocation
|
||||
|
||||
@testable import RiotSwiftUI
|
||||
|
||||
@available(iOS 14.0, *)
|
||||
class StaticLocationViewingViewModelTests: XCTestCase {
|
||||
|
||||
var cancellables = Set<AnyCancellable>()
|
||||
|
||||
func testInitialState() {
|
||||
let viewModel = buildViewModel()
|
||||
|
||||
XCTAssertTrue(viewModel.context.viewState.shareButtonEnabled)
|
||||
XCTAssertFalse(viewModel.context.viewState.showLoadingIndicator)
|
||||
|
||||
XCTAssertNotNil(viewModel.context.viewState.mapStyleURL)
|
||||
XCTAssertNotNil(viewModel.context.viewState.userAvatarData)
|
||||
|
||||
XCTAssertNil(viewModel.context.viewState.bindings.alertInfo)
|
||||
}
|
||||
|
||||
func testCancellation() {
|
||||
let viewModel = buildViewModel()
|
||||
|
||||
let expectation = self.expectation(description: "Cancellation completion should be invoked")
|
||||
|
||||
viewModel.completion = { result in
|
||||
switch result {
|
||||
case .share:
|
||||
XCTFail()
|
||||
case .close:
|
||||
expectation.fulfill()
|
||||
}
|
||||
}
|
||||
|
||||
viewModel.context.send(viewAction: .close)
|
||||
|
||||
waitForExpectations(timeout: 3)
|
||||
}
|
||||
|
||||
func testShareExistingLocation() {
|
||||
let viewModel = buildViewModel()
|
||||
|
||||
let expectation = self.expectation(description: "Share completion should be invoked")
|
||||
|
||||
viewModel.completion = { result in
|
||||
switch result {
|
||||
case .share(let coordinate):
|
||||
XCTAssertEqual(coordinate.latitude, viewModel.context.viewState.sharedAnnotation.coordinate.latitude)
|
||||
XCTAssertEqual(coordinate.longitude, viewModel.context.viewState.sharedAnnotation.coordinate.longitude)
|
||||
expectation.fulfill()
|
||||
case .close:
|
||||
XCTFail()
|
||||
}
|
||||
}
|
||||
|
||||
XCTAssertNotNil(viewModel.context.viewState.sharedAnnotation)
|
||||
|
||||
viewModel.context.send(viewAction: .share)
|
||||
|
||||
XCTAssertNil(viewModel.context.viewState.bindings.alertInfo)
|
||||
|
||||
waitForExpectations(timeout: 3)
|
||||
}
|
||||
|
||||
private func buildViewModel() -> StaticLocationViewingViewModel {
|
||||
StaticLocationViewingViewModel(mapStyleURL: URL(string: "http://empty.com")!,
|
||||
avatarData: AvatarInput(mxContentUri: "", matrixItemId: "", displayName: ""),
|
||||
location: CLLocationCoordinate2D(latitude: 51.4932641, longitude: -0.257096),
|
||||
coordinateType: .user)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,98 @@
|
|||
//
|
||||
// Copyright 2021 New Vector Ltd
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
@available(iOS 14.0, *)
|
||||
struct StaticLocationView: View {
|
||||
|
||||
// MARK: - Properties
|
||||
|
||||
// MARK: Private
|
||||
|
||||
@Environment(\.theme) private var theme
|
||||
|
||||
// MARK: Public
|
||||
|
||||
@ObservedObject var viewModel: StaticLocationViewingViewModel.Context
|
||||
|
||||
// MARK: Views
|
||||
|
||||
var body: some View {
|
||||
NavigationView {
|
||||
ZStack(alignment: .bottom) {
|
||||
LocationSharingMapView(tileServerMapURL: viewModel.viewState.mapStyleURL,
|
||||
annotations: [viewModel.viewState.sharedAnnotation],
|
||||
highlightedAnnotation: viewModel.viewState.sharedAnnotation,
|
||||
userAvatarData: viewModel.viewState.userAvatarData,
|
||||
showsUserLocation: false,
|
||||
userLocation: Binding.constant(nil),
|
||||
mapCenterCoordinate: Binding.constant(nil),
|
||||
errorSubject: viewModel.viewState.errorSubject)
|
||||
MapCreditsView()
|
||||
}
|
||||
.ignoresSafeArea(.all, edges: [.bottom])
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .navigationBarLeading) {
|
||||
Button(VectorL10n.cancel, action: {
|
||||
viewModel.send(viewAction: .close)
|
||||
})
|
||||
}
|
||||
ToolbarItem(placement: .principal) {
|
||||
Text(VectorL10n.locationSharingTitle)
|
||||
.font(.headline)
|
||||
.foregroundColor(theme.colors.primaryContent)
|
||||
}
|
||||
ToolbarItem(placement: .navigationBarTrailing) {
|
||||
Button {
|
||||
viewModel.send(viewAction: .share)
|
||||
} label: {
|
||||
Image(uiImage: Asset.Images.locationShareIcon.image)
|
||||
.accessibilityIdentifier("LocationSharingView.shareButton")
|
||||
}
|
||||
.disabled(!viewModel.viewState.shareButtonEnabled)
|
||||
}
|
||||
}
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.introspectNavigationController { navigationController in
|
||||
ThemeService.shared().theme.applyStyle(onNavigationBar: navigationController.navigationBar)
|
||||
}
|
||||
.alert(item: $viewModel.alertInfo) { info in
|
||||
info.alert
|
||||
}
|
||||
}
|
||||
.accentColor(theme.colors.accent)
|
||||
.activityIndicator(show: viewModel.viewState.showLoadingIndicator)
|
||||
.navigationViewStyle(StackNavigationViewStyle())
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private var activityIndicator: some View {
|
||||
if viewModel.viewState.showLoadingIndicator {
|
||||
ActivityIndicator()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Previews
|
||||
|
||||
@available(iOS 14.0, *)
|
||||
struct StaticLocationSharingViewer_Previews: PreviewProvider {
|
||||
static let stateRenderer = MockStaticLocationViewingScreenState.stateRenderer
|
||||
static var previews: some View {
|
||||
stateRenderer.screenGroup()
|
||||
}
|
||||
}
|
1
changelog.d/5982.change
Normal file
1
changelog.d/5982.change
Normal file
|
@ -0,0 +1 @@
|
|||
Location sharing: Create a screen specific for viewing static shared location
|
Loading…
Reference in a new issue