[ios] implement TrackRecording place page

1. add an new screen (layout)
2. add TR icon for the bottom tabbar
3. share current location from the TR PP
4. refactor TR manager to properly handle state updates and pass them to the LiveActivityManager and PlacePage
5. add init/update with TrackInfo/EleInfo methods to the PlacePageData and PlacePagePreviewData to update the PP state
Signed-off-by: Kiryl Kaveryn <kirylkaveryn@gmail.com>
This commit is contained in:
Kiryl Kaveryn
2025-01-09 16:35:31 +04:00
committed by Yannik Bloscheck
parent 5d0b8f1c04
commit b79724f248
35 changed files with 554 additions and 216 deletions

View File

@@ -4,7 +4,7 @@
@implementation TrackInfo
+ (TrackInfo *)emptyInfo {
return [[TrackInfo alloc] init];
return [[TrackInfo alloc] initWithTrackStatistics:TrackStatistics()];
}
@end

View File

@@ -21,12 +21,12 @@ 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.

View File

@@ -239,8 +239,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 {
@@ -252,7 +252,7 @@ static Framework::ProductsPopupCloseReason ConvertProductPopupCloseReasonToCore(
}
+ (ElevationProfileData * _Nonnull)trackRecordingElevationInfo {
return [[ElevationProfileData alloc] initWithElevationInfo:GetFramework().GetTrackRecordingCurrentElevationInfo()];
return [[ElevationProfileData alloc] initWithElevationInfo:GetFramework().GetTrackRecordingElevationInfo()];
}
// MARK: - ProductsManager

View File

@@ -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

View File

@@ -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,16 @@ 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 = @" • ";
_title = [@[trackInfo.duration, trackInfo.distance] componentsJoinedByString:kSeparator];
}
return self;
}
@end
@implementation PlacePagePreviewData (Core)

View File

@@ -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

View File

@@ -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)

View File

@@ -11,6 +11,7 @@ NS_ASSUME_NONNULL_BEGIN
elevationInfo:(ElevationInfo const &)elevationInfo
activePoint:(double)activePoint
myPosition:(double)myPosition;
- (instancetype)initWithElevationInfo:(ElevationInfo const &)elevationInfo;
@end

View File

@@ -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;

View File

@@ -30,7 +30,25 @@ static ElevationDifficulty convertDifficulty(uint8_t difficulty) {
if (self) {
_trackId = trackId;
_difficulty = convertDifficulty(elevationInfo.GetDifficulty());
_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) {
@@ -41,12 +59,7 @@ static ElevationDifficulty convertDifficulty(uint8_t difficulty) {
andAltitude:point.m_point.GetAltitude()];
[pointsArray addObject:elevationPoint];
}
_points = [pointsArray copy];
_activePoint = activePoint;
_myPosition = myPosition;
}
return self;
return [pointsArray copy];
}
@end

View File

@@ -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

View File

@@ -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];

View File

@@ -0,0 +1,7 @@
extension UIAlertController {
static func unknownCurrentPosition() -> UIAlertController {
let alert = UIAlertController(title: L("unknown_current_position"), message: nil, preferredStyle: .alert)
alert.addAction(UIAlertAction(title: L("ok"), style: .default, handler: nil))
return alert
}
}

View File

@@ -4,9 +4,15 @@
@class MapViewController;
@class BottomTabBarViewController;
@class TrackRecordingViewController;
@class TrackRecordingButtonViewController;
@class SearchQuery;
typedef NS_ENUM(NSUInteger, TrackRecordingButtonState) {
TrackRecordingButtonStateHidden,
TrackRecordingButtonStateVisible,
TrackRecordingButtonStateClosed,
};
@protocol MWMFeatureHolder;
@interface MWMMapViewControlsManager : NSObject
@@ -21,7 +27,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;
@@ -35,6 +41,8 @@
- (void)viewWillTransitionToSize:(CGSize)size
withTransitionCoordinator:(id<UIViewControllerTransitionCoordinator>)coordinator;
- (void)setTrackRecordingButtonState:(TrackRecordingButtonState)state;
#pragma mark - MWMNavigationDashboardManager
- (void)onRoutePrepare;

View File

@@ -28,15 +28,15 @@ NSString *const kMapToCategorySelectorSegue = @"MapToCategorySelectorSegue";
@interface MWMMapViewControlsManager () <BottomMenuDelegate>
@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<MWMPlacePageProtocol> 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:^{
- (void)setTrackRecordingButtonState:(TrackRecordingButtonState)state {
if (!_trackRecordingButton) {
_trackRecordingButton = [[TrackRecordingButtonViewController alloc] init];
}
[self.trackRecordingButton setState:state completion:^{
[MWMMapWidgetsHelper updateLayoutForAvailableArea];
}];
if (state == TrackRecordingButtonStateClosed)
_trackRecordingButton = nil;
}
else if (!trackRecordingButtonHidden && !_trackRecordingButton) {
_trackRecordingButton = [[TrackRecordingViewController alloc] init];
[MWMMapWidgetsHelper updateLayoutForAvailableArea];
}
}
- (void)setMenuState:(MWMBottomMenuState)menuState {

View File

@@ -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()
}
}

View File

@@ -37,6 +37,7 @@
- (void)openBookmarkEditor;
- (void)openFullPlaceDescriptionWithHtml:(NSString *_Nonnull)htmlString;
- (void)openDrivingOptions;
- (void)showTrackRecordingPlacePage;
- (void)setPlacePageTopBound:(CGFloat)bound duration:(double)duration;

View File

@@ -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,7 +117,7 @@ NSString *const kSettingsSegue = @"Map2Settings";
return [MapsAppDelegate theApp].mapViewController;
}
#pragma mark - Map Navigation
#pragma mark - PlacePage
- (void)showOrUpdatePlacePage:(PlacePageData *)data {
if (self.searchManager.isSearching)
@@ -124,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];
}
@@ -204,6 +206,7 @@ NSString *const kSettingsSegue = @"Map2Settings";
}
- (void)hideRegularPlacePage {
[self stopObservingTrackRecordingUpdates];
[self.placePageVC closeAnimatedWithCompletion:^{
[self.placePageVC.view removeFromSuperview];
[self.placePageVC willMoveToParentViewController:nil];
@@ -247,6 +250,7 @@ NSString *const kSettingsSegue = @"Map2Settings";
return;
}
PlacePageData * data = [[PlacePageData alloc] initWithLocalizationProvider:[[OpeinigHoursLocalization alloc] init]];
[self stopObservingTrackRecordingUpdates];
[self showOrUpdatePlacePage:data];
}
@@ -425,6 +429,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)
@@ -742,6 +749,12 @@ NSString *const kSettingsSegue = @"Map2Settings";
return _searchManager;
}
- (TrackRecordingManager *)trackRecordingManager {
if (!_trackRecordingManager)
_trackRecordingManager = TrackRecordingManager.shared;
return _trackRecordingManager;
}
- (UIView * _Nullable)searchViewAvailableArea {
return self.searchManager.viewController.availableAreaView;
}
@@ -858,6 +871,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)) {

View File

@@ -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;

View File

@@ -198,6 +198,7 @@ extension GlobalStyleSheet: IStyleSheet {
case .trackRecordingWidgetButton:
return .addFrom(Self.bottomTabBarButton) { s in
s.cornerRadius = .custom(23)
s.coloring = .red
}
case .blackOpaqueBackground:
return .add { s in

View File

@@ -4,31 +4,47 @@ 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 }
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 {
typealias CompletionHandler = () -> Void
private enum SavingOption {
case withoutSaving
case saveWithName(String? = nil)
}
typealias CompletionHandler = (TrackRecordingActionResult) -> Void
fileprivate struct Observation {
weak var observer: AnyObject?
@@ -37,28 +53,40 @@ 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 trackRecordingElevationProfileData: ElevationProfileData {
FrameworkHelper.trackRecordingElevationInfo()
}
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()
self.subscribeOnTheAppLifecycleEvents()
}
// MARK: - Public methods
@@ -84,22 +112,42 @@ final class TrackRecordingManager: NSObject {
}
func processAction(_ action: TrackRecordingAction, completion: (CompletionHandler)? = nil) {
do {
switch action {
case .start:
start(completion: completion)
case .stop:
stop(completion: completion)
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 subscribeOnTheAppLifecycleEvents() {
NotificationCenter.default.addObserver(self,
selector: #selector(notifyObservers),
name: UIApplication.didBecomeActiveNotification,
object: nil)
}
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,95 +161,68 @@ 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 {
try checkIsLocationEnabled()
private func startRecording() throws(TrackRecordingError) {
switch recordingState {
case .inactive:
try checkIsLocationEnabled()
subscribeOnTrackRecordingProgressUpdates()
trackRecorder.startTrackRecording()
notifyObservers()
try? activityManager?.start(with: trackRecordingInfo ?? .empty())
do {
try activityManager?.start(with: trackRecordingInfo)
} catch {
LOG(.warning, "Failed to start activity manager")
handleError(.systemError(error))
}
case .active:
break
}
completion?()
} catch {
handleError(error, completion: completion)
}
}
private func stop(completion: (CompletionHandler)? = nil) {
guard !trackRecorder.isTrackRecordingEmpty() else {
Toast.show(withText: L("track_recording_toast_nothing_to_save"))
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):
private func saveTrackRecording(name: String) {
trackRecorder.saveTrackRecording(withName: name)
}
completion?()
}
private func handleError(_ error: Error, completion: (CompletionHandler)? = nil) {
LOG(.error, error.localizedDescription)
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.show(withText: L("track_recording_toast_nothing_to_save"))
case TrackRecordingError.systemError(let error):
LOG(.error, error.localizedDescription)
break
}
stopRecording(.withoutSaving, completion: completion)
DispatchQueue.main.async {
completion?(.error(error))
}
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)
}
}
// 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)
recordingIsActiveDidChangeHandler(recordingState, trackRecordingInfo) {
self.trackRecordingElevationProfileData
}
}
@objc
@@ -209,8 +230,16 @@ extension TrackRecordingManager: TrackRecordingObserver {
observations.removeAll { $0.observer === observer }
}
@objc
func contains(_ observer: AnyObject) -> Bool {
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 })
}
}
}

View File

@@ -0,0 +1,15 @@
{
"images" : [
{
"filename" : "ic_track_save.png",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
},
"properties" : {
"template-rendering-intent" : "template"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

View File

@@ -470,7 +470,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 */; };
@@ -510,6 +510,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 */; };
EDB71D8C2D8474A0004A6A7F /* CornerRadius.swift in Sources */ = {isa = PBXBuildFile; fileRef = EDB71D8B2D8474A0004A6A7F /* CornerRadius.swift */; };
EDB71E002D8B0338004A6A7F /* ModalPresentationAnimator.swift in Sources */ = {isa = PBXBuildFile; fileRef = EDB71DFF2D8B0338004A6A7F /* ModalPresentationAnimator.swift */; };
@@ -541,6 +542,7 @@
EDFDFB4C2B722C9C0013A44C /* InfoTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = EDFDFB4B2B722C9C0013A44C /* InfoTableViewCell.swift */; };
EDFDFB522B726F1A0013A44C /* ButtonsStackView.swift in Sources */ = {isa = PBXBuildFile; fileRef = EDFDFB512B726F1A0013A44C /* ButtonsStackView.swift */; };
EDFDFB612B74E2500013A44C /* DonationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = EDFDFB602B74E2500013A44C /* DonationView.swift */; };
EDFE1A4A2DF1989700FDEA38 /* UIAlertController+UnknownCurrentPosition.swift in Sources */ = {isa = PBXBuildFile; fileRef = EDFE1A492DF1989700FDEA38 /* UIAlertController+UnknownCurrentPosition.swift */; };
F607C1881C032A8800B53A87 /* resources-hdpi_light in Resources */ = {isa = PBXBuildFile; fileRef = F607C1831C032A8800B53A87 /* resources-hdpi_light */; };
F607C18A1C032A8800B53A87 /* resources-hdpi_dark in Resources */ = {isa = PBXBuildFile; fileRef = F607C1841C032A8800B53A87 /* resources-hdpi_dark */; };
F623DA6C1C9C2731006A3436 /* opening_hours_how_to_edit.html in Resources */ = {isa = PBXBuildFile; fileRef = F623DA6A1C9C2731006A3436 /* opening_hours_how_to_edit.html */; };
@@ -1444,7 +1446,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>"; };
@@ -1525,6 +1527,7 @@
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>"; };
EDA1EAA32CC7ECAD00DBDCAA /* ElevationProfileFormatter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ElevationProfileFormatter.swift; sourceTree = "<group>"; };
EDB71D8B2D8474A0004A6A7F /* CornerRadius.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CornerRadius.swift; sourceTree = "<group>"; };
EDB71DFF2D8B0338004A6A7F /* ModalPresentationAnimator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ModalPresentationAnimator.swift; sourceTree = "<group>"; };
@@ -1556,6 +1559,7 @@
EDFDFB4B2B722C9C0013A44C /* InfoTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InfoTableViewCell.swift; sourceTree = "<group>"; };
EDFDFB512B726F1A0013A44C /* ButtonsStackView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ButtonsStackView.swift; sourceTree = "<group>"; };
EDFDFB602B74E2500013A44C /* DonationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DonationView.swift; sourceTree = "<group>"; };
EDFE1A492DF1989700FDEA38 /* UIAlertController+UnknownCurrentPosition.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIAlertController+UnknownCurrentPosition.swift"; sourceTree = "<group>"; };
EE026F0511D6AC0D00645242 /* classificator.txt */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; name = classificator.txt; path = ../../data/classificator.txt; sourceTree = SOURCE_ROOT; };
EED10A4411F78D120095FAD4 /* MapViewController.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; lineEnding = 0; path = MapViewController.mm; sourceTree = "<group>"; xcLanguageSpecificationIdentifier = xcode.lang.objcpp; };
F607C1831C032A8800B53A87 /* resources-hdpi_light */ = {isa = PBXFileReference; lastKnownFileType = folder; name = "resources-hdpi_light"; path = "../../data/resources-hdpi_light"; sourceTree = "<group>"; };
@@ -2596,7 +2600,7 @@
34BC72091B0DECAE0012A34B /* MapViewControls */ = {
isa = PBXGroup;
children = (
ED49D76F2CF0E3A8004AF27E /* TrackRecordingViewController.swift */,
ED49D76F2CF0E3A8004AF27E /* TrackRecordingButtonViewController.swift */,
340537621BBED98600D452C6 /* MWMMapViewControlsCommon.h */,
34BC72101B0DECAE0012A34B /* MWMMapViewControlsManager.h */,
34BC72111B0DECAE0012A34B /* MWMMapViewControlsManager.mm */,
@@ -3060,6 +3064,7 @@
99C6532123F2F506004322F3 /* IPlacePageLayout.swift */,
99F3EB0223F4178200C713F8 /* PlacePageCommonLayout.swift */,
993DF0B423F6B2EF00AC231A /* PlacePageTrackLayout.swift */,
ED9DDF872D6F151000645BC8 /* PlacePageTrackRecordingLayout.swift */,
);
path = Layouts;
sourceTree = "<group>";
@@ -3446,6 +3451,14 @@
path = Views;
sourceTree = "<group>";
};
EDFE1A462DF1986900FDEA38 /* UnknownCurrentPositionAlert */ = {
isa = PBXGroup;
children = (
EDFE1A492DF1989700FDEA38 /* UIAlertController+UnknownCurrentPosition.swift */,
);
path = UnknownCurrentPositionAlert;
sourceTree = "<group>";
};
F607C18B1C047FCA00B53A87 /* Segue */ = {
isa = PBXGroup;
children = (
@@ -3496,6 +3509,7 @@
F64F195F1AB8125C006EAF7E /* CustomAlert */ = {
isa = PBXGroup;
children = (
EDFE1A462DF1986900FDEA38 /* UnknownCurrentPositionAlert */,
447DB4B72BA7826D000DF4C2 /* ReauthAlert */,
F62607FB207B78E300176C5A /* SpinnerAlert */,
F6D67CDA2062B9810032FD38 /* CreateBookmarkCategory */,
@@ -4489,8 +4503,9 @@
99F3EB1223F418C900C713F8 /* PlacePageInteractor.swift in Sources */,
340708651F2905A500029ECC /* NavigationInfoArea.swift in Sources */,
993DF0CC23F6BD0600AC231A /* ElevationDetailsPresenter.swift in Sources */,
EDFE1A4A2DF1989700FDEA38 /* UIAlertController+UnknownCurrentPosition.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 */,
@@ -4633,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 */,

View File

@@ -10,7 +10,7 @@ final class TrackRecordingButtonArea: AvailableArea {
}
override func notifyObserver() {
TrackRecordingViewController.updateAvailableArea(areaFrame)
TrackRecordingButtonViewController.updateAvailableArea(areaFrame)
}
}

View File

@@ -66,12 +66,9 @@ extension BottomMenuInteractor: BottomMenuInteractorProtocol {
}
func shareLocation(cell: BottomMenuItemCell) {
let lastLocation = LocationManager.lastLocation()
guard let coordinates = lastLocation?.coordinate else {
let alert = UIAlertController(title: L("unknown_current_position"), message: nil, preferredStyle: .alert)
alert.addAction(UIAlertAction(title: L("ok"), style: .default, handler: nil))
viewController?.present(alert, animated: true, completion: nil)
return;
guard let coordinates = LocationManager.lastLocation()?.coordinate else {
viewController?.present(UIAlertController.unknownCurrentPosition(), animated: true, completion: nil)
return
}
guard let viewController = viewController else { return }
let vc = ActivityViewController.share(forMyPosition: coordinates)
@@ -79,8 +76,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()
}
}

View File

@@ -62,7 +62,10 @@ final class ActionBarViewController: UIViewController {
fatalError()
}
}
var buttons: [ActionBarButtonType] = []
switch placePageData.objectType {
case .POI, .bookmark, .track:
if isRoutePlanning {
buttons.append(.routeFrom)
}
@@ -73,8 +76,13 @@ final class ActionBarViewController: UIViewController {
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))
@@ -83,21 +91,24 @@ final class ActionBarViewController: UIViewController {
private func configButton2() {
var buttons: [ActionBarButtonType] = []
switch placePageData.objectType {
case .POI, .bookmark:
if canAddStop {
buttons.append(.routeAddStop)
}
switch placePageData.objectType {
case .POI, .bookmark:
buttons.append(.bookmark)
case .track:
if canAddStop {
buttons.append(.routeAddStop)
}
buttons.append(.track)
case .trackRecording:
// TODO: implement for track recording
break
buttons.append(.saveTrackRecording)
@unknown default:
fatalError()
}
assert(buttons.count > 0)
visibleButtons.append(buttons[0])
if buttons.count > 1 {
additionalButtons.append(contentsOf: buttons.suffix(from: 1))
@@ -105,7 +116,14 @@ final class ActionBarViewController: UIViewController {
}
private func configButton3() {
switch placePageData.objectType {
case .POI, .bookmark, .track:
visibleButtons.append(.routeTo)
case .trackRecording:
break
@unknown default:
fatalError()
}
}
private func configButton4() {

View File

@@ -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

View File

@@ -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() {

View File

@@ -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

View File

@@ -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()
}

View File

@@ -246,9 +246,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()
}
@@ -298,8 +308,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)
}
}
@@ -323,7 +333,12 @@ extension PlacePageInteractor: PlacePageHeaderViewControllerDelegate {
case .track:
presenter?.showShareTrackMenu()
default:
fatalError()
guard let coordinates = LocationManager.lastLocation()?.coordinate else {
viewController?.present(UIAlertController.unknownCurrentPosition(), animated: true, completion: nil)
return
}
let activity = ActivityViewController.share(forMyPosition: coordinates)
activity.present(inParentViewController: mapViewController, anchorView: sourceView)
}
}

View File

@@ -3,6 +3,7 @@ typedef NS_ENUM(NSInteger, MWMActionBarButtonType) {
MWMActionBarButtonTypeBookingSearch,
MWMActionBarButtonTypeBookmark,
MWMActionBarButtonTypeTrack,
MWMActionBarButtonTypeSaveTrackRecording,
MWMActionBarButtonTypeCall,
MWMActionBarButtonTypeDownload,
MWMActionBarButtonTypeMore,

View File

@@ -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,6 +57,7 @@ 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: {
@@ -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;

View File

@@ -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()
}
}