WIP: [ios] Add icons to the live activity widget #10408
|
@ -5,6 +5,7 @@
|
|||
|
||||
@class MWMMapSearchResult;
|
||||
@class TrackInfo;
|
||||
@class ElevationProfileData;
|
||||
|
||||
typedef NS_ENUM(NSUInteger, MWMZoomMode) { MWMZoomModeIn = 0, MWMZoomModeOut };
|
||||
|
||||
|
@ -20,14 +21,17 @@ NS_ASSUME_NONNULL_BEGIN
|
|||
typedef void (^SearchInDownloaderCompletions)(NSArray<MWMMapSearchResult *> *results, BOOL finished);
|
||||
typedef void (^TrackRecordingUpdatedHandler)(TrackInfo * _Nonnull trackInfo);
|
||||
|
||||
@protocol TrackRecorder <NSObject>
|
||||
@protocol TrackRecorder
|
||||
|
||||
+ (void)startTrackRecording;
|
||||
+ (void)setTrackRecordingUpdateHandler:(TrackRecordingUpdatedHandler _Nullable)trackRecordingDidUpdate;
|
||||
+ (void)stopTrackRecording;
|
||||
+ (void)saveTrackRecordingWithName:(nullable NSString *)name;
|
||||
+ (void)saveTrackRecordingWithName:(nonnull NSString *)name;
|
||||
+ (BOOL)isTrackRecordingEnabled;
|
||||
+ (BOOL)isTrackRecordingEmpty;
|
||||
/// Returns current track recording elevation info.
|
||||
/// If the track recording is not in progress, returns empty ElevationProfileData.
|
||||
+ (ElevationProfileData * _Nonnull)trackRecordingElevationInfo;
|
||||
|
||||
@end
|
||||
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
#import "ProductsConfiguration+Core.h"
|
||||
#import "Product+Core.h"
|
||||
#import "TrackInfo+Core.h"
|
||||
#import "ElevationProfileData+Core.h"
|
||||
|
||||
#include "Framework.h"
|
||||
|
||||
|
@ -234,8 +235,8 @@ static Framework::ProductsPopupCloseReason ConvertProductPopupCloseReasonToCore(
|
|||
GetFramework().StopTrackRecording();
|
||||
}
|
||||
|
||||
+ (void)saveTrackRecordingWithName:(nullable NSString *)name {
|
||||
GetFramework().SaveTrackRecordingWithName(name == nil ? "" : name.UTF8String);
|
||||
+ (void)saveTrackRecordingWithName:(nonnull NSString *)name {
|
||||
GetFramework().SaveTrackRecordingWithName(name.UTF8String);
|
||||
}
|
||||
|
||||
+ (BOOL)isTrackRecordingEnabled {
|
||||
|
@ -246,6 +247,10 @@ static Framework::ProductsPopupCloseReason ConvertProductPopupCloseReasonToCore(
|
|||
return GetFramework().IsTrackRecordingEmpty();
|
||||
}
|
||||
|
||||
+ (ElevationProfileData * _Nonnull)trackRecordingElevationInfo {
|
||||
return [[ElevationProfileData alloc] initWithElevationInfo:GetFramework().GetTrackRecordingElevationInfo()];
|
||||
}
|
||||
|
||||
// MARK: - ProductsManager
|
||||
|
||||
+ (nullable ProductsConfiguration *)getProductsConfiguration {
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
#import <Foundation/Foundation.h>
|
||||
|
||||
@class PlacePageScheduleData;
|
||||
@class TrackInfo;
|
||||
|
||||
typedef NS_ENUM(NSInteger, PlacePageDataHotelType) {
|
||||
PlacePageDataHotelTypeHotel,
|
||||
|
@ -39,6 +40,8 @@ NS_ASSUME_NONNULL_BEGIN
|
|||
@property(nonatomic, readonly) PlacePageDataSchedule schedule;
|
||||
@property(nonatomic, readonly) BOOL isMyPosition;
|
||||
|
||||
- (instancetype)initWithTrackInfo:(TrackInfo * _Nonnull)trackInfo;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
|
|
|
@ -1,4 +1,8 @@
|
|||
#import "PlacePagePreviewData+Core.h"
|
||||
#import "DistanceFormatter.h"
|
||||
#import "AltitudeFormatter.h"
|
||||
#import "DurationFormatter.h"
|
||||
#import "TrackInfo.h"
|
||||
|
||||
#include "3party/opening_hours/opening_hours.hpp"
|
||||
|
||||
|
@ -46,6 +50,18 @@ static PlacePageDataSchedule convertOpeningHours(std::string_view rawOH)
|
|||
|
||||
@implementation PlacePagePreviewData
|
||||
|
||||
- (instancetype)initWithTrackInfo:(TrackInfo * _Nonnull)trackInfo {
|
||||
self = [super init];
|
||||
if (self) {
|
||||
// TODO: (KK) Replace separator with a shared static constant.
|
||||
NSString * kSeparator = @" • ";
|
||||
NSString * duration = [DurationFormatter durationStringFromTimeInterval:trackInfo.duration];
|
||||
NSString * distance = [DistanceFormatter distanceStringFromMeters:trackInfo.distance];
|
||||
_title = [@[duration, distance] componentsJoinedByString:kSeparator];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@implementation PlacePagePreviewData (Core)
|
||||
|
|
|
@ -10,8 +10,10 @@ NS_ASSUME_NONNULL_BEGIN
|
|||
|
||||
@property(nonatomic, readonly) MWMTrackID trackId;
|
||||
@property(nonatomic, readonly) MWMMarkGroupID groupId;
|
||||
@property(nonatomic, readonly, nonnull) TrackInfo * trackInfo;
|
||||
@property(nonatomic, readonly, nullable) ElevationProfileData * elevationProfileData;
|
||||
@property(nonatomic, readwrite, nonnull) TrackInfo * trackInfo;
|
||||
@property(nonatomic, readwrite, nullable) ElevationProfileData * elevationProfileData;
|
||||
|
||||
- (instancetype)initWithTrackInfo:(TrackInfo * _Nonnull)trackInfo elevationInfo:(ElevationProfileData * _Nullable)elevationInfo;
|
||||
|
||||
@end
|
||||
|
||||
|
|
|
@ -4,6 +4,15 @@
|
|||
|
||||
@implementation PlacePageTrackData
|
||||
|
||||
- (nonnull instancetype)initWithTrackInfo:(TrackInfo *)trackInfo elevationInfo:(ElevationProfileData * _Nullable)elevationInfo {
|
||||
self = [super init];
|
||||
if (self) {
|
||||
_trackInfo = trackInfo;
|
||||
_elevationProfileData = elevationInfo;
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@implementation PlacePageTrackData (Core)
|
||||
|
|
|
@ -11,6 +11,7 @@ NS_ASSUME_NONNULL_BEGIN
|
|||
elevationInfo:(ElevationInfo const &)elevationInfo
|
||||
activePoint:(double)activePoint
|
||||
myPosition:(double)myPosition;
|
||||
- (instancetype)initWithElevationInfo:(ElevationInfo const &)elevationInfo;
|
||||
|
||||
@end
|
||||
|
||||
|
|
|
@ -13,6 +13,7 @@ typedef NS_ENUM(NSInteger, ElevationDifficulty) {
|
|||
@interface ElevationProfileData : NSObject
|
||||
|
||||
@property(nonatomic, readonly) uint64_t trackId;
|
||||
@property(nonatomic, readonly) BOOL isTrackRecording;
|
||||
@property(nonatomic, readonly) ElevationDifficulty difficulty;
|
||||
@property(nonatomic, readonly) NSArray<ElevationHeightPoint *> * points;
|
||||
@property(nonatomic, readonly) double activePoint;
|
||||
|
|
|
@ -30,23 +30,36 @@ static ElevationDifficulty convertDifficulty(uint8_t difficulty) {
|
|||
if (self) {
|
||||
_trackId = trackId;
|
||||
_difficulty = convertDifficulty(elevationInfo.GetDifficulty());
|
||||
|
||||
auto const & points = elevationInfo.GetPoints();
|
||||
NSMutableArray * pointsArray = [[NSMutableArray alloc] initWithCapacity:points.size()];
|
||||
for (auto const & point : points) {
|
||||
auto pointLatLon = mercator::ToLatLon(point.m_point.GetPoint());
|
||||
CLLocationCoordinate2D coordinates = CLLocationCoordinate2DMake(pointLatLon.m_lat, pointLatLon.m_lon);
|
||||
ElevationHeightPoint * elevationPoint = [[ElevationHeightPoint alloc] initWithCoordinates:coordinates
|
||||
distance:point.m_distance
|
||||
andAltitude:point.m_point.GetAltitude()];
|
||||
[pointsArray addObject:elevationPoint];
|
||||
}
|
||||
_points = [pointsArray copy];
|
||||
_points = [ElevationProfileData pointsFromElevationInfo:elevationInfo];
|
||||
_activePoint = activePoint;
|
||||
_myPosition = myPosition;
|
||||
_isTrackRecording = false;
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (instancetype)initWithElevationInfo:(ElevationInfo const &)elevationInfo {
|
||||
self = [super init];
|
||||
if (self) {
|
||||
_difficulty = convertDifficulty(elevationInfo.GetDifficulty());
|
||||
_points = [ElevationProfileData pointsFromElevationInfo:elevationInfo];
|
||||
_isTrackRecording = true;
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
+ (NSArray<ElevationHeightPoint *> *)pointsFromElevationInfo:(ElevationInfo const &)elevationInfo {
|
||||
auto const & points = elevationInfo.GetPoints();
|
||||
NSMutableArray * pointsArray = [[NSMutableArray alloc] initWithCapacity:points.size()];
|
||||
for (auto const & point : points) {
|
||||
auto pointLatLon = mercator::ToLatLon(point.m_point.GetPoint());
|
||||
CLLocationCoordinate2D coordinates = CLLocationCoordinate2DMake(pointLatLon.m_lat, pointLatLon.m_lon);
|
||||
ElevationHeightPoint * elevationPoint = [[ElevationHeightPoint alloc] initWithCoordinates:coordinates
|
||||
distance:point.m_distance
|
||||
andAltitude:point.m_point.GetAltitude()];
|
||||
[pointsArray addObject:elevationPoint];
|
||||
}
|
||||
return [pointsArray copy];
|
||||
}
|
||||
|
||||
@end
|
||||
|
|
|
@ -10,6 +10,7 @@
|
|||
@class PlacePageBookmarkData;
|
||||
@class MWMMapNodeAttributes;
|
||||
@class TrackInfo;
|
||||
@class ElevationProfileData;
|
||||
|
||||
typedef NS_ENUM(NSInteger, PlacePageRoadType) {
|
||||
PlacePageRoadTypeToll,
|
||||
|
@ -49,12 +50,15 @@ NS_ASSUME_NONNULL_BEGIN
|
|||
@property(nonatomic, readonly) CLLocationCoordinate2D locationCoordinate;
|
||||
@property(nonatomic, copy, nullable) MWMVoidBlock onBookmarkStatusUpdate;
|
||||
@property(nonatomic, copy, nullable) MWMVoidBlock onMapNodeStatusUpdate;
|
||||
@property(nonatomic, copy, nullable) MWMVoidBlock onTrackRecordingProgressUpdate;
|
||||
@property(nonatomic, copy, nullable) void (^onMapNodeProgressUpdate)(uint64_t downloadedBytes, uint64_t totalBytes);
|
||||
|
||||
- (instancetype)initWithLocalizationProvider:(id<IOpeningHoursLocalization>)localization;
|
||||
- (instancetype)initWithTrackInfo:(TrackInfo * _Nonnull)trackInfo elevationInfo:(ElevationProfileData * _Nullable)elevationInfo;
|
||||
- (instancetype)init NS_UNAVAILABLE;
|
||||
|
||||
- (void)updateBookmarkStatus;
|
||||
- (void)updateWithTrackInfo:(TrackInfo * _Nonnull)trackInfo elevationInfo:(ElevationProfileData * _Nullable)elevationInfo;
|
||||
|
||||
@end
|
||||
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
#import "PlacePageInfoData+Core.h"
|
||||
#import "PlacePageBookmarkData+Core.h"
|
||||
#import "PlacePageTrackData+Core.h"
|
||||
#import "ElevationProfileData+Core.h"
|
||||
#import "MWMMapNodeAttributes.h"
|
||||
|
||||
#include <CoreApi/CoreApi.h>
|
||||
|
@ -84,6 +85,25 @@ static PlacePageRoadType convertRoadType(RoadWarningMarkType roadType) {
|
|||
return self;
|
||||
}
|
||||
|
||||
- (instancetype)initWithTrackInfo:(TrackInfo * _Nonnull)trackInfo elevationInfo:(ElevationProfileData * _Nullable)elevationInfo {
|
||||
self = [super init];
|
||||
if (self) {
|
||||
_objectType = PlacePageObjectTypeTrackRecording;
|
||||
_roadType = PlacePageRoadTypeNone;
|
||||
_previewData = [[PlacePagePreviewData alloc] initWithTrackInfo:trackInfo];
|
||||
_trackData = [[PlacePageTrackData alloc] initWithTrackInfo:trackInfo elevationInfo:elevationInfo];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)updateWithTrackInfo:(TrackInfo * _Nonnull)trackInfo elevationInfo:(ElevationProfileData * _Nullable)elevationInfo {
|
||||
_previewData = [[PlacePagePreviewData alloc] initWithTrackInfo:trackInfo];
|
||||
_trackData.trackInfo = trackInfo;
|
||||
_trackData.elevationProfileData = elevationInfo;
|
||||
if (self.onTrackRecordingProgressUpdate != nil)
|
||||
self.onTrackRecordingProgressUpdate();
|
||||
}
|
||||
|
||||
- (void)dealloc {
|
||||
if (self.mapNodeAttributes != nil) {
|
||||
[[MWMStorage sharedStorage] removeObserver:self];
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
|
||||
@class MapViewController;
|
||||
@class BottomTabBarViewController;
|
||||
@class TrackRecordingViewController;
|
||||
@class TrackRecordingButtonViewController;
|
||||
|
||||
@protocol MWMFeatureHolder;
|
||||
|
||||
|
@ -20,7 +20,7 @@
|
|||
@property(nonatomic) MWMBottomMenuState menuRestoreState;
|
||||
@property(nonatomic) BOOL isDirectionViewHidden;
|
||||
@property(nonatomic) BottomTabBarViewController * tabBarController;
|
||||
@property(nonatomic) TrackRecordingViewController * trackRecordingButton;
|
||||
@property(nonatomic) TrackRecordingButtonViewController * trackRecordingButton;
|
||||
|
||||
- (instancetype)init __attribute__((unavailable("init is not available")));
|
||||
- (instancetype)initWithParentController:(MapViewController *)controller;
|
||||
|
|
|
@ -288,7 +288,7 @@ NSString *const kMapToCategorySelectorSegue = @"MapToCategorySelectorSegue";
|
|||
_trackRecordingButton = nil;
|
||||
}
|
||||
else if (!trackRecordingButtonHidden && !_trackRecordingButton) {
|
||||
_trackRecordingButton = [[TrackRecordingViewController alloc] init];
|
||||
_trackRecordingButton = [[TrackRecordingButtonViewController alloc] init];
|
||||
[MWMMapWidgetsHelper updateLayoutForAvailableArea];
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
final class TrackRecordingViewController: MWMViewController {
|
||||
final class TrackRecordingButtonViewController: MWMViewController {
|
||||
|
||||
private enum Constants {
|
||||
static let buttonDiameter = CGFloat(48)
|
||||
|
@ -50,6 +50,11 @@ final class TrackRecordingViewController: MWMViewController {
|
|||
|
||||
// MARK: - Public methods
|
||||
|
||||
@objc
|
||||
func setHidden(_ hidden: Bool) {
|
||||
button.isHidden = hidden
|
||||
}
|
||||
|
||||
@objc
|
||||
func close(completion: @escaping (() -> Void)) {
|
||||
stopTimer()
|
||||
|
@ -75,7 +80,7 @@ final class TrackRecordingViewController: MWMViewController {
|
|||
button.tintColor = Constants.color.darker
|
||||
button.translatesAutoresizingMaskIntoConstraints = false
|
||||
button.setImage(UIImage(resource: .icMenuBookmarkTrackRecording), for: .normal)
|
||||
button.addTarget(self, action: #selector(onTrackRecordingButtonPressed), for: .touchUpInside)
|
||||
button.addTarget(self, action: #selector(didTap), for: .touchUpInside)
|
||||
button.isHidden = true
|
||||
}
|
||||
|
||||
|
@ -134,12 +139,7 @@ final class TrackRecordingViewController: MWMViewController {
|
|||
// MARK: - Actions
|
||||
|
||||
@objc
|
||||
private func onTrackRecordingButtonPressed(_ sender: Any) {
|
||||
switch trackRecordingManager.recordingState {
|
||||
case .inactive:
|
||||
trackRecordingManager.processAction(.start)
|
||||
case .active:
|
||||
trackRecordingManager.processAction(.stop)
|
||||
}
|
||||
private func didTap(_ sender: Any) {
|
||||
MapViewController.shared()?.showTrackRecordingPlacePage()
|
||||
}
|
||||
}
|
|
@ -33,6 +33,7 @@
|
|||
- (void)openFullPlaceDescriptionWithHtml:(NSString *_Nonnull)htmlString;
|
||||
- (void)searchText:(NSString *_Nonnull)text;
|
||||
- (void)openDrivingOptions;
|
||||
- (void)showTrackRecordingPlacePage;
|
||||
|
||||
- (void)setPlacePageTopBound:(CGFloat)bound duration:(double)duration;
|
||||
|
||||
|
|
|
@ -76,6 +76,7 @@ NSString *const kSettingsSegue = @"Map2Settings";
|
|||
|
||||
@property(nonatomic, readwrite) MWMMapViewControlsManager *controlsManager;
|
||||
@property(nonatomic, readwrite) SearchOnMapManager *searchManager;
|
||||
@property(nonatomic, readwrite) TrackRecordingManager *trackRecordingManager;
|
||||
|
||||
@property(nonatomic) BOOL disableStandbyOnLocationStateMode;
|
||||
|
||||
|
@ -118,15 +119,48 @@ NSString *const kSettingsSegue = @"Map2Settings";
|
|||
|
||||
#pragma mark - Map Navigation
|
||||
|
||||
- (void)showTrackRecordingPlacePage {
|
||||
if ([self.trackRecordingManager contains:self]) {
|
||||
[self dismissPlacePage];
|
||||
return;
|
||||
}
|
||||
__block PlacePageData * placePageData = [[PlacePageData alloc] initWithTrackInfo:TrackRecordingManager.shared.trackRecordingInfo
|
||||
elevationInfo:[MWMFrameworkHelper trackRecordingElevationInfo]];
|
||||
__weak __typeof(self) weakSelf = self;
|
||||
[self.trackRecordingManager addObserver:self recordingIsActiveDidChangeHandler:^(TrackRecordingState state, TrackInfo * _Nonnull trackInfo) {
|
||||
__strong __typeof(weakSelf) self = weakSelf;
|
||||
switch (state) {
|
||||
case TrackRecordingStateInactive:
|
||||
[self stopObservingTrackRecordingUpdates];
|
||||
break;
|
||||
case TrackRecordingStateActive:
|
||||
if (UIApplication.sharedApplication.applicationState != UIApplicationStateActive)
|
||||
return;
|
||||
[self.controlsManager.trackRecordingButton setHidden:YES];
|
||||
[placePageData updateWithTrackInfo:trackInfo elevationInfo:[MWMFrameworkHelper trackRecordingElevationInfo]];
|
||||
break;
|
||||
}
|
||||
}];
|
||||
[self.controlsManager.trackRecordingButton setHidden:YES];
|
||||
[self showOrUpdatePlacePage:placePageData];
|
||||
}
|
||||
|
||||
- (void)stopObservingTrackRecordingUpdates {
|
||||
[self.trackRecordingManager removeObserver:self];
|
||||
if (self.trackRecordingManager.isActive)
|
||||
[self.controlsManager.trackRecordingButton setHidden:NO];
|
||||
}
|
||||
|
||||
- (void)showOrUpdatePlacePage:(PlacePageData *)data {
|
||||
if (self.searchManager.isSearching)
|
||||
[self.searchManager setPlaceOnMapSelected:YES];
|
||||
|
||||
self.controlsManager.trafficButtonHidden = YES;
|
||||
if (self.placePageVC != nil) {
|
||||
[PlacePageBuilder update:(PlacePageViewController *)self.placePageVC with:data];
|
||||
[PlacePageBuilder update:self.placePageVC with:data];
|
||||
return;
|
||||
}
|
||||
|
||||
[self showPlacePageFor:data];
|
||||
}
|
||||
|
||||
|
@ -190,6 +224,7 @@ NSString *const kSettingsSegue = @"Map2Settings";
|
|||
}
|
||||
|
||||
- (void)hideRegularPlacePage {
|
||||
[self stopObservingTrackRecordingUpdates];
|
||||
[self.placePageVC closeAnimatedWithCompletion:^{
|
||||
[self.placePageVC.view removeFromSuperview];
|
||||
[self.placePageVC willMoveToParentViewController:nil];
|
||||
|
@ -233,6 +268,7 @@ NSString *const kSettingsSegue = @"Map2Settings";
|
|||
return;
|
||||
}
|
||||
PlacePageData * data = [[PlacePageData alloc] initWithLocalizationProvider:[[OpeinigHoursLocalization alloc] init]];
|
||||
[self stopObservingTrackRecordingUpdates];
|
||||
[self showOrUpdatePlacePage:data];
|
||||
}
|
||||
|
||||
|
@ -730,6 +766,12 @@ NSString *const kSettingsSegue = @"Map2Settings";
|
|||
return _searchManager;
|
||||
}
|
||||
|
||||
- (TrackRecordingManager *)trackRecordingManager {
|
||||
if (!_trackRecordingManager)
|
||||
_trackRecordingManager = TrackRecordingManager.shared;
|
||||
return _trackRecordingManager;
|
||||
}
|
||||
|
||||
- (UIView * _Nullable)searchViewContainer {
|
||||
return self.searchManager.viewController.view;
|
||||
}
|
||||
|
|
|
@ -3,8 +3,15 @@
|
|||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@protocol LocationService
|
||||
|
||||
+ (BOOL)isLocationProhibited;
|
||||
+ (void)checkLocationStatus;
|
||||
|
||||
@end
|
||||
|
||||
NS_SWIFT_NAME(LocationManager)
|
||||
@interface MWMLocationManager : NSObject
|
||||
@interface MWMLocationManager : NSObject<LocationService>
|
||||
|
||||
+ (void)start;
|
||||
+ (void)stop;
|
||||
|
@ -14,10 +21,8 @@ NS_SWIFT_NAME(LocationManager)
|
|||
+ (void)removeObserver:(id<MWMLocationObserver>)observer NS_SWIFT_NAME(remove(observer:));
|
||||
|
||||
+ (void)setMyPositionMode:(MWMMyPositionMode)mode;
|
||||
+ (void)checkLocationStatus;
|
||||
|
||||
+ (nullable CLLocation *)lastLocation;
|
||||
+ (BOOL)isLocationProhibited;
|
||||
+ (nullable CLHeading *)lastHeading;
|
||||
|
||||
+ (void)applicationDidBecomeActive;
|
||||
|
|
|
@ -185,6 +185,7 @@ extension GlobalStyleSheet: IStyleSheet {
|
|||
case .trackRecordingWidgetButton:
|
||||
return .addFrom(Self.bottomTabBarButton) { s in
|
||||
s.cornerRadius = 23
|
||||
s.coloring = .red
|
||||
}
|
||||
case .blackOpaqueBackground:
|
||||
return .add { s in
|
||||
|
|
|
@ -55,12 +55,20 @@ private extension TrackRecordingLiveActivityAttributes.ContentState {
|
|||
let ascent = AltitudeFormatter.altitudeString(fromMeters: Double(trackInfo.ascent))
|
||||
let descent = AltitudeFormatter.altitudeString(fromMeters: Double(trackInfo.descent))
|
||||
|
||||
self.distance = StatisticsViewModel(key: "", value: distance)
|
||||
self.duration = StatisticsViewModel(key: "", value: duration)
|
||||
self.maxElevation = StatisticsViewModel(key: L("elevation_profile_max_elevation"), value: maxElevation)
|
||||
self.minElevation = StatisticsViewModel(key: L("elevation_profile_min_elevation"), value: minElevation)
|
||||
self.ascent = StatisticsViewModel(key: L("elevation_profile_ascent"), value: ascent)
|
||||
self.descent = StatisticsViewModel(key: L("elevation_profile_descent"), value: descent)
|
||||
self.distance = ValueViewModel(value: distance)
|
||||
self.duration = ValueViewModel(value: duration)
|
||||
self.ascent = DetailViewModel(.ascent, value: ascent)
|
||||
self.descent = DetailViewModel(.descent, value: descent)
|
||||
self.minElevation = DetailViewModel(.minElevation, value: minElevation)
|
||||
self.maxElevation = DetailViewModel(.maxElevation, value: maxElevation)
|
||||
}
|
||||
}
|
||||
|
||||
private extension TrackRecordingLiveActivityAttributes.ContentState.DetailViewModel {
|
||||
init(_ elevationDescription: ElevationDescription, value: String) {
|
||||
self.value = value
|
||||
self.key = elevationDescription.title
|
||||
self.imageName = elevationDescription.imageName
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -4,31 +4,38 @@ enum TrackRecordingState: Int, Equatable {
|
|||
case active
|
||||
}
|
||||
|
||||
enum TrackRecordingAction: String, CaseIterable {
|
||||
enum TrackRecordingAction {
|
||||
case start
|
||||
case stop
|
||||
case stopAndSave(name: String)
|
||||
}
|
||||
|
||||
enum TrackRecordingError: Error {
|
||||
case locationIsProhibited
|
||||
case trackIsEmpty
|
||||
case systemError(Error)
|
||||
}
|
||||
|
||||
protocol TrackRecordingObserver: AnyObject {
|
||||
enum TrackRecordingActionResult {
|
||||
case success
|
||||
case error(TrackRecordingError)
|
||||
}
|
||||
|
||||
@objc
|
||||
protocol TrackRecordingObservable: AnyObject {
|
||||
var recordingState: TrackRecordingState { get }
|
||||
var trackRecordingInfo: TrackInfo { get }
|
||||
|
||||
func addObserver(_ observer: AnyObject, recordingIsActiveDidChangeHandler: @escaping TrackRecordingStateHandler)
|
||||
func removeObserver(_ observer: AnyObject)
|
||||
func contains(_ observer: AnyObject) -> Bool
|
||||
}
|
||||
|
||||
typealias TrackRecordingStateHandler = (TrackRecordingState, TrackInfo?) -> Void
|
||||
typealias TrackRecordingStateHandler = (TrackRecordingState, TrackInfo) -> Void
|
||||
|
||||
@objcMembers
|
||||
final class TrackRecordingManager: NSObject {
|
||||
|
||||
typealias CompletionHandler = () -> Void
|
||||
|
||||
private enum SavingOption {
|
||||
case withoutSaving
|
||||
case saveWithName(String? = nil)
|
||||
}
|
||||
typealias CompletionHandler = (TrackRecordingActionResult) -> Void
|
||||
|
||||
fileprivate struct Observation {
|
||||
weak var observer: AnyObject?
|
||||
|
@ -37,26 +44,33 @@ final class TrackRecordingManager: NSObject {
|
|||
|
||||
static let shared: TrackRecordingManager = {
|
||||
let trackRecorder = FrameworkHelper.self
|
||||
let locationManager = LocationManager.self
|
||||
var activityManager: TrackRecordingActivityManager? = nil
|
||||
#if canImport(ActivityKit)
|
||||
if #available(iOS 16.2, *) {
|
||||
activityManager = TrackRecordingLiveActivityManager.shared
|
||||
}
|
||||
#endif
|
||||
return TrackRecordingManager(trackRecorder: trackRecorder, activityManager: activityManager)
|
||||
return TrackRecordingManager(trackRecorder: trackRecorder,
|
||||
locationService: locationManager,
|
||||
activityManager: activityManager)
|
||||
}()
|
||||
|
||||
private let trackRecorder: TrackRecorder.Type
|
||||
private var locationService: LocationService.Type
|
||||
private var activityManager: TrackRecordingActivityManager?
|
||||
private var observations: [Observation] = []
|
||||
private var trackRecordingInfo: TrackInfo?
|
||||
private(set) var trackRecordingInfo: TrackInfo = .empty()
|
||||
|
||||
var recordingState: TrackRecordingState {
|
||||
trackRecorder.isTrackRecordingEnabled() ? .active : .inactive
|
||||
}
|
||||
|
||||
private init(trackRecorder: TrackRecorder.Type, activityManager: TrackRecordingActivityManager?) {
|
||||
init(trackRecorder: TrackRecorder.Type,
|
||||
locationService: LocationService.Type,
|
||||
activityManager: TrackRecordingActivityManager?) {
|
||||
self.trackRecorder = trackRecorder
|
||||
self.locationService = locationService
|
||||
self.activityManager = activityManager
|
||||
super.init()
|
||||
}
|
||||
|
@ -84,22 +98,35 @@ final class TrackRecordingManager: NSObject {
|
|||
}
|
||||
|
||||
func processAction(_ action: TrackRecordingAction, completion: (CompletionHandler)? = nil) {
|
||||
switch action {
|
||||
case .start:
|
||||
start(completion: completion)
|
||||
case .stop:
|
||||
stop(completion: completion)
|
||||
do {
|
||||
switch action {
|
||||
case .start:
|
||||
try startRecording()
|
||||
case .stopAndSave(let name):
|
||||
stopRecording()
|
||||
try checkIsTrackNotEmpty()
|
||||
saveTrackRecording(name: name)
|
||||
}
|
||||
completion?(.success)
|
||||
} catch {
|
||||
handleError(error, completion: completion)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Private methods
|
||||
|
||||
private func checkIsLocationEnabled() throws {
|
||||
if LocationManager.isLocationProhibited() {
|
||||
private func checkIsLocationEnabled() throws(TrackRecordingError) {
|
||||
if locationService.isLocationProhibited() {
|
||||
throw TrackRecordingError.locationIsProhibited
|
||||
}
|
||||
}
|
||||
|
||||
private func checkIsTrackNotEmpty() throws(TrackRecordingError) {
|
||||
if trackRecorder.isTrackRecordingEmpty() {
|
||||
throw TrackRecordingError.trackIsEmpty
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Handle track recording process
|
||||
|
||||
private func subscribeOnTrackRecordingProgressUpdates() {
|
||||
|
@ -113,92 +140,63 @@ final class TrackRecordingManager: NSObject {
|
|||
|
||||
private func unsubscribeFromTrackRecordingProgressUpdates() {
|
||||
trackRecorder.setTrackRecordingUpdateHandler(nil)
|
||||
trackRecordingInfo = nil
|
||||
}
|
||||
|
||||
// MARK: - Handle Start/Stop event and Errors
|
||||
|
||||
private func start(completion: (CompletionHandler)? = nil) {
|
||||
do {
|
||||
private func startRecording() throws(TrackRecordingError) {
|
||||
switch recordingState {
|
||||
case .inactive:
|
||||
try checkIsLocationEnabled()
|
||||
switch recordingState {
|
||||
case .inactive:
|
||||
subscribeOnTrackRecordingProgressUpdates()
|
||||
trackRecorder.startTrackRecording()
|
||||
notifyObservers()
|
||||
try? activityManager?.start(with: trackRecordingInfo ?? .empty())
|
||||
case .active:
|
||||
break
|
||||
subscribeOnTrackRecordingProgressUpdates()
|
||||
trackRecorder.startTrackRecording()
|
||||
notifyObservers()
|
||||
do {
|
||||
try activityManager?.start(with: trackRecordingInfo)
|
||||
} catch {
|
||||
LOG(.warning, "Failed to start activity manager")
|
||||
handleError(.systemError(error))
|
||||
}
|
||||
completion?()
|
||||
} catch {
|
||||
handleError(error, completion: completion)
|
||||
case .active:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
private func stop(completion: (CompletionHandler)? = nil) {
|
||||
guard !trackRecorder.isTrackRecordingEmpty() else {
|
||||
Toast.toast(withText: L("track_recording_toast_nothing_to_save")).show()
|
||||
stopRecording(.withoutSaving, completion: completion)
|
||||
return
|
||||
}
|
||||
Self.showOnFinishRecordingAlert(onSave: { [weak self] in
|
||||
guard let self else { return }
|
||||
// TODO: (KK) pass the user provided name from the track saving screen (when it will be implemented)
|
||||
self.stopRecording(.saveWithName(), completion: completion)
|
||||
},
|
||||
onStop: { [weak self] in
|
||||
guard let self else { return }
|
||||
self.stopRecording(.withoutSaving, completion: completion)
|
||||
},
|
||||
onContinue: {
|
||||
completion?()
|
||||
})
|
||||
}
|
||||
|
||||
private func stopRecording(_ savingOption: SavingOption, completion: (CompletionHandler)? = nil) {
|
||||
private func stopRecording() {
|
||||
unsubscribeFromTrackRecordingProgressUpdates()
|
||||
trackRecorder.stopTrackRecording()
|
||||
trackRecordingInfo = .empty()
|
||||
activityManager?.stop()
|
||||
notifyObservers()
|
||||
|
||||
switch savingOption {
|
||||
case .withoutSaving:
|
||||
break
|
||||
case .saveWithName(let name):
|
||||
trackRecorder.saveTrackRecording(withName: name)
|
||||
}
|
||||
completion?()
|
||||
}
|
||||
|
||||
private func handleError(_ error: Error, completion: (CompletionHandler)? = nil) {
|
||||
LOG(.error, error.localizedDescription)
|
||||
private func saveTrackRecording(name: String) {
|
||||
trackRecorder.saveTrackRecording(withName: name)
|
||||
}
|
||||
|
||||
private func handleError(_ error: TrackRecordingError, completion: (CompletionHandler)? = nil) {
|
||||
switch error {
|
||||
case TrackRecordingError.locationIsProhibited:
|
||||
// Show alert to enable location
|
||||
LocationManager.checkLocationStatus()
|
||||
default:
|
||||
locationService.checkLocationStatus()
|
||||
case TrackRecordingError.trackIsEmpty:
|
||||
Toast.toast(withText: L("track_recording_toast_nothing_to_save")).show()
|
||||
case TrackRecordingError.systemError(let error):
|
||||
LOG(.error, error.localizedDescription)
|
||||
break
|
||||
}
|
||||
stopRecording(.withoutSaving, completion: completion)
|
||||
}
|
||||
|
||||
private static func showOnFinishRecordingAlert(onSave: @escaping CompletionHandler,
|
||||
onStop: @escaping CompletionHandler,
|
||||
onContinue: @escaping CompletionHandler) {
|
||||
let alert = UIAlertController(title: L("track_recording_alert_title"), message: nil, preferredStyle: .alert)
|
||||
alert.addAction(UIAlertAction(title: L("continue_recording"), style: .default, handler: { _ in onContinue() }))
|
||||
alert.addAction(UIAlertAction(title: L("stop_without_saving"), style: .default, handler: { _ in onStop() }))
|
||||
alert.addAction(UIAlertAction(title: L("save"), style: .cancel, handler: { _ in onSave() }))
|
||||
UIViewController.topViewController().present(alert, animated: true)
|
||||
DispatchQueue.main.async {
|
||||
completion?(.error(error))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - TrackRecordingObserver
|
||||
|
||||
extension TrackRecordingManager: TrackRecordingObserver {
|
||||
extension TrackRecordingManager: TrackRecordingObservable {
|
||||
@objc
|
||||
func addObserver(_ observer: AnyObject, recordingIsActiveDidChangeHandler: @escaping TrackRecordingStateHandler) {
|
||||
guard !observations.contains(where: { $0.observer === observer }) else { return }
|
||||
let observation = Observation(observer: observer, recordingStateDidChangeHandler: recordingIsActiveDidChangeHandler)
|
||||
observations.append(observation)
|
||||
recordingIsActiveDidChangeHandler(recordingState, trackRecordingInfo)
|
||||
|
@ -209,6 +207,11 @@ extension TrackRecordingManager: TrackRecordingObserver {
|
|||
observations.removeAll { $0.observer === observer }
|
||||
}
|
||||
|
||||
@objc
|
||||
func contains(_ observer: AnyObject) -> Bool {
|
||||
observations.contains { $0.observer === observer }
|
||||
}
|
||||
|
||||
private func notifyObservers() {
|
||||
observations = observations.filter { $0.observer != nil }
|
||||
observations.forEach { $0.recordingStateDidChangeHandler?(recordingState, trackRecordingInfo) }
|
||||
|
|
15
iphone/Maps/Images.xcassets/Place Page/ic_placepage_save_track_recording.imageset/Contents.json
vendored
Normal file
|
@ -0,0 +1,15 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "ic_track_save.png",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
},
|
||||
"properties" : {
|
||||
"template-rendering-intent" : "template"
|
||||
}
|
||||
}
|
BIN
iphone/Maps/Images.xcassets/Place Page/ic_placepage_save_track_recording.imageset/ic_track_save.png
vendored
Normal file
After Width: | Height: | Size: 4.2 KiB |
|
@ -481,7 +481,7 @@
|
|||
ED2D74652D14357F00660FBF /* TrackRecordingLiveActivityAttributes.swift in Sources */ = {isa = PBXBuildFile; fileRef = ED2D74302D14337500660FBF /* TrackRecordingLiveActivityAttributes.swift */; };
|
||||
ED2D74662D1435A600660FBF /* LiveActivityManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = ED2D742D2D14337500660FBF /* LiveActivityManager.swift */; };
|
||||
ED2E328E2D10500900807A08 /* TrackRecordingButtonArea.swift in Sources */ = {isa = PBXBuildFile; fileRef = ED46DD922D06F804007CACD6 /* TrackRecordingButtonArea.swift */; };
|
||||
ED2E32912D10501700807A08 /* TrackRecordingViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = ED49D76F2CF0E3A8004AF27E /* TrackRecordingViewController.swift */; };
|
||||
ED2E32912D10501700807A08 /* TrackRecordingButtonViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = ED49D76F2CF0E3A8004AF27E /* TrackRecordingButtonViewController.swift */; };
|
||||
ED3EAC202B03C88100220A4A /* BottomTabBarButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = ED3EAC1F2B03C88100220A4A /* BottomTabBarButton.swift */; };
|
||||
ED43B8BD2C12063500D07BAA /* DocumentPicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = ED43B8BC2C12063500D07BAA /* DocumentPicker.swift */; };
|
||||
ED46DDCE2D098A0B007CACD6 /* WidgetKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = ED46DDCD2D098A0B007CACD6 /* WidgetKit.framework */; };
|
||||
|
@ -519,11 +519,14 @@
|
|||
ED810EC52D566E9B00ECDE2C /* SearchOnMapTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = ED810EC42D566E9B00ECDE2C /* SearchOnMapTests.swift */; };
|
||||
ED8270F02C2071A3005966DA /* SettingsTableViewDetailedSwitchCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = ED8270EF2C2071A3005966DA /* SettingsTableViewDetailedSwitchCell.swift */; };
|
||||
ED83880F2D54DEB3002A0536 /* UIImage+FilledWithColor.swift in Sources */ = {isa = PBXBuildFile; fileRef = ED83880E2D54DEA4002A0536 /* UIImage+FilledWithColor.swift */; };
|
||||
ED8A921C2D772904009E063B /* ElevationDescription.swift in Sources */ = {isa = PBXBuildFile; fileRef = ED8A921B2D772904009E063B /* ElevationDescription.swift */; };
|
||||
ED914AB22D35063A00973C45 /* TextColorStyleSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = ED914AB12D35063A00973C45 /* TextColorStyleSheet.swift */; };
|
||||
ED914AB82D351DF000973C45 /* StyleApplicable.swift in Sources */ = {isa = PBXBuildFile; fileRef = ED914AB72D351DF000973C45 /* StyleApplicable.swift */; };
|
||||
ED914ABE2D351FF800973C45 /* UILabel+SetFontStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = ED914ABD2D351FF800973C45 /* UILabel+SetFontStyle.swift */; };
|
||||
ED9857082C4ED02D00694F6C /* MailComposer.swift in Sources */ = {isa = PBXBuildFile; fileRef = ED9857072C4ED02D00694F6C /* MailComposer.swift */; };
|
||||
ED9966802B94FBC20083CE55 /* ColorPicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = ED99667D2B94FBC20083CE55 /* ColorPicker.swift */; };
|
||||
ED9DDF882D6F151000645BC8 /* PlacePageTrackRecordingLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = ED9DDF872D6F151000645BC8 /* PlacePageTrackRecordingLayout.swift */; };
|
||||
ED9DDF9D2D6F6F7900645BC8 /* TrackRecordingManagerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = ED9DDF9B2D6F6DA300645BC8 /* TrackRecordingManagerTests.swift */; };
|
||||
EDA1EAA42CC7ECAD00DBDCAA /* ElevationProfileFormatter.swift in Sources */ = {isa = PBXBuildFile; fileRef = EDA1EAA32CC7ECAD00DBDCAA /* ElevationProfileFormatter.swift */; };
|
||||
EDBD68072B625724005DD151 /* LocationServicesDisabledAlert.xib in Resources */ = {isa = PBXBuildFile; fileRef = EDBD68062B625724005DD151 /* LocationServicesDisabledAlert.xib */; };
|
||||
EDBD680B2B62572E005DD151 /* LocationServicesDisabledAlert.swift in Sources */ = {isa = PBXBuildFile; fileRef = EDBD680A2B62572E005DD151 /* LocationServicesDisabledAlert.swift */; };
|
||||
|
@ -1449,7 +1452,7 @@
|
|||
ED46DDCF2D098A0B007CACD6 /* SwiftUI.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = SwiftUI.framework; path = System/Library/Frameworks/SwiftUI.framework; sourceTree = SDKROOT; };
|
||||
ED48BBB817C2B1E2003E7E92 /* CircleView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = CircleView.h; sourceTree = "<group>"; };
|
||||
ED48BBB917C2B1E2003E7E92 /* CircleView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = CircleView.m; sourceTree = "<group>"; };
|
||||
ED49D76F2CF0E3A8004AF27E /* TrackRecordingViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrackRecordingViewController.swift; sourceTree = "<group>"; };
|
||||
ED49D76F2CF0E3A8004AF27E /* TrackRecordingButtonViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrackRecordingButtonViewController.swift; sourceTree = "<group>"; };
|
||||
ED4DC7732CAEDECC0029B338 /* ProductButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProductButton.swift; sourceTree = "<group>"; };
|
||||
ED4DC7742CAEDECC0029B338 /* ProductsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProductsViewController.swift; sourceTree = "<group>"; };
|
||||
ED4DC7752CAEDECC0029B338 /* ProductsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProductsViewModel.swift; sourceTree = "<group>"; };
|
||||
|
@ -1485,11 +1488,14 @@
|
|||
ED810EC42D566E9B00ECDE2C /* SearchOnMapTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchOnMapTests.swift; sourceTree = "<group>"; };
|
||||
ED8270EF2C2071A3005966DA /* SettingsTableViewDetailedSwitchCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsTableViewDetailedSwitchCell.swift; sourceTree = "<group>"; };
|
||||
ED83880E2D54DEA4002A0536 /* UIImage+FilledWithColor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIImage+FilledWithColor.swift"; sourceTree = "<group>"; };
|
||||
ED8A921B2D772904009E063B /* ElevationDescription.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ElevationDescription.swift; sourceTree = "<group>"; };
|
||||
ED914AB12D35063A00973C45 /* TextColorStyleSheet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextColorStyleSheet.swift; sourceTree = "<group>"; };
|
||||
ED914AB72D351DF000973C45 /* StyleApplicable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StyleApplicable.swift; sourceTree = "<group>"; };
|
||||
ED914ABD2D351FF800973C45 /* UILabel+SetFontStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UILabel+SetFontStyle.swift"; sourceTree = "<group>"; };
|
||||
ED9857072C4ED02D00694F6C /* MailComposer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MailComposer.swift; sourceTree = "<group>"; };
|
||||
ED99667D2B94FBC20083CE55 /* ColorPicker.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ColorPicker.swift; sourceTree = "<group>"; };
|
||||
ED9DDF872D6F151000645BC8 /* PlacePageTrackRecordingLayout.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlacePageTrackRecordingLayout.swift; sourceTree = "<group>"; };
|
||||
ED9DDF9B2D6F6DA300645BC8 /* TrackRecordingManagerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrackRecordingManagerTests.swift; sourceTree = "<group>"; };
|
||||
EDA1EAA32CC7ECAD00DBDCAA /* ElevationProfileFormatter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ElevationProfileFormatter.swift; sourceTree = "<group>"; };
|
||||
EDBD68062B625724005DD151 /* LocationServicesDisabledAlert.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = LocationServicesDisabledAlert.xib; sourceTree = "<group>"; };
|
||||
EDBD680A2B62572E005DD151 /* LocationServicesDisabledAlert.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationServicesDisabledAlert.swift; sourceTree = "<group>"; };
|
||||
|
@ -2569,7 +2575,7 @@
|
|||
34BC72091B0DECAE0012A34B /* MapViewControls */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
ED49D76F2CF0E3A8004AF27E /* TrackRecordingViewController.swift */,
|
||||
ED49D76F2CF0E3A8004AF27E /* TrackRecordingButtonViewController.swift */,
|
||||
340537621BBED98600D452C6 /* MWMMapViewControlsCommon.h */,
|
||||
34BC72101B0DECAE0012A34B /* MWMMapViewControlsManager.h */,
|
||||
34BC72111B0DECAE0012A34B /* MWMMapViewControlsManager.mm */,
|
||||
|
@ -2765,6 +2771,7 @@
|
|||
4B4153B62BF9709100EE4B02 /* Core */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
ED9DDF982D6F6D8000645BC8 /* TrackRecorder */,
|
||||
EDF838AB2C00B9C7007E4E67 /* iCloudTests */,
|
||||
4B4153B72BF970A000EE4B02 /* TextToSpeech */,
|
||||
);
|
||||
|
@ -3031,6 +3038,7 @@
|
|||
99C6532123F2F506004322F3 /* IPlacePageLayout.swift */,
|
||||
99F3EB0223F4178200C713F8 /* PlacePageCommonLayout.swift */,
|
||||
993DF0B423F6B2EF00AC231A /* PlacePageTrackLayout.swift */,
|
||||
ED9DDF872D6F151000645BC8 /* PlacePageTrackRecordingLayout.swift */,
|
||||
);
|
||||
path = Layouts;
|
||||
sourceTree = "<group>";
|
||||
|
@ -3050,6 +3058,7 @@
|
|||
99DEF9D523E420D2006BFD21 /* ElevationProfile */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
ED8A921B2D772904009E063B /* ElevationDescription.swift */,
|
||||
99514BB223E82B450085D3A7 /* ElevationProfilePresenter.swift */,
|
||||
EDA1EAA32CC7ECAD00DBDCAA /* ElevationProfileFormatter.swift */,
|
||||
99514BB423E82B450085D3A7 /* ElevationProfileViewController.swift */,
|
||||
|
@ -3312,6 +3321,14 @@
|
|||
path = ColorPicker;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
ED9DDF982D6F6D8000645BC8 /* TrackRecorder */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
ED9DDF9B2D6F6DA300645BC8 /* TrackRecordingManagerTests.swift */,
|
||||
);
|
||||
path = TrackRecorder;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
EDC4E3422C5D1BD3009286A2 /* RecentlyDeletedTests */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
|
@ -4488,8 +4505,9 @@
|
|||
99F3EB1223F418C900C713F8 /* PlacePageInteractor.swift in Sources */,
|
||||
340708651F2905A500029ECC /* NavigationInfoArea.swift in Sources */,
|
||||
993DF0CC23F6BD0600AC231A /* ElevationDetailsPresenter.swift in Sources */,
|
||||
ED8A921C2D772904009E063B /* ElevationDescription.swift in Sources */,
|
||||
34AB666B1FC5AA330078E451 /* TransportTransitCell.swift in Sources */,
|
||||
ED2E32912D10501700807A08 /* TrackRecordingViewController.swift in Sources */,
|
||||
ED2E32912D10501700807A08 /* TrackRecordingButtonViewController.swift in Sources */,
|
||||
47E8163323B17734008FD836 /* MWMStorage+UI.m in Sources */,
|
||||
993DF11123F6BDB100AC231A /* UILabelRenderer.swift in Sources */,
|
||||
34AB66471FC5AA330078E451 /* RouteManagerTableView.swift in Sources */,
|
||||
|
@ -4630,6 +4648,7 @@
|
|||
1DFA2F6A20D3B57400FB2C66 /* UIColor+PartnerColor.m in Sources */,
|
||||
9989273B2449E60200260CE2 /* BottomMenuBuilder.swift in Sources */,
|
||||
993DF10F23F6BDB100AC231A /* UIActivityIndicatorRenderer.swift in Sources */,
|
||||
ED9DDF882D6F151000645BC8 /* PlacePageTrackRecordingLayout.swift in Sources */,
|
||||
ED0B1FEF2CAA9A25006E31A4 /* UIView+Highlight.swift in Sources */,
|
||||
99A614E423CDD1D900D8D8D0 /* UIButton+RuntimeAttributes.m in Sources */,
|
||||
343E75981E5B1EE20041226A /* MWMCollectionViewController.m in Sources */,
|
||||
|
@ -4907,6 +4926,7 @@
|
|||
EDF838C32C00B9D6007E4E67 /* UbiquitousDirectoryMonitorDelegateMock.swift in Sources */,
|
||||
EDF838BE2C00B9D0007E4E67 /* LocalDirectoryMonitorDelegateMock.swift in Sources */,
|
||||
EDC4E3692C5E6F5B009286A2 /* MockRecentlyDeletedCategoriesManager.swift in Sources */,
|
||||
ED9DDF9D2D6F6F7900645BC8 /* TrackRecordingManagerTests.swift in Sources */,
|
||||
EDF838BF2C00B9D0007E4E67 /* SynchronizationStateManagerTests.swift in Sources */,
|
||||
ED810EC52D566E9B00ECDE2C /* SearchOnMapTests.swift in Sources */,
|
||||
4B83AE4B2C2E642100B0C3BC /* TTSTesterTest.m in Sources */,
|
||||
|
|
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
After Width: | Height: | Size: 391 B |
After Width: | Height: | Size: 711 B |
After Width: | Height: | Size: 954 B |
|
@ -0,0 +1,26 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "Ascent.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"filename" : "Ascent@2x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"filename" : "Ascent@3x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
},
|
||||
"properties" : {
|
||||
"template-rendering-intent" : "template"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,26 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "Descent.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"filename" : "Descent@2x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"filename" : "Descent@3x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
},
|
||||
"properties" : {
|
||||
"template-rendering-intent" : "template"
|
||||
}
|
||||
}
|
After Width: | Height: | Size: 427 B |
After Width: | Height: | Size: 791 B |
After Width: | Height: | Size: 1.1 KiB |
|
@ -0,0 +1,26 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "maxAltitude.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"filename" : "maxAltitude@2x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"filename" : "maxAltitude@3x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
},
|
||||
"properties" : {
|
||||
"template-rendering-intent" : "template"
|
||||
}
|
||||
}
|
After Width: | Height: | Size: 296 B |
After Width: | Height: | Size: 489 B |
After Width: | Height: | Size: 627 B |
|
@ -0,0 +1,26 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "minAltitude.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"filename" : "minAltitude@2x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"filename" : "minAltitude@3x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
},
|
||||
"properties" : {
|
||||
"template-rendering-intent" : "template"
|
||||
}
|
||||
}
|
After Width: | Height: | Size: 291 B |
After Width: | Height: | Size: 481 B |
After Width: | Height: | Size: 664 B |
|
@ -1,23 +1,24 @@
|
|||
import SwiftUI
|
||||
|
||||
struct StatisticDetailView: View {
|
||||
private let title: String
|
||||
private let subtitle: String
|
||||
|
||||
init(title: String, subtitle: String) {
|
||||
self.title = title
|
||||
self.subtitle = subtitle
|
||||
}
|
||||
let viewModel: TrackRecordingLiveActivityAttributes.ContentState.DetailViewModel
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading) {
|
||||
Text(title)
|
||||
.contentTransition(.numericText())
|
||||
.font(.system(size: 14, weight: .bold).monospacedDigit())
|
||||
.minimumScaleFactor(0.5)
|
||||
.lineLimit(1)
|
||||
.foregroundStyle(.white)
|
||||
Text(subtitle)
|
||||
HStack {
|
||||
Image(uiImage: UIImage(named: viewModel.imageName)!)
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fit)
|
||||
.frame(width: 16, height: 16)
|
||||
.foregroundStyle(.white)
|
||||
Text(viewModel.value)
|
||||
.contentTransition(.numericText())
|
||||
.font(.system(size: 14, weight: .bold).monospacedDigit())
|
||||
.minimumScaleFactor(0.5)
|
||||
.lineLimit(1)
|
||||
.foregroundStyle(.white)
|
||||
}
|
||||
Text(viewModel.key)
|
||||
.font(.system(size: 10))
|
||||
.minimumScaleFactor(0.5)
|
||||
.lineLimit(2)
|
||||
|
|
|
@ -1,14 +1,10 @@
|
|||
import SwiftUI
|
||||
|
||||
struct StatisticValueView: View {
|
||||
private let value: String
|
||||
|
||||
init(_ value: String) {
|
||||
self.value = value
|
||||
}
|
||||
let viewModel: TrackRecordingLiveActivityAttributes.ContentState.ValueViewModel
|
||||
|
||||
var body: some View {
|
||||
Text(value)
|
||||
Text(viewModel.value)
|
||||
.contentTransition(.numericText())
|
||||
.minimumScaleFactor(0.1)
|
||||
.font(.title3.bold().monospacedDigit())
|
||||
|
|
|
@ -3,15 +3,21 @@ import AppIntents
|
|||
|
||||
struct TrackRecordingLiveActivityAttributes: ActivityAttributes {
|
||||
struct ContentState: Codable, Hashable {
|
||||
struct StatisticsViewModel: Codable, Hashable {
|
||||
let key: String
|
||||
struct ValueViewModel: Codable, Hashable {
|
||||
let value: String
|
||||
}
|
||||
let duration: StatisticsViewModel
|
||||
let distance: StatisticsViewModel
|
||||
let ascent: StatisticsViewModel
|
||||
let descent: StatisticsViewModel
|
||||
let maxElevation: StatisticsViewModel
|
||||
let minElevation: StatisticsViewModel
|
||||
|
||||
struct DetailViewModel: Codable, Hashable {
|
||||
let key: String
|
||||
let value: String
|
||||
let imageName: String
|
||||
}
|
||||
|
||||
let duration: ValueViewModel
|
||||
let distance: ValueViewModel
|
||||
let ascent: DetailViewModel
|
||||
let descent: DetailViewModel
|
||||
let maxElevation: DetailViewModel
|
||||
let minElevation: DetailViewModel
|
||||
}
|
||||
}
|
||||
|
|
|
@ -15,7 +15,7 @@ struct TrackRecordingLiveActivityConfiguration: Widget {
|
|||
} compactLeading: {
|
||||
AppLogo()
|
||||
} compactTrailing: {
|
||||
StatisticValueView(context.state.duration.value)
|
||||
StatisticValueView(viewModel: context.state.duration)
|
||||
} minimal: {
|
||||
// TODO: Implement the minimal view
|
||||
}
|
||||
|
|
|
@ -9,18 +9,18 @@ struct TrackRecordingLiveActivityView: View {
|
|||
var body: some View {
|
||||
VStack(alignment: .leading, spacing: 12) {
|
||||
HStack(alignment: .top, spacing: 24) {
|
||||
StatisticValueView(state.duration.value)
|
||||
StatisticValueView(state.distance.value)
|
||||
StatisticValueView(viewModel: state.duration)
|
||||
StatisticValueView(viewModel: state.distance)
|
||||
Spacer()
|
||||
AppLogo()
|
||||
.frame(width: 20, height: 20)
|
||||
}
|
||||
.padding([.top, .leading, .trailing], 16)
|
||||
HStack(alignment: .top, spacing: 20) {
|
||||
StatisticDetailView(title: state.ascent.value, subtitle: state.ascent.key)
|
||||
StatisticDetailView(title: state.descent.value, subtitle: state.descent.key)
|
||||
StatisticDetailView(title: state.maxElevation.value, subtitle: state.maxElevation.key)
|
||||
StatisticDetailView(title: state.minElevation.value, subtitle: state.minElevation.key)
|
||||
StatisticDetailView(viewModel: state.ascent)
|
||||
StatisticDetailView(viewModel: state.descent)
|
||||
StatisticDetailView(viewModel: state.maxElevation)
|
||||
StatisticDetailView(viewModel: state.minElevation)
|
||||
}
|
||||
.padding([.leading, .trailing, .bottom], 16)
|
||||
}
|
||||
|
@ -37,28 +37,30 @@ struct TrackRecordingLiveActivityWidget_Previews: PreviewProvider {
|
|||
let activityAttributes = TrackRecordingLiveActivityAttributes()
|
||||
let activityState = TrackRecordingLiveActivityAttributes.ContentState(
|
||||
duration: .init(
|
||||
key: "Duration",
|
||||
value: "1h 12min"
|
||||
value: "10d 10h 12min"
|
||||
),
|
||||
distance: .init(
|
||||
key: "Distance",
|
||||
value: "12 km"
|
||||
value: "1245 km"
|
||||
),
|
||||
ascent: .init(
|
||||
key: "Ascent",
|
||||
value: "100 m"
|
||||
value: "100 m",
|
||||
imageName: "ic_em_ascent_24"
|
||||
),
|
||||
descent: .init(
|
||||
key: "Descent",
|
||||
value: "100 m"
|
||||
value: "100 m",
|
||||
imageName: "ic_em_descent_24"
|
||||
),
|
||||
maxElevation: .init(
|
||||
key: "Max Elevation",
|
||||
value: "100 m"
|
||||
value: "9999 m",
|
||||
imageName: "ic_em_max_attitude_24"
|
||||
),
|
||||
minElevation: .init(
|
||||
key: "Min Elevation",
|
||||
value: "10 m"
|
||||
value: "1000 m",
|
||||
imageName: "ic_em_min_attitude_24"
|
||||
)
|
||||
)
|
||||
|
||||
|
|
|
@ -0,0 +1,198 @@
|
|||
import XCTest
|
||||
@testable import Organic_Maps__Debug_
|
||||
|
||||
final class TrackRecordingManagerTests: XCTestCase {
|
||||
|
||||
private var trackRecordingManager: TrackRecordingManager!
|
||||
|
||||
private var mockTrackRecorder: MockTrackRecorder.Type!
|
||||
private var mockLocationService: MockLocationService.Type!
|
||||
private var mockActivityManager: MockTrackRecordingActivityManager!
|
||||
|
||||
override func setUp() {
|
||||
super.setUp()
|
||||
mockTrackRecorder = MockTrackRecorder.self
|
||||
mockLocationService = MockLocationService.self
|
||||
mockActivityManager = MockTrackRecordingActivityManager()
|
||||
|
||||
trackRecordingManager = TrackRecordingManager(
|
||||
trackRecorder: mockTrackRecorder,
|
||||
locationService: mockLocationService,
|
||||
activityManager: mockActivityManager
|
||||
)
|
||||
}
|
||||
|
||||
override func tearDown() {
|
||||
trackRecordingManager = nil
|
||||
mockTrackRecorder.reset()
|
||||
mockLocationService.reset()
|
||||
mockActivityManager = nil
|
||||
super.tearDown()
|
||||
}
|
||||
|
||||
func test_GivenInitialSetup_WhenLocationEnabled_ThenStateIsInactive() {
|
||||
mockLocationService.locationIsProhibited = false
|
||||
mockTrackRecorder.trackRecordingIsEnabled = false
|
||||
|
||||
trackRecordingManager.setup()
|
||||
XCTAssertTrue(trackRecordingManager.recordingState == .inactive)
|
||||
}
|
||||
|
||||
func test_GivenInitialSetup_WhenLocationDisabled_ThenShouldHandleErrorAndIncativeState() {
|
||||
mockLocationService.locationIsProhibited = true
|
||||
|
||||
trackRecordingManager.setup()
|
||||
|
||||
XCTAssertTrue(mockLocationService.checkLocationStatusCalled)
|
||||
XCTAssertTrue(trackRecordingManager.recordingState == .inactive)
|
||||
}
|
||||
|
||||
func test_GivenStartRecording_WhenLocationEnabled_ThenSuccess() {
|
||||
mockLocationService.locationIsProhibited = false
|
||||
mockTrackRecorder.trackRecordingIsEnabled = false
|
||||
|
||||
trackRecordingManager.processAction(.start)
|
||||
|
||||
XCTAssertTrue(mockTrackRecorder.startTrackRecordingCalled)
|
||||
XCTAssertTrue(mockActivityManager.startCalled)
|
||||
XCTAssertTrue(trackRecordingManager.recordingState == .active)
|
||||
}
|
||||
|
||||
func test_GivenStartRecording_WhenLocationDisabled_ThenShouldFail() {
|
||||
mockLocationService.locationIsProhibited = true
|
||||
|
||||
trackRecordingManager.processAction(.start) { result in
|
||||
switch result {
|
||||
case .success:
|
||||
XCTFail("Should not succeed")
|
||||
case .error(let error):
|
||||
switch error {
|
||||
case .locationIsProhibited:
|
||||
XCTAssertTrue(true)
|
||||
default:
|
||||
XCTFail("Unexpected error: \(error)")
|
||||
}
|
||||
}
|
||||
}
|
||||
XCTAssertFalse(self.mockTrackRecorder.startTrackRecordingCalled)
|
||||
XCTAssertTrue(trackRecordingManager.recordingState == .inactive)
|
||||
}
|
||||
|
||||
func test_GivenStopRecording_WhenLocationEnabled_ThenSuccess() {
|
||||
mockTrackRecorder.trackRecordingIsEnabled = true
|
||||
mockTrackRecorder.trackRecordingIsEmpty = false
|
||||
|
||||
trackRecordingManager.processAction(.stopAndSave(name: "Test Track")) { result in
|
||||
switch result {
|
||||
case .success:
|
||||
XCTAssertTrue(true)
|
||||
case .error(let error):
|
||||
XCTFail("Unexpected error: \(error)")
|
||||
}
|
||||
}
|
||||
XCTAssertTrue(mockTrackRecorder.stopTrackRecordingCalled)
|
||||
XCTAssertTrue(mockTrackRecorder.saveTrackRecordingCalled)
|
||||
XCTAssertTrue(mockActivityManager.stopCalled)
|
||||
XCTAssertTrue(trackRecordingManager.recordingState == .inactive)
|
||||
}
|
||||
|
||||
func test_GivenStopRecording_WhenTrackIsEmpty_ThenShouldFail() {
|
||||
mockTrackRecorder.trackRecordingIsEnabled = true
|
||||
mockTrackRecorder.trackRecordingIsEmpty = true
|
||||
|
||||
trackRecordingManager.processAction(.stopAndSave(name: "Test Track")) { result in
|
||||
switch result {
|
||||
case .success:
|
||||
XCTFail("Should not succeed")
|
||||
case .error(let error):
|
||||
switch error {
|
||||
case .trackIsEmpty:
|
||||
XCTAssertTrue(true)
|
||||
default:
|
||||
XCTFail("Unexpected error: \(error)")
|
||||
}
|
||||
}
|
||||
}
|
||||
XCTAssertFalse(mockTrackRecorder.saveTrackRecordingCalled)
|
||||
XCTAssertTrue(trackRecordingManager.recordingState == .inactive)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Mock Classes
|
||||
|
||||
private final class MockTrackRecorder: TrackRecorder {
|
||||
static var trackRecordingIsEnabled = false
|
||||
static var trackRecordingIsEmpty = false
|
||||
static var startTrackRecordingCalled = false
|
||||
static var stopTrackRecordingCalled = false
|
||||
static var saveTrackRecordingCalled = false
|
||||
|
||||
static func reset() {
|
||||
trackRecordingIsEnabled = false
|
||||
trackRecordingIsEmpty = false
|
||||
startTrackRecordingCalled = false
|
||||
stopTrackRecordingCalled = false
|
||||
saveTrackRecordingCalled = false
|
||||
}
|
||||
|
||||
static func isTrackRecordingEnabled() -> Bool {
|
||||
return trackRecordingIsEnabled
|
||||
}
|
||||
|
||||
static func isTrackRecordingEmpty() -> Bool {
|
||||
return trackRecordingIsEmpty
|
||||
}
|
||||
|
||||
static func startTrackRecording() {
|
||||
startTrackRecordingCalled = true
|
||||
trackRecordingIsEnabled = true
|
||||
}
|
||||
|
||||
static func stopTrackRecording() {
|
||||
stopTrackRecordingCalled = true
|
||||
trackRecordingIsEnabled = false
|
||||
}
|
||||
|
||||
static func saveTrackRecording(withName name: String) {
|
||||
saveTrackRecordingCalled = true
|
||||
}
|
||||
|
||||
static func setTrackRecordingUpdateHandler(_ handler: ((TrackInfo) -> Void)?) {}
|
||||
|
||||
static func trackRecordingElevationInfo() -> ElevationProfileData {
|
||||
ElevationProfileData()
|
||||
}
|
||||
}
|
||||
|
||||
private final class MockLocationService: LocationService {
|
||||
static var locationIsProhibited = false
|
||||
static var checkLocationStatusCalled = false
|
||||
|
||||
static func reset() {
|
||||
locationIsProhibited = false
|
||||
checkLocationStatusCalled = false
|
||||
}
|
||||
|
||||
static func isLocationProhibited() -> Bool {
|
||||
return locationIsProhibited
|
||||
}
|
||||
|
||||
static func checkLocationStatus() {
|
||||
checkLocationStatusCalled = true
|
||||
}
|
||||
}
|
||||
|
||||
final class MockTrackRecordingActivityManager: TrackRecordingActivityManager {
|
||||
var startCalled = false
|
||||
var stopCalled = false
|
||||
|
||||
func start(with info: TrackInfo) throws {
|
||||
startCalled = true
|
||||
}
|
||||
|
||||
func stop() {
|
||||
stopCalled = true
|
||||
}
|
||||
|
||||
func update(_ info: TrackInfo) {}
|
||||
}
|
|
@ -10,7 +10,7 @@ final class TrackRecordingButtonArea: AvailableArea {
|
|||
}
|
||||
|
||||
override func notifyObserver() {
|
||||
TrackRecordingViewController.updateAvailableArea(areaFrame)
|
||||
TrackRecordingButtonViewController.updateAvailableArea(areaFrame)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -79,8 +79,13 @@ extension BottomMenuInteractor: BottomMenuInteractorProtocol {
|
|||
}
|
||||
|
||||
func toggleTrackRecording() {
|
||||
trackRecorder.processAction(trackRecorder.recordingState == .active ? .stop : .start) { [weak self] in
|
||||
self?.close()
|
||||
switch trackRecorder.recordingState {
|
||||
case .active:
|
||||
break
|
||||
case .inactive:
|
||||
trackRecorder.processAction(.start)
|
||||
}
|
||||
close()
|
||||
MapViewController.shared()?.showTrackRecordingPlacePage()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -62,18 +62,26 @@ final class ActionBarViewController: UIViewController {
|
|||
fatalError()
|
||||
}
|
||||
}
|
||||
|
||||
var buttons: [ActionBarButtonType] = []
|
||||
if isRoutePlanning {
|
||||
buttons.append(.routeFrom)
|
||||
}
|
||||
if placePageData.infoData?.phone != nil, AppInfo.shared().canMakeCalls {
|
||||
buttons.append(.call)
|
||||
}
|
||||
if !isRoutePlanning {
|
||||
buttons.append(.routeFrom)
|
||||
switch placePageData.objectType {
|
||||
case .POI, .bookmark, .track:
|
||||
if isRoutePlanning {
|
||||
buttons.append(.routeFrom)
|
||||
}
|
||||
if placePageData.infoData?.phone != nil, AppInfo.shared().canMakeCalls {
|
||||
buttons.append(.call)
|
||||
}
|
||||
if !isRoutePlanning {
|
||||
buttons.append(.routeFrom)
|
||||
}
|
||||
case .trackRecording:
|
||||
break
|
||||
@unknown default:
|
||||
fatalError()
|
||||
}
|
||||
|
||||
assert(buttons.count > 0)
|
||||
guard !buttons.isEmpty else { return }
|
||||
visibleButtons.append(buttons[0])
|
||||
if buttons.count > 1 {
|
||||
additionalButtons.append(contentsOf: buttons.suffix(from: 1))
|
||||
|
@ -91,8 +99,7 @@ final class ActionBarViewController: UIViewController {
|
|||
case .track:
|
||||
buttons.append(.track)
|
||||
case .trackRecording:
|
||||
// TODO: implement for track recording
|
||||
break
|
||||
buttons.append(.saveTrackRecording)
|
||||
@unknown default:
|
||||
fatalError()
|
||||
}
|
||||
|
@ -104,7 +111,14 @@ final class ActionBarViewController: UIViewController {
|
|||
}
|
||||
|
||||
private func configButton3() {
|
||||
visibleButtons.append(.routeTo)
|
||||
switch placePageData.objectType {
|
||||
case .POI, .bookmark, .track:
|
||||
visibleButtons.append(.routeTo)
|
||||
case .trackRecording:
|
||||
break
|
||||
@unknown default:
|
||||
fatalError()
|
||||
}
|
||||
}
|
||||
|
||||
private func configButton4() {
|
||||
|
|
|
@ -0,0 +1,26 @@
|
|||
enum ElevationDescription {
|
||||
case ascent
|
||||
case descent
|
||||
case maxElevation
|
||||
case minElevation
|
||||
}
|
||||
|
||||
extension ElevationDescription {
|
||||
var title: String {
|
||||
switch self {
|
||||
case .ascent: return L("elevation_profile_ascent")
|
||||
case .descent: return L("elevation_profile_descent")
|
||||
case .maxElevation: return L("elevation_profile_max_elevation")
|
||||
case .minElevation: return L("elevation_profile_min_elevation")
|
||||
}
|
||||
}
|
||||
|
||||
var imageName: String {
|
||||
switch self {
|
||||
case .ascent: return "ic_em_ascent_24"
|
||||
case .descent: return "ic_em_descent_24"
|
||||
case .maxElevation: return "ic_em_max_attitude_24"
|
||||
case .minElevation: return "ic_em_min_attitude_24"
|
||||
}
|
||||
}
|
||||
}
|
|
@ -5,7 +5,7 @@ final class ElevationProfileFormatter {
|
|||
|
||||
private enum Constants {
|
||||
static let metricToImperialMultiplier: CGFloat = 0.3048
|
||||
static var metricAltitudeStep: CGFloat = 50
|
||||
static var metricAltitudeStep: CGFloat = 25
|
||||
static var imperialAltitudeStep: CGFloat = 100
|
||||
}
|
||||
|
||||
|
|
|
@ -18,6 +18,12 @@ fileprivate struct DescriptionsViewModel {
|
|||
let title: String
|
||||
let value: UInt
|
||||
let imageName: String
|
||||
|
||||
init(_ description: ElevationDescription, value: UInt) {
|
||||
self.title = description.title
|
||||
self.value = value
|
||||
self.imageName = description.imageName
|
||||
}
|
||||
}
|
||||
|
||||
final class ElevationProfilePresenter: NSObject {
|
||||
|
@ -50,10 +56,10 @@ final class ElevationProfilePresenter: NSObject {
|
|||
|
||||
private static func descriptionModels(for trackInfo: TrackInfo) -> [DescriptionsViewModel] {
|
||||
[
|
||||
DescriptionsViewModel(title: L("elevation_profile_ascent"), value: trackInfo.ascent, imageName: "ic_em_ascent_24"),
|
||||
DescriptionsViewModel(title: L("elevation_profile_descent"), value: trackInfo.descent, imageName: "ic_em_descent_24"),
|
||||
DescriptionsViewModel(title: L("elevation_profile_max_elevation"), value: trackInfo.maxElevation, imageName: "ic_em_max_attitude_24"),
|
||||
DescriptionsViewModel(title: L("elevation_profile_min_elevation"), value: trackInfo.minElevation, imageName: "ic_em_min_attitude_24")
|
||||
DescriptionsViewModel(.ascent, value: trackInfo.ascent),
|
||||
DescriptionsViewModel(.descent, value: trackInfo.descent),
|
||||
DescriptionsViewModel(.minElevation, value: trackInfo.maxElevation),
|
||||
DescriptionsViewModel(.maxElevation, value: trackInfo.minElevation)
|
||||
]
|
||||
}
|
||||
|
||||
|
@ -85,6 +91,11 @@ extension ElevationProfilePresenter: ElevationProfilePresenterProtocol {
|
|||
view?.setChartData(ChartPresentationData(chartData, formatter: formatter))
|
||||
view?.reloadDescription()
|
||||
|
||||
guard !profileData.isTrackRecording else {
|
||||
view?.isChartViewInfoHidden = true
|
||||
return
|
||||
}
|
||||
|
||||
view?.setActivePoint(profileData.activePoint)
|
||||
view?.setMyPosition(profileData.myPosition)
|
||||
bookmarkManager.setElevationActivePointChanged(profileData.trackId) { [weak self] distance in
|
||||
|
|
|
@ -51,8 +51,6 @@ extension PlacePageHeaderPresenter: PlacePageHeaderPresenterProtocol {
|
|||
view?.isExpandViewHidden = true
|
||||
view?.isShadowViewHidden = false
|
||||
}
|
||||
// TODO: (KK) Enable share button for the tracks to share the whole track gpx/kml
|
||||
view?.isShareButtonHidden = false
|
||||
}
|
||||
|
||||
func onClosePress() {
|
||||
|
|
|
@ -2,7 +2,6 @@ protocol PlacePageHeaderViewProtocol: AnyObject {
|
|||
var presenter: PlacePageHeaderPresenterProtocol? { get set }
|
||||
var isExpandViewHidden: Bool { get set }
|
||||
var isShadowViewHidden: Bool { get set }
|
||||
var isShareButtonHidden: Bool { get set }
|
||||
|
||||
func setTitle(_ title: String?, secondaryTitle: String?)
|
||||
func showShareTrackMenu()
|
||||
|
@ -78,15 +77,6 @@ extension PlacePageHeaderViewController: PlacePageHeaderViewProtocol {
|
|||
}
|
||||
}
|
||||
|
||||
var isShareButtonHidden: Bool {
|
||||
get {
|
||||
shareButton.isHidden
|
||||
}
|
||||
set {
|
||||
shareButton.isHidden = newValue
|
||||
}
|
||||
}
|
||||
|
||||
func setTitle(_ title: String?, secondaryTitle: String?) {
|
||||
titleText = title
|
||||
secondaryText = secondaryTitle
|
||||
|
|
|
@ -15,8 +15,7 @@
|
|||
case .track:
|
||||
layout = PlacePageTrackLayout(interactor: interactor, storyboard: storyboard, data: data)
|
||||
case .trackRecording:
|
||||
// TODO: Implement PlacePageTrackRecordingLayout
|
||||
fatalError("PlacePageTrackRecordingLayout is not implemented")
|
||||
layout = PlacePageTrackRecordingLayout(interactor: interactor, storyboard: storyboard, data: data)
|
||||
@unknown default:
|
||||
fatalError()
|
||||
}
|
||||
|
@ -34,14 +33,14 @@
|
|||
data: data,
|
||||
mapViewController: MapViewController.shared()!)
|
||||
let layout: IPlacePageLayout
|
||||
let storyboard = viewController.storyboard!
|
||||
switch data.objectType {
|
||||
case .POI, .bookmark:
|
||||
layout = PlacePageCommonLayout(interactor: interactor, storyboard: viewController.storyboard!, data: data)
|
||||
layout = PlacePageCommonLayout(interactor: interactor, storyboard: storyboard, data: data)
|
||||
case .track:
|
||||
layout = PlacePageTrackLayout(interactor: interactor, storyboard: viewController.storyboard!, data: data)
|
||||
layout = PlacePageTrackLayout(interactor: interactor, storyboard: storyboard, data: data)
|
||||
case .trackRecording:
|
||||
// TODO: Implement PlacePageTrackRecordingLayout
|
||||
fatalError("PlacePageTrackRecordingLayout is not implemented")
|
||||
layout = PlacePageTrackRecordingLayout(interactor: interactor, storyboard: storyboard, data: data)
|
||||
@unknown default:
|
||||
fatalError()
|
||||
}
|
||||
|
|
|
@ -24,14 +24,29 @@ class PlacePageInteractor: NSObject {
|
|||
}
|
||||
|
||||
private func updatePlacePageIfNeeded() {
|
||||
let isBookmark = placePageData.bookmarkData != nil && bookmarksManager.hasBookmark(placePageData.bookmarkData!.bookmarkId)
|
||||
let isTrack = placePageData.trackData != nil && bookmarksManager.hasTrack(placePageData.trackData!.trackId)
|
||||
guard isBookmark || isTrack else {
|
||||
presenter?.closeAnimated()
|
||||
return
|
||||
func updatePlacePage() {
|
||||
FrameworkHelper.updatePlacePageData()
|
||||
placePageData.updateBookmarkStatus()
|
||||
}
|
||||
|
||||
switch placePageData.objectType {
|
||||
case .POI, .trackRecording:
|
||||
break
|
||||
case .bookmark:
|
||||
guard bookmarksManager.hasBookmark(placePageData.bookmarkData!.bookmarkId) else {
|
||||
presenter?.closeAnimated()
|
||||
return
|
||||
}
|
||||
updatePlacePage()
|
||||
case .track:
|
||||
guard bookmarksManager.hasTrack(placePageData.trackData!.trackId) else {
|
||||
presenter?.closeAnimated()
|
||||
return
|
||||
}
|
||||
updatePlacePage()
|
||||
@unknown default:
|
||||
fatalError("Unknown object type")
|
||||
}
|
||||
FrameworkHelper.updatePlacePageData()
|
||||
placePageData.updateBookmarkStatus()
|
||||
}
|
||||
|
||||
private func addToBookmarksManagerObserverList() {
|
||||
|
@ -239,9 +254,19 @@ extension PlacePageInteractor: ActionBarViewControllerDelegate {
|
|||
fatalError("More button should've been handled in ActionBarViewContoller")
|
||||
case .track:
|
||||
guard placePageData.trackData != nil else { return }
|
||||
// TODO: This is temporary solution. Remove the dialog and use the MWMPlacePageManagerHelper.removeTrack
|
||||
// TODO: (KK) This is temporary solution. Remove the dialog and use the MWMPlacePageManagerHelper.removeTrack
|
||||
// directly here when the track recovery mechanism will be implemented.
|
||||
showTrackDeletionConfirmationDialog()
|
||||
case .saveTrackRecording:
|
||||
// TODO: (KK) pass name typed by user
|
||||
TrackRecordingManager.shared.processAction(.stopAndSave(name: "")) { [weak self] result in
|
||||
switch result {
|
||||
case .success:
|
||||
break
|
||||
case .error:
|
||||
self?.presenter?.closeAnimated()
|
||||
}
|
||||
}
|
||||
@unknown default:
|
||||
fatalError()
|
||||
}
|
||||
|
@ -277,8 +302,8 @@ extension PlacePageInteractor: ElevationProfileViewControllerDelegate {
|
|||
}
|
||||
|
||||
func updateMapPoint(_ point: CLLocationCoordinate2D, distance: Double) {
|
||||
guard let trackId = placePageData.trackData?.trackId else { return }
|
||||
BookmarksManager.shared().setElevationActivePoint(point, distance: distance, trackId: trackId)
|
||||
guard let trackData = placePageData.trackData, trackData.elevationProfileData?.isTrackRecording == false else { return }
|
||||
BookmarksManager.shared().setElevationActivePoint(point, distance: distance, trackId: trackData.trackId)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -301,6 +326,10 @@ extension PlacePageInteractor: PlacePageHeaderViewControllerDelegate {
|
|||
shareViewController.present(inParentViewController: mapViewController, anchorView: sourceView)
|
||||
case .track:
|
||||
presenter?.showShareTrackMenu()
|
||||
case .trackRecording:
|
||||
let currentLocation = LocationManager.lastLocation()?.coordinate ?? placePageData.locationCoordinate
|
||||
let shareMyPositionViewController = ActivityViewController.share(forMyPosition: currentLocation)
|
||||
shareMyPositionViewController.present(inParentViewController: mapViewController, anchorView: sourceView)
|
||||
default:
|
||||
fatalError()
|
||||
}
|
||||
|
|
|
@ -3,6 +3,7 @@ typedef NS_ENUM(NSInteger, MWMActionBarButtonType) {
|
|||
MWMActionBarButtonTypeBookingSearch,
|
||||
MWMActionBarButtonTypeBookmark,
|
||||
MWMActionBarButtonTypeTrack,
|
||||
MWMActionBarButtonTypeSaveTrackRecording,
|
||||
MWMActionBarButtonTypeCall,
|
||||
MWMActionBarButtonTypeDownload,
|
||||
MWMActionBarButtonTypeMore,
|
||||
|
|
|
@ -19,6 +19,8 @@ NSString *titleForButton(MWMActionBarButtonType type, BOOL isSelected) {
|
|||
case MWMActionBarButtonTypeBookmark:
|
||||
case MWMActionBarButtonTypeTrack:
|
||||
return L(isSelected ? @"delete" : @"save");
|
||||
case MWMActionBarButtonTypeSaveTrackRecording:
|
||||
return L(@"save");
|
||||
case MWMActionBarButtonTypeRouteFrom:
|
||||
return L(@"p2p_from_here");
|
||||
case MWMActionBarButtonTypeRouteTo:
|
||||
|
@ -55,7 +57,8 @@ NSString *titleForButton(MWMActionBarButtonType type, BOOL isSelected) {
|
|||
self.label.text = titleForButton(self.type, isSelected);
|
||||
self.extraBackground.hidden = YES;
|
||||
self.button.coloring = MWMButtonColoringBlack;
|
||||
|
||||
[self.button.imageView setContentMode:UIViewContentModeScaleAspectFit];
|
||||
|
||||
switch (self.type) {
|
||||
case MWMActionBarButtonTypeDownload: {
|
||||
if (self.mapDownloadProgress)
|
||||
|
@ -108,6 +111,9 @@ NSString *titleForButton(MWMActionBarButtonType type, BOOL isSelected) {
|
|||
[self.button setImage:[[UIImage imageNamed:@"ic_route_manager_trash"] imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate] forState:UIControlStateNormal];
|
||||
self.button.coloring = MWMButtonColoringRed;
|
||||
break;
|
||||
case MWMActionBarButtonTypeSaveTrackRecording:
|
||||
[self.button setImage:[UIImage imageNamed:@"ic_placepage_save_track_recording"] forState:UIControlStateNormal];
|
||||
break;
|
||||
case MWMActionBarButtonTypeRouteFrom:
|
||||
[self.button setImage:[UIImage imageNamed:@"ic_route_from"] forState:UIControlStateNormal];
|
||||
break;
|
||||
|
|
|
@ -0,0 +1,92 @@
|
|||
final class PlacePageTrackRecordingLayout: IPlacePageLayout {
|
||||
private var placePageData: PlacePageData
|
||||
private var interactor: PlacePageInteractor
|
||||
private let storyboard: UIStoryboard
|
||||
weak var presenter: PlacePagePresenterProtocol?
|
||||
|
||||
lazy var bodyViewControllers: [UIViewController] = {
|
||||
return configureViewControllers()
|
||||
}()
|
||||
|
||||
var actionBar: ActionBarViewController? {
|
||||
actionBarViewController
|
||||
}
|
||||
|
||||
var navigationBar: UIViewController? {
|
||||
placePageNavigationViewController
|
||||
}
|
||||
|
||||
lazy var headerViewControllers: [UIViewController] = {
|
||||
[headerViewController]
|
||||
}()
|
||||
|
||||
lazy var headerViewController: PlacePageHeaderViewController = {
|
||||
return PlacePageHeaderBuilder.build(data: placePageData, delegate: interactor, headerType: .flexible)
|
||||
}()
|
||||
|
||||
lazy var placePageNavigationViewController: PlacePageHeaderViewController = {
|
||||
return PlacePageHeaderBuilder.build(data: placePageData, delegate: interactor, headerType: .fixed)
|
||||
}()
|
||||
|
||||
lazy var editTrackViewController: PlacePageEditBookmarkOrTrackViewController = {
|
||||
let vc = storyboard.instantiateViewController(ofType: PlacePageEditBookmarkOrTrackViewController.self)
|
||||
vc.view.isHidden = true
|
||||
vc.delegate = interactor
|
||||
return vc
|
||||
}()
|
||||
|
||||
lazy var elevationProfileViewController: ElevationProfileViewController? = {
|
||||
guard let trackData = placePageData.trackData else {
|
||||
return nil
|
||||
}
|
||||
return ElevationProfileBuilder.build(trackInfo: trackData.trackInfo,
|
||||
elevationProfileData: trackData.elevationProfileData,
|
||||
delegate: interactor)
|
||||
}()
|
||||
|
||||
lazy var actionBarViewController: ActionBarViewController = {
|
||||
let vc = storyboard.instantiateViewController(ofType: ActionBarViewController.self)
|
||||
vc.placePageData = placePageData
|
||||
vc.canAddStop = MWMRouter.canAddIntermediatePoint()
|
||||
vc.isRoutePlanning = MWMNavigationDashboardManager.shared().state != .hidden
|
||||
vc.delegate = interactor
|
||||
return vc
|
||||
}()
|
||||
|
||||
init(interactor: PlacePageInteractor, storyboard: UIStoryboard, data: PlacePageData) {
|
||||
self.interactor = interactor
|
||||
self.storyboard = storyboard
|
||||
self.placePageData = data
|
||||
}
|
||||
|
||||
private func configureViewControllers() -> [UIViewController] {
|
||||
var viewControllers = [UIViewController]()
|
||||
|
||||
if let elevationProfileViewController {
|
||||
viewControllers.append(elevationProfileViewController)
|
||||
}
|
||||
|
||||
placePageData.onTrackRecordingProgressUpdate = { [weak self] in
|
||||
self?.updateTrackRecordingRelatedSections()
|
||||
}
|
||||
|
||||
return viewControllers
|
||||
}
|
||||
|
||||
func calculateSteps(inScrollView scrollView: UIScrollView, compact: Bool) -> [PlacePageState] {
|
||||
var steps: [PlacePageState] = []
|
||||
let scrollHeight = scrollView.height
|
||||
steps.append(.closed(-scrollHeight))
|
||||
steps.append(.full(0))
|
||||
return steps
|
||||
}
|
||||
}
|
||||
|
||||
private extension PlacePageTrackRecordingLayout {
|
||||
func updateTrackRecordingRelatedSections() {
|
||||
guard let elevationProfileViewController, let trackInfo = placePageData.trackData?.trackInfo else { return }
|
||||
headerViewController.setTitle(placePageData.previewData.title, secondaryTitle: nil)
|
||||
elevationProfileViewController.presenter?.update(trackInfo: trackInfo, profileData: placePageData.trackData?.elevationProfileData)
|
||||
presenter?.layoutIfNeeded()
|
||||
}
|
||||
}
|