From 15fb80a43ea6d7df6555cc4b7303e59e64a16113 Mon Sep 17 00:00:00 2001 From: Kiryl Kaveryn Date: Thu, 9 Jan 2025 14:19:42 +0400 Subject: [PATCH 1/8] [ios] return the ElevationInfo for the current track recording Signed-off-by: Kiryl Kaveryn --- iphone/CoreApi/CoreApi/Framework/MWMFrameworkHelper.h | 4 ++++ iphone/CoreApi/CoreApi/Framework/MWMFrameworkHelper.mm | 5 +++++ 2 files changed, 9 insertions(+) diff --git a/iphone/CoreApi/CoreApi/Framework/MWMFrameworkHelper.h b/iphone/CoreApi/CoreApi/Framework/MWMFrameworkHelper.h index a81f261b43..cbadf8945c 100644 --- a/iphone/CoreApi/CoreApi/Framework/MWMFrameworkHelper.h +++ b/iphone/CoreApi/CoreApi/Framework/MWMFrameworkHelper.h @@ -5,6 +5,7 @@ @class MWMMapSearchResult; @class TrackInfo; +@class ElevationProfileData; typedef NS_ENUM(NSUInteger, MWMZoomMode) { MWMZoomModeIn = 0, MWMZoomModeOut }; @@ -28,6 +29,9 @@ typedef void (^TrackRecordingUpdatedHandler)(TrackInfo * _Nonnull trackInfo); + (void)saveTrackRecordingWithName:(nullable 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 diff --git a/iphone/CoreApi/CoreApi/Framework/MWMFrameworkHelper.mm b/iphone/CoreApi/CoreApi/Framework/MWMFrameworkHelper.mm index d00c60df64..6b8ea0aa9a 100644 --- a/iphone/CoreApi/CoreApi/Framework/MWMFrameworkHelper.mm +++ b/iphone/CoreApi/CoreApi/Framework/MWMFrameworkHelper.mm @@ -3,6 +3,7 @@ #import "ProductsConfiguration+Core.h" #import "Product+Core.h" #import "TrackInfo+Core.h" +#import "ElevationProfileData+Core.h" #include "Framework.h" @@ -246,6 +247,10 @@ static Framework::ProductsPopupCloseReason ConvertProductPopupCloseReasonToCore( return GetFramework().IsTrackRecordingEmpty(); } ++ (ElevationProfileData * _Nonnull)trackRecordingElevationInfo { + return [[ElevationProfileData alloc] initWithElevationInfo:GetFramework().GetTrackRecordingCurrentElevationInfo()]; +} + // MARK: - ProductsManager + (nullable ProductsConfiguration *)getProductsConfiguration { -- 2.45.3 From 032845b478e43027a76973a4a6313bc0a10b8765 Mon Sep 17 00:00:00 2001 From: Kiryl Kaveryn Date: Thu, 9 Jan 2025 16:35:31 +0400 Subject: [PATCH 2/8] [ios] subscribe on the ElevationInfo updates when the TR PP is opened Signed-off-by: Kiryl Kaveryn --- .../Common/PlacePagePreviewData.h | 3 ++ .../Common/PlacePagePreviewData.mm | 16 ++++++++ .../PlacePageData/Common/PlacePageTrackData.h | 6 ++- .../Common/PlacePageTrackData.mm | 9 +++++ .../ElevationProfileData+Core.h | 1 + .../ElevationProfile/ElevationProfileData.h | 1 + .../ElevationProfile/ElevationProfileData.mm | 37 +++++++++++++------ .../CoreApi/PlacePageData/PlacePageData.h | 4 ++ .../CoreApi/PlacePageData/PlacePageData.mm | 20 ++++++++++ iphone/Maps/Classes/MapViewController.mm | 24 ++++++++++++ 10 files changed, 107 insertions(+), 14 deletions(-) diff --git a/iphone/CoreApi/CoreApi/PlacePageData/Common/PlacePagePreviewData.h b/iphone/CoreApi/CoreApi/PlacePageData/Common/PlacePagePreviewData.h index ab25289cb4..8b5a10fa6d 100644 --- a/iphone/CoreApi/CoreApi/PlacePageData/Common/PlacePagePreviewData.h +++ b/iphone/CoreApi/CoreApi/PlacePageData/Common/PlacePagePreviewData.h @@ -1,6 +1,7 @@ #import @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 diff --git a/iphone/CoreApi/CoreApi/PlacePageData/Common/PlacePagePreviewData.mm b/iphone/CoreApi/CoreApi/PlacePageData/Common/PlacePagePreviewData.mm index 9aed885cbb..785d906302 100644 --- a/iphone/CoreApi/CoreApi/PlacePageData/Common/PlacePagePreviewData.mm +++ b/iphone/CoreApi/CoreApi/PlacePageData/Common/PlacePagePreviewData.mm @@ -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) diff --git a/iphone/CoreApi/CoreApi/PlacePageData/Common/PlacePageTrackData.h b/iphone/CoreApi/CoreApi/PlacePageData/Common/PlacePageTrackData.h index 0a6ad57bf4..fa83eb897a 100644 --- a/iphone/CoreApi/CoreApi/PlacePageData/Common/PlacePageTrackData.h +++ b/iphone/CoreApi/CoreApi/PlacePageData/Common/PlacePageTrackData.h @@ -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 diff --git a/iphone/CoreApi/CoreApi/PlacePageData/Common/PlacePageTrackData.mm b/iphone/CoreApi/CoreApi/PlacePageData/Common/PlacePageTrackData.mm index ad421d8f16..d8b4ff89fe 100644 --- a/iphone/CoreApi/CoreApi/PlacePageData/Common/PlacePageTrackData.mm +++ b/iphone/CoreApi/CoreApi/PlacePageData/Common/PlacePageTrackData.mm @@ -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) diff --git a/iphone/CoreApi/CoreApi/PlacePageData/ElevationProfile/ElevationProfileData+Core.h b/iphone/CoreApi/CoreApi/PlacePageData/ElevationProfile/ElevationProfileData+Core.h index 48c63cde20..0a00cabca2 100644 --- a/iphone/CoreApi/CoreApi/PlacePageData/ElevationProfile/ElevationProfileData+Core.h +++ b/iphone/CoreApi/CoreApi/PlacePageData/ElevationProfile/ElevationProfileData+Core.h @@ -11,6 +11,7 @@ NS_ASSUME_NONNULL_BEGIN elevationInfo:(ElevationInfo const &)elevationInfo activePoint:(double)activePoint myPosition:(double)myPosition; +- (instancetype)initWithElevationInfo:(ElevationInfo const &)elevationInfo; @end diff --git a/iphone/CoreApi/CoreApi/PlacePageData/ElevationProfile/ElevationProfileData.h b/iphone/CoreApi/CoreApi/PlacePageData/ElevationProfile/ElevationProfileData.h index f8b5a070e3..2087a30a2f 100644 --- a/iphone/CoreApi/CoreApi/PlacePageData/ElevationProfile/ElevationProfileData.h +++ b/iphone/CoreApi/CoreApi/PlacePageData/ElevationProfile/ElevationProfileData.h @@ -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 * points; @property(nonatomic, readonly) double activePoint; diff --git a/iphone/CoreApi/CoreApi/PlacePageData/ElevationProfile/ElevationProfileData.mm b/iphone/CoreApi/CoreApi/PlacePageData/ElevationProfile/ElevationProfileData.mm index 69e89c9388..ffdd63c2d4 100644 --- a/iphone/CoreApi/CoreApi/PlacePageData/ElevationProfile/ElevationProfileData.mm +++ b/iphone/CoreApi/CoreApi/PlacePageData/ElevationProfile/ElevationProfileData.mm @@ -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 *)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 diff --git a/iphone/CoreApi/CoreApi/PlacePageData/PlacePageData.h b/iphone/CoreApi/CoreApi/PlacePageData/PlacePageData.h index c3c198816d..99736a9707 100644 --- a/iphone/CoreApi/CoreApi/PlacePageData/PlacePageData.h +++ b/iphone/CoreApi/CoreApi/PlacePageData/PlacePageData.h @@ -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)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 diff --git a/iphone/CoreApi/CoreApi/PlacePageData/PlacePageData.mm b/iphone/CoreApi/CoreApi/PlacePageData/PlacePageData.mm index cda7f27918..491452b0a2 100644 --- a/iphone/CoreApi/CoreApi/PlacePageData/PlacePageData.mm +++ b/iphone/CoreApi/CoreApi/PlacePageData/PlacePageData.mm @@ -5,6 +5,7 @@ #import "PlacePageInfoData+Core.h" #import "PlacePageBookmarkData+Core.h" #import "PlacePageTrackData+Core.h" +#import "ElevationProfileData+Core.h" #import "MWMMapNodeAttributes.h" #include @@ -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]; diff --git a/iphone/Maps/Classes/MapViewController.mm b/iphone/Maps/Classes/MapViewController.mm index 93fc70cb37..37944d4297 100644 --- a/iphone/Maps/Classes/MapViewController.mm +++ b/iphone/Maps/Classes/MapViewController.mm @@ -118,6 +118,28 @@ NSString *const kSettingsSegue = @"Map2Settings"; #pragma mark - Map Navigation +- (void)showTrackRecordingPlacePage { + __block PlacePageData * placePageData = [[PlacePageData alloc] initWithTrackInfo:TrackRecordingManager.shared.trackRecordingInfo + elevationInfo:[MWMFrameworkHelper trackRecordingElevationInfo]]; + [TrackRecordingManager.shared addObserver:self recordingIsActiveDidChangeHandler:^(TrackRecordingState state, TrackInfo * _Nonnull trackInfo) { + switch (state) { + case TrackRecordingStateInactive: + [self stopObservingTrackRecordingUpdates]; + break; + case TrackRecordingStateActive: + if (UIApplication.sharedApplication.applicationState != UIApplicationStateActive) + return; + [placePageData updateWithTrackInfo:trackInfo elevationInfo:[MWMFrameworkHelper trackRecordingElevationInfo]]; + break; + } + }]; + [self showOrUpdatePlacePage:placePageData]; +} + +- (void)stopObservingTrackRecordingUpdates { + [TrackRecordingManager.shared removeObserver:self]; +} + - (void)showOrUpdatePlacePage:(PlacePageData *)data { if (self.searchManager.isSearching) [self.searchManager setPlaceOnMapSelected:YES]; @@ -190,6 +212,7 @@ NSString *const kSettingsSegue = @"Map2Settings"; } - (void)hideRegularPlacePage { + [self stopObservingTrackRecordingUpdates]; [self.placePageVC closeAnimatedWithCompletion:^{ [self.placePageVC.view removeFromSuperview]; [self.placePageVC willMoveToParentViewController:nil]; @@ -233,6 +256,7 @@ NSString *const kSettingsSegue = @"Map2Settings"; return; } PlacePageData * data = [[PlacePageData alloc] initWithLocalizationProvider:[[OpeinigHoursLocalization alloc] init]]; + [self stopObservingTrackRecordingUpdates]; [self showOrUpdatePlacePage:data]; } -- 2.45.3 From bfb2dc0154cb737b3e2c90f34923bee899f949a8 Mon Sep 17 00:00:00 2001 From: Kiryl Kaveryn Date: Thu, 9 Jan 2025 16:38:13 +0400 Subject: [PATCH 3/8] [ios] refactor TrackRecordingManager to return the proper state Signed-off-by: Kiryl Kaveryn --- .../CoreApi/Framework/MWMFrameworkHelper.h | 4 +- .../CoreApi/Framework/MWMFrameworkHelper.mm | 6 +- .../Maps/Core/Location/MWMLocationManager.h | 11 +- .../TrackRecorder/TrackRecordingManager.swift | 161 +++++++++--------- .../UI/PlacePage/PlacePageInteractor.swift | 10 ++ 5 files changed, 105 insertions(+), 87 deletions(-) diff --git a/iphone/CoreApi/CoreApi/Framework/MWMFrameworkHelper.h b/iphone/CoreApi/CoreApi/Framework/MWMFrameworkHelper.h index cbadf8945c..0272047ba5 100644 --- a/iphone/CoreApi/CoreApi/Framework/MWMFrameworkHelper.h +++ b/iphone/CoreApi/CoreApi/Framework/MWMFrameworkHelper.h @@ -21,12 +21,12 @@ NS_ASSUME_NONNULL_BEGIN typedef void (^SearchInDownloaderCompletions)(NSArray *results, BOOL finished); typedef void (^TrackRecordingUpdatedHandler)(TrackInfo * _Nonnull trackInfo); -@protocol TrackRecorder +@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. diff --git a/iphone/CoreApi/CoreApi/Framework/MWMFrameworkHelper.mm b/iphone/CoreApi/CoreApi/Framework/MWMFrameworkHelper.mm index 6b8ea0aa9a..47f6fe1461 100644 --- a/iphone/CoreApi/CoreApi/Framework/MWMFrameworkHelper.mm +++ b/iphone/CoreApi/CoreApi/Framework/MWMFrameworkHelper.mm @@ -235,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 { @@ -248,7 +248,7 @@ static Framework::ProductsPopupCloseReason ConvertProductPopupCloseReasonToCore( } + (ElevationProfileData * _Nonnull)trackRecordingElevationInfo { - return [[ElevationProfileData alloc] initWithElevationInfo:GetFramework().GetTrackRecordingCurrentElevationInfo()]; + return [[ElevationProfileData alloc] initWithElevationInfo:GetFramework().GetTrackRecordingElevationInfo()]; } // MARK: - ProductsManager diff --git a/iphone/Maps/Core/Location/MWMLocationManager.h b/iphone/Maps/Core/Location/MWMLocationManager.h index e662c47921..2bb3b033f7 100644 --- a/iphone/Maps/Core/Location/MWMLocationManager.h +++ b/iphone/Maps/Core/Location/MWMLocationManager.h @@ -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 + (void)start; + (void)stop; @@ -14,10 +21,8 @@ NS_SWIFT_NAME(LocationManager) + (void)removeObserver:(id)observer NS_SWIFT_NAME(remove(observer:)); + (void)setMyPositionMode:(MWMMyPositionMode)mode; -+ (void)checkLocationStatus; + (nullable CLLocation *)lastLocation; -+ (BOOL)isLocationProhibited; + (nullable CLHeading *)lastHeading; + (void)applicationDidBecomeActive; diff --git a/iphone/Maps/Core/TrackRecorder/TrackRecordingManager.swift b/iphone/Maps/Core/TrackRecorder/TrackRecordingManager.swift index 5012b2ed65..0cef948908 100644 --- a/iphone/Maps/Core/TrackRecorder/TrackRecordingManager.swift +++ b/iphone/Maps/Core/TrackRecorder/TrackRecordingManager.swift @@ -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) } diff --git a/iphone/Maps/UI/PlacePage/PlacePageInteractor.swift b/iphone/Maps/UI/PlacePage/PlacePageInteractor.swift index bb2e8161b3..d316324134 100644 --- a/iphone/Maps/UI/PlacePage/PlacePageInteractor.swift +++ b/iphone/Maps/UI/PlacePage/PlacePageInteractor.swift @@ -242,6 +242,16 @@ extension PlacePageInteractor: ActionBarViewControllerDelegate { // TODO: 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 + TrackRecordingManager.shared.processAction(.stopAndSave(name: "")) { [weak self] result in + switch result { + case .success: + break + case .error: + self?.presenter?.closeAnimated() + } + } @unknown default: fatalError() } -- 2.45.3 From 9ada5eeb6b00f79f39f24a4e5589f41137872733 Mon Sep 17 00:00:00 2001 From: Kiryl Kaveryn Date: Thu, 9 Jan 2025 16:46:22 +0400 Subject: [PATCH 4/8] [ios] implement PP for the track recording Signed-off-by: Kiryl Kaveryn --- .../MWMMapViewControlsManager.h | 12 ++- .../MWMMapViewControlsManager.mm | 37 +++---- ... TrackRecordingButtonViewController.swift} | 79 +++++++++------ iphone/Maps/Classes/MapViewController.h | 1 + iphone/Maps/Classes/MapViewController.mm | 81 ++++++++++----- iphone/Maps/Core/Theme/GlobalStyleSheet.swift | 1 + .../TrackRecorder/TrackRecordingManager.swift | 34 ++++++- .../Contents.json | 15 +++ .../ic_track_save.png | Bin 0 -> 4317 bytes iphone/Maps/Maps.xcodeproj/project.pbxproj | 12 ++- .../TrackRecordingButtonArea.swift | 2 +- .../Menu/BottomMenuInteractor.swift | 9 +- .../Components/ActionBarViewController.swift | 38 +++++--- .../PlacePageHeaderPresenter.swift | 2 - .../PlacePageHeaderViewController.swift | 10 -- .../Maps/UI/PlacePage/PlacePageBuilder.swift | 11 +-- .../UI/PlacePage/PlacePageInteractor.swift | 10 +- .../ActionBar/MWMActionBarButton.h | 1 + .../ActionBar/MWMActionBarButton.m | 8 +- .../PlacePageTrackRecordingLayout.swift | 92 ++++++++++++++++++ 20 files changed, 327 insertions(+), 128 deletions(-) rename iphone/Maps/Classes/CustomViews/MapViewControls/{TrackRecordingViewController.swift => TrackRecordingButtonViewController.swift} (73%) create mode 100644 iphone/Maps/Images.xcassets/Place Page/ic_placepage_save_track_recording.imageset/Contents.json create mode 100644 iphone/Maps/Images.xcassets/Place Page/ic_placepage_save_track_recording.imageset/ic_track_save.png create mode 100644 iphone/Maps/UI/PlacePage/PlacePageLayout/Layouts/PlacePageTrackRecordingLayout.swift diff --git a/iphone/Maps/Classes/CustomViews/MapViewControls/MWMMapViewControlsManager.h b/iphone/Maps/Classes/CustomViews/MapViewControls/MWMMapViewControlsManager.h index b57d9bef95..3d109865f3 100644 --- a/iphone/Maps/Classes/CustomViews/MapViewControls/MWMMapViewControlsManager.h +++ b/iphone/Maps/Classes/CustomViews/MapViewControls/MWMMapViewControlsManager.h @@ -4,7 +4,13 @@ @class MapViewController; @class BottomTabBarViewController; -@class TrackRecordingViewController; +@class TrackRecordingButtonViewController; + +typedef NS_ENUM(NSUInteger, TrackRecordingButtonState) { + TrackRecordingButtonStateHidden, + TrackRecordingButtonStateVisible, + TrackRecordingButtonStateClosed, +}; @protocol MWMFeatureHolder; @@ -20,7 +26,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; @@ -34,6 +40,8 @@ - (void)viewWillTransitionToSize:(CGSize)size withTransitionCoordinator:(id)coordinator; +- (void)setTrackRecordingButtonState:(TrackRecordingButtonState)state; + #pragma mark - MWMNavigationDashboardManager - (void)onRoutePrepare; diff --git a/iphone/Maps/Classes/CustomViews/MapViewControls/MWMMapViewControlsManager.mm b/iphone/Maps/Classes/CustomViews/MapViewControls/MWMMapViewControlsManager.mm index 8357f5e63e..703e2df9ea 100644 --- a/iphone/Maps/Classes/CustomViews/MapViewControls/MWMMapViewControlsManager.mm +++ b/iphone/Maps/Classes/CustomViews/MapViewControls/MWMMapViewControlsManager.mm @@ -28,15 +28,15 @@ NSString *const kMapToCategorySelectorSegue = @"MapToCategorySelectorSegue"; @interface MWMMapViewControlsManager () -@property(nonatomic) MWMSideButtons *sideButtons; -@property(nonatomic) MWMTrafficButtonViewController *trafficButton; -@property(nonatomic) UIButton *promoButton; -@property(nonatomic) UIViewController *menuController; +@property(nonatomic) MWMSideButtons * sideButtons; +@property(nonatomic) MWMTrafficButtonViewController * trafficButton; +@property(nonatomic) UIButton * promoButton; +@property(nonatomic) UIViewController * menuController; @property(nonatomic) id placePageManager; -@property(nonatomic) MWMNavigationDashboardManager *navigationManager; -@property(nonatomic) SearchOnMapManager *searchManager; +@property(nonatomic) MWMNavigationDashboardManager * navigationManager; +@property(nonatomic) SearchOnMapManager * searchManager; -@property(weak, nonatomic) MapViewController *ownerController; +@property(weak, nonatomic) MapViewController * ownerController; @property(nonatomic) BOOL disableStandbyOnRouteFollowing; @property(nonatomic) BOOL isAddingPlace; @@ -63,17 +63,10 @@ NSString *const kMapToCategorySelectorSegue = @"MapToCategorySelectorSegue"; self.menuState = MWMBottomMenuStateInactive; self.menuRestoreState = MWMBottomMenuStateInactive; self.isAddingPlace = NO; - [TrackRecordingManager.shared addObserver:self recordingIsActiveDidChangeHandler:^(TrackRecordingState state, TrackInfo * trackInfo) { - [self setTrackRecordingButtonHidden:state == TrackRecordingStateInactive]; - }]; self.searchManager = controller.searchManager; return self; } -- (void)dealloc { - [TrackRecordingManager.shared removeObserver:self]; -} - - (UIStatusBarStyle)preferredStatusBarStyle { BOOL const isNavigationUnderStatusBar = self.navigationManager.state != MWMNavigationDashboardStateHidden && self.navigationManager.state != MWMNavigationDashboardStateNavigation; @@ -280,17 +273,15 @@ NSString *const kMapToCategorySelectorSegue = @"MapToCategorySelectorSegue"; self.trafficButton.hidden = self.hidden || _trafficButtonHidden; } -- (void)setTrackRecordingButtonHidden:(BOOL)trackRecordingButtonHidden { - if (trackRecordingButtonHidden && _trackRecordingButton) { - [self.trackRecordingButton closeWithCompletion:^{ - [MWMMapWidgetsHelper updateLayoutForAvailableArea]; - }]; - _trackRecordingButton = nil; +- (void)setTrackRecordingButtonState:(TrackRecordingButtonState)state { + if (!_trackRecordingButton) { + _trackRecordingButton = [[TrackRecordingButtonViewController alloc] init]; } - else if (!trackRecordingButtonHidden && !_trackRecordingButton) { - _trackRecordingButton = [[TrackRecordingViewController alloc] init]; + [self.trackRecordingButton setState:state completion:^{ [MWMMapWidgetsHelper updateLayoutForAvailableArea]; - } + }]; + if (state == TrackRecordingButtonStateClosed) + _trackRecordingButton = nil; } - (void)setMenuState:(MWMBottomMenuState)menuState { diff --git a/iphone/Maps/Classes/CustomViews/MapViewControls/TrackRecordingViewController.swift b/iphone/Maps/Classes/CustomViews/MapViewControls/TrackRecordingButtonViewController.swift similarity index 73% rename from iphone/Maps/Classes/CustomViews/MapViewControls/TrackRecordingViewController.swift rename to iphone/Maps/Classes/CustomViews/MapViewControls/TrackRecordingButtonViewController.swift index a4385108aa..fd864d2b08 100644 --- a/iphone/Maps/Classes/CustomViews/MapViewControls/TrackRecordingViewController.swift +++ b/iphone/Maps/Classes/CustomViews/MapViewControls/TrackRecordingButtonViewController.swift @@ -1,4 +1,4 @@ -final class TrackRecordingViewController: MWMViewController { +final class TrackRecordingButtonViewController: MWMViewController { private enum Constants { static let buttonDiameter = CGFloat(48) @@ -13,6 +13,7 @@ final class TrackRecordingViewController: MWMViewController { private var blinkingTimer: Timer? private var topConstraint = NSLayoutConstraint() private var trailingConstraint = NSLayoutConstraint() + private var state: TrackRecordingButtonState = .hidden private static var availableArea: CGRect = .zero private static var topConstraintValue: CGFloat { @@ -38,31 +39,29 @@ final class TrackRecordingViewController: MWMViewController { fatalError("init(coder:) has not been implemented") } - override func viewDidAppear(_ animated: Bool) { - super.viewDidAppear(animated) - UIView.transition(with: self.view, - duration: kDefaultAnimationDuration, - options: .transitionCrossDissolve, - animations: { - self.button.isHidden = false - }) + override func viewDidLoad() { + super.viewDidLoad() + // async is for smoother appearance + DispatchQueue.main.asyncAfter(deadline: .now() + kDefaultAnimationDuration) { + self.setState(self.state, completion: nil) + } } // MARK: - Public methods @objc - func close(completion: @escaping (() -> Void)) { - stopTimer() - UIView.transition(with: self.view, - duration: kDefaultAnimationDuration, - options: .transitionCrossDissolve, - animations: { - self.button.isHidden = true - }, completion: { _ in - self.removeFromParent() - self.view.removeFromSuperview() - completion() - }) + func setState(_ state: TrackRecordingButtonState, completion: (() -> Void)?) { + self.state = state + switch state { + case .visible: + setHidden(false, completion: nil) + case .hidden: + setHidden(true, completion: completion) + case .closed: + close(completion: completion) + @unknown default: + fatalError() + } } // MARK: - Private methods @@ -75,7 +74,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 } @@ -97,7 +96,7 @@ final class TrackRecordingViewController: MWMViewController { } private func updateLayout() { - guard let superview = self.view.superview else { return } + guard let superview = view.superview else { return } superview.animateConstraints { self.topConstraint.constant = Self.topConstraintValue self.trailingConstraint.constant = Self.trailingConstraintValue @@ -123,23 +122,39 @@ final class TrackRecordingViewController: MWMViewController { blinkingTimer = nil } + private func setHidden(_ hidden: Bool, completion: (() -> Void)?) { + UIView.transition(with: self.view, + duration: kDefaultAnimationDuration, + options: .transitionCrossDissolve, + animations: { + self.button.isHidden = hidden + }) { _ in + completion?() + } + } + + private func close(completion: (() -> Void)?) { + stopTimer() + setHidden(true) { [weak self] in + guard let self else { return } + self.removeFromParent() + self.view.removeFromSuperview() + completion?() + } + } + static func updateAvailableArea(_ frame: CGRect) { availableArea = frame - guard let controller = MapViewController.shared()?.controlsManager.trackRecordingButton else { return } + guard let button = MapViewController.shared()?.controlsManager.trackRecordingButton else { return } DispatchQueue.main.async { - controller.updateLayout() + button.updateLayout() } } // 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() } } diff --git a/iphone/Maps/Classes/MapViewController.h b/iphone/Maps/Classes/MapViewController.h index c6c1a3e3d5..e45d05b848 100644 --- a/iphone/Maps/Classes/MapViewController.h +++ b/iphone/Maps/Classes/MapViewController.h @@ -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; diff --git a/iphone/Maps/Classes/MapViewController.mm b/iphone/Maps/Classes/MapViewController.mm index 37944d4297..00f0f76e9d 100644 --- a/iphone/Maps/Classes/MapViewController.mm +++ b/iphone/Maps/Classes/MapViewController.mm @@ -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; @@ -116,29 +117,7 @@ NSString *const kSettingsSegue = @"Map2Settings"; return [MapsAppDelegate theApp].mapViewController; } -#pragma mark - Map Navigation - -- (void)showTrackRecordingPlacePage { - __block PlacePageData * placePageData = [[PlacePageData alloc] initWithTrackInfo:TrackRecordingManager.shared.trackRecordingInfo - elevationInfo:[MWMFrameworkHelper trackRecordingElevationInfo]]; - [TrackRecordingManager.shared addObserver:self recordingIsActiveDidChangeHandler:^(TrackRecordingState state, TrackInfo * _Nonnull trackInfo) { - switch (state) { - case TrackRecordingStateInactive: - [self stopObservingTrackRecordingUpdates]; - break; - case TrackRecordingStateActive: - if (UIApplication.sharedApplication.applicationState != UIApplicationStateActive) - return; - [placePageData updateWithTrackInfo:trackInfo elevationInfo:[MWMFrameworkHelper trackRecordingElevationInfo]]; - break; - } - }]; - [self showOrUpdatePlacePage:placePageData]; -} - -- (void)stopObservingTrackRecordingUpdates { - [TrackRecordingManager.shared removeObserver:self]; -} +#pragma mark - PlacePage - (void)showOrUpdatePlacePage:(PlacePageData *)data { if (self.searchManager.isSearching) @@ -146,9 +125,10 @@ NSString *const kSettingsSegue = @"Map2Settings"; 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]; } @@ -433,6 +413,9 @@ NSString *const kSettingsSegue = @"Map2Settings"; // After all users migrate to OAuth2 we can remove next code [self migrateOAuthCredentials]; + if (self.trackRecordingManager.isActive) + [self showTrackRecordingPlacePage]; + /// @todo: Uncomment update dialog when will be ready to handle big traffic bursts. /* if (!DeepLinkHandler.shared.isLaunchedByDeeplink) @@ -754,6 +737,12 @@ NSString *const kSettingsSegue = @"Map2Settings"; return _searchManager; } +- (TrackRecordingManager *)trackRecordingManager { + if (!_trackRecordingManager) + _trackRecordingManager = TrackRecordingManager.shared; + return _trackRecordingManager; +} + - (UIView * _Nullable)searchViewContainer { return self.searchManager.viewController.view; } @@ -873,6 +862,50 @@ NSString *const kSettingsSegue = @"Map2Settings"; } } +// MARK: - Track Recording Place Page + +- (void)showTrackRecordingPlacePage { + if ([self.trackRecordingManager contains:self]) { + [self dismissPlacePage]; + return; + } + PlacePageData * placePageData = [[PlacePageData alloc] initWithTrackInfo:self.trackRecordingManager.trackRecordingInfo + elevationInfo:self.trackRecordingManager.trackRecordingElevationProfileData]; + [self.controlsManager setTrackRecordingButtonState:TrackRecordingButtonStateHidden]; + [self showOrUpdatePlacePage:placePageData]; + [self startObservingTrackRecordingUpdatesForPlacePageData:placePageData]; +} + +- (void)startObservingTrackRecordingUpdatesForPlacePageData:(PlacePageData *)placePageData { + __weak __typeof(self) weakSelf = self; + [self.trackRecordingManager addObserver:self + recordingIsActiveDidChangeHandler:^(TrackRecordingState state, + TrackInfo * _Nonnull trackInfo, + ElevationProfileData * _Nonnull (^ _Nullable elevationData) ()) { + __strong __typeof(weakSelf) self = weakSelf; + + switch (state) { + case TrackRecordingStateInactive: + [self stopObservingTrackRecordingUpdates]; + [self.controlsManager setTrackRecordingButtonState:TrackRecordingButtonStateClosed]; + break; + case TrackRecordingStateActive: + if (UIApplication.sharedApplication.applicationState != UIApplicationStateActive) + return; + [self.controlsManager setTrackRecordingButtonState:TrackRecordingButtonStateHidden]; + [placePageData updateWithTrackInfo:trackInfo + elevationInfo:elevationData()]; + break; + } + }]; +} + +- (void)stopObservingTrackRecordingUpdates { + [self.trackRecordingManager removeObserver:self]; + if (self.trackRecordingManager.isActive) + [self.controlsManager setTrackRecordingButtonState:TrackRecordingButtonStateVisible]; +} + // MARK: - Handle macOS trackpad gestures - (void)handlePan:(UIPanGestureRecognizer *)recognizer API_AVAILABLE(ios(14.0)) { diff --git a/iphone/Maps/Core/Theme/GlobalStyleSheet.swift b/iphone/Maps/Core/Theme/GlobalStyleSheet.swift index 60590d985b..e5bf69bc67 100644 --- a/iphone/Maps/Core/Theme/GlobalStyleSheet.swift +++ b/iphone/Maps/Core/Theme/GlobalStyleSheet.swift @@ -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 diff --git a/iphone/Maps/Core/TrackRecorder/TrackRecordingManager.swift b/iphone/Maps/Core/TrackRecorder/TrackRecordingManager.swift index 0cef948908..0e4e72e57b 100644 --- a/iphone/Maps/Core/TrackRecorder/TrackRecordingManager.swift +++ b/iphone/Maps/Core/TrackRecorder/TrackRecordingManager.swift @@ -24,13 +24,22 @@ enum TrackRecordingActionResult { protocol TrackRecordingObservable: AnyObject { var recordingState: TrackRecordingState { get } var trackRecordingInfo: TrackInfo { get } + var trackRecordingElevationProfileData: ElevationProfileData { get } func addObserver(_ observer: AnyObject, recordingIsActiveDidChangeHandler: @escaping TrackRecordingStateHandler) func removeObserver(_ observer: AnyObject) func contains(_ observer: AnyObject) -> Bool } -typealias TrackRecordingStateHandler = (TrackRecordingState, TrackInfo) -> Void +/// A handler type for extracting elevation profile data on demand. +typealias ElevationProfileDataExtractionHandler = () -> ElevationProfileData + +/// A callback type that notifies observers about track recording state changes. +/// - Parameters: +/// - state: The current recording state. +/// - info: The current track recording info. +/// - elevationProfileExtractor: A closure to fetch elevation profile data lazily. +typealias TrackRecordingStateHandler = (TrackRecordingState, TrackInfo, ElevationProfileDataExtractionHandler?) -> Void @objcMembers final class TrackRecordingManager: NSObject { @@ -62,6 +71,10 @@ final class TrackRecordingManager: NSObject { private var observations: [Observation] = [] private(set) var trackRecordingInfo: TrackInfo = .empty() + var trackRecordingElevationProfileData: ElevationProfileData { + FrameworkHelper.trackRecordingElevationInfo() + } + var recordingState: TrackRecordingState { trackRecorder.isTrackRecordingEnabled() ? .active : .inactive } @@ -73,6 +86,7 @@ final class TrackRecordingManager: NSObject { self.locationService = locationService self.activityManager = activityManager super.init() + self.subscribeOnTheAppLifecycleEvents() } // MARK: - Public methods @@ -115,6 +129,13 @@ final class TrackRecordingManager: NSObject { // MARK: - Private methods + private func subscribeOnTheAppLifecycleEvents() { + NotificationCenter.default.addObserver(self, + selector: #selector(notifyObservers), + name: UIApplication.didBecomeActiveNotification, + object: nil) + } + private func checkIsLocationEnabled() throws(TrackRecordingError) { if locationService.isLocationProhibited() { throw TrackRecordingError.locationIsProhibited @@ -199,7 +220,9 @@ extension TrackRecordingManager: TrackRecordingObservable { guard !observations.contains(where: { $0.observer === observer }) else { return } let observation = Observation(observer: observer, recordingStateDidChangeHandler: recordingIsActiveDidChangeHandler) observations.append(observation) - recordingIsActiveDidChangeHandler(recordingState, trackRecordingInfo) + recordingIsActiveDidChangeHandler(recordingState, trackRecordingInfo) { + self.trackRecordingElevationProfileData + } } @objc @@ -212,8 +235,11 @@ extension TrackRecordingManager: TrackRecordingObservable { observations.contains { $0.observer === observer } } + @objc private func notifyObservers() { - observations = observations.filter { $0.observer != nil } - observations.forEach { $0.recordingStateDidChangeHandler?(recordingState, trackRecordingInfo) } + observations.removeAll { $0.observer == nil } + observations.forEach { + $0.recordingStateDidChangeHandler?(recordingState, trackRecordingInfo, { self.trackRecordingElevationProfileData }) + } } } diff --git a/iphone/Maps/Images.xcassets/Place Page/ic_placepage_save_track_recording.imageset/Contents.json b/iphone/Maps/Images.xcassets/Place Page/ic_placepage_save_track_recording.imageset/Contents.json new file mode 100644 index 0000000000..4c2879a332 --- /dev/null +++ b/iphone/Maps/Images.xcassets/Place Page/ic_placepage_save_track_recording.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "ic_track_save.png", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "template-rendering-intent" : "template" + } +} diff --git a/iphone/Maps/Images.xcassets/Place Page/ic_placepage_save_track_recording.imageset/ic_track_save.png b/iphone/Maps/Images.xcassets/Place Page/ic_placepage_save_track_recording.imageset/ic_track_save.png new file mode 100644 index 0000000000000000000000000000000000000000..d0bee6031e9efa62c6d8111eb1f6bedd96fdb328 GIT binary patch literal 4317 zcma)fXH*kR(>6s^5Tr`qFIoO2Q7#J8hOzs-k&_~)o zIe(Vk->)6@p%1KqcO8Qn7$h|Q38P{Db$2?*7;Iy#$AFj=*`_OJeD0XvVPI&?yg>6{ zVPN12GBLPg8_xI(n;t4W%r``+6%pWqG}QD2ovUTrv*l`KTIAzGT(M#ClFM>te8cw0 zz9EpsMvl@{?r6Z3hTIqpg`=~?e@lJk|xtGDX>uw(^$0k8eK2yr=VvSMfRb7~lC-Gs%(!zn6IW`uud^^&5@0O- zD0>&fKh*xdOsQ#YoaHrIiAhJFh}}me;o`fJpU9&89ZZN_=P92@ERlmxGJe}WGDnKW zI28XsM<8I~R(d(*?W3P>4|2X@#saFye4>n{*&jpQ9G!vo(_`JJcA7UmtB7qy9f3W0 z`Go}`1iS>%>5COt;AYZM0y+pSJ5oqdr>6P{u!td&sk_TYQocjc;kbP zcE02Vw)c`N%rl$iwfGl14Z$*f$5d;WDA=5DALZ}LxHgL}ukHVM``6Fq+cFvCWJ_f1 zJH?;B(K(jd*D+{Kn>T6HY}UwqpI z%cH6uEz!uxpvxY$BXzI zm>OSc^iys{H-~R0x=1F%!v3BVQ&pcq{JR#Zt~wT&x$t96SI$ifPxE?J2WR~A@HfGg zU#`Z1*R-@5f%DSA_tfxMS zU^b|X6?8$;D!!Eow?wylgo$2}GaT7^zngHP33H$3Xxe44mJKozpdtu?z1yVVbJaKGJfCcg_6UPJCIo9<2DdxnnZ-O2ttMZ#SyWQvwXJch z;GPerN3v1y<%wIeZb7R~&*Tvu3+o}4AG+9F<7s(wJ54J~2d74i(2;8V%{d?EI&%xp53YRQb@HdJ|q zC#^KF>DxyOpFf^yJBBHD&e&zl(8zU0vBQth52bV_!vD$^qhT3-*#CLA9S zaRZa1!~`W6;8@_2O_(?z56yS^ZPB&9%?)_EPBYFZ`9?O<#ew*xj#3{PLDJx z$gbr~P^&=M8DWZX3x8(<3+`%Yrqd{dw;t1F#q+L3N*Z@zkj2bZgWrBUGk=J@nM-k9eRzVoh}U<)BrY3$5RB-3G-qoia>pAVuh& zX>C<6qWc=z8M!wY4tqtc(uHDs5~!-6$C zaqt4RzHjE=SFga*5uY@gcDP45hd~?QKRkyOz&zx-Sr4rDVPOW!^`zsWyHIo66-W;J z>O?Ou>d*q##gGQxpjKgL;@f`>L&39XZbc}gv+k=ng4b%o+BNJBL8k-0CFid>Tl5*f@?HK1UU|}If-q1+A>ppi=-tn6(D``(3`f&Ej){D>BX* zM;-sw$M{VHha;`{lpfl?O0!H()8z#$wBRj%j28=E283rk@?qx%(oLYVBbkn#6x_h= zmQ-o+gHW6xX6>w{*2ge5@p;y|(*BJajRnvHi`BU!_ZemI^B8yq98FVR@sSS*cz&;tuz5Pm zW^_T^D1hMCQf{P*+Bj7?Wo%Z9kHX~ez3TkqL34j>Fb@1cA8I9=YMy z+SQ$Fs&9fm(6tISN|xuy+Hg6m-F>~ZSk9lo{2@2~$^LLZ_D!e=n0&7G45PSF%{;M_ zWle4WUg6zn6{gc%cOXfmVGa=ccxchjHnr2_s#Bi{^^&`PDkAaAv}~tX%!Vb1)rrDT z^$XFc{!n|MNFxY4|MAM8Z}}c!zN*R{a-I^nMrQ4 zv+1~#K1^%Jn^g&LwbXU@PUsGMfj)^@(y3{*po+X5w8>GUkJ8+9OtIAMLTycy7D8f{ z5TP+eh!2Fnb8^*o*OvKSy}`>T#k}lJsZsm!lzZQPdh@{xfP`T{bpJ%NU04TRZ37lk zqZx7?HfCOJ4Hc3<^S-CD+^LK>M9t08L(f{nvp8w$V=+Zj^K0|6LEF{yGRp^0)4M1O zRx@Li5!DXnBKW;hIQP|)Y?@JtNC-&NdppyLxw1Uprspdg6<*hgwV1hD(U3*ki@fJ6 zpDS4h+B$^O#-^`P1#t_`4ZQYW4Y}>*LpoB?3lIC6dI<~tk?Be%+tpd?(jwUG8DC&^ zJE0vzNF4{;g+-_!&9*J#`96*6vx4N;Q`lOnHJ63ItLiXJnU=pmRPZ1`GEkkk;J4e8 zw?5B`uaq#E^*?-dJg-tUP|Ifu;|kfu5Kbn%nmyQKz*p~mAJnF9ES(C#X}mPq;BC7I zIs9cO3iA+XBjGCB5P@;(5TcC2H+G;?oTz}_DA5++3Q9GR|D=J(%y@7b40VHp?iGfV zD0FQ!5qb7g@*K3}lEoD=eqV$thB^6@-!0ifmLyg<HJS*MC}R^#Ov*;0{4O*nAL~YwoDk~zYR$5O=XNz5T0TfrgsI8Jtf&Ek=Sy7Z3!Lr^Z z#K6Jh_3Nx)F_JlFsV@QBM#YvR0X^raAr^>R&QHq%Wc}rVkBri5af1FaH&^)AnMb2! z&~q^TB#+1jd$xt+A8pIWA(_}K(Is7(31nMzBW=ukx(N+2FMe)(?WPjrdRbD$9-qu= zMIYoBCXB^pd2FuO*omkg=i9W+-u@nCzmk|U=8n?Cv=Hmr?;7eXoO217MicPGeXWbn zv4!=ZN73?@tg;p5xZW<+t@u=3Lko?M!vEk`oq3EOWu986LwxY6>X!kbGOO$6Ao-gy zxUw5pSOz~<&x*I%SC6 zzfpEh@>AcB#4N@K`GS2gC%$BUH-B8?q-b!I3N$V@Q{J0rWN8-bi-F{)UI`%akzay?73j$7Tko(X46iSWS03jZkYw3dR4i4Gd+*P#Y!;6qBfirsr{et_-;aqEGmVK*G;gEoH_W94l{ zn0cQ`nImy?P`?crT(7`m!8GwYAmW^&x{TjUG91O&S~R#syM|8YzV=V`$Ove sN)9vU4sN=}B%`JO*AM^y3kw`LWe})oNuvB#=KZ(8#L&V3q30g|e{aZt7XSbN literal 0 HcmV?d00001 diff --git a/iphone/Maps/Maps.xcodeproj/project.pbxproj b/iphone/Maps/Maps.xcodeproj/project.pbxproj index 1023b1e8e8..d1066ddea5 100644 --- a/iphone/Maps/Maps.xcodeproj/project.pbxproj +++ b/iphone/Maps/Maps.xcodeproj/project.pbxproj @@ -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 */; }; @@ -524,6 +524,7 @@ 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 */; }; 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 +1450,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 = ""; }; ED48BBB917C2B1E2003E7E92 /* CircleView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = CircleView.m; sourceTree = ""; }; - ED49D76F2CF0E3A8004AF27E /* TrackRecordingViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrackRecordingViewController.swift; sourceTree = ""; }; + ED49D76F2CF0E3A8004AF27E /* TrackRecordingButtonViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrackRecordingButtonViewController.swift; sourceTree = ""; }; ED4DC7732CAEDECC0029B338 /* ProductButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProductButton.swift; sourceTree = ""; }; ED4DC7742CAEDECC0029B338 /* ProductsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProductsViewController.swift; sourceTree = ""; }; ED4DC7752CAEDECC0029B338 /* ProductsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProductsViewModel.swift; sourceTree = ""; }; @@ -1490,6 +1491,7 @@ ED914ABD2D351FF800973C45 /* UILabel+SetFontStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UILabel+SetFontStyle.swift"; sourceTree = ""; }; ED9857072C4ED02D00694F6C /* MailComposer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MailComposer.swift; sourceTree = ""; }; ED99667D2B94FBC20083CE55 /* ColorPicker.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ColorPicker.swift; sourceTree = ""; }; + ED9DDF872D6F151000645BC8 /* PlacePageTrackRecordingLayout.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlacePageTrackRecordingLayout.swift; sourceTree = ""; }; EDA1EAA32CC7ECAD00DBDCAA /* ElevationProfileFormatter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ElevationProfileFormatter.swift; sourceTree = ""; }; EDBD68062B625724005DD151 /* LocationServicesDisabledAlert.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = LocationServicesDisabledAlert.xib; sourceTree = ""; }; EDBD680A2B62572E005DD151 /* LocationServicesDisabledAlert.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationServicesDisabledAlert.swift; sourceTree = ""; }; @@ -2569,7 +2571,7 @@ 34BC72091B0DECAE0012A34B /* MapViewControls */ = { isa = PBXGroup; children = ( - ED49D76F2CF0E3A8004AF27E /* TrackRecordingViewController.swift */, + ED49D76F2CF0E3A8004AF27E /* TrackRecordingButtonViewController.swift */, 340537621BBED98600D452C6 /* MWMMapViewControlsCommon.h */, 34BC72101B0DECAE0012A34B /* MWMMapViewControlsManager.h */, 34BC72111B0DECAE0012A34B /* MWMMapViewControlsManager.mm */, @@ -3031,6 +3033,7 @@ 99C6532123F2F506004322F3 /* IPlacePageLayout.swift */, 99F3EB0223F4178200C713F8 /* PlacePageCommonLayout.swift */, 993DF0B423F6B2EF00AC231A /* PlacePageTrackLayout.swift */, + ED9DDF872D6F151000645BC8 /* PlacePageTrackRecordingLayout.swift */, ); path = Layouts; sourceTree = ""; @@ -4489,7 +4492,7 @@ 340708651F2905A500029ECC /* NavigationInfoArea.swift in Sources */, 993DF0CC23F6BD0600AC231A /* ElevationDetailsPresenter.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 +4633,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 */, diff --git a/iphone/Maps/UI/AvailableArea/TrackRecordingButtonArea.swift b/iphone/Maps/UI/AvailableArea/TrackRecordingButtonArea.swift index 5272dd4931..9f1ca5e4a2 100644 --- a/iphone/Maps/UI/AvailableArea/TrackRecordingButtonArea.swift +++ b/iphone/Maps/UI/AvailableArea/TrackRecordingButtonArea.swift @@ -10,7 +10,7 @@ final class TrackRecordingButtonArea: AvailableArea { } override func notifyObserver() { - TrackRecordingViewController.updateAvailableArea(areaFrame) + TrackRecordingButtonViewController.updateAvailableArea(areaFrame) } } diff --git a/iphone/Maps/UI/BottomMenu/Menu/BottomMenuInteractor.swift b/iphone/Maps/UI/BottomMenu/Menu/BottomMenuInteractor.swift index 8457445c3c..d761d01e18 100644 --- a/iphone/Maps/UI/BottomMenu/Menu/BottomMenuInteractor.swift +++ b/iphone/Maps/UI/BottomMenu/Menu/BottomMenuInteractor.swift @@ -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() } } diff --git a/iphone/Maps/UI/PlacePage/Components/ActionBarViewController.swift b/iphone/Maps/UI/PlacePage/Components/ActionBarViewController.swift index 1954964d90..f88c528079 100644 --- a/iphone/Maps/UI/PlacePage/Components/ActionBarViewController.swift +++ b/iphone/Maps/UI/PlacePage/Components/ActionBarViewController.swift @@ -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() { diff --git a/iphone/Maps/UI/PlacePage/Components/PlacePageHeader/PlacePageHeaderPresenter.swift b/iphone/Maps/UI/PlacePage/Components/PlacePageHeader/PlacePageHeaderPresenter.swift index 8278ae5e67..daf2b7f323 100644 --- a/iphone/Maps/UI/PlacePage/Components/PlacePageHeader/PlacePageHeaderPresenter.swift +++ b/iphone/Maps/UI/PlacePage/Components/PlacePageHeader/PlacePageHeaderPresenter.swift @@ -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() { diff --git a/iphone/Maps/UI/PlacePage/Components/PlacePageHeader/PlacePageHeaderViewController.swift b/iphone/Maps/UI/PlacePage/Components/PlacePageHeader/PlacePageHeaderViewController.swift index 04c05e7a76..b77bfda011 100644 --- a/iphone/Maps/UI/PlacePage/Components/PlacePageHeader/PlacePageHeaderViewController.swift +++ b/iphone/Maps/UI/PlacePage/Components/PlacePageHeader/PlacePageHeaderViewController.swift @@ -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 diff --git a/iphone/Maps/UI/PlacePage/PlacePageBuilder.swift b/iphone/Maps/UI/PlacePage/PlacePageBuilder.swift index cac8307f61..8502a0d496 100644 --- a/iphone/Maps/UI/PlacePage/PlacePageBuilder.swift +++ b/iphone/Maps/UI/PlacePage/PlacePageBuilder.swift @@ -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() } diff --git a/iphone/Maps/UI/PlacePage/PlacePageInteractor.swift b/iphone/Maps/UI/PlacePage/PlacePageInteractor.swift index d316324134..a685ff2d91 100644 --- a/iphone/Maps/UI/PlacePage/PlacePageInteractor.swift +++ b/iphone/Maps/UI/PlacePage/PlacePageInteractor.swift @@ -25,7 +25,7 @@ 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) + let isTrack = placePageData.trackData != nil/* && bookmarksManager.hasTrack(placePageData.trackData!.trackId)*/ guard isBookmark || isTrack else { presenter?.closeAnimated() return @@ -239,11 +239,11 @@ 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 + // TODO: (KK) pass name typed by user TrackRecordingManager.shared.processAction(.stopAndSave(name: "")) { [weak self] result in switch result { case .success: @@ -287,8 +287,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) } } diff --git a/iphone/Maps/UI/PlacePage/PlacePageLayout/ActionBar/MWMActionBarButton.h b/iphone/Maps/UI/PlacePage/PlacePageLayout/ActionBar/MWMActionBarButton.h index a82a84c698..d8c5f33ab1 100644 --- a/iphone/Maps/UI/PlacePage/PlacePageLayout/ActionBar/MWMActionBarButton.h +++ b/iphone/Maps/UI/PlacePage/PlacePageLayout/ActionBar/MWMActionBarButton.h @@ -3,6 +3,7 @@ typedef NS_ENUM(NSInteger, MWMActionBarButtonType) { MWMActionBarButtonTypeBookingSearch, MWMActionBarButtonTypeBookmark, MWMActionBarButtonTypeTrack, + MWMActionBarButtonTypeSaveTrackRecording, MWMActionBarButtonTypeCall, MWMActionBarButtonTypeDownload, MWMActionBarButtonTypeMore, diff --git a/iphone/Maps/UI/PlacePage/PlacePageLayout/ActionBar/MWMActionBarButton.m b/iphone/Maps/UI/PlacePage/PlacePageLayout/ActionBar/MWMActionBarButton.m index 0a7401bafa..dd38a97b9d 100644 --- a/iphone/Maps/UI/PlacePage/PlacePageLayout/ActionBar/MWMActionBarButton.m +++ b/iphone/Maps/UI/PlacePage/PlacePageLayout/ActionBar/MWMActionBarButton.m @@ -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; diff --git a/iphone/Maps/UI/PlacePage/PlacePageLayout/Layouts/PlacePageTrackRecordingLayout.swift b/iphone/Maps/UI/PlacePage/PlacePageLayout/Layouts/PlacePageTrackRecordingLayout.swift new file mode 100644 index 0000000000..5dba5ce430 --- /dev/null +++ b/iphone/Maps/UI/PlacePage/PlacePageLayout/Layouts/PlacePageTrackRecordingLayout.swift @@ -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() + } +} -- 2.45.3 From 0bb11f992a705a1a2e9c5370ffad750284f2ba01 Mon Sep 17 00:00:00 2001 From: Kiryl Kaveryn Date: Thu, 9 Jan 2025 16:57:11 +0400 Subject: [PATCH 5/8] [ios] refactor ElevationProfileViewController to handle live ele info updates 1. remove a stroryboard and implement VC and ElevationProfileDescriptionCell programmatically 2. move the description collection view over the chart 3. remove some unused code 4. add isChartViewInfoHidden to show/hide the info view and enable/disable user interation (will be used for the track recording) Signed-off-by: Kiryl Kaveryn --- .../ElevationProfile/ElevationProfilePresenter.swift | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/iphone/Maps/UI/PlacePage/Components/ElevationProfile/ElevationProfilePresenter.swift b/iphone/Maps/UI/PlacePage/Components/ElevationProfile/ElevationProfilePresenter.swift index 585ae633a5..d4540645f7 100644 --- a/iphone/Maps/UI/PlacePage/Components/ElevationProfile/ElevationProfilePresenter.swift +++ b/iphone/Maps/UI/PlacePage/Components/ElevationProfile/ElevationProfilePresenter.swift @@ -85,6 +85,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 -- 2.45.3 From 5ba9b4dd30d31b678073984f51ecfb3fbe07d7ca Mon Sep 17 00:00:00 2001 From: Kiryl Kaveryn Date: Thu, 9 Jan 2025 15:38:51 +0400 Subject: [PATCH 6/8] [ios] decrease minimum altitude step to 25 to show ele info for flat areas Signed-off-by: Kiryl Kaveryn --- .../Components/ElevationProfile/ElevationProfileFormatter.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/iphone/Maps/UI/PlacePage/Components/ElevationProfile/ElevationProfileFormatter.swift b/iphone/Maps/UI/PlacePage/Components/ElevationProfile/ElevationProfileFormatter.swift index 4a17c5c4c5..a6dbab87a8 100644 --- a/iphone/Maps/UI/PlacePage/Components/ElevationProfile/ElevationProfileFormatter.swift +++ b/iphone/Maps/UI/PlacePage/Components/ElevationProfile/ElevationProfileFormatter.swift @@ -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 } -- 2.45.3 From 6d5c06272e2c20b232f45398ce523619a2ab9894 Mon Sep 17 00:00:00 2001 From: Kiryl Kaveryn Date: Fri, 10 Jan 2025 15:15:57 +0400 Subject: [PATCH 7/8] [ios] fix bm/ track/track recording updates handling on the PP Signed-off-by: Kiryl Kaveryn --- .../UI/PlacePage/PlacePageInteractor.swift | 33 +++++++++++++++---- 1 file changed, 26 insertions(+), 7 deletions(-) diff --git a/iphone/Maps/UI/PlacePage/PlacePageInteractor.swift b/iphone/Maps/UI/PlacePage/PlacePageInteractor.swift index a685ff2d91..28a1ebb2a1 100644 --- a/iphone/Maps/UI/PlacePage/PlacePageInteractor.swift +++ b/iphone/Maps/UI/PlacePage/PlacePageInteractor.swift @@ -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() { @@ -311,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() } -- 2.45.3 From e001e75aba0332d66b41c92a002289d89f952fd1 Mon Sep 17 00:00:00 2001 From: Kiryl Kaveryn Date: Wed, 26 Feb 2025 20:17:55 +0400 Subject: [PATCH 8/8] [ios] add TrackRecordingManager unit tests Signed-off-by: Kiryl Kaveryn --- iphone/Maps/Maps.xcodeproj/project.pbxproj | 12 ++ .../TrackRecordingManagerTests.swift | 198 ++++++++++++++++++ 2 files changed, 210 insertions(+) create mode 100644 iphone/Maps/Tests/Core/TrackRecorder/TrackRecordingManagerTests.swift diff --git a/iphone/Maps/Maps.xcodeproj/project.pbxproj b/iphone/Maps/Maps.xcodeproj/project.pbxproj index d1066ddea5..0cf4419213 100644 --- a/iphone/Maps/Maps.xcodeproj/project.pbxproj +++ b/iphone/Maps/Maps.xcodeproj/project.pbxproj @@ -525,6 +525,7 @@ 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 */; }; @@ -1492,6 +1493,7 @@ ED9857072C4ED02D00694F6C /* MailComposer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MailComposer.swift; sourceTree = ""; }; ED99667D2B94FBC20083CE55 /* ColorPicker.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ColorPicker.swift; sourceTree = ""; }; ED9DDF872D6F151000645BC8 /* PlacePageTrackRecordingLayout.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlacePageTrackRecordingLayout.swift; sourceTree = ""; }; + ED9DDF9B2D6F6DA300645BC8 /* TrackRecordingManagerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrackRecordingManagerTests.swift; sourceTree = ""; }; EDA1EAA32CC7ECAD00DBDCAA /* ElevationProfileFormatter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ElevationProfileFormatter.swift; sourceTree = ""; }; EDBD68062B625724005DD151 /* LocationServicesDisabledAlert.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = LocationServicesDisabledAlert.xib; sourceTree = ""; }; EDBD680A2B62572E005DD151 /* LocationServicesDisabledAlert.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationServicesDisabledAlert.swift; sourceTree = ""; }; @@ -2767,6 +2769,7 @@ 4B4153B62BF9709100EE4B02 /* Core */ = { isa = PBXGroup; children = ( + ED9DDF982D6F6D8000645BC8 /* TrackRecorder */, EDF838AB2C00B9C7007E4E67 /* iCloudTests */, 4B4153B72BF970A000EE4B02 /* TextToSpeech */, ); @@ -3315,6 +3318,14 @@ path = ColorPicker; sourceTree = ""; }; + ED9DDF982D6F6D8000645BC8 /* TrackRecorder */ = { + isa = PBXGroup; + children = ( + ED9DDF9B2D6F6DA300645BC8 /* TrackRecordingManagerTests.swift */, + ); + path = TrackRecorder; + sourceTree = ""; + }; EDC4E3422C5D1BD3009286A2 /* RecentlyDeletedTests */ = { isa = PBXGroup; children = ( @@ -4911,6 +4922,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 */, diff --git a/iphone/Maps/Tests/Core/TrackRecorder/TrackRecordingManagerTests.swift b/iphone/Maps/Tests/Core/TrackRecorder/TrackRecordingManagerTests.swift new file mode 100644 index 0000000000..ecb44380d2 --- /dev/null +++ b/iphone/Maps/Tests/Core/TrackRecorder/TrackRecordingManagerTests.swift @@ -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) {} +} -- 2.45.3