Repo created
This commit is contained in:
parent
4af19165ec
commit
68073add76
12458 changed files with 12350765 additions and 2 deletions
|
|
@ -0,0 +1,13 @@
|
|||
import CoreApi
|
||||
|
||||
class ElevationProfileBuilder {
|
||||
static func build(trackData: PlacePageTrackData,
|
||||
delegate: ElevationProfileViewControllerDelegate?) -> ElevationProfileViewController {
|
||||
let viewController = ElevationProfileViewController();
|
||||
let presenter = ElevationProfilePresenter(view: viewController,
|
||||
trackData: trackData,
|
||||
delegate: delegate)
|
||||
viewController.presenter = presenter
|
||||
return viewController
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,82 @@
|
|||
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()
|
||||
valueLabel.text = ""
|
||||
subtitleLabel.text = ""
|
||||
imageView.image = nil
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,67 @@
|
|||
import Chart
|
||||
import CoreApi
|
||||
|
||||
final class ElevationProfileFormatter {
|
||||
|
||||
private enum Constants {
|
||||
static let metricToImperialMultiplier: CGFloat = 0.3048
|
||||
static var metricAltitudeStep: CGFloat = 50
|
||||
static var imperialAltitudeStep: CGFloat = 100
|
||||
}
|
||||
|
||||
private let distanceFormatter: DistanceFormatter.Type
|
||||
private let altitudeFormatter: AltitudeFormatter.Type
|
||||
private let unitSystemMultiplier: CGFloat
|
||||
private let altitudeStep: CGFloat
|
||||
private let units: Units
|
||||
|
||||
init(units: Units = SettingsBridge.measurementUnits()) {
|
||||
self.units = units
|
||||
self.distanceFormatter = DistanceFormatter.self
|
||||
self.altitudeFormatter = AltitudeFormatter.self
|
||||
switch units {
|
||||
case .metric:
|
||||
self.altitudeStep = Constants.metricAltitudeStep
|
||||
self.unitSystemMultiplier = 1
|
||||
case .imperial:
|
||||
self.altitudeStep = Constants.imperialAltitudeStep
|
||||
self.unitSystemMultiplier = Constants.metricToImperialMultiplier
|
||||
@unknown default:
|
||||
fatalError("Unsupported units")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension ElevationProfileFormatter: ChartFormatter {
|
||||
func xAxisString(from value: Double) -> String {
|
||||
distanceFormatter.distanceString(fromMeters: value)
|
||||
}
|
||||
|
||||
func yAxisString(from value: Double) -> String {
|
||||
altitudeFormatter.altitudeString(fromMeters: value)
|
||||
}
|
||||
|
||||
func yAxisLowerBound(from value: CGFloat) -> CGFloat {
|
||||
floor((value / unitSystemMultiplier) / altitudeStep) * altitudeStep * unitSystemMultiplier
|
||||
}
|
||||
|
||||
func yAxisUpperBound(from value: CGFloat) -> CGFloat {
|
||||
ceil((value / unitSystemMultiplier) / altitudeStep) * altitudeStep * unitSystemMultiplier
|
||||
}
|
||||
|
||||
func yAxisSteps(lowerBound: CGFloat, upperBound: CGFloat) -> [CGFloat] {
|
||||
let lower = yAxisLowerBound(from: lowerBound)
|
||||
let upper = yAxisUpperBound(from: upperBound)
|
||||
let range = upper - lower
|
||||
var stepSize = altitudeStep
|
||||
var stepsCount = Int((range / stepSize).rounded(.up))
|
||||
|
||||
while stepsCount > 6 {
|
||||
stepSize *= 2 // Double the step size to reduce the step count
|
||||
stepsCount = Int((range / stepSize).rounded(.up))
|
||||
}
|
||||
|
||||
let steps = stride(from: lower, through: upper, by: stepSize)
|
||||
return Array(steps)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,191 @@
|
|||
import Chart
|
||||
import CoreApi
|
||||
|
||||
protocol TrackActivePointPresenter: AnyObject {
|
||||
func updateActivePointDistance(_ distance: Double)
|
||||
func updateMyPositionDistance(_ distance: Double)
|
||||
}
|
||||
|
||||
protocol ElevationProfilePresenterProtocol: UICollectionViewDataSource, UICollectionViewDelegateFlowLayout, TrackActivePointPresenter {
|
||||
func configure()
|
||||
func update(with trackData: PlacePageTrackData)
|
||||
func onDifficultyButtonPressed()
|
||||
func onSelectedPointChanged(_ point: CGFloat)
|
||||
}
|
||||
|
||||
protocol ElevationProfileViewControllerDelegate: AnyObject {
|
||||
func openDifficultyPopup()
|
||||
func updateMapPoint(_ point: CLLocationCoordinate2D, distance: Double)
|
||||
}
|
||||
|
||||
fileprivate struct DescriptionsViewModel {
|
||||
let title: String
|
||||
let value: String
|
||||
let imageName: String
|
||||
}
|
||||
|
||||
final class ElevationProfilePresenter: NSObject {
|
||||
private weak var view: ElevationProfileViewProtocol?
|
||||
private weak var trackData: PlacePageTrackData?
|
||||
private weak var delegate: ElevationProfileViewControllerDelegate?
|
||||
private let bookmarkManager: BookmarksManager = .shared()
|
||||
|
||||
private let cellSpacing: CGFloat = 8
|
||||
private var descriptionModels: [DescriptionsViewModel]
|
||||
private var chartData: ElevationProfileChartData?
|
||||
private let formatter: ElevationProfileFormatter
|
||||
|
||||
init(view: ElevationProfileViewProtocol,
|
||||
trackData: PlacePageTrackData,
|
||||
formatter: ElevationProfileFormatter = ElevationProfileFormatter(),
|
||||
delegate: ElevationProfileViewControllerDelegate?) {
|
||||
self.view = view
|
||||
self.delegate = delegate
|
||||
self.formatter = formatter
|
||||
self.trackData = trackData
|
||||
if let profileData = trackData.elevationProfileData {
|
||||
self.chartData = ElevationProfileChartData(profileData)
|
||||
}
|
||||
self.descriptionModels = Self.descriptionModels(for: trackData.trackInfo)
|
||||
}
|
||||
|
||||
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"),
|
||||
DescriptionsViewModel(title: L("elevation_profile_min_elevation"), value: trackInfo.minElevation, imageName: "ic_em_min_attitude_24")
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
extension ElevationProfilePresenter: ElevationProfilePresenterProtocol {
|
||||
func update(with trackData: PlacePageTrackData) {
|
||||
self.trackData = trackData
|
||||
if let profileData = trackData.elevationProfileData {
|
||||
self.chartData = ElevationProfileChartData(profileData)
|
||||
} else {
|
||||
self.chartData = nil
|
||||
}
|
||||
descriptionModels = Self.descriptionModels(for: trackData.trackInfo)
|
||||
configure()
|
||||
}
|
||||
|
||||
func updateActivePointDistance(_ distance: Double) {
|
||||
guard let view, view.canReceiveUpdates else { return }
|
||||
view.setActivePointDistance(distance)
|
||||
}
|
||||
|
||||
func updateMyPositionDistance(_ distance: Double) {
|
||||
guard let view, view.canReceiveUpdates else { return }
|
||||
view.setMyPositionDistance(distance)
|
||||
}
|
||||
|
||||
func configure() {
|
||||
view?.isChartViewHidden = false
|
||||
|
||||
let kMinPointsToDraw = 2
|
||||
guard let trackData = trackData,
|
||||
let profileData = trackData.elevationProfileData,
|
||||
let chartData,
|
||||
chartData.points.count >= kMinPointsToDraw else {
|
||||
view?.userInteractionEnabled = false
|
||||
return
|
||||
}
|
||||
|
||||
view?.setChartData(ChartPresentationData(chartData, formatter: formatter))
|
||||
view?.reloadDescription()
|
||||
|
||||
guard !profileData.isTrackRecording else {
|
||||
view?.isChartViewInfoHidden = true
|
||||
return
|
||||
}
|
||||
|
||||
view?.userInteractionEnabled = true
|
||||
view?.setActivePointDistance(trackData.activePointDistance)
|
||||
view?.setMyPositionDistance(trackData.myPositionDistance)
|
||||
}
|
||||
|
||||
func onDifficultyButtonPressed() {
|
||||
delegate?.openDifficultyPopup()
|
||||
}
|
||||
|
||||
func onSelectedPointChanged(_ point: CGFloat) {
|
||||
guard let chartData else { return }
|
||||
let distance: Double = floor(point) / CGFloat(chartData.points.count) * chartData.maxDistance
|
||||
let point = chartData.points.first { $0.distance >= distance } ?? chartData.points[0]
|
||||
delegate?.updateMapPoint(point.coordinates, distance: point.distance)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - UICollectionDataSource
|
||||
|
||||
extension ElevationProfilePresenter {
|
||||
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
|
||||
return descriptionModels.count
|
||||
}
|
||||
|
||||
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
|
||||
let cell = collectionView.dequeueReusableCell(cell: ElevationProfileDescriptionCell.self, indexPath: indexPath)
|
||||
let model = descriptionModels[indexPath.row]
|
||||
cell.configure(subtitle: model.title, value: model.value, imageName: model.imageName)
|
||||
return cell
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - UICollectionViewDelegateFlowLayout
|
||||
|
||||
extension ElevationProfilePresenter {
|
||||
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
|
||||
let width = collectionView.width
|
||||
let cellHeight = collectionView.height
|
||||
let modelsCount = CGFloat(descriptionModels.count)
|
||||
let cellWidth = (width - cellSpacing * (modelsCount - 1) - collectionView.contentInset.right - collectionView.contentInset.left) / modelsCount
|
||||
return CGSize(width: cellWidth, height: cellHeight)
|
||||
}
|
||||
|
||||
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumInteritemSpacingForSectionAt section: Int) -> CGFloat {
|
||||
return cellSpacing
|
||||
}
|
||||
}
|
||||
|
||||
fileprivate struct ElevationProfileChartData {
|
||||
|
||||
struct Line: ChartLine {
|
||||
var values: [ChartValue]
|
||||
var color: UIColor
|
||||
var type: ChartLineType
|
||||
}
|
||||
|
||||
fileprivate let chartValues: [ChartValue]
|
||||
fileprivate let chartLines: [Line]
|
||||
fileprivate let distances: [Double]
|
||||
fileprivate let maxDistance: Double
|
||||
fileprivate let points: [ElevationHeightPoint]
|
||||
|
||||
init(_ elevationData: ElevationProfileData) {
|
||||
self.points = elevationData.points
|
||||
self.chartValues = points.map { ChartValue(xValues: $0.distance, y: $0.altitude) }
|
||||
self.distances = points.map { $0.distance }
|
||||
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, color: lineColor, type: .line)
|
||||
let l2 = Line(values: chartValues, color: lineShadowColor, type: .lineArea)
|
||||
chartLines = [l1, l2]
|
||||
}
|
||||
|
||||
private static func altBetweenPoints(_ p1: ElevationHeightPoint,
|
||||
_ 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)
|
||||
}
|
||||
}
|
||||
|
||||
extension ElevationProfileChartData: ChartData {
|
||||
public var xAxisValues: [Double] { distances }
|
||||
public var lines: [ChartLine] { chartLines }
|
||||
public var type: ChartType { .regular }
|
||||
}
|
||||
|
|
@ -0,0 +1,162 @@
|
|||
import Chart
|
||||
|
||||
protocol ElevationProfileViewProtocol: AnyObject {
|
||||
var presenter: ElevationProfilePresenterProtocol? { get set }
|
||||
|
||||
var userInteractionEnabled: Bool { get set }
|
||||
var isChartViewHidden: Bool { get set }
|
||||
var isChartViewInfoHidden: Bool { get set }
|
||||
var canReceiveUpdates: Bool { get }
|
||||
|
||||
func setChartData(_ data: ChartPresentationData)
|
||||
func setActivePointDistance(_ distance: Double)
|
||||
func setMyPositionDistance(_ distance: Double)
|
||||
func reloadDescription()
|
||||
}
|
||||
|
||||
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 = .zero
|
||||
}
|
||||
|
||||
var presenter: ElevationProfilePresenterProtocol?
|
||||
|
||||
init() {
|
||||
super.init(nibName: nil, bundle: nil)
|
||||
}
|
||||
|
||||
@available(*, unavailable)
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
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!
|
||||
|
||||
|
||||
// MARK: - Lifecycle
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
setupViews()
|
||||
layoutViews()
|
||||
presenter?.configure()
|
||||
}
|
||||
|
||||
override func viewWillLayoutSubviews() {
|
||||
super.viewWillLayoutSubviews()
|
||||
descriptionCollectionView.reloadData()
|
||||
}
|
||||
|
||||
// MARK: - Private methods
|
||||
|
||||
private func setupViews() {
|
||||
view.setStyle(.background)
|
||||
setupDescriptionCollectionView()
|
||||
setupChartView()
|
||||
}
|
||||
|
||||
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
|
||||
descriptionCollectionView.showsHorizontalScrollIndicator = false
|
||||
descriptionCollectionView.showsVerticalScrollIndicator = 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 userInteractionEnabled: Bool {
|
||||
get { chartView.isUserInteractionEnabled }
|
||||
set { chartView.isUserInteractionEnabled = newValue }
|
||||
}
|
||||
|
||||
var isChartViewHidden: Bool {
|
||||
get { chartView.isHidden }
|
||||
set {
|
||||
chartView.isHidden = newValue
|
||||
graphViewContainer.isHidden = newValue
|
||||
chartViewHeightConstraint.constant = newValue ? Constants.chartViewHiddenHeight : Constants.chartViewVisibleHeight
|
||||
}
|
||||
}
|
||||
|
||||
var isChartViewInfoHidden: Bool {
|
||||
get { chartView.isChartViewInfoHidden }
|
||||
set { chartView.isChartViewInfoHidden = newValue }
|
||||
}
|
||||
|
||||
var canReceiveUpdates: Bool {
|
||||
chartView.chartData != nil
|
||||
}
|
||||
|
||||
func setChartData(_ data: ChartPresentationData) {
|
||||
chartView.chartData = data
|
||||
}
|
||||
|
||||
func setActivePointDistance(_ distance: Double) {
|
||||
chartView.setSelectedPoint(distance)
|
||||
}
|
||||
|
||||
func setMyPositionDistance(_ distance: Double) {
|
||||
chartView.myPosition = distance
|
||||
}
|
||||
|
||||
func reloadDescription() {
|
||||
descriptionCollectionView.reloadData()
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue