[ios] refactor ElevationProfileViewController

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 <kirylkaveryn@gmail.com>
This commit is contained in:
Kiryl Kaveryn 2025-01-09 16:57:11 +04:00 committed by Kiryl Kaveryn
parent 5841d0f96e
commit 1790a4dfca
9 changed files with 215 additions and 307 deletions

View file

@ -6,8 +6,8 @@ public enum ChartType {
}
public enum ChartLineType: String {
case line = "line"
case lineArea = "lineArea"
case line
case lineArea
}
public protocol ChartFormatter {
@ -26,7 +26,6 @@ public protocol ChartData {
public protocol ChartLine {
var values: [ChartValue] { get }
var name: String { get }
var color: UIColor { get }
var type: ChartLineType { get }
}

View file

@ -11,7 +11,6 @@ final class ChartPresentationLine {
var values: [ChartValue] { chartLine.values }
var color: UIColor { chartLine.color }
var name: String { chartLine.name }
var type: ChartLineType { chartLine.type }
init(_ chartLine: ChartLine) {

View file

@ -1,7 +1,6 @@
import UIKit
struct ChartLineInfo {
let name: String
let color: UIColor
let point: CGPoint
let formattedValue: String

View file

@ -121,6 +121,13 @@ public class ChartView: UIView {
}
}
public var isChartViewInfoHidden: Bool = false {
didSet {
chartInfoView.isHidden = isChartViewInfoHidden
chartInfoView.isUserInteractionEnabled = !isChartViewInfoHidden
}
}
public typealias OnSelectedPointChangedClosure = (_ px: CGFloat) -> Void
public var onSelectedPointChanged: OnSelectedPointChangedClosure?
@ -332,11 +339,11 @@ extension ChartView: ChartInfoViewDelegate {
}
func chartInfoView(_ view: ChartInfoView, infoAtPointX pointX: CGFloat) -> (String, [ChartLineInfo])? {
let p = convert(CGPoint(x: pointX, y: 0), from: view)
let p = convert(CGPoint(x: pointX, y: .zero), from: view)
let x = (p.x / bounds.width) * CGFloat(xAxisView.upperBound - xAxisView.lowerBound) + CGFloat(xAxisView.lowerBound)
let x1 = floor(x)
let x2 = ceil(x)
guard Int(x1) < chartData.labels.count && x >= 0 else { return nil }
guard !pointX.isZero, Int(x1) < chartData.labels.count && x >= 0 else { return nil }
let label = chartData.labelAt(x)
var result: [ChartLineInfo] = []
@ -352,8 +359,7 @@ extension ChartView: ChartInfoViewDelegate {
CGFloat(yAxisView.upperBound - yAxisView.lowerBound))
let v = round(dx * CGFloat(y2 - y1)) + CGFloat(y1)
result.append(ChartLineInfo(name: line.name,
color: line.color,
result.append(ChartLineInfo(color: line.color,
point: chartsContainerView.convert(CGPoint(x: p.x, y: py), to: view),
formattedValue: chartData.formatter.yAxisString(from: Double(v))))
}

View file

@ -4,15 +4,12 @@ class ElevationProfileBuilder {
static func build(trackInfo: TrackInfo,
elevationProfileData: ElevationProfileData?,
delegate: ElevationProfileViewControllerDelegate?) -> ElevationProfileViewController {
let storyboard = UIStoryboard.instance(.placePage)
let viewController = storyboard.instantiateViewController(ofType: ElevationProfileViewController.self);
let viewController = ElevationProfileViewController();
let presenter = ElevationProfilePresenter(view: viewController,
trackInfo: trackInfo,
profileData: elevationProfileData,
delegate: delegate)
viewController.presenter = presenter
return viewController
}
}

View file

@ -1,18 +1,82 @@
class ElevationProfileDescriptionCell: UICollectionViewCell {
@IBOutlet private var titleLabel: UILabel!
@IBOutlet private var valueLabel: UILabel!
@IBOutlet var imageView: UIImageView!
func configure(title: String, value: String, imageName: String) {
titleLabel.text = title
final class ElevationProfileDescriptionCell: UICollectionViewCell {
private enum Constants {
static let insets = UIEdgeInsets(top: 2, left: 0, bottom: -2, right: 0)
static let valueSpacing: CGFloat = 8.0
static let imageSize: CGSize = CGSize(width: 20, height: 20)
}
private let valueLabel = UILabel()
private let subtitleLabel = UILabel()
private let imageView = UIImageView()
override init(frame: CGRect) {
super.init(frame: frame)
setupViews()
layoutViews()
}
@available(*, unavailable)
required init?(coder: NSCoder) {
super.init(coder: coder)
setupViews()
layoutViews()
}
private func setupViews() {
valueLabel.font = .medium14()
valueLabel.styleName = "blackSecondaryText"
valueLabel.numberOfLines = 1
valueLabel.minimumScaleFactor = 0.1
valueLabel.adjustsFontSizeToFitWidth = true
valueLabel.allowsDefaultTighteningForTruncation = true
subtitleLabel.font = .regular10()
subtitleLabel.styleName = "blackSecondaryText"
subtitleLabel.numberOfLines = 1
subtitleLabel.minimumScaleFactor = 0.1
subtitleLabel.adjustsFontSizeToFitWidth = true
subtitleLabel.allowsDefaultTighteningForTruncation = true
imageView.contentMode = .scaleAspectFit
imageView.styleName = "MWMBlack"
}
private func layoutViews() {
contentView.addSubview(imageView)
contentView.addSubview(valueLabel)
contentView.addSubview(subtitleLabel)
imageView.translatesAutoresizingMaskIntoConstraints = false
valueLabel.translatesAutoresizingMaskIntoConstraints = false
subtitleLabel.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
imageView.topAnchor.constraint(equalTo: contentView.topAnchor, constant: Constants.insets.top),
imageView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor),
imageView.widthAnchor.constraint(equalToConstant: Constants.imageSize.width),
imageView.heightAnchor.constraint(equalToConstant: Constants.imageSize.height),
valueLabel.leadingAnchor.constraint(equalTo: imageView.trailingAnchor, constant: Constants.valueSpacing),
valueLabel.trailingAnchor.constraint(equalTo: contentView.trailingAnchor),
valueLabel.centerYAnchor.constraint(equalTo: imageView.centerYAnchor),
subtitleLabel.topAnchor.constraint(equalTo: imageView.bottomAnchor),
subtitleLabel.leadingAnchor.constraint(equalTo: imageView.leadingAnchor),
subtitleLabel.bottomAnchor.constraint(lessThanOrEqualTo: contentView.bottomAnchor, constant: Constants.insets.bottom)
])
subtitleLabel.setContentHuggingPriority(.defaultHigh, for: .vertical)
}
func configure(subtitle: String, value: String, imageName: String) {
subtitleLabel.text = subtitle
valueLabel.text = value
imageView.image = UIImage(named: imageName)
}
override func prepareForReuse() {
super.prepareForReuse()
titleLabel.text = ""
valueLabel.text = ""
subtitleLabel.text = ""
imageView.image = nil
}
}

View file

@ -3,6 +3,7 @@ import CoreApi
protocol ElevationProfilePresenterProtocol: UICollectionViewDataSource, UICollectionViewDelegateFlowLayout {
func configure()
func update(trackInfo: TrackInfo, profileData: ElevationProfileData?)
func onDifficultyButtonPressed()
func onSelectedPointChanged(_ point: CGFloat)
@ -21,13 +22,14 @@ fileprivate struct DescriptionsViewModel {
final class ElevationProfilePresenter: NSObject {
private weak var view: ElevationProfileViewProtocol?
private let trackInfo: TrackInfo
private let profileData: ElevationProfileData?
private var trackInfo: TrackInfo
private var profileData: ElevationProfileData?
private let delegate: ElevationProfileViewControllerDelegate?
private let bookmarkManager: BookmarksManager = .shared()
private let cellSpacing: CGFloat = 8
private let descriptionModels: [DescriptionsViewModel]
private let chartData: ElevationProfileChartData?
private var descriptionModels: [DescriptionsViewModel]
private var chartData: ElevationProfileChartData?
private let formatter: ElevationProfileFormatter
init(view: ElevationProfileViewProtocol,
@ -36,17 +38,18 @@ final class ElevationProfilePresenter: NSObject {
formatter: ElevationProfileFormatter = ElevationProfileFormatter(),
delegate: ElevationProfileViewControllerDelegate?) {
self.view = view
self.delegate = delegate
self.formatter = formatter
self.trackInfo = trackInfo
self.profileData = profileData
self.delegate = delegate
if let profileData {
self.chartData = ElevationProfileChartData(profileData)
} else {
self.chartData = nil
}
self.formatter = formatter
self.descriptionModels = Self.descriptionModels(for: trackInfo)
}
descriptionModels = [
private static func descriptionModels(for trackInfo: TrackInfo) -> [DescriptionsViewModel] {
[
DescriptionsViewModel(title: L("elevation_profile_ascent"), value: trackInfo.ascent, imageName: "ic_em_ascent_24"),
DescriptionsViewModel(title: L("elevation_profile_descent"), value: trackInfo.descent, imageName: "ic_em_descent_24"),
DescriptionsViewModel(title: L("elevation_profile_max_elevation"), value: trackInfo.maxElevation, imageName: "ic_em_max_attitude_24"),
@ -55,41 +58,39 @@ final class ElevationProfilePresenter: NSObject {
}
deinit {
BookmarksManager.shared().resetElevationActivePointChanged()
BookmarksManager.shared().resetElevationMyPositionChanged()
bookmarkManager.resetElevationActivePointChanged()
bookmarkManager.resetElevationMyPositionChanged()
}
}
extension ElevationProfilePresenter: ElevationProfilePresenterProtocol {
func update(trackInfo: TrackInfo, profileData: ElevationProfileData?) {
self.profileData = profileData
if let profileData {
self.chartData = ElevationProfileChartData(profileData)
} else {
self.chartData = nil
}
descriptionModels = Self.descriptionModels(for: trackInfo)
configure()
}
func configure() {
guard let profileData, let chartData else {
let kMinPointsToDraw = 3
guard let profileData, let chartData, chartData.points.count >= kMinPointsToDraw else {
view?.isChartViewHidden = true
view?.isDifficultyHidden = true
view?.isExtendedDifficultyLabelHidden = true
view?.isBottomPanelHidden = true
return
}
view?.isChartViewHidden = false
view?.setChartData(ChartPresentationData(chartData, formatter: formatter))
view?.reloadDescription()
if profileData.difficulty != .disabled {
view?.isDifficultyHidden = false
view?.setDifficulty(profileData.difficulty)
} else {
view?.isDifficultyHidden = true
}
view?.isBottomPanelHidden = profileData.difficulty == .disabled
view?.isExtendedDifficultyLabelHidden = true
let presentationData = ChartPresentationData(chartData, formatter: formatter)
view?.setChartData(presentationData)
view?.setActivePoint(profileData.activePoint)
view?.setMyPosition(profileData.myPosition)
BookmarksManager.shared().setElevationActivePointChanged(profileData.trackId) { [weak self] distance in
bookmarkManager.setElevationActivePointChanged(profileData.trackId) { [weak self] distance in
self?.view?.setActivePoint(distance)
}
BookmarksManager.shared().setElevationMyPositionChanged(profileData.trackId) { [weak self] distance in
bookmarkManager.setElevationMyPositionChanged(profileData.trackId) { [weak self] distance in
self?.view?.setMyPosition(distance)
}
}
@ -114,9 +115,9 @@ extension ElevationProfilePresenter {
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "ElevationProfileDescriptionCell", for: indexPath) as! ElevationProfileDescriptionCell
let cell = collectionView.dequeueReusableCell(cell: ElevationProfileDescriptionCell.self, indexPath: indexPath)
let model = descriptionModels[indexPath.row]
cell.configure(title: model.title, value: formatter.yAxisString(from: Double(model.value)), imageName: model.imageName)
cell.configure(subtitle: model.title, value: formatter.yAxisString(from: Double(model.value)), imageName: model.imageName)
return cell
}
}
@ -128,7 +129,7 @@ extension ElevationProfilePresenter {
let width = collectionView.width
let cellHeight = collectionView.height
let modelsCount = CGFloat(descriptionModels.count)
let cellWidth = (width - cellSpacing * (modelsCount - 1)) / modelsCount
let cellWidth = (width - cellSpacing * (modelsCount - 1) - collectionView.contentInset.right) / modelsCount
return CGSize(width: cellWidth, height: cellHeight)
}
@ -141,7 +142,6 @@ fileprivate struct ElevationProfileChartData {
struct Line: ChartLine {
var values: [ChartValue]
var name: String
var color: UIColor
var type: ChartLineType
}
@ -159,8 +159,8 @@ fileprivate struct ElevationProfileChartData {
self.maxDistance = distances.last ?? 0
let lineColor = StyleManager.shared.theme?.colors.chartLine ?? .blue
let lineShadowColor = StyleManager.shared.theme?.colors.chartShadow ?? .lightGray
let l1 = Line(values: chartValues, name: "Altitude", color: lineColor, type: .line)
let l2 = Line(values: chartValues, name: "Altitude", color: lineShadowColor, type: .lineArea)
let l1 = Line(values: chartValues, color: lineColor, type: .line)
let l2 = Line(values: chartValues, color: lineShadowColor, type: .lineArea)
chartLines = [l1, l2]
}
@ -168,7 +168,6 @@ fileprivate struct ElevationProfileChartData {
_ p2: ElevationHeightPoint,
at distance: Double) -> Double {
assert(distance > p1.distance && distance < p2.distance, "distance must be between points")
let d = (distance - p1.distance) / (p2.distance - p1.distance)
return p1.altitude + round(Double(p2.altitude - p1.altitude) * d)
}

View file

@ -4,49 +4,54 @@ protocol ElevationProfileViewProtocol: AnyObject {
var presenter: ElevationProfilePresenterProtocol? { get set }
var isChartViewHidden: Bool { get set }
var isExtendedDifficultyLabelHidden: Bool { get set }
var isDifficultyHidden: Bool { get set }
var isBottomPanelHidden: Bool { get set }
var isChartViewInfoHidden: Bool { get set }
func setExtendedDifficultyGrade(_ value: String)
func setDifficulty(_ value: ElevationDifficulty)
func setChartData(_ data: ChartPresentationData)
func setActivePoint(_ distance: Double)
func setMyPosition(_ distance: Double)
func reloadDescription()
}
class ElevationProfileViewController: UIViewController {
final class ElevationProfileViewController: UIViewController {
private enum Constants {
static let descriptionCollectionViewHeight: CGFloat = 52
static let descriptionCollectionViewContentInsets = UIEdgeInsets(top: 20, left: 16, bottom: 4, right: 16)
static let graphViewContainerInsets = UIEdgeInsets(top: -4, left: 0, bottom: 0, right: 0)
static let chartViewInsets = UIEdgeInsets(top: 0, left: 16, bottom: 0, right: -16)
static let chartViewVisibleHeight: CGFloat = 176
static let chartViewHiddenHeight: CGFloat = 20
static let difficultyVisibleHeight: CGFloat = 60
static let difficultyHiddenHeight: CGFloat = 20
static let chartViewHiddenHeight: CGFloat = .zero
}
var presenter: ElevationProfilePresenterProtocol?
init() {
super.init(nibName: nil, bundle: nil)
}
var presenter: ElevationProfilePresenterProtocol?
@available(*, unavailable)
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
@IBOutlet private weak var chartView: ChartView!
@IBOutlet private weak var graphViewContainer: UIView!
@IBOutlet private weak var descriptionCollectionView: UICollectionView!
@IBOutlet private weak var difficultyView: DifficultyView!
@IBOutlet private weak var difficultyTitle: UILabel!
@IBOutlet private weak var extendedDifficultyGradeLabel: UILabel!
@IBOutlet private weak var extendedGradeButton: UIButton!
@IBOutlet private weak var chartHeightConstraint: NSLayoutConstraint!
@IBOutlet private weak var difficultyConstraint: NSLayoutConstraint!
private var chartView = ChartView()
private var graphViewContainer = UIView()
private var descriptionCollectionView: UICollectionView = {
let layout = UICollectionViewFlowLayout()
layout.scrollDirection = .horizontal
layout.minimumInteritemSpacing = 0
return UICollectionView(frame: .zero, collectionViewLayout: layout)
}()
private var chartViewHeightConstraint: NSLayoutConstraint!
private var difficultyHidden: Bool = false
private var bottomPanelHidden: Bool = false
// MARK: - Lifecycle
override func viewDidLoad() {
super.viewDidLoad()
descriptionCollectionView.dataSource = presenter
descriptionCollectionView.delegate = presenter
setupViews()
layoutViews()
presenter?.configure()
chartView.onSelectedPointChanged = { [weak self] in
self?.presenter?.onSelectedPointChanged($0)
}
}
override func viewWillLayoutSubviews() {
@ -54,60 +59,75 @@ class ElevationProfileViewController: UIViewController {
descriptionCollectionView.reloadData()
}
@IBAction func onExtendedDifficultyButtonPressed(_ sender: Any) {
presenter?.onDifficultyButtonPressed()
// MARK: - Private methods
private func setupViews() {
view.styleName = "Background"
setupDescriptionCollectionView()
setupChartView()
}
func getPreviewHeight() -> CGFloat {
return view.height - descriptionCollectionView.frame.minY
private func setupChartView() {
graphViewContainer.translatesAutoresizingMaskIntoConstraints = false
chartView.translatesAutoresizingMaskIntoConstraints = false
chartView.onSelectedPointChanged = { [weak self] in
self?.presenter?.onSelectedPointChanged($0)
}
}
private func setupDescriptionCollectionView() {
descriptionCollectionView.backgroundColor = .clear
descriptionCollectionView.register(cell: ElevationProfileDescriptionCell.self)
descriptionCollectionView.dataSource = presenter
descriptionCollectionView.delegate = presenter
descriptionCollectionView.isScrollEnabled = false
descriptionCollectionView.contentInset = Constants.descriptionCollectionViewContentInsets
descriptionCollectionView.translatesAutoresizingMaskIntoConstraints = false
}
private func layoutViews() {
view.addSubview(descriptionCollectionView)
graphViewContainer.addSubview(chartView)
view.addSubview(graphViewContainer)
chartViewHeightConstraint = chartView.heightAnchor.constraint(equalToConstant: Constants.chartViewVisibleHeight)
NSLayoutConstraint.activate([
descriptionCollectionView.topAnchor.constraint(equalTo: view.topAnchor),
descriptionCollectionView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
descriptionCollectionView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
descriptionCollectionView.heightAnchor.constraint(equalToConstant: Constants.descriptionCollectionViewHeight),
descriptionCollectionView.bottomAnchor.constraint(equalTo: graphViewContainer.topAnchor, constant: Constants.graphViewContainerInsets.top),
graphViewContainer.leadingAnchor.constraint(equalTo: view.leadingAnchor),
graphViewContainer.trailingAnchor.constraint(equalTo: view.trailingAnchor),
graphViewContainer.bottomAnchor.constraint(equalTo: view.bottomAnchor),
chartView.topAnchor.constraint(equalTo: graphViewContainer.topAnchor),
chartView.leadingAnchor.constraint(equalTo: graphViewContainer.leadingAnchor, constant: Constants.chartViewInsets.left),
chartView.trailingAnchor.constraint(equalTo: graphViewContainer.trailingAnchor, constant: Constants.chartViewInsets.right),
chartView.bottomAnchor.constraint(equalTo: graphViewContainer.bottomAnchor),
chartViewHeightConstraint,
])
}
private func getPreviewHeight() -> CGFloat {
view.height - descriptionCollectionView.frame.minY
}
}
// MARK: - ElevationProfileViewProtocol
extension ElevationProfileViewController: ElevationProfileViewProtocol {
var isChartViewHidden: Bool {
get { return chartView.isHidden }
get { chartView.isHidden }
set {
chartView.isHidden = newValue
graphViewContainer.isHidden = newValue
chartHeightConstraint.constant = newValue ? Constants.chartViewHiddenHeight : Constants.chartViewVisibleHeight
chartViewHeightConstraint.constant = newValue ? Constants.chartViewHiddenHeight : Constants.chartViewVisibleHeight
}
}
var isExtendedDifficultyLabelHidden: Bool {
get { return extendedDifficultyGradeLabel.isHidden }
set {
extendedDifficultyGradeLabel.isHidden = newValue
extendedGradeButton.isHidden = newValue
}
}
var isDifficultyHidden: Bool {
get { difficultyHidden }
set {
difficultyHidden = newValue
difficultyTitle.isHidden = newValue
difficultyView.isHidden = newValue
}
}
var isBottomPanelHidden: Bool {
get { bottomPanelHidden }
set {
bottomPanelHidden = newValue
if newValue == true {
isExtendedDifficultyLabelHidden = true
isDifficultyHidden = true
}
difficultyConstraint.constant = newValue ? Constants.difficultyHiddenHeight : Constants.difficultyVisibleHeight
}
}
func setExtendedDifficultyGrade(_ value: String) {
extendedDifficultyGradeLabel.text = value
}
func setDifficulty(_ value: ElevationDifficulty) {
difficultyView.difficulty = value
var isChartViewInfoHidden: Bool {
get { chartView.isChartViewInfoHidden }
set { chartView.isChartViewInfoHidden = newValue }
}
func setChartData(_ data: ChartPresentationData) {
@ -121,4 +141,8 @@ extension ElevationProfileViewController: ElevationProfileViewProtocol {
func setMyPosition(_ distance: Double) {
chartView.myPosition = distance
}
func reloadDescription() {
descriptionCollectionView.reloadData()
}
}

View file

@ -7,7 +7,6 @@
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
<capability name="Stack View standard spacing" minToolsVersion="9.0"/>
<capability name="System colors in document resources" minToolsVersion="11.0"/>
<capability name="collection view cell content view" minToolsVersion="11.0"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<scenes>
@ -1281,181 +1280,6 @@
</objects>
<point key="canvasLocation" x="928.79999999999995" y="-703.44827586206907"/>
</scene>
<!--Elevation Profile View Controller-->
<scene sceneID="0yF-nr-ALU">
<objects>
<viewController storyboardIdentifier="ElevationProfileViewController" id="d1y-Na-lDm" customClass="ElevationProfileViewController" customModule="Organic_Maps" customModuleProvider="target" sceneMemberID="viewController">
<view key="view" contentMode="scaleToFill" ambiguous="YES" id="7Mx-au-yIa">
<rect key="frame" x="0.0" y="0.0" width="375" height="319"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="jKi-gT-ZfM">
<rect key="frame" x="0.0" y="20" width="375" height="176"/>
<subviews>
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="jIS-0e-Ztd" customClass="ChartView" customModule="Chart">
<rect key="frame" x="16" y="0.0" width="343" height="176"/>
<color key="backgroundColor" systemColor="systemBackgroundColor"/>
</view>
</subviews>
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<constraints>
<constraint firstItem="jIS-0e-Ztd" firstAttribute="top" secondItem="jKi-gT-ZfM" secondAttribute="top" id="QeA-Yb-58l"/>
<constraint firstAttribute="trailing" secondItem="jIS-0e-Ztd" secondAttribute="trailing" constant="16" id="XRb-7G-y3q"/>
<constraint firstAttribute="bottom" secondItem="jIS-0e-Ztd" secondAttribute="bottom" id="g8g-f5-krt"/>
<constraint firstItem="jIS-0e-Ztd" firstAttribute="leading" secondItem="jKi-gT-ZfM" secondAttribute="leading" constant="16" id="khr-Sp-8jS"/>
<constraint firstAttribute="height" constant="176" id="utH-YA-2pe"/>
</constraints>
</view>
<collectionView clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" scrollEnabled="NO" showsHorizontalScrollIndicator="NO" showsVerticalScrollIndicator="NO" dataMode="prototypes" translatesAutoresizingMaskIntoConstraints="NO" id="Xc9-ED-V4K">
<rect key="frame" x="16" y="200" width="343" height="68"/>
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<constraints>
<constraint firstAttribute="height" constant="68" id="AM4-tj-liE"/>
</constraints>
<collectionViewFlowLayout key="collectionViewLayout" minimumLineSpacing="10" minimumInteritemSpacing="10" id="gL4-id-6En">
<size key="itemSize" width="50" height="50"/>
<size key="headerReferenceSize" width="0.0" height="0.0"/>
<size key="footerReferenceSize" width="0.0" height="0.0"/>
<inset key="sectionInset" minX="0.0" minY="0.0" maxX="0.0" maxY="0.0"/>
</collectionViewFlowLayout>
<cells>
<collectionViewCell opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" reuseIdentifier="ElevationProfileDescriptionCell" id="ubO-dg-082" customClass="ElevationProfileDescriptionCell" customModule="Organic_Maps" customModuleProvider="target">
<rect key="frame" x="0.0" y="0.0" width="69" height="68"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<collectionViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" insetsLayoutMarginsFromSafeArea="NO" id="CH9-Og-i2q">
<rect key="frame" x="0.0" y="0.0" width="69" height="68"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" translatesAutoresizingMaskIntoConstraints="NO" id="7Xw-zI-6aP">
<rect key="frame" x="22.5" y="6" width="24" height="24"/>
<constraints>
<constraint firstAttribute="height" constant="24" id="9eF-wN-2uC"/>
<constraint firstAttribute="width" constant="24" id="ct1-1m-XeC"/>
</constraints>
<userDefinedRuntimeAttributes>
<userDefinedRuntimeAttribute type="string" keyPath="styleName" value="MWMBlack"/>
</userDefinedRuntimeAttributes>
</imageView>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Label" textAlignment="center" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="mAR-lR-4BZ">
<rect key="frame" x="1" y="33" width="67" height="12"/>
<fontDescription key="fontDescription" type="system" pointSize="10"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
<userDefinedRuntimeAttributes>
<userDefinedRuntimeAttribute type="string" keyPath="styleName" value="regular10:blackSecondaryText"/>
</userDefinedRuntimeAttributes>
</label>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Label" textAlignment="center" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="8rf-1Z-YJM">
<rect key="frame" x="1" y="45" width="67" height="17"/>
<fontDescription key="fontDescription" type="system" pointSize="14"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
<userDefinedRuntimeAttributes>
<userDefinedRuntimeAttribute type="string" keyPath="styleName" value="medium14:blackSecondaryText"/>
</userDefinedRuntimeAttributes>
</label>
</subviews>
<color key="backgroundColor" systemColor="opaqueSeparatorColor"/>
<constraints>
<constraint firstItem="8rf-1Z-YJM" firstAttribute="centerX" secondItem="mAR-lR-4BZ" secondAttribute="centerX" id="2bV-Eg-iTv"/>
<constraint firstItem="8rf-1Z-YJM" firstAttribute="leading" secondItem="CH9-Og-i2q" secondAttribute="leading" constant="1" id="2zh-El-kBm"/>
<constraint firstAttribute="trailing" secondItem="mAR-lR-4BZ" secondAttribute="trailing" constant="1" id="Brm-sc-JFK"/>
<constraint firstAttribute="trailing" secondItem="8rf-1Z-YJM" secondAttribute="trailing" constant="1" id="FJf-4o-P8q"/>
<constraint firstItem="7Xw-zI-6aP" firstAttribute="top" secondItem="CH9-Og-i2q" secondAttribute="top" constant="6" id="Wpx-pf-pO9"/>
<constraint firstItem="mAR-lR-4BZ" firstAttribute="top" secondItem="7Xw-zI-6aP" secondAttribute="bottom" constant="3" id="Xq7-6S-AJb"/>
<constraint firstItem="8rf-1Z-YJM" firstAttribute="top" secondItem="mAR-lR-4BZ" secondAttribute="bottom" id="kyB-4q-1Ms"/>
<constraint firstItem="mAR-lR-4BZ" firstAttribute="leading" secondItem="CH9-Og-i2q" secondAttribute="leading" constant="1" id="rOY-yv-hrs"/>
<constraint firstItem="mAR-lR-4BZ" firstAttribute="centerX" secondItem="CH9-Og-i2q" secondAttribute="centerX" id="u1A-tO-NFc"/>
<constraint firstItem="7Xw-zI-6aP" firstAttribute="centerX" secondItem="CH9-Og-i2q" secondAttribute="centerX" id="yNO-Ee-f6X"/>
</constraints>
<userDefinedRuntimeAttributes>
<userDefinedRuntimeAttribute type="string" keyPath="styleName" value="ElevationProfileDescriptionCell"/>
</userDefinedRuntimeAttributes>
</collectionViewCellContentView>
<size key="customSize" width="69" height="68"/>
<connections>
<outlet property="imageView" destination="7Xw-zI-6aP" id="Mih-v4-OqB"/>
<outlet property="titleLabel" destination="mAR-lR-4BZ" id="Yd1-qq-FYk"/>
<outlet property="valueLabel" destination="8rf-1Z-YJM" id="voz-9x-ymv"/>
</connections>
</collectionViewCell>
</cells>
</collectionView>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" horizontalCompressionResistancePriority="250" text="Difficulty" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="FIo-No-CbK">
<rect key="frame" x="16" y="280" width="68" height="20.5"/>
<fontDescription key="fontDescription" type="system" pointSize="17"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
<userDefinedRuntimeAttributes>
<userDefinedRuntimeAttribute type="string" keyPath="localizedText" value="elevation_profile_difficulty"/>
<userDefinedRuntimeAttribute type="string" keyPath="styleName" value="regular14:blackSecondaryText"/>
</userDefinedRuntimeAttributes>
</label>
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="bc9-z0-p88" customClass="DifficultyView" customModule="Organic_Maps" customModuleProvider="target">
<rect key="frame" x="91" y="286" width="40" height="10"/>
<color key="backgroundColor" white="0.66666666666666663" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<constraints>
<constraint firstAttribute="height" constant="10" id="2Tg-JW-8Tr"/>
<constraint firstAttribute="width" constant="40" id="Sor-5l-zjy"/>
</constraints>
</view>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="g6D-fD-0Ug">
<rect key="frame" x="133" y="275.5" width="30" height="30"/>
<connections>
<action selector="onExtendedDifficultyButtonPressed:" destination="d1y-Na-lDm" eventType="touchUpInside" id="4zH-m2-OSE"/>
</connections>
</button>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="S1" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="GPk-XR-oL1" customClass="InsetsLabel" customModule="Organic_Maps" customModuleProvider="target">
<rect key="frame" x="138.5" y="280" width="19" height="20.5"/>
<color key="backgroundColor" white="0.66666666666666663" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<fontDescription key="fontDescription" type="boldSystem" pointSize="17"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
<userDefinedRuntimeAttributes>
<userDefinedRuntimeAttribute type="string" keyPath="styleName" value="ElevationProfileExtendedDifficulty"/>
</userDefinedRuntimeAttributes>
</label>
</subviews>
<viewLayoutGuide key="safeArea" id="ezp-sJ-36x"/>
<color key="backgroundColor" systemColor="systemBackgroundColor"/>
<constraints>
<constraint firstItem="bc9-z0-p88" firstAttribute="leading" secondItem="FIo-No-CbK" secondAttribute="trailing" constant="7" id="CDd-Zf-CvI"/>
<constraint firstItem="Xc9-ED-V4K" firstAttribute="top" secondItem="jKi-gT-ZfM" secondAttribute="bottom" constant="4" id="Izs-S0-cku"/>
<constraint firstItem="g6D-fD-0Ug" firstAttribute="centerY" secondItem="GPk-XR-oL1" secondAttribute="centerY" id="P9X-9S-8dI"/>
<constraint firstItem="GPk-XR-oL1" firstAttribute="leading" secondItem="bc9-z0-p88" secondAttribute="trailing" constant="7.6666666666666856" id="W9l-Ip-nhH"/>
<constraint firstItem="g6D-fD-0Ug" firstAttribute="centerX" secondItem="GPk-XR-oL1" secondAttribute="centerX" id="YFV-Au-wTO"/>
<constraint firstItem="FIo-No-CbK" firstAttribute="leading" secondItem="ezp-sJ-36x" secondAttribute="leading" constant="16" id="eg2-uX-NgT"/>
<constraint firstItem="jKi-gT-ZfM" firstAttribute="leading" secondItem="ezp-sJ-36x" secondAttribute="leading" id="kKJ-Jg-wRO"/>
<constraint firstItem="ezp-sJ-36x" firstAttribute="trailing" secondItem="Xc9-ED-V4K" secondAttribute="trailing" constant="16" id="mxE-Mk-VH2"/>
<constraint firstItem="bc9-z0-p88" firstAttribute="bottom" secondItem="FIo-No-CbK" secondAttribute="baseline" id="opM-hk-CFP"/>
<constraint firstAttribute="bottom" secondItem="Xc9-ED-V4K" secondAttribute="bottom" constant="60" id="vaG-aV-kw5"/>
<constraint firstItem="Xc9-ED-V4K" firstAttribute="leading" secondItem="ezp-sJ-36x" secondAttribute="leading" constant="16" id="vpI-N0-eIg"/>
<constraint firstItem="jKi-gT-ZfM" firstAttribute="top" secondItem="ezp-sJ-36x" secondAttribute="top" id="ySA-vA-GW9"/>
<constraint firstItem="GPk-XR-oL1" firstAttribute="centerY" secondItem="FIo-No-CbK" secondAttribute="centerY" id="yey-Sw-JqF"/>
<constraint firstItem="FIo-No-CbK" firstAttribute="top" secondItem="Xc9-ED-V4K" secondAttribute="bottom" constant="12" id="zDN-ZF-3Ex"/>
<constraint firstItem="ezp-sJ-36x" firstAttribute="trailing" secondItem="jKi-gT-ZfM" secondAttribute="trailing" id="zN2-OH-sDZ"/>
</constraints>
<userDefinedRuntimeAttributes>
<userDefinedRuntimeAttribute type="string" keyPath="styleName" value="Background"/>
</userDefinedRuntimeAttributes>
</view>
<size key="freeformSize" width="375" height="319"/>
<connections>
<outlet property="chartHeightConstraint" destination="utH-YA-2pe" id="ee1-mM-L7n"/>
<outlet property="chartView" destination="jIS-0e-Ztd" id="KHY-Bn-Pe6"/>
<outlet property="descriptionCollectionView" destination="Xc9-ED-V4K" id="dHB-dH-HYE"/>
<outlet property="difficultyConstraint" destination="vaG-aV-kw5" id="fkz-u2-wYh"/>
<outlet property="difficultyTitle" destination="FIo-No-CbK" id="Rbh-8b-zK9"/>
<outlet property="difficultyView" destination="bc9-z0-p88" id="p5u-Au-7i2"/>
<outlet property="extendedDifficultyGradeLabel" destination="GPk-XR-oL1" id="SpR-XZ-6ou"/>
<outlet property="extendedGradeButton" destination="g6D-fD-0Ug" id="8br-bF-NqA"/>
<outlet property="graphViewContainer" destination="jKi-gT-ZfM" id="SUq-a3-G5F"/>
</connections>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="mfQ-ai-TWx" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="925.60000000000002" y="2501.1994002998504"/>
</scene>
<!--Opening Hours Day View Controller-->
<scene sceneID="4L0-Kt-il9">
<objects>
@ -1679,17 +1503,14 @@
<image name="ic_operator" width="28" height="28"/>
<image name="ic_placepage_open_hours" width="28" height="28"/>
<image name="img_direction_light" width="32" height="32"/>
<systemColor name="opaqueSeparatorColor">
<color red="0.77647058823529413" green="0.77647058823529413" blue="0.78431372549019607" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
</systemColor>
<systemColor name="separatorColor">
<color red="0.23529411764705882" green="0.23529411764705882" blue="0.2627450980392157" alpha="0.28999999999999998" colorSpace="custom" customColorSpace="sRGB"/>
<color red="0.23529411759999999" green="0.23529411759999999" blue="0.26274509800000001" alpha="0.28999999999999998" colorSpace="custom" customColorSpace="sRGB"/>
</systemColor>
<systemColor name="systemBackgroundColor">
<color white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
</systemColor>
<systemColor name="systemRedColor">
<color red="1" green="0.23137254901960785" blue="0.18823529411764706" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<color red="1" green="0.23137254900000001" blue="0.18823529410000001" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
</systemColor>
</resources>
</document>