[ios] Review fixes

Signed-off-by: Kiryl Kaveryn <kirylkaveryn@gmail.com>
This commit is contained in:
Kiryl Kaveryn
2025-06-10 13:34:25 +04:00
committed by Yannik Bloscheck
parent d677112edd
commit e2efbbe68c
10 changed files with 79 additions and 99 deletions

View File

@@ -67,7 +67,7 @@ class ChartLineView: UIView {
} }
func setY(min: CGFloat, max: CGFloat, animationStyle: ChartAnimation = .none) { func setY(min: CGFloat, max: CGFloat, animationStyle: ChartAnimation = .none) {
assert(min < max) assert(min <= max)
minY = min minY = min
maxY = max maxY = max
updateGraph(animationStyle: animationStyle) updateGraph(animationStyle: animationStyle)

View File

@@ -53,7 +53,6 @@ static PlacePageDataSchedule convertOpeningHours(std::string_view rawOH)
- (instancetype)initWithTrackInfo:(TrackInfo * _Nonnull)trackInfo { - (instancetype)initWithTrackInfo:(TrackInfo * _Nonnull)trackInfo {
self = [super init]; self = [super init];
if (self) { if (self) {
// TODO: (KK) Replace separator with a shared static constant.
NSString * kSeparator = @" • "; NSString * kSeparator = @" • ";
_title = [@[trackInfo.duration, trackInfo.distance] componentsJoinedByString:kSeparator]; _title = [@[trackInfo.duration, trackInfo.distance] componentsJoinedByString:kSeparator];
} }

View File

@@ -59,7 +59,7 @@ static ElevationDifficulty convertDifficulty(uint8_t difficulty) {
andAltitude:point.m_point.GetAltitude()]; andAltitude:point.m_point.GetAltitude()];
[pointsArray addObject:elevationPoint]; [pointsArray addObject:elevationPoint];
} }
return [pointsArray copy]; return pointsArray;
} }
@end @end

View File

@@ -41,10 +41,7 @@ final class TrackRecordingButtonViewController: MWMViewController {
override func viewDidLoad() { override func viewDidLoad() {
super.viewDidLoad() super.viewDidLoad()
// async is for smoother appearance setState(self.state, completion: nil)
DispatchQueue.main.asyncAfter(deadline: .now() + kDefaultAnimationDuration) {
self.setState(self.state, completion: nil)
}
} }
// MARK: - Public methods // MARK: - Public methods

View File

@@ -892,7 +892,7 @@ NSString *const kSettingsSegue = @"Map2Settings";
TrackInfo * _Nonnull trackInfo, TrackInfo * _Nonnull trackInfo,
ElevationProfileData * _Nonnull (^ _Nullable elevationData) ()) { ElevationProfileData * _Nonnull (^ _Nullable elevationData) ()) {
__strong __typeof(weakSelf) self = weakSelf; __strong __typeof(weakSelf) self = weakSelf;
if (!self) return;
switch (state) { switch (state) {
case TrackRecordingStateInactive: case TrackRecordingStateInactive:
[self stopObservingTrackRecordingUpdates]; [self stopObservingTrackRecordingUpdates];

View File

@@ -9,15 +9,18 @@ enum TrackRecordingAction {
case stopAndSave(name: String) case stopAndSave(name: String)
} }
enum TrackRecordingError: Error { enum LocationError: Error {
case locationIsProhibited case locationIsProhibited
case trackIsEmpty
case systemError(Error)
} }
enum TrackRecordingActionResult { enum StartTrackRecordingResult {
case success case success
case error(TrackRecordingError) case failure(Error)
}
enum StopTrackRecordingResult {
case success
case trackIsEmpty
} }
@objc @objc
@@ -44,8 +47,6 @@ typealias TrackRecordingStateHandler = (TrackRecordingState, TrackInfo, Elevatio
@objcMembers @objcMembers
final class TrackRecordingManager: NSObject { final class TrackRecordingManager: NSObject {
typealias CompletionHandler = (TrackRecordingActionResult) -> Void
fileprivate struct Observation { fileprivate struct Observation {
weak var observer: AnyObject? weak var observer: AnyObject?
var recordingStateDidChangeHandler: TrackRecordingStateHandler? var recordingStateDidChangeHandler: TrackRecordingStateHandler?
@@ -111,22 +112,47 @@ final class TrackRecordingManager: NSObject {
recordingState == .active recordingState == .active
} }
func processAction(_ action: TrackRecordingAction, completion: (CompletionHandler)? = nil) { func start(completion: ((StartTrackRecordingResult) -> Void)? = nil) {
do { do {
switch action { switch recordingState {
case .start: case .inactive:
try startRecording() try checkIsLocationEnabled()
case .stopAndSave(let name): subscribeOnTrackRecordingProgressUpdates()
stopRecording() trackRecorder.startTrackRecording()
try checkIsTrackNotEmpty() notifyObservers()
saveTrackRecording(name: name) do {
try activityManager?.start(with: trackRecordingInfo)
} catch {
LOG(.warning, "Failed to start activity manager")
handleError(error)
}
case .active:
break
} }
completion?(.success) completion?(.success)
} catch { } catch {
handleError(error, completion: completion) handleError(error)
completion?(.failure(error))
} }
} }
func stopAndSave(withName name: String = "", completion: ((StopTrackRecordingResult) -> Void)? = nil) {
unsubscribeFromTrackRecordingProgressUpdates()
trackRecorder.stopTrackRecording()
trackRecordingInfo = .empty()
activityManager?.stop()
notifyObservers()
guard !trackRecorder.isTrackRecordingEmpty() else {
Toast.show(withText: L("track_recording_toast_nothing_to_save"))
completion?(.trackIsEmpty)
return
}
trackRecorder.saveTrackRecording(withName: name)
completion?(.success)
}
// MARK: - Private methods // MARK: - Private methods
private func subscribeOnTheAppLifecycleEvents() { private func subscribeOnTheAppLifecycleEvents() {
@@ -136,20 +162,12 @@ final class TrackRecordingManager: NSObject {
object: nil) object: nil)
} }
private func checkIsLocationEnabled() throws(TrackRecordingError) { private func checkIsLocationEnabled() throws {
if locationService.isLocationProhibited() { if locationService.isLocationProhibited() {
throw TrackRecordingError.locationIsProhibited throw LocationError.locationIsProhibited
} }
} }
private func checkIsTrackNotEmpty() throws(TrackRecordingError) {
if trackRecorder.isTrackRecordingEmpty() {
throw TrackRecordingError.trackIsEmpty
}
}
// MARK: - Handle track recording process
private func subscribeOnTrackRecordingProgressUpdates() { private func subscribeOnTrackRecordingProgressUpdates() {
trackRecorder.setTrackRecordingUpdateHandler { [weak self] info in trackRecorder.setTrackRecordingUpdateHandler { [weak self] info in
guard let self else { return } guard let self else { return }
@@ -163,52 +181,15 @@ final class TrackRecordingManager: NSObject {
trackRecorder.setTrackRecordingUpdateHandler(nil) trackRecorder.setTrackRecordingUpdateHandler(nil)
} }
// MARK: - Handle Start/Stop event and Errors private func handleError(_ error: Error) {
private func startRecording() throws(TrackRecordingError) {
switch recordingState {
case .inactive:
try checkIsLocationEnabled()
subscribeOnTrackRecordingProgressUpdates()
trackRecorder.startTrackRecording()
notifyObservers()
do {
try activityManager?.start(with: trackRecordingInfo)
} catch {
LOG(.warning, "Failed to start activity manager")
handleError(.systemError(error))
}
case .active:
break
}
}
private func stopRecording() {
unsubscribeFromTrackRecordingProgressUpdates()
trackRecorder.stopTrackRecording()
trackRecordingInfo = .empty()
activityManager?.stop()
notifyObservers()
}
private func saveTrackRecording(name: String) {
trackRecorder.saveTrackRecording(withName: name)
}
private func handleError(_ error: TrackRecordingError, completion: (CompletionHandler)? = nil) {
switch error { switch error {
case TrackRecordingError.locationIsProhibited: case LocationError.locationIsProhibited:
// Show alert to enable location // Show alert to enable location
locationService.checkLocationStatus() locationService.checkLocationStatus()
case TrackRecordingError.trackIsEmpty: default:
Toast.show(withText: L("track_recording_toast_nothing_to_save"))
case TrackRecordingError.systemError(let error):
LOG(.error, error.localizedDescription) LOG(.error, error.localizedDescription)
break break
} }
DispatchQueue.main.async {
completion?(.error(error))
}
} }
} }

View File

@@ -51,7 +51,7 @@ final class TrackRecordingManagerTests: XCTestCase {
mockLocationService.locationIsProhibited = false mockLocationService.locationIsProhibited = false
mockTrackRecorder.trackRecordingIsEnabled = false mockTrackRecorder.trackRecordingIsEnabled = false
trackRecordingManager.processAction(.start) trackRecordingManager.start()
XCTAssertTrue(mockTrackRecorder.startTrackRecordingCalled) XCTAssertTrue(mockTrackRecorder.startTrackRecordingCalled)
XCTAssertTrue(mockActivityManager.startCalled) XCTAssertTrue(mockActivityManager.startCalled)
@@ -60,20 +60,21 @@ final class TrackRecordingManagerTests: XCTestCase {
func test_GivenStartRecording_WhenLocationDisabled_ThenShouldFail() { func test_GivenStartRecording_WhenLocationDisabled_ThenShouldFail() {
mockLocationService.locationIsProhibited = true mockLocationService.locationIsProhibited = true
let expectation = expectation(description: "Location is prohibited")
trackRecordingManager.processAction(.start) { result in trackRecordingManager.start() { result in
switch result { switch result {
case .success: case .success:
XCTFail("Should not succeed") XCTFail("Should not succeed")
case .error(let error): case .failure(let error):
switch error { switch error {
case .locationIsProhibited: case LocationError.locationIsProhibited:
XCTAssertTrue(true) expectation.fulfill()
default: default:
XCTFail("Unexpected error: \(error)") XCTFail("Unexpected error: \(error)")
} }
} }
} }
wait(for: [expectation], timeout: 1.0)
XCTAssertFalse(self.mockTrackRecorder.startTrackRecordingCalled) XCTAssertFalse(self.mockTrackRecorder.startTrackRecordingCalled)
XCTAssertTrue(trackRecordingManager.recordingState == .inactive) XCTAssertTrue(trackRecordingManager.recordingState == .inactive)
} }
@@ -82,12 +83,12 @@ final class TrackRecordingManagerTests: XCTestCase {
mockTrackRecorder.trackRecordingIsEnabled = true mockTrackRecorder.trackRecordingIsEnabled = true
mockTrackRecorder.trackRecordingIsEmpty = false mockTrackRecorder.trackRecordingIsEmpty = false
trackRecordingManager.processAction(.stopAndSave(name: "Test Track")) { result in trackRecordingManager.stopAndSave(withName: "Test Track") { result in
switch result { switch result {
case .success: case .success:
XCTAssertTrue(true) XCTAssertTrue(true)
case .error(let error): case .trackIsEmpty:
XCTFail("Unexpected error: \(error)") XCTFail("Track should not be empty")
} }
} }
XCTAssertTrue(mockTrackRecorder.stopTrackRecordingCalled) XCTAssertTrue(mockTrackRecorder.stopTrackRecordingCalled)
@@ -99,20 +100,16 @@ final class TrackRecordingManagerTests: XCTestCase {
func test_GivenStopRecording_WhenTrackIsEmpty_ThenShouldFail() { func test_GivenStopRecording_WhenTrackIsEmpty_ThenShouldFail() {
mockTrackRecorder.trackRecordingIsEnabled = true mockTrackRecorder.trackRecordingIsEnabled = true
mockTrackRecorder.trackRecordingIsEmpty = true mockTrackRecorder.trackRecordingIsEmpty = true
let expectation = expectation(description: "Track recording should be empty")
trackRecordingManager.processAction(.stopAndSave(name: "Test Track")) { result in trackRecordingManager.stopAndSave(withName: "Test Track") { result in
switch result { switch result {
case .success: case .success:
XCTFail("Should not succeed") XCTFail("Should not succeed")
case .error(let error):
switch error {
case .trackIsEmpty: case .trackIsEmpty:
XCTAssertTrue(true) expectation.fulfill()
default:
XCTFail("Unexpected error: \(error)")
}
} }
} }
wait(for: [expectation], timeout: 1.0)
XCTAssertFalse(mockTrackRecorder.saveTrackRecordingCalled) XCTAssertFalse(mockTrackRecorder.saveTrackRecordingCalled)
XCTAssertTrue(trackRecordingManager.recordingState == .inactive) XCTAssertTrue(trackRecordingManager.recordingState == .inactive)
} }

View File

@@ -76,13 +76,19 @@ extension BottomMenuInteractor: BottomMenuInteractorProtocol {
} }
func toggleTrackRecording() { func toggleTrackRecording() {
close()
switch trackRecorder.recordingState { switch trackRecorder.recordingState {
case .active: case .active:
break break
case .inactive: case .inactive:
trackRecorder.processAction(.start) trackRecorder.start { result in
} switch result {
close() case .success:
MapViewController.shared()?.showTrackRecordingPlacePage() MapViewController.shared()?.showTrackRecordingPlacePage()
case .failure:
break
}
}
}
} }
} }

View File

@@ -76,12 +76,12 @@ extension ElevationProfilePresenter: ElevationProfilePresenterProtocol {
} }
func configure() { func configure() {
view?.isChartViewHidden = false
let kMinPointsToDraw = 3 let kMinPointsToDraw = 3
guard let profileData, let chartData, chartData.points.count >= kMinPointsToDraw else { guard let profileData, let chartData, chartData.points.count >= kMinPointsToDraw else {
view?.isChartViewHidden = true
return return
} }
view?.isChartViewHidden = false
view?.setChartData(ChartPresentationData(chartData, formatter: formatter)) view?.setChartData(ChartPresentationData(chartData, formatter: formatter))
view?.reloadDescription() view?.reloadDescription()

View File

@@ -251,11 +251,11 @@ extension PlacePageInteractor: ActionBarViewControllerDelegate {
showTrackDeletionConfirmationDialog() showTrackDeletionConfirmationDialog()
case .saveTrackRecording: case .saveTrackRecording:
// TODO: (KK) pass name typed by user // TODO: (KK) pass name typed by user
TrackRecordingManager.shared.processAction(.stopAndSave(name: "")) { [weak self] result in TrackRecordingManager.shared.stopAndSave() { [weak self] result in
switch result { switch result {
case .success: case .success:
break break
case .error: case .trackIsEmpty:
self?.presenter?.closeAnimated() self?.presenter?.closeAnimated()
} }
} }