[ios] add TrackRecording widget to the top-right

Signed-off-by: Kiryl Kaveryn <kirylkaveryn@gmail.com>
This commit is contained in:
Kiryl Kaveryn 2024-11-25 12:56:30 +04:00 committed by Roman Tsisyk
parent 4a33a609d8
commit b03108318c
14 changed files with 241 additions and 5 deletions

View file

@ -5,6 +5,7 @@
@class MapViewController;
@class BottomTabBarViewController;
@class TrackRecordingViewController;
@protocol MWMFeatureHolder;
@interface MWMMapViewControlsManager : NSObject
@ -18,7 +19,8 @@
@property(nonatomic) MWMBottomMenuState menuState;
@property(nonatomic) MWMBottomMenuState menuRestoreState;
@property(nonatomic) BOOL isDirectionViewHidden;
@property(nonatomic) BottomTabBarViewController *tabBarController;
@property(nonatomic) BottomTabBarViewController * tabBarController;
@property(nonatomic) TrackRecordingViewController * trackRecordingButton;
- (instancetype)init __attribute__((unavailable("init is not available")));
- (instancetype)initWithParentController:(MapViewController *)controller;

View file

@ -8,6 +8,7 @@
#import "MWMSearchManager.h"
#import "MWMSideButtons.h"
#import "MWMTrafficButtonViewController.h"
#import "MWMMapWidgetsHelper.h"
#import "MapViewController.h"
#import "MapsAppDelegate.h"
#import "SwiftBridge.h"
@ -63,9 +64,16 @@ NSString *const kMapToCategorySelectorSegue = @"MapToCategorySelectorSegue";
self.menuState = MWMBottomMenuStateInactive;
self.menuRestoreState = MWMBottomMenuStateInactive;
self.isAddingPlace = NO;
[TrackRecordingManager.shared addObserver:self recordingIsActiveDidChangeHandler:^(BOOL isActive) {
[self setTrackRecordingButtonHidden:!isActive];
}];
return self;
}
- (void)dealloc {
[TrackRecordingManager.shared removeObserver:self];
}
- (UIStatusBarStyle)preferredStatusBarStyle {
BOOL const isSearchUnderStatusBar = (self.searchManager.state != MWMSearchManagerStateHidden);
BOOL const isNavigationUnderStatusBar = self.navigationManager.state != MWMNavigationDashboardStateHidden &&
@ -91,6 +99,7 @@ NSString *const kMapToCategorySelectorSegue = @"MapToCategorySelectorSegue";
- (void)viewWillTransitionToSize:(CGSize)size
withTransitionCoordinator:(id<UIViewControllerTransitionCoordinator>)coordinator {
[self.trafficButton viewWillTransitionToSize:size withTransitionCoordinator:coordinator];
[self.trackRecordingButton viewWillTransitionToSize:size withTransitionCoordinator:coordinator];
[self.tabBarController viewWillTransitionToSize:size withTransitionCoordinator:coordinator];
[self.searchManager viewWillTransitionToSize:size withTransitionCoordinator:coordinator];
}
@ -311,6 +320,19 @@ NSString *const kMapToCategorySelectorSegue = @"MapToCategorySelectorSegue";
self.trafficButton.hidden = self.hidden || _trafficButtonHidden;
}
- (void)setTrackRecordingButtonHidden:(BOOL)trackRecordingButtonHidden {
if (trackRecordingButtonHidden && _trackRecordingButton) {
[self.trackRecordingButton closeWithCompletion:^{
[MWMMapWidgetsHelper updateLayoutForAvailableArea];
}];
_trackRecordingButton = nil;
}
else if (!trackRecordingButtonHidden && !_trackRecordingButton) {
_trackRecordingButton = [[TrackRecordingViewController alloc] init];
[MWMMapWidgetsHelper updateLayoutForAvailableArea];
}
}
- (void)setMenuState:(MWMBottomMenuState)menuState {
_menuState = menuState;
MapViewController * ownerController = _ownerController;

View file

@ -0,0 +1,145 @@
final class TrackRecordingViewController: MWMViewController {
private enum Constants {
static let buttonDiameter = CGFloat(48)
static let topOffset = CGFloat(6)
static let trailingOffset = CGFloat(10)
static let blinkingDuration = 1.0
static let color: (lighter: UIColor, darker: UIColor) = (.red, .red.darker(percent: 0.3))
}
private let trackRecordingManager: TrackRecordingManager = .shared
private let button = BottomTabBarButton()
private var blinkingTimer: Timer?
private var topConstraint = NSLayoutConstraint()
private var trailingConstraint = NSLayoutConstraint()
private static var availableArea: CGRect = .zero
private static var topConstraintValue: CGFloat {
availableArea.origin.y + Constants.topOffset
}
private static var trailingConstraintValue: CGFloat {
-(UIScreen.main.bounds.maxX - availableArea.maxX + Constants.trailingOffset)
}
@objc
init() {
super.init(nibName: nil, bundle: nil)
let ownerViewController = MapViewController.shared()
ownerViewController?.addChild(self)
ownerViewController?.controlsView.addSubview(view)
self.setupView()
self.layout()
self.startTimer()
}
@available(*, unavailable)
required init?(coder: NSCoder) {
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
})
}
// 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()
})
}
// MARK: - Private methods
private func setupView() {
view.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(button)
button.setStyleAndApply("TrackRecordingWidgetButton")
button.tintColor = Constants.color.darker
button.translatesAutoresizingMaskIntoConstraints = false
button.setImage(UIImage(resource: .icMenuBookmarkTrackRecording), for: .normal)
button.addTarget(self, action: #selector(onTrackRecordingButtonPressed), for: .touchUpInside)
button.isHidden = true
}
private func layout() {
guard let superview = view.superview else { return }
topConstraint = view.topAnchor.constraint(equalTo: superview.topAnchor, constant: Self.topConstraintValue)
trailingConstraint = view.trailingAnchor.constraint(equalTo: superview.trailingAnchor, constant: Self.trailingConstraintValue)
NSLayoutConstraint.activate([
topConstraint,
trailingConstraint,
view.widthAnchor.constraint(equalToConstant: Constants.buttonDiameter),
view.heightAnchor.constraint(equalToConstant: Constants.buttonDiameter),
button.leadingAnchor.constraint(equalTo: view.leadingAnchor),
button.trailingAnchor.constraint(equalTo: view.trailingAnchor),
button.topAnchor.constraint(equalTo: view.topAnchor),
button.bottomAnchor.constraint(equalTo: view.bottomAnchor),
])
}
private func updateLayout() {
guard let superview = self.view.superview else { return }
superview.animateConstraints {
self.topConstraint.constant = Self.topConstraintValue
self.trailingConstraint.constant = Self.trailingConstraintValue
}
}
private func startTimer() {
guard blinkingTimer == nil else { return }
var lighter = false
let timer = Timer.scheduledTimer(withTimeInterval: Constants.blinkingDuration, repeats: true) { [weak self] _ in
guard let self = self else { return }
UIView.animate(withDuration: Constants.blinkingDuration, animations: {
self.button.tintColor = lighter ? Constants.color.lighter : Constants.color.darker
lighter.toggle()
})
}
blinkingTimer = timer
RunLoop.current.add(timer, forMode: .common)
}
private func stopTimer() {
blinkingTimer?.invalidate()
blinkingTimer = nil
}
static func updateAvailableArea(_ frame: CGRect) {
availableArea = frame
guard let controller = MapViewController.shared()?.controlsManager.trackRecordingButton else { return }
DispatchQueue.main.async {
controller.updateLayout()
}
}
// MARK: - Actions
@objc
private func onTrackRecordingButtonPressed(_ sender: Any) {
switch trackRecordingManager.recordingState {
case .inactive, .error:
trackRecordingManager.processAction(.start)
case .active:
trackRecordingManager.processAction(.stop)
}
}
}

View file

@ -268,4 +268,8 @@ final class NavigationControlView: SolidTouchView, MWMTextToSpeechObserver, MapO
override var widgetsAreaAffectDirections: MWMAvailableAreaAffectDirections {
return alternative(iPhone: .bottom, iPad: [])
}
override var trackRecordingButtonAreaAffectDirections: MWMAvailableAreaAffectDirections {
return .bottom
}
}

View file

@ -10,4 +10,8 @@ final class NavigationStreetNameView: SolidTouchView {
override var sideButtonsAreaAffectDirections: MWMAvailableAreaAffectDirections {
return .top
}
override var trackRecordingButtonAreaAffectDirections: MWMAvailableAreaAffectDirections {
return .top
}
}

View file

@ -87,4 +87,11 @@
return MWMAvailableAreaAffectDirectionsLeft;
}
#pragma mark - AvailableArea / TrackRecordingButtonArea
- (MWMAvailableAreaAffectDirections)trackRecordingButtonAreaAffectDirections
{
return MWMAvailableAreaAffectDirectionsRight;
}
@end

View file

@ -68,4 +68,11 @@
return MWMAvailableAreaAffectDirectionsTop;
}
#pragma mark - AvailableArea / TrackRecordingButtonArea
- (MWMAvailableAreaAffectDirections)trackRecordingButtonAreaAffectDirections
{
return MWMAvailableAreaAffectDirectionsTop;
}
@end

View file

@ -70,8 +70,9 @@
auto const viewWidth = [MapViewController sharedController].mapView.width;
auto const rulerOffset =
m2::PointF(frame.origin.x * vs, (frame.origin.y + frame.size.height - viewHeight) * vs);
auto const kCompassAdditionalYOffset = [TrackRecordingManager.shared isActive] ? 50 : 0;
auto const compassOffset =
m2::PointF((frame.origin.x + frame.size.width - viewWidth) * vs, frame.origin.y * vs);
m2::PointF((frame.origin.x + frame.size.width - viewWidth) * vs, (frame.origin.y + kCompassAdditionalYOffset) * vs);
m_skin->ForEach([&](gui::EWidget w, gui::Position const & pos) {
m2::PointF pivot = pos.m_pixelPivot;
switch (w)

View file

@ -119,6 +119,10 @@ class GlobalStyleSheet: IStyleSheet {
s.onTintColor = .red
}
theme.add(styleName: "TrackRecordingWidgetButton", from: "BottomTabBarButton") { (s) -> (Void) in
s.cornerRadius = 23
}
theme.add(styleName: "BlackOpaqueBackground") { (s) -> (Void) in
s.backgroundColor = colors.blackOpaque
}

View file

@ -475,6 +475,8 @@
ED1080A72B791CFE0023F27E /* SocialMediaCollectionViewHeader.swift in Sources */ = {isa = PBXBuildFile; fileRef = ED1080A62B791CFE0023F27E /* SocialMediaCollectionViewHeader.swift */; };
ED1263AB2B6F99F900AD99F3 /* UIView+AddSeparator.swift in Sources */ = {isa = PBXBuildFile; fileRef = ED1263AA2B6F99F900AD99F3 /* UIView+AddSeparator.swift */; };
ED1ADA332BC6B1B40029209F /* CarPlayServiceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = ED1ADA322BC6B1B40029209F /* CarPlayServiceTests.swift */; };
ED2E328E2D10500900807A08 /* TrackRecordingButtonArea.swift in Sources */ = {isa = PBXBuildFile; fileRef = ED46DD922D06F804007CACD6 /* TrackRecordingButtonArea.swift */; };
ED2E32912D10501700807A08 /* TrackRecordingViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = ED49D76F2CF0E3A8004AF27E /* TrackRecordingViewController.swift */; };
ED3EAC202B03C88100220A4A /* BottomTabBarButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = ED3EAC1F2B03C88100220A4A /* BottomTabBarButton.swift */; };
ED43B8BD2C12063500D07BAA /* DocumentPicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = ED43B8BC2C12063500D07BAA /* DocumentPicker.swift */; };
ED4DC7772CAEDECC0029B338 /* ProductsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = ED4DC7752CAEDECC0029B338 /* ProductsViewModel.swift */; };
@ -1406,8 +1408,10 @@
ED1ADA322BC6B1B40029209F /* CarPlayServiceTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CarPlayServiceTests.swift; sourceTree = "<group>"; };
ED3EAC1F2B03C88100220A4A /* BottomTabBarButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BottomTabBarButton.swift; sourceTree = "<group>"; };
ED43B8BC2C12063500D07BAA /* DocumentPicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DocumentPicker.swift; sourceTree = "<group>"; };
ED46DD922D06F804007CACD6 /* TrackRecordingButtonArea.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrackRecordingButtonArea.swift; sourceTree = "<group>"; };
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>"; };
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>"; };
@ -2508,6 +2512,7 @@
34BC72091B0DECAE0012A34B /* MapViewControls */ = {
isa = PBXGroup;
children = (
ED49D76F2CF0E3A8004AF27E /* TrackRecordingViewController.swift */,
340537621BBED98600D452C6 /* MWMMapViewControlsCommon.h */,
34BC72101B0DECAE0012A34B /* MWMMapViewControlsManager.h */,
34BC72111B0DECAE0012A34B /* MWMMapViewControlsManager.mm */,
@ -2566,6 +2571,7 @@
34FE5A6D1F18F30F00BCA729 /* TrafficButtonArea.swift */,
340708631F2905A500029ECC /* NavigationInfoArea.swift */,
9989272F2449DE1500260CE2 /* TabBarArea.swift */,
ED46DD922D06F804007CACD6 /* TrackRecordingButtonArea.swift */,
);
path = AvailableArea;
sourceTree = "<group>";
@ -4361,6 +4367,7 @@
340708651F2905A500029ECC /* NavigationInfoArea.swift in Sources */,
993DF0CC23F6BD0600AC231A /* ElevationDetailsPresenter.swift in Sources */,
34AB666B1FC5AA330078E451 /* TransportTransitCell.swift in Sources */,
ED2E32912D10501700807A08 /* TrackRecordingViewController.swift in Sources */,
47E8163323B17734008FD836 /* MWMStorage+UI.m in Sources */,
993DF11123F6BDB100AC231A /* UILabelRenderer.swift in Sources */,
34AB66471FC5AA330078E451 /* RouteManagerTableView.swift in Sources */,
@ -4565,6 +4572,7 @@
995739062355CAC40019AEE7 /* ImageViewCrossDisolve.swift in Sources */,
47B9065221C7FA400079C85E /* MWMWebImage.m in Sources */,
47A13CAD24BE9AA500027D4F /* DatePickerViewRenderer.swift in Sources */,
ED2E328E2D10500900807A08 /* TrackRecordingButtonArea.swift in Sources */,
F6E2FE7C1E097BA00083EBEC /* MWMPlacePageOpeningHoursCell.mm in Sources */,
340E1EFB1E2F614400CE49BF /* Storyboard.swift in Sources */,
34E776331F15FAC2003040B3 /* MWMPlacePageManagerHelper.mm in Sources */,

View file

@ -0,0 +1,21 @@
final class TrackRecordingButtonArea: AvailableArea {
override func isAreaAffectingView(_ other: UIView) -> Bool {
return !other.trackRecordingButtonAreaAffectDirections.isEmpty
}
override func addAffectingView(_ other: UIView) {
let ov = other.trackRecordingButtonAreaAffectView
let directions = ov.trackRecordingButtonAreaAffectDirections
addConstraints(otherView: ov, directions: directions)
}
override func notifyObserver() {
TrackRecordingViewController.updateAvailableArea(areaFrame)
}
}
extension UIView {
@objc var trackRecordingButtonAreaAffectDirections: MWMAvailableAreaAffectDirections { return [] }
var trackRecordingButtonAreaAffectView: UIView { return self }
}

View file

@ -1,9 +1,11 @@
import UIKit
final class BottomTabBarButton: MWMButton {
class BottomTabBarButton: MWMButton {
@objc override func applyTheme() {
styleName = "BottomTabBarButton"
if styleName.isEmpty {
styleName = "BottomTabBarButton"
}
for style in StyleManager.shared.getStyle(styleName) where !style.isEmpty && !style.hasExclusion(view: self) {
BottomTabBarButtonRenderer.render(self, style: style)
}

View file

@ -21,6 +21,8 @@ final class SearchBar: SolidTouchView {
override var tabBarAreaAffectDirections: MWMAvailableAreaAffectDirections { return alternative(iPhone: [], iPad: .left) }
override var trackRecordingButtonAreaAffectDirections: MWMAvailableAreaAffectDirections { return alternative(iPhone: .top, iPad: .left) }
@objc var state: SearchBarState = .ready {
didSet {
if state != oldValue {

View file

@ -84,6 +84,9 @@
</view>
<view hidden="YES" userInteractionEnabled="NO" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="QKu-4A-UgP" customClass="TrafficButtonArea" customModule="Organic_Maps" customModuleProvider="target">
<rect key="frame" x="59" y="0.0" width="814" height="409"/>
</view>
<view hidden="YES" userInteractionEnabled="NO" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="yto-Gu-Ugz" userLabel="Track Recording Button Area" customClass="TrackRecordingButtonArea" customModule="Organic_Maps" customModuleProvider="target">
<rect key="frame" x="0.0" y="59" width="430" height="839"/>
<color key="backgroundColor" red="0.0" green="1" blue="0.0" alpha="0.20000000000000001" colorSpace="calibratedRGB"/>
</view>
</subviews>
@ -179,6 +182,7 @@
<constraint firstItem="utd-Jy-pE5" firstAttribute="top" secondItem="xJx-UU-IdV" secondAttribute="top" priority="100" id="BMq-jc-qfO"/>
<constraint firstItem="ixC-IZ-Pvs" firstAttribute="top" secondItem="USG-6L-Uhw" secondAttribute="top" id="DSJ-0D-hwO"/>
<constraint firstItem="rL1-9E-4b7" firstAttribute="top" secondItem="USG-6L-Uhw" secondAttribute="top" id="E89-WV-ZTh"/>
<constraint firstItem="yto-Gu-Ugz" firstAttribute="top" secondItem="utd-Jy-pE5" secondAttribute="top" priority="100" id="FGp-pl-Mgy"/>
<constraint firstItem="utd-Jy-pE5" firstAttribute="trailing" secondItem="QKu-4A-UgP" secondAttribute="trailing" id="Fjy-Cy-dHS"/>
<constraint firstItem="utd-Jy-pE5" firstAttribute="trailing" secondItem="at1-V1-pzl" secondAttribute="trailing" id="GKG-4Y-2Pq"/>
<constraint firstAttribute="trailing" secondItem="aPn-pa-nCx" secondAttribute="trailing" id="GKJ-zm-8xb"/>
@ -188,12 +192,15 @@
<constraint firstAttribute="trailing" secondItem="ixC-IZ-Pvs" secondAttribute="trailing" id="O7f-qU-UN2"/>
<constraint firstItem="utd-Jy-pE5" firstAttribute="bottom" secondItem="at1-V1-pzl" secondAttribute="bottom" priority="750" constant="48" id="O8L-nd-nOa"/>
<constraint firstItem="utd-Jy-pE5" firstAttribute="bottom" secondItem="FFY-Dy-Wou" secondAttribute="bottom" priority="100" id="OE7-Qb-J0v"/>
<constraint firstItem="utd-Jy-pE5" firstAttribute="bottom" secondItem="yto-Gu-Ugz" secondAttribute="bottom" priority="100" id="PBh-hG-PzA"/>
<constraint firstItem="utd-Jy-pE5" firstAttribute="bottom" relation="greaterThanOrEqual" secondItem="awj-9E-eBS" secondAttribute="bottom" id="PFs-sL-oVA"/>
<constraint firstAttribute="trailing" secondItem="rL1-9E-4b7" secondAttribute="trailing" id="QdS-Yx-ADV"/>
<constraint firstItem="utd-Jy-pE5" firstAttribute="trailing" secondItem="TdT-ia-GP9" secondAttribute="trailing" id="Rsb-fB-8bn"/>
<constraint firstItem="jio-3T-E6G" firstAttribute="leading" secondItem="utd-Jy-pE5" secondAttribute="leading" constant="-320" id="SAj-bF-qrr"/>
<constraint firstItem="utd-Jy-pE5" firstAttribute="bottom" relation="greaterThanOrEqual" secondItem="xJx-UU-IdV" secondAttribute="bottom" id="SDX-4J-Jz5"/>
<constraint firstItem="utd-Jy-pE5" firstAttribute="bottom" secondItem="FFY-Dy-Wou" secondAttribute="bottom" id="TZk-MH-pMV"/>
<constraint firstItem="yto-Gu-Ugz" firstAttribute="leading" secondItem="utd-Jy-pE5" secondAttribute="leading" id="TgS-dg-g9B"/>
<constraint firstItem="utd-Jy-pE5" firstAttribute="trailing" secondItem="yto-Gu-Ugz" secondAttribute="trailing" id="URB-3l-JiC"/>
<constraint firstItem="utd-Jy-pE5" firstAttribute="bottom" secondItem="xJx-UU-IdV" secondAttribute="bottom" priority="100" id="VfU-Zk-8IU"/>
<constraint firstItem="rbx-Oj-jeo" firstAttribute="leading" secondItem="jio-3T-E6G" secondAttribute="trailing" constant="20" id="W0l-NG-7lt"/>
<constraint firstItem="utd-Jy-pE5" firstAttribute="top" secondItem="QKu-4A-UgP" secondAttribute="top" priority="100" id="X96-y7-5oI"/>