Repo created
This commit is contained in:
parent
4af19165ec
commit
68073add76
12458 changed files with 12350765 additions and 2 deletions
|
|
@ -0,0 +1,244 @@
|
|||
protocol ActionBarViewControllerDelegate: AnyObject {
|
||||
func actionBar(_ actionBar: ActionBarViewController, didPressButton type: ActionBarButtonType)
|
||||
}
|
||||
|
||||
final class ActionBarViewController: UIViewController {
|
||||
@IBOutlet var stackView: UIStackView!
|
||||
private(set) var downloadButton: ActionBarButton? = nil
|
||||
private(set) var bookmarkButton: ActionBarButton? = nil
|
||||
private var popoverSourceView: UIView? {
|
||||
stackView.arrangedSubviews.last
|
||||
}
|
||||
|
||||
var placePageData: PlacePageData!
|
||||
var isRoutePlanning = false
|
||||
var canAddStop = false
|
||||
|
||||
private var visibleButtons: [ActionBarButtonType] = []
|
||||
private var additionalButtons: [ActionBarButtonType] = []
|
||||
|
||||
weak var delegate: ActionBarViewControllerDelegate?
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
configureButtons()
|
||||
}
|
||||
|
||||
// MARK: - Private methods
|
||||
|
||||
private func configureButtons() {
|
||||
if placePageData.isRoutePoint {
|
||||
visibleButtons.append(.routeRemoveStop)
|
||||
} else if placePageData.roadType != .none {
|
||||
switch placePageData.roadType {
|
||||
case .toll:
|
||||
visibleButtons.append(.avoidToll)
|
||||
case .ferry:
|
||||
visibleButtons.append(.avoidFerry)
|
||||
case .dirty:
|
||||
visibleButtons.append(.avoidDirty)
|
||||
default:
|
||||
fatalError()
|
||||
}
|
||||
} else {
|
||||
configButton1()
|
||||
configButton2()
|
||||
configButton3()
|
||||
configButton4()
|
||||
}
|
||||
|
||||
setupButtonsState()
|
||||
}
|
||||
|
||||
private func configButton1() {
|
||||
if let mapNodeAttributes = placePageData.mapNodeAttributes {
|
||||
switch mapNodeAttributes.nodeStatus {
|
||||
case .onDiskOutOfDate, .onDisk, .undefined:
|
||||
break
|
||||
case .downloading, .applying, .inQueue, .error, .notDownloaded, .partly:
|
||||
visibleButtons.append(.download)
|
||||
return
|
||||
@unknown default:
|
||||
fatalError()
|
||||
}
|
||||
}
|
||||
|
||||
var buttons: [ActionBarButtonType] = []
|
||||
switch placePageData.objectType {
|
||||
case .POI, .bookmark, .track:
|
||||
if isRoutePlanning {
|
||||
buttons.append(.routeFrom)
|
||||
}
|
||||
let hasAnyPhones = !(placePageData.infoData?.phones ?? []).isEmpty
|
||||
if hasAnyPhones, AppInfo.shared().canMakeCalls {
|
||||
buttons.append(.call)
|
||||
}
|
||||
if !isRoutePlanning {
|
||||
buttons.append(.routeFrom)
|
||||
}
|
||||
case .trackRecording:
|
||||
break
|
||||
@unknown default:
|
||||
fatalError()
|
||||
}
|
||||
|
||||
guard !buttons.isEmpty else { return }
|
||||
visibleButtons.append(buttons[0])
|
||||
if buttons.count > 1 {
|
||||
additionalButtons.append(contentsOf: buttons.suffix(from: 1))
|
||||
}
|
||||
}
|
||||
|
||||
private func configButton2() {
|
||||
var buttons: [ActionBarButtonType] = []
|
||||
switch placePageData.objectType {
|
||||
case .POI, .bookmark:
|
||||
if canAddStop {
|
||||
buttons.append(.routeAddStop)
|
||||
}
|
||||
buttons.append(.bookmark)
|
||||
case .track:
|
||||
if canAddStop {
|
||||
buttons.append(.routeAddStop)
|
||||
}
|
||||
buttons.append(.track)
|
||||
case .trackRecording:
|
||||
buttons.append(.notSaveTrackRecording)
|
||||
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))
|
||||
}
|
||||
}
|
||||
|
||||
private func configButton3() {
|
||||
switch placePageData.objectType {
|
||||
case .POI, .bookmark, .track:
|
||||
visibleButtons.append(.routeTo)
|
||||
case .trackRecording:
|
||||
break
|
||||
@unknown default:
|
||||
fatalError()
|
||||
}
|
||||
}
|
||||
|
||||
private func configButton4() {
|
||||
guard !additionalButtons.isEmpty else { return }
|
||||
additionalButtons.count == 1 ? visibleButtons.append(additionalButtons[0]) : visibleButtons.append(.more)
|
||||
}
|
||||
|
||||
private func setupButtonsState() {
|
||||
for buttonType in visibleButtons {
|
||||
let (selected, enabled) = buttonState(buttonType)
|
||||
let button = ActionBarButton(delegate: self,
|
||||
buttonType: buttonType,
|
||||
isSelected: selected,
|
||||
isEnabled: enabled)
|
||||
stackView.addArrangedSubview(button)
|
||||
switch buttonType {
|
||||
case .download:
|
||||
downloadButton = button
|
||||
updateDownloadButtonState(placePageData.mapNodeAttributes!.nodeStatus)
|
||||
case .bookmark:
|
||||
bookmarkButton = button
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func buttonState(_ buttonType: ActionBarButtonType) -> (selected: Bool, enabled: Bool) {
|
||||
var selected = false
|
||||
let enabled = true
|
||||
switch buttonType {
|
||||
case .bookmark:
|
||||
selected = placePageData.bookmarkData != nil
|
||||
case .track:
|
||||
selected = placePageData.trackData != nil
|
||||
default:
|
||||
break
|
||||
}
|
||||
return (selected, enabled)
|
||||
}
|
||||
|
||||
private func showMore() {
|
||||
let actionSheet = UIAlertController(title: placePageData.previewData.title,
|
||||
message: placePageData.previewData.subtitle,
|
||||
preferredStyle: .actionSheet)
|
||||
for button in additionalButtons {
|
||||
let (selected, enabled) = buttonState(button)
|
||||
let action = UIAlertAction(title: titleForButton(button, selected),
|
||||
style: .default,
|
||||
handler: { [weak self] _ in
|
||||
guard let self = self else { return }
|
||||
self.delegate?.actionBar(self, didPressButton: button)
|
||||
})
|
||||
action.isEnabled = enabled
|
||||
actionSheet.addAction(action)
|
||||
}
|
||||
actionSheet.addAction(UIAlertAction(title: L("cancel"), style: .cancel))
|
||||
if let popover = actionSheet.popoverPresentationController, let sourceView = stackView.arrangedSubviews.last {
|
||||
popover.sourceView = sourceView
|
||||
popover.sourceRect = sourceView.bounds
|
||||
}
|
||||
present(actionSheet, animated: true)
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Public methods
|
||||
|
||||
func resetButtons() {
|
||||
stackView.arrangedSubviews.forEach {
|
||||
stackView.removeArrangedSubview($0)
|
||||
$0.removeFromSuperview()
|
||||
}
|
||||
visibleButtons.removeAll()
|
||||
additionalButtons.removeAll()
|
||||
downloadButton = nil
|
||||
bookmarkButton = nil
|
||||
configureButtons()
|
||||
}
|
||||
|
||||
func updateDownloadButtonState(_ nodeStatus: MapNodeStatus) {
|
||||
guard let downloadButton = downloadButton, let mapNodeAttributes = placePageData.mapNodeAttributes else { return }
|
||||
switch mapNodeAttributes.nodeStatus {
|
||||
case .downloading:
|
||||
downloadButton.mapDownloadProgress?.state = .progress
|
||||
case .applying, .inQueue:
|
||||
downloadButton.mapDownloadProgress?.state = .spinner
|
||||
case .error:
|
||||
downloadButton.mapDownloadProgress?.state = .failed
|
||||
case .onDisk, .undefined, .onDiskOutOfDate:
|
||||
downloadButton.mapDownloadProgress?.state = .completed
|
||||
case .notDownloaded, .partly:
|
||||
downloadButton.mapDownloadProgress?.state = .normal
|
||||
@unknown default:
|
||||
fatalError()
|
||||
}
|
||||
}
|
||||
|
||||
func updateBookmarkButtonState(isSelected: Bool) {
|
||||
guard let bookmarkButton else { return }
|
||||
if !isSelected && BookmarksManager.shared().hasRecentlyDeletedBookmark() {
|
||||
bookmarkButton.setBookmarkButtonState(.recover)
|
||||
return
|
||||
}
|
||||
bookmarkButton.setBookmarkButtonState(isSelected ? .delete : .save)
|
||||
}
|
||||
}
|
||||
|
||||
extension ActionBarViewController: ActionBarButtonDelegate {
|
||||
func tapOnButton(with type: ActionBarButtonType) {
|
||||
switch type {
|
||||
case .more:
|
||||
showMore()
|
||||
default:
|
||||
delegate?.actionBar(self, didPressButton: type)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
@objc class ElevationDetailsBuilder: NSObject {
|
||||
@objc static func build(data: PlacePageData) -> UIViewController {
|
||||
guard let elevationProfileData = data.trackData?.elevationProfileData else {
|
||||
LOG(.critical, "Elevation profile data should not be nil when building elevation details")
|
||||
fatalError()
|
||||
}
|
||||
let viewController = ElevationDetailsViewController(nibName: toString(ElevationDetailsViewController.self), bundle: nil)
|
||||
let router = ElevationDetailsRouter(viewController: viewController)
|
||||
let presenter = ElevationDetailsPresenter(view: viewController, router: router, data: elevationProfileData)
|
||||
viewController.presenter = presenter
|
||||
return viewController
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
protocol ElevationDetailsPresenterProtocol: AnyObject {
|
||||
func configure()
|
||||
func onOkButtonPressed()
|
||||
}
|
||||
|
||||
class ElevationDetailsPresenter {
|
||||
private weak var view: ElevationDetailsViewProtocol?
|
||||
private let router: ElevationDetailsRouterProtocol
|
||||
private let data: ElevationProfileData
|
||||
|
||||
init(view: ElevationDetailsViewProtocol,
|
||||
router: ElevationDetailsRouterProtocol,
|
||||
data: ElevationProfileData) {
|
||||
self.view = view
|
||||
self.router = router
|
||||
self.data = data
|
||||
}
|
||||
}
|
||||
|
||||
extension ElevationDetailsPresenter: ElevationDetailsPresenterProtocol {
|
||||
func configure() {
|
||||
view?.setDifficulty(data.difficulty)
|
||||
}
|
||||
|
||||
func onOkButtonPressed() {
|
||||
router.close()
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
protocol ElevationDetailsRouterProtocol: AnyObject {
|
||||
func close()
|
||||
}
|
||||
|
||||
class ElevationDetailsRouter {
|
||||
private weak var viewController: UIViewController?
|
||||
|
||||
init(viewController: UIViewController) {
|
||||
self.viewController = viewController
|
||||
}
|
||||
}
|
||||
|
||||
extension ElevationDetailsRouter: ElevationDetailsRouterProtocol {
|
||||
func close() {
|
||||
viewController?.dismiss(animated: true, completion: nil)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,60 @@
|
|||
protocol ElevationDetailsViewProtocol: AnyObject {
|
||||
var presenter: ElevationDetailsPresenterProtocol? { get set }
|
||||
|
||||
func setExtendedDifficultyGrade (_ value: String)
|
||||
func setDifficulty(_ value: ElevationDifficulty)
|
||||
func setDifficultyDescription(_ value: String)
|
||||
}
|
||||
|
||||
class ElevationDetailsViewController: MWMViewController {
|
||||
private let transitioning = FadeTransitioning<AlertPresentationController>()
|
||||
var presenter: ElevationDetailsPresenterProtocol?
|
||||
@IBOutlet var headerTitle: UILabel!
|
||||
@IBOutlet var difficultyView: DifficultyView!
|
||||
@IBOutlet var difficultyLabel: UILabel!
|
||||
@IBOutlet private var extendedDifficultyGradeLabel: UILabel!
|
||||
@IBOutlet var difficultyDescriptionLabel: UILabel!
|
||||
|
||||
override init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?) {
|
||||
super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil)
|
||||
transitioningDelegate = transitioning
|
||||
modalPresentationStyle = .custom
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
presenter?.configure()
|
||||
}
|
||||
|
||||
@IBAction func onOkButtonPressed(_ sender: Any) {
|
||||
presenter?.onOkButtonPressed()
|
||||
}
|
||||
}
|
||||
|
||||
extension ElevationDetailsViewController: ElevationDetailsViewProtocol {
|
||||
func setExtendedDifficultyGrade (_ value: String) {
|
||||
extendedDifficultyGradeLabel.text = value
|
||||
}
|
||||
|
||||
func setDifficulty(_ value: ElevationDifficulty) {
|
||||
difficultyView.difficulty = value
|
||||
switch value {
|
||||
case .easy:
|
||||
difficultyLabel.text = L("elevation_profile_diff_level_easy")
|
||||
case .medium:
|
||||
difficultyLabel.text = L("elevation_profile_diff_level_moderate")
|
||||
case .hard:
|
||||
difficultyLabel.text = L("elevation_profile_diff_level_hard")
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
func setDifficultyDescription(_ value: String) {
|
||||
difficultyDescriptionLabel.text = value
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,126 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="23094" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
|
||||
<device id="retina6_1" orientation="portrait" appearance="light"/>
|
||||
<dependencies>
|
||||
<deployment identifier="iOS"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="23084"/>
|
||||
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
|
||||
<capability name="System colors in document resources" minToolsVersion="11.0"/>
|
||||
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||
</dependencies>
|
||||
<objects>
|
||||
<placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner" customClass="ElevationDetailsViewController">
|
||||
<connections>
|
||||
<outlet property="difficultyDescriptionLabel" destination="7Ed-wc-w74" id="9Bw-DB-bu8"/>
|
||||
<outlet property="difficultyLabel" destination="kDg-1H-1c1" id="azu-cC-qCf"/>
|
||||
<outlet property="difficultyView" destination="sd4-IT-eto" id="eB9-Dr-I51"/>
|
||||
<outlet property="extendedDifficultyGradeLabel" destination="Dke-As-9Jm" id="Tcj-7G-nQ6"/>
|
||||
<outlet property="headerTitle" destination="AUN-AX-DfY" id="taz-UO-a7n"/>
|
||||
<outlet property="view" destination="iN0-l3-epB" id="qok-Te-Q7Q"/>
|
||||
</connections>
|
||||
</placeholder>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="-2" customClass="UIResponder"/>
|
||||
<view contentMode="scaleToFill" id="iN0-l3-epB" customClass="SolidTouchView">
|
||||
<rect key="frame" x="0.0" y="0.0" width="312" height="484"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<subviews>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" ambiguous="YES" text="Difficulty" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="AUN-AX-DfY">
|
||||
<rect key="frame" x="16" y="68" width="280" height="21"/>
|
||||
<fontDescription key="fontDescription" type="system" weight="semibold" pointSize="18"/>
|
||||
<nil key="textColor"/>
|
||||
<nil key="highlightedColor"/>
|
||||
<userDefinedRuntimeAttributes>
|
||||
<userDefinedRuntimeAttribute type="string" keyPath="localizedText" value="elevation_profile_diff_level"/>
|
||||
<userDefinedRuntimeAttribute type="string" keyPath="styleName" value="semibold18:blackPrimaryText"/>
|
||||
</userDefinedRuntimeAttributes>
|
||||
</label>
|
||||
<view contentMode="scaleToFill" ambiguous="YES" translatesAutoresizingMaskIntoConstraints="NO" id="sd4-IT-eto" customClass="DifficultyView" customModule="CoMaps" customModuleProvider="target">
|
||||
<rect key="frame" x="16" y="105" width="40" height="10"/>
|
||||
<color key="backgroundColor" white="0.66666666666666663" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="width" constant="40" id="Nq9-uQ-GCS"/>
|
||||
<constraint firstAttribute="height" constant="10" id="gSS-Bl-tXK"/>
|
||||
</constraints>
|
||||
</view>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" ambiguous="YES" text="Moderate difficulty" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="kDg-1H-1c1">
|
||||
<rect key="frame" x="16" y="125" width="280" height="17"/>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="14"/>
|
||||
<nil key="textColor"/>
|
||||
<nil key="highlightedColor"/>
|
||||
<userDefinedRuntimeAttributes>
|
||||
<userDefinedRuntimeAttribute type="string" keyPath="styleName" value="regular14:blackSecondaryText"/>
|
||||
</userDefinedRuntimeAttributes>
|
||||
</label>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" ambiguous="YES" text="S1" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="Dke-As-9Jm" customClass="InsetsLabel" customModule="CoMaps" customModuleProvider="target">
|
||||
<rect key="frame" x="16" y="158" width="15.5" height="17"/>
|
||||
<color key="backgroundColor" white="0.66666666669999997" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="14"/>
|
||||
<nil key="textColor"/>
|
||||
<nil key="highlightedColor"/>
|
||||
<userDefinedRuntimeAttributes>
|
||||
<userDefinedRuntimeAttribute type="string" keyPath="styleName" value="ElevationProfileExtendedDifficulty"/>
|
||||
</userDefinedRuntimeAttributes>
|
||||
</label>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" ambiguous="YES" lineBreakMode="tailTruncation" numberOfLines="0" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="7Ed-wc-w74">
|
||||
<rect key="frame" x="16" y="183" width="280" height="221"/>
|
||||
<string key="text">Vivamus eu mattis lectus. Phasellus eu ex risus. Quisque ornare augue lectus, eget dignissim turpis ultrices quis. In sit amet sapien laoreet, gravida lorem eget, pharetra ipsum. Morbi ut massa dui. Aenean placerat libero ac ante finibus semper. Nullam semper nibh eget mauris vestibulum, eu cursus nunc finibus. Aliquam fringilla fermentum libero fringilla dictum. Donec eu semper ipsum. Sed in purus neque.</string>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="14"/>
|
||||
<nil key="textColor"/>
|
||||
<nil key="highlightedColor"/>
|
||||
<userDefinedRuntimeAttributes>
|
||||
<userDefinedRuntimeAttribute type="string" keyPath="styleName" value="regular14:blackSecondaryText"/>
|
||||
</userDefinedRuntimeAttributes>
|
||||
</label>
|
||||
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="vDr-Ie-c5L">
|
||||
<rect key="frame" x="16" y="386" width="280" height="48"/>
|
||||
<color key="backgroundColor" systemColor="linkColor"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="height" constant="48" id="DsE-3h-I1o"/>
|
||||
<constraint firstAttribute="width" constant="280" id="JhA-fQ-QGN"/>
|
||||
</constraints>
|
||||
<state key="normal" title="Button">
|
||||
<color key="titleColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
</state>
|
||||
<userDefinedRuntimeAttributes>
|
||||
<userDefinedRuntimeAttribute type="string" keyPath="localizedText" value="ok"/>
|
||||
<userDefinedRuntimeAttribute type="string" keyPath="styleName" value="FlatNormalButton"/>
|
||||
</userDefinedRuntimeAttributes>
|
||||
<connections>
|
||||
<action selector="onOkButtonPressed:" destination="-1" eventType="touchUpInside" id="V2n-kF-R7p"/>
|
||||
</connections>
|
||||
</button>
|
||||
</subviews>
|
||||
<viewLayoutGuide key="safeArea" id="vUN-kp-3ea"/>
|
||||
<color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
<constraints>
|
||||
<constraint firstItem="vDr-Ie-c5L" firstAttribute="leading" secondItem="vUN-kp-3ea" secondAttribute="leading" constant="16" id="03I-DI-Xet"/>
|
||||
<constraint firstItem="sd4-IT-eto" firstAttribute="leading" secondItem="vUN-kp-3ea" secondAttribute="leading" constant="16" id="4Fs-jA-Nr5"/>
|
||||
<constraint firstItem="kDg-1H-1c1" firstAttribute="top" secondItem="sd4-IT-eto" secondAttribute="bottom" constant="10" id="590-Gb-F57"/>
|
||||
<constraint firstItem="AUN-AX-DfY" firstAttribute="top" secondItem="vUN-kp-3ea" secondAttribute="top" constant="24" id="5Df-xg-87e"/>
|
||||
<constraint firstItem="7Ed-wc-w74" firstAttribute="leading" secondItem="vUN-kp-3ea" secondAttribute="leading" constant="16" id="6RJ-WV-Wku"/>
|
||||
<constraint firstItem="vDr-Ie-c5L" firstAttribute="top" secondItem="7Ed-wc-w74" secondAttribute="bottom" constant="16" id="6ma-Oj-SbC"/>
|
||||
<constraint firstItem="vUN-kp-3ea" firstAttribute="bottom" secondItem="vDr-Ie-c5L" secondAttribute="bottom" constant="16" id="93T-wX-CAW"/>
|
||||
<constraint firstItem="vUN-kp-3ea" firstAttribute="trailing" secondItem="vDr-Ie-c5L" secondAttribute="trailing" constant="16" id="KPu-Wp-KPH"/>
|
||||
<constraint firstItem="vUN-kp-3ea" firstAttribute="trailing" secondItem="7Ed-wc-w74" secondAttribute="trailing" constant="16" id="NxM-48-Xf1"/>
|
||||
<constraint firstItem="vUN-kp-3ea" firstAttribute="trailing" secondItem="kDg-1H-1c1" secondAttribute="trailing" constant="16" id="PrX-vn-5lX"/>
|
||||
<constraint firstItem="Dke-As-9Jm" firstAttribute="leading" secondItem="vUN-kp-3ea" secondAttribute="leading" constant="16" id="TA1-c3-FjG"/>
|
||||
<constraint firstItem="Dke-As-9Jm" firstAttribute="top" secondItem="kDg-1H-1c1" secondAttribute="bottom" constant="16" id="fIj-xm-f9G"/>
|
||||
<constraint firstItem="kDg-1H-1c1" firstAttribute="leading" secondItem="vUN-kp-3ea" secondAttribute="leading" constant="16" id="fuT-eE-uMX"/>
|
||||
<constraint firstItem="sd4-IT-eto" firstAttribute="top" secondItem="AUN-AX-DfY" secondAttribute="bottom" constant="16" id="heF-rz-x1k"/>
|
||||
<constraint firstItem="vUN-kp-3ea" firstAttribute="trailing" secondItem="AUN-AX-DfY" secondAttribute="trailing" constant="16" id="kmB-vg-oQY"/>
|
||||
<constraint firstItem="7Ed-wc-w74" firstAttribute="top" secondItem="Dke-As-9Jm" secondAttribute="bottom" constant="8" id="mRe-aS-Zva"/>
|
||||
<constraint firstItem="AUN-AX-DfY" firstAttribute="leading" secondItem="vUN-kp-3ea" secondAttribute="leading" constant="16" id="wm3-Eh-4No"/>
|
||||
</constraints>
|
||||
<freeformSimulatedSizeMetrics key="simulatedDestinationMetrics"/>
|
||||
<userDefinedRuntimeAttributes>
|
||||
<userDefinedRuntimeAttribute type="string" keyPath="styleName" value="Background"/>
|
||||
</userDefinedRuntimeAttributes>
|
||||
<point key="canvasLocation" x="131.8840579710145" y="291.29464285714283"/>
|
||||
</view>
|
||||
</objects>
|
||||
<resources>
|
||||
<systemColor name="linkColor">
|
||||
<color red="0.0" green="0.47843137254901963" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
</systemColor>
|
||||
</resources>
|
||||
</document>
|
||||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,114 @@
|
|||
enum OpenInApplication: Int, CaseIterable {
|
||||
case osm
|
||||
case googleMaps
|
||||
case appleMaps
|
||||
case osmAnd
|
||||
case yandexMaps
|
||||
case dGis
|
||||
case cityMapper
|
||||
case moovit
|
||||
case uber
|
||||
case waze
|
||||
case goMap
|
||||
}
|
||||
|
||||
extension OpenInApplication {
|
||||
static var availableApps: [OpenInApplication] {
|
||||
// OSM should always be first in the list.
|
||||
let sortedApps: [OpenInApplication] = [.osm] + allCases.filter { $0 != .osm }.sorted(by: { $0.name < $1.name })
|
||||
return sortedApps.filter { UIApplication.shared.canOpenURL(URL(string: $0.scheme)!) }
|
||||
}
|
||||
|
||||
var name: String {
|
||||
switch self {
|
||||
case .osm:
|
||||
return "OpenStreetMap"
|
||||
case .googleMaps:
|
||||
return "Google Maps"
|
||||
case .appleMaps:
|
||||
return "Apple Maps"
|
||||
case .osmAnd:
|
||||
return "OsmAnd"
|
||||
case .yandexMaps:
|
||||
return "Yandex Maps"
|
||||
case .dGis:
|
||||
return "2GIS"
|
||||
case .cityMapper:
|
||||
return "Citymapper"
|
||||
case .moovit:
|
||||
return "Moovit"
|
||||
case .uber:
|
||||
return "Uber"
|
||||
case .waze:
|
||||
return "Waze"
|
||||
case .goMap:
|
||||
return "Go Map!!"
|
||||
}
|
||||
}
|
||||
|
||||
// Schemes should be registered in LSApplicationQueriesSchemes - see Info.plist.
|
||||
var scheme: String {
|
||||
switch self {
|
||||
case .osm:
|
||||
return "https://osm.org/go/"
|
||||
case .googleMaps:
|
||||
return "comgooglemaps://"
|
||||
case .appleMaps:
|
||||
return "https://maps.apple.com/"
|
||||
case .osmAnd:
|
||||
return "osmandmaps://"
|
||||
case .yandexMaps:
|
||||
return "yandexmaps://"
|
||||
case .dGis:
|
||||
return "dgis://"
|
||||
case .cityMapper:
|
||||
return "citymapper://"
|
||||
case .moovit:
|
||||
return "moovit://"
|
||||
case .uber:
|
||||
return "uber://"
|
||||
case .waze:
|
||||
return "waze://"
|
||||
case .goMap:
|
||||
return "gomaposm://"
|
||||
}
|
||||
}
|
||||
|
||||
func linkWith(coordinates: CLLocationCoordinate2D, zoomLevel: Int = Int(FrameworkHelper.currentZoomLevel()), destinationName: String? = nil) -> String {
|
||||
let latitude = String(format: "%.6f", coordinates.latitude)
|
||||
let longitude = String(format: "%.6f", coordinates.longitude)
|
||||
switch self {
|
||||
case .osm:
|
||||
return GeoUtil.formattedOsmLink(for: coordinates, zoomLevel: Int32(zoomLevel))
|
||||
case .googleMaps:
|
||||
return "\(scheme)?&q=\(latitude),\(longitude)&z=\(zoomLevel)"
|
||||
case .appleMaps:
|
||||
if let destinationName {
|
||||
return "\(scheme)?q=\(destinationName)&ll=\(latitude),\(longitude)&z=\(zoomLevel)"
|
||||
}
|
||||
return "\(scheme)?ll=\(latitude),\(longitude)&z=\(zoomLevel)"
|
||||
case .osmAnd:
|
||||
if let destinationName {
|
||||
return "\(scheme)?lat=\(latitude)&lon=\(longitude)&z=\(zoomLevel)&title=\(destinationName)"
|
||||
}
|
||||
return "\(scheme)?lat=\(latitude)&lon=\(longitude)&z=\(zoomLevel)"
|
||||
case .yandexMaps:
|
||||
return "\(scheme)maps.yandex.ru/?pt=\(longitude),\(latitude)&z=\(zoomLevel)"
|
||||
case .dGis:
|
||||
return "\(scheme)2gis.ru/geo/\(longitude),\(latitude)"
|
||||
case .cityMapper:
|
||||
return "\(scheme)directions?endcoord=\(latitude),\(longitude)&endname=\(destinationName ?? "")"
|
||||
case .moovit:
|
||||
if let destinationName {
|
||||
return "\(scheme)directions?dest_lat=\(latitude)&dest_lon=\(longitude)&dest_name=\(destinationName)"
|
||||
}
|
||||
return "\(scheme)directions?dest_lat=\(latitude)&dest_lon=\(longitude)"
|
||||
case .uber:
|
||||
return "\(scheme)?client_id=&action=setPickup&pickup=my_location&dropoff[latitude]=\(latitude)&dropoff[longitude]=\(longitude)"
|
||||
case .waze:
|
||||
return "\(scheme)?ll=\(latitude),\(longitude)"
|
||||
case .goMap:
|
||||
return "\(scheme)edit?center=\(latitude),\(longitude)&zoom=\(zoomLevel)"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
typealias OpenInApplicationCompletionHandler = (OpenInApplication) -> Void
|
||||
|
||||
extension UIAlertController {
|
||||
static func presentInAppActionSheet(from sourceView: UIView,
|
||||
apps: [OpenInApplication] = OpenInApplication.availableApps,
|
||||
didSelectApp: @escaping OpenInApplicationCompletionHandler) -> UIAlertController {
|
||||
let alertController = UIAlertController(title: nil, message: nil, preferredStyle: .actionSheet)
|
||||
|
||||
apps.forEach { app in
|
||||
let action = UIAlertAction(title: app.name, style: .default) { _ in
|
||||
didSelectApp(app)
|
||||
}
|
||||
alertController.addAction(action)
|
||||
}
|
||||
|
||||
let cancelAction = UIAlertAction(title: L("cancel"), style: .cancel)
|
||||
alertController.addAction(cancelAction)
|
||||
|
||||
iPadSpecific {
|
||||
alertController.popoverPresentationController?.sourceView = sourceView
|
||||
alertController.popoverPresentationController?.sourceRect = sourceView.bounds
|
||||
}
|
||||
return alertController
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,122 @@
|
|||
import UIKit
|
||||
|
||||
class OpeningHoursTodayViewController: UIViewController {
|
||||
@IBOutlet var todayLabel: UILabel!
|
||||
@IBOutlet var scheduleLabel: UILabel!
|
||||
@IBOutlet var breaksLabel: UILabel!
|
||||
@IBOutlet var closedLabel: UILabel!
|
||||
@IBOutlet var arrowImageView: UIImageView!
|
||||
|
||||
var workingDay: WorkingDay!
|
||||
var closedNow = false
|
||||
var onExpand: MWMVoidBlock?
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
|
||||
todayLabel.text = workingDay.workingDays
|
||||
scheduleLabel.text = workingDay.workingTimes
|
||||
breaksLabel.text = workingDay.breaks
|
||||
closedLabel.isHidden = !closedNow
|
||||
}
|
||||
|
||||
@IBAction func onTap(_ sender: UITapGestureRecognizer) {
|
||||
onExpand?()
|
||||
}
|
||||
}
|
||||
|
||||
class OpeningHoursDayViewController: UIViewController {
|
||||
@IBOutlet var todayLabel: UILabel!
|
||||
@IBOutlet var scheduleLabel: UILabel!
|
||||
@IBOutlet var breaksLabel: UILabel!
|
||||
|
||||
var workingDay: WorkingDay!
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
|
||||
todayLabel.text = workingDay.workingDays
|
||||
scheduleLabel.text = workingDay.workingTimes
|
||||
breaksLabel.text = workingDay.breaks
|
||||
}
|
||||
}
|
||||
|
||||
class OpeningHoursViewController: UIViewController {
|
||||
@IBOutlet var stackView: UIStackView!
|
||||
@IBOutlet var checkDateLabel: UILabel!
|
||||
@IBOutlet var checkDateLabelTopLayoutConstraint: NSLayoutConstraint!
|
||||
@IBOutlet var checkDateLabelBottomLayoutConstraint: NSLayoutConstraint!
|
||||
|
||||
private var otherDaysViews: [OpeningHoursDayViewController] = []
|
||||
|
||||
private lazy var todayView: OpeningHoursTodayViewController = {
|
||||
let vc = storyboard!.instantiateViewController(ofType: OpeningHoursTodayViewController.self)
|
||||
vc.workingDay = openingHours.days[0]
|
||||
vc.closedNow = openingHours.isClosedNow
|
||||
return vc
|
||||
}()
|
||||
|
||||
private var expanded = false
|
||||
|
||||
var openingHours: OpeningHours!
|
||||
var openingHoursCheckDate: Date?
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
|
||||
addToStack(todayView)
|
||||
|
||||
if openingHours.days.count == 1 {
|
||||
todayView.arrowImageView.isHidden = true
|
||||
return
|
||||
}
|
||||
|
||||
openingHours.days.suffix(from: 1).forEach {
|
||||
let vc = createDayItem($0)
|
||||
otherDaysViews.append(vc)
|
||||
addToStack(vc)
|
||||
}
|
||||
|
||||
todayView.onExpand = { [unowned self] in
|
||||
self.expanded = !self.expanded
|
||||
UIView.animate(withDuration: kDefaultAnimationDuration) {
|
||||
self.otherDaysViews.forEach { vc in
|
||||
vc.view.isHidden = !self.expanded
|
||||
}
|
||||
|
||||
if let checkDate = self.openingHoursCheckDate, self.expanded {
|
||||
let checkDateFormatter = RelativeDateTimeFormatter()
|
||||
checkDateFormatter.unitsStyle = .spellOut
|
||||
checkDateFormatter.localizedString(for: checkDate, relativeTo: Date.now)
|
||||
self.checkDateLabel.text = String(format: L("hours_confirmed_time_ago"), checkDateFormatter.localizedString(for: checkDate, relativeTo: Date.now))
|
||||
|
||||
NSLayoutConstraint.activate([self.checkDateLabelTopLayoutConstraint])
|
||||
NSLayoutConstraint.activate([self.checkDateLabelBottomLayoutConstraint])
|
||||
} else {
|
||||
self.checkDateLabel.text = String()
|
||||
|
||||
NSLayoutConstraint.deactivate([self.checkDateLabelTopLayoutConstraint])
|
||||
NSLayoutConstraint.deactivate([self.checkDateLabelBottomLayoutConstraint])
|
||||
}
|
||||
self.checkDateLabel.isHidden = !self.expanded
|
||||
|
||||
self.todayView.arrowImageView.transform = self.expanded ? CGAffineTransform(rotationAngle: -CGFloat.pi + 0.01)
|
||||
: CGAffineTransform.identity
|
||||
self.view.layoutIfNeeded()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func createDayItem(_ workingDay: WorkingDay) -> OpeningHoursDayViewController {
|
||||
let vc = storyboard!.instantiateViewController(ofType: OpeningHoursDayViewController.self)
|
||||
vc.workingDay = workingDay
|
||||
vc.view.isHidden = true
|
||||
return vc
|
||||
}
|
||||
|
||||
private func addToStack(_ viewController: UIViewController) {
|
||||
addChild(viewController)
|
||||
stackView.addArrangedSubview(viewController.view)
|
||||
viewController.didMove(toParent: self)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,42 @@
|
|||
protocol PlacePageButtonsViewControllerDelegate: AnyObject {
|
||||
func didPressAddPlace()
|
||||
func didPressEditPlace()
|
||||
}
|
||||
|
||||
class PlacePageButtonsViewController: UIViewController {
|
||||
@IBOutlet var addPlaceButton: UIButton!
|
||||
@IBOutlet var editPlaceButton: UIButton!
|
||||
|
||||
private var buttons: [UIButton?] {
|
||||
[addPlaceButton, editPlaceButton]
|
||||
}
|
||||
|
||||
var buttonsData: PlacePageButtonsData!
|
||||
var buttonsEnabled = true {
|
||||
didSet {
|
||||
buttons.forEach {
|
||||
$0?.isEnabled = buttonsEnabled
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
weak var delegate: PlacePageButtonsViewControllerDelegate?
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
|
||||
addPlaceButton.isHidden = !buttonsData.showAddPlace
|
||||
editPlaceButton.isHidden = !buttonsData.showEditPlace
|
||||
|
||||
addPlaceButton.isEnabled = buttonsData.enableAddPlace
|
||||
editPlaceButton.isEnabled = buttonsData.enableEditPlace
|
||||
}
|
||||
|
||||
@IBAction func onAddPlace(_ sender: UIButton) {
|
||||
delegate?.didPressAddPlace()
|
||||
}
|
||||
|
||||
@IBAction func onEditPlace(_ sender: UIButton) {
|
||||
delegate?.didPressEditPlace()
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
final class PlacePageDescriptionViewController: WebViewController {
|
||||
override func configuredHtml(withText htmlText: String) -> String {
|
||||
let scale = UIScreen.main.scale
|
||||
let styleTags = """
|
||||
<head>
|
||||
<style type=\"text/css\">
|
||||
body{font-family: -apple-system, BlinkMacSystemFont, "Helvetica Neue", Helvetica, Arial, sans-serif, "Apple Color Emoji"; font-size:\(14 * scale); line-height:1.5em; color: \(UIColor.blackPrimaryText().hexString); padding: 24px 16px;}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
"""
|
||||
var html = htmlText.replacingOccurrences(of: "<body>", with: styleTags)
|
||||
html = html.replacingOccurrences(of: "</body>", with: "<p><b>wikipedia.org</b></p></body>")
|
||||
return html
|
||||
}
|
||||
|
||||
override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
|
||||
super.traitCollectionDidChange(previousTraitCollection)
|
||||
if previousTraitCollection?.userInterfaceStyle != traitCollection.userInterfaceStyle {
|
||||
guard let m_htmlText else { return }
|
||||
webView.loadHTMLString(configuredHtml(withText: m_htmlText), baseURL: nil)
|
||||
}
|
||||
}
|
||||
|
||||
private func isOnBottom(_ scrollView: UIScrollView) -> Bool {
|
||||
let bottom = scrollView.contentSize.height + scrollView.contentInset.bottom - scrollView.bounds.height
|
||||
return scrollView.contentOffset.y >= bottom
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,186 @@
|
|||
protocol PlacePageEditBookmarkOrTrackViewControllerDelegate: AnyObject {
|
||||
func didUpdate(color: UIColor, category: MWMMarkGroupID, for data: PlacePageEditData)
|
||||
func didPressEdit(_ data: PlacePageEditData)
|
||||
}
|
||||
|
||||
enum PlacePageEditData {
|
||||
case bookmark(PlacePageBookmarkData)
|
||||
case track(PlacePageTrackData)
|
||||
}
|
||||
|
||||
final class PlacePageEditBookmarkOrTrackViewController: UIViewController {
|
||||
|
||||
@IBOutlet var stackView: UIStackView!
|
||||
@IBOutlet var editView: InfoItemView!
|
||||
@IBOutlet var expandableLabelContainer: UIView!
|
||||
@IBOutlet var expandableLabel: ExpandableLabel! {
|
||||
didSet {
|
||||
updateExpandableLabelStyle()
|
||||
}
|
||||
}
|
||||
|
||||
var data: PlacePageEditData? {
|
||||
didSet {
|
||||
updateViews()
|
||||
}
|
||||
}
|
||||
|
||||
weak var delegate: PlacePageEditBookmarkOrTrackViewControllerDelegate?
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
updateViews()
|
||||
}
|
||||
|
||||
override func applyTheme() {
|
||||
super.applyTheme()
|
||||
updateViews()
|
||||
}
|
||||
|
||||
override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
|
||||
super.traitCollectionDidChange(previousTraitCollection)
|
||||
if traitCollection.hasDifferentColorAppearance(comparedTo: previousTraitCollection) {
|
||||
applyTheme()
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Private methods
|
||||
|
||||
private func updateViews() {
|
||||
guard let data else { return }
|
||||
|
||||
let iconColor: UIColor
|
||||
let category: String?
|
||||
let description: String?
|
||||
let isHtmlDescription: Bool
|
||||
|
||||
switch data {
|
||||
case .bookmark(let bookmarkData):
|
||||
iconColor = bookmarkData.color.color
|
||||
category = bookmarkData.bookmarkCategory
|
||||
description = bookmarkData.bookmarkDescription
|
||||
isHtmlDescription = bookmarkData.isHtmlDescription
|
||||
case .track(let trackData):
|
||||
iconColor = trackData.color ?? UIColor.buttonRed()
|
||||
category = trackData.trackCategory
|
||||
description = trackData.trackDescription
|
||||
isHtmlDescription = false
|
||||
}
|
||||
|
||||
let editColorImage = circleImageForColor(iconColor, frameSize: 28, diameter: 22, iconName: "ic_bm_none")
|
||||
editView.iconButton.setImage(editColorImage, for: .normal)
|
||||
editView.infoLabel.text = category
|
||||
editView.setStyle(.link)
|
||||
|
||||
editView.iconButtonTapHandler = { [weak self] in
|
||||
guard let self else { return }
|
||||
self.showColorPicker()
|
||||
}
|
||||
editView.infoLabelTapHandler = { [weak self] in
|
||||
guard let self else { return }
|
||||
self.showGroupPicker()
|
||||
}
|
||||
editView.setAccessory(image: UIImage(resource: .ic24PxEdit), tapHandler: { [weak self] in
|
||||
guard let self, let data = self.data else { return }
|
||||
self.delegate?.didPressEdit(data)
|
||||
})
|
||||
|
||||
if let description, !description.isEmpty {
|
||||
expandableLabelContainer.isHidden = false
|
||||
if isHtmlDescription {
|
||||
setHtmlDescription(description)
|
||||
} else {
|
||||
expandableLabel.text = description
|
||||
}
|
||||
updateExpandableLabelStyle()
|
||||
} else {
|
||||
expandableLabelContainer.isHidden = true
|
||||
}
|
||||
}
|
||||
|
||||
private func updateExpandableLabelStyle() {
|
||||
expandableLabel.font = UIFont.regular14()
|
||||
expandableLabel.textColor = UIColor.blackPrimaryText()
|
||||
expandableLabel.numberOfLines = 5
|
||||
expandableLabel.expandColor = UIColor.linkBlue()
|
||||
expandableLabel.expandText = L("placepage_more_button")
|
||||
}
|
||||
|
||||
private func showColorPicker() {
|
||||
guard let data else { return }
|
||||
switch data {
|
||||
case .bookmark(let bookmarkData):
|
||||
ColorPicker.shared.present(from: self, pickerType: .bookmarkColorPicker(bookmarkData.color)) { [weak self] color in
|
||||
self?.update(color: color)
|
||||
}
|
||||
case .track(let trackData):
|
||||
ColorPicker.shared.present(from: self, pickerType: .defaultColorPicker(trackData.color ?? .buttonRed())) { [weak self] color in
|
||||
self?.update(color: color)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func showGroupPicker() {
|
||||
guard let data else { return }
|
||||
let groupId: MWMMarkGroupID
|
||||
let groupName: String?
|
||||
switch data {
|
||||
case .bookmark(let bookmarkData):
|
||||
groupId = bookmarkData.bookmarkGroupId
|
||||
groupName = bookmarkData.bookmarkCategory
|
||||
case .track(let trackData):
|
||||
groupId = trackData.groupId
|
||||
groupName = trackData.trackCategory
|
||||
}
|
||||
let groupViewController = SelectBookmarkGroupViewController(groupName: groupName ?? "", groupId: groupId)
|
||||
let navigationController = UINavigationController(rootViewController: groupViewController)
|
||||
groupViewController.delegate = self
|
||||
present(navigationController, animated: true, completion: nil)
|
||||
}
|
||||
|
||||
private func update(color: UIColor? = nil, category: MWMMarkGroupID? = nil) {
|
||||
guard let data else { return }
|
||||
switch data {
|
||||
case .bookmark(let bookmarkData):
|
||||
delegate?.didUpdate(color: color ?? bookmarkData.color.color, category: category ?? bookmarkData.bookmarkGroupId, for: data)
|
||||
case .track(let trackData):
|
||||
delegate?.didUpdate(color: color ?? trackData.color!, category: category ?? trackData.groupId, for: data)
|
||||
}
|
||||
}
|
||||
|
||||
private func setHtmlDescription(_ htmlDescription: String) {
|
||||
DispatchQueue.global().async {
|
||||
let font = UIFont.regular14()
|
||||
let color = UIColor.blackPrimaryText()
|
||||
let paragraphStyle = NSMutableParagraphStyle()
|
||||
paragraphStyle.lineSpacing = 4
|
||||
|
||||
let attributedString: NSAttributedString
|
||||
if let str = NSMutableAttributedString(htmlString: htmlDescription, baseFont: font, paragraphStyle: paragraphStyle) {
|
||||
str.addAttribute(NSAttributedString.Key.foregroundColor,
|
||||
value: color,
|
||||
range: NSRange(location: 0, length: str.length))
|
||||
attributedString = str;
|
||||
} else {
|
||||
attributedString = NSAttributedString(string: htmlDescription,
|
||||
attributes: [NSAttributedString.Key.font : font,
|
||||
NSAttributedString.Key.foregroundColor: color,
|
||||
NSAttributedString.Key.paragraphStyle: paragraphStyle])
|
||||
}
|
||||
|
||||
DispatchQueue.main.async {
|
||||
self.expandableLabel.attributedText = attributedString
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - SelectBookmarkGroupViewControllerDelegate
|
||||
extension PlacePageEditBookmarkOrTrackViewController: SelectBookmarkGroupViewControllerDelegate {
|
||||
func bookmarkGroupViewController(_ viewController: SelectBookmarkGroupViewController,
|
||||
didSelect groupTitle: String,
|
||||
groupId: MWMMarkGroupID) {
|
||||
viewController.dismiss(animated: true)
|
||||
update(category: groupId)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,59 @@
|
|||
final class CircleImageButton: UIButton {
|
||||
|
||||
private static let expandedTappableAreaInsets = UIEdgeInsets(top: -5, left: -5, bottom: -5, right: -5)
|
||||
|
||||
private let circleImageView = UIImageView()
|
||||
|
||||
override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
setupView()
|
||||
}
|
||||
|
||||
@available(*, unavailable)
|
||||
required init?(coder: NSCoder) {
|
||||
super.init(coder: coder)
|
||||
setupView()
|
||||
}
|
||||
|
||||
override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
|
||||
super.traitCollectionDidChange(previousTraitCollection)
|
||||
if traitCollection.hasDifferentColorAppearance(comparedTo: previousTraitCollection) {
|
||||
circleImageView.applyTheme()
|
||||
}
|
||||
}
|
||||
|
||||
private func setupView() {
|
||||
backgroundColor = .clear
|
||||
circleImageView.setStyle(.ppHeaderCircleIcon)
|
||||
circleImageView.contentMode = .scaleAspectFill
|
||||
circleImageView.clipsToBounds = true
|
||||
circleImageView.isUserInteractionEnabled = false
|
||||
circleImageView.layer.masksToBounds = true
|
||||
circleImageView.translatesAutoresizingMaskIntoConstraints = false
|
||||
addSubview(circleImageView)
|
||||
|
||||
let aspectRatioConstraint = circleImageView.widthAnchor.constraint(equalTo: circleImageView.heightAnchor)
|
||||
aspectRatioConstraint.priority = .defaultHigh
|
||||
NSLayoutConstraint.activate([
|
||||
circleImageView.centerXAnchor.constraint(equalTo: centerXAnchor),
|
||||
circleImageView.centerYAnchor.constraint(equalTo: centerYAnchor),
|
||||
circleImageView.widthAnchor.constraint(lessThanOrEqualTo: widthAnchor),
|
||||
circleImageView.heightAnchor.constraint(lessThanOrEqualTo: heightAnchor),
|
||||
aspectRatioConstraint
|
||||
])
|
||||
}
|
||||
|
||||
override func layoutSubviews() {
|
||||
super.layoutSubviews()
|
||||
circleImageView.layer.cornerRadius = circleImageView.bounds.width / 2.0
|
||||
}
|
||||
|
||||
override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
|
||||
let expandedBounds = bounds.inset(by: Self.expandedTappableAreaInsets)
|
||||
return expandedBounds.contains(point)
|
||||
}
|
||||
|
||||
func setImage(_ image: UIImage?) {
|
||||
circleImageView.image = image
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
class PlacePageHeaderBuilder {
|
||||
static func build(data: PlacePageData,
|
||||
delegate: PlacePageHeaderViewControllerDelegate?,
|
||||
headerType: PlacePageHeaderPresenter.HeaderType) -> PlacePageHeaderViewController {
|
||||
let storyboard = UIStoryboard.instance(.placePage)
|
||||
let viewController = storyboard.instantiateViewController(ofType: PlacePageHeaderViewController.self);
|
||||
let presenter = PlacePageHeaderPresenter(view: viewController,
|
||||
placePagePreviewData: data.previewData,
|
||||
objectType: data.objectType,
|
||||
delegate: delegate,
|
||||
headerType: headerType)
|
||||
|
||||
viewController.presenter = presenter
|
||||
|
||||
return viewController
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,71 @@
|
|||
protocol PlacePageHeaderPresenterProtocol: AnyObject {
|
||||
var objectType: PlacePageObjectType { get }
|
||||
|
||||
func configure()
|
||||
func onClosePress()
|
||||
func onExpandPress()
|
||||
func onShareButtonPress(from sourceView: UIView)
|
||||
func onExportTrackButtonPress(_ type: KmlFileType, from sourceView: UIView)
|
||||
}
|
||||
|
||||
protocol PlacePageHeaderViewControllerDelegate: AnyObject {
|
||||
func previewDidPressClose()
|
||||
func previewDidPressExpand()
|
||||
func previewDidPressShare(from sourceView: UIView)
|
||||
func previewDidPressExportTrack(_ type: KmlFileType, from sourceView: UIView)
|
||||
}
|
||||
|
||||
class PlacePageHeaderPresenter {
|
||||
enum HeaderType {
|
||||
case flexible
|
||||
case fixed
|
||||
}
|
||||
|
||||
private weak var view: PlacePageHeaderViewProtocol?
|
||||
private let placePagePreviewData: PlacePagePreviewData
|
||||
let objectType: PlacePageObjectType
|
||||
private weak var delegate: PlacePageHeaderViewControllerDelegate?
|
||||
private let headerType: HeaderType
|
||||
|
||||
init(view: PlacePageHeaderViewProtocol,
|
||||
placePagePreviewData: PlacePagePreviewData,
|
||||
objectType: PlacePageObjectType,
|
||||
delegate: PlacePageHeaderViewControllerDelegate?,
|
||||
headerType: HeaderType) {
|
||||
self.view = view
|
||||
self.delegate = delegate
|
||||
self.placePagePreviewData = placePagePreviewData
|
||||
self.objectType = objectType
|
||||
self.headerType = headerType
|
||||
}
|
||||
}
|
||||
|
||||
extension PlacePageHeaderPresenter: PlacePageHeaderPresenterProtocol {
|
||||
func configure() {
|
||||
view?.setTitle(placePagePreviewData.title, secondaryTitle: placePagePreviewData.secondaryTitle, branch: placePagePreviewData.branch)
|
||||
switch headerType {
|
||||
case .flexible:
|
||||
view?.isExpandViewHidden = false
|
||||
view?.isShadowViewHidden = true
|
||||
case .fixed:
|
||||
view?.isExpandViewHidden = true
|
||||
view?.isShadowViewHidden = false
|
||||
}
|
||||
}
|
||||
|
||||
func onClosePress() {
|
||||
delegate?.previewDidPressClose()
|
||||
}
|
||||
|
||||
func onExpandPress() {
|
||||
delegate?.previewDidPressExpand()
|
||||
}
|
||||
|
||||
func onShareButtonPress(from sourceView: UIView) {
|
||||
delegate?.previewDidPressShare(from: sourceView)
|
||||
}
|
||||
|
||||
func onExportTrackButtonPress(_ type: KmlFileType, from sourceView: UIView) {
|
||||
delegate?.previewDidPressExportTrack(type, from: sourceView)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
class PlacePageHeaderView: UIView {
|
||||
override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
|
||||
for subview in subviews {
|
||||
if !subview.isHidden && subview.isUserInteractionEnabled && subview.point(inside: convert(point, to: subview), with: event) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,157 @@
|
|||
protocol PlacePageHeaderViewProtocol: AnyObject {
|
||||
var presenter: PlacePageHeaderPresenterProtocol? { get set }
|
||||
var isExpandViewHidden: Bool { get set }
|
||||
var isShadowViewHidden: Bool { get set }
|
||||
|
||||
func setTitle(_ title: String?, secondaryTitle: String?, branch: String?)
|
||||
func showShareTrackMenu()
|
||||
}
|
||||
|
||||
class PlacePageHeaderViewController: UIViewController {
|
||||
var presenter: PlacePageHeaderPresenterProtocol?
|
||||
|
||||
@IBOutlet private var headerView: PlacePageHeaderView!
|
||||
@IBOutlet private var titleLabel: UILabel?
|
||||
@IBOutlet private var expandView: UIView!
|
||||
@IBOutlet private var shadowView: UIView!
|
||||
@IBOutlet private var grabberView: UIView!
|
||||
|
||||
@IBOutlet weak var closeButton: CircleImageButton!
|
||||
@IBOutlet weak var shareButton: CircleImageButton!
|
||||
|
||||
private var titleText: String?
|
||||
private var secondaryText: String?
|
||||
private var branchText: String?
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
presenter?.configure()
|
||||
let tap = UITapGestureRecognizer(target: self, action: #selector(onExpandPressed(sender:)))
|
||||
expandView.addGestureRecognizer(tap)
|
||||
headerView.layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner]
|
||||
iPadSpecific { [weak self] in
|
||||
self?.grabberView.isHidden = true
|
||||
}
|
||||
closeButton.setImage(UIImage(named: "ic_close")!)
|
||||
shareButton.setImage(UIImage(named: "ic_share")!)
|
||||
|
||||
if presenter?.objectType == .track {
|
||||
configureTrackSharingMenu()
|
||||
}
|
||||
|
||||
let interaction = UIContextMenuInteraction(delegate: self)
|
||||
titleLabel?.addInteraction(interaction)
|
||||
}
|
||||
|
||||
@objc func onExpandPressed(sender: UITapGestureRecognizer) {
|
||||
presenter?.onExpandPress()
|
||||
}
|
||||
|
||||
@IBAction private func onCloseButtonPressed(_ sender: Any) {
|
||||
presenter?.onClosePress()
|
||||
}
|
||||
|
||||
@IBAction func onShareButtonPressed(_ sender: Any) {
|
||||
presenter?.onShareButtonPress(from: shareButton)
|
||||
}
|
||||
|
||||
override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
|
||||
super.traitCollectionDidChange(previousTraitCollection)
|
||||
guard traitCollection.userInterfaceStyle != previousTraitCollection?.userInterfaceStyle else { return }
|
||||
setTitle(titleText, secondaryTitle: secondaryText, branch: branchText)
|
||||
}
|
||||
}
|
||||
|
||||
extension PlacePageHeaderViewController: PlacePageHeaderViewProtocol {
|
||||
var isExpandViewHidden: Bool {
|
||||
get {
|
||||
expandView.isHidden
|
||||
}
|
||||
set {
|
||||
expandView.isHidden = newValue
|
||||
}
|
||||
}
|
||||
|
||||
var isShadowViewHidden: Bool {
|
||||
get {
|
||||
shadowView.isHidden
|
||||
}
|
||||
set {
|
||||
shadowView.isHidden = newValue
|
||||
}
|
||||
}
|
||||
|
||||
func setTitle(_ title: String?, secondaryTitle: String?, branch: String? = nil) {
|
||||
titleText = title
|
||||
secondaryText = secondaryTitle
|
||||
branchText = branch
|
||||
|
||||
// XCode 13 is not smart enough to detect that title is used below, and requires explicit unwrapped variable.
|
||||
guard let unwrappedTitle = title else {
|
||||
titleLabel?.attributedText = nil
|
||||
return
|
||||
}
|
||||
|
||||
let titleAttributes: [NSAttributedString.Key: Any] = [
|
||||
.font: StyleManager.shared.theme!.fonts.semibold20,
|
||||
.foregroundColor: UIColor.blackPrimaryText()
|
||||
]
|
||||
|
||||
let attributedText = NSMutableAttributedString(string: unwrappedTitle, attributes: titleAttributes)
|
||||
|
||||
// Add branch with thinner font weight if present and not already in title
|
||||
if let branch = branch, !branch.isEmpty, !unwrappedTitle.contains(branch) {
|
||||
let branchAttributes: [NSAttributedString.Key: Any] = [
|
||||
.font: StyleManager.shared.theme!.fonts.regular20,
|
||||
.foregroundColor: UIColor.blackPrimaryText()
|
||||
]
|
||||
attributedText.append(NSAttributedString(string: " \(branch)", attributes: branchAttributes))
|
||||
}
|
||||
|
||||
guard let unwrappedSecondaryTitle = secondaryTitle else {
|
||||
titleLabel?.attributedText = attributedText
|
||||
return
|
||||
}
|
||||
|
||||
let paragraphStyle = NSMutableParagraphStyle()
|
||||
paragraphStyle.paragraphSpacingBefore = 2
|
||||
let secondaryTitleAttributes: [NSAttributedString.Key: Any] = [
|
||||
.font: StyleManager.shared.theme!.fonts.medium16,
|
||||
.foregroundColor: UIColor.blackPrimaryText(),
|
||||
.paragraphStyle: paragraphStyle
|
||||
]
|
||||
|
||||
attributedText.append(NSAttributedString(string: "\n" + unwrappedSecondaryTitle, attributes: secondaryTitleAttributes))
|
||||
titleLabel?.attributedText = attributedText
|
||||
}
|
||||
|
||||
func showShareTrackMenu() {
|
||||
// The menu will be shown by the shareButton itself
|
||||
}
|
||||
|
||||
private func configureTrackSharingMenu() {
|
||||
let menu = UIMenu(title: "", image: nil, children: [
|
||||
UIAction(title: L("export_file"), image: nil, handler: { [weak self] _ in
|
||||
guard let self else { return }
|
||||
self.presenter?.onExportTrackButtonPress(.text, from: self.shareButton)
|
||||
}),
|
||||
UIAction(title: L("export_file_gpx"), image: nil, handler: { [weak self] _ in
|
||||
guard let self else { return }
|
||||
self.presenter?.onExportTrackButtonPress(.gpx, from: self.shareButton)
|
||||
}),
|
||||
])
|
||||
shareButton.menu = menu
|
||||
shareButton.showsMenuAsPrimaryAction = true
|
||||
}
|
||||
}
|
||||
|
||||
extension PlacePageHeaderViewController: UIContextMenuInteractionDelegate {
|
||||
func contextMenuInteraction(_ interaction: UIContextMenuInteraction, configurationForMenuAtLocation location: CGPoint) -> UIContextMenuConfiguration? {
|
||||
return UIContextMenuConfiguration(identifier: nil, previewProvider: nil, actionProvider: { suggestedActions in
|
||||
let copyAction = UIAction(title: L("copy_to_clipboard"), image: UIImage(systemName: "document.on.clipboard")) { action in
|
||||
UIPasteboard.general.string = self.titleLabel?.text
|
||||
}
|
||||
return UIMenu(title: "", children: [copyAction])
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,568 @@
|
|||
final class InfoItemView: UIView {
|
||||
private enum Constants {
|
||||
static let viewHeight: CGFloat = 44
|
||||
static let stackViewSpacing: CGFloat = 0
|
||||
static let iconButtonSize: CGFloat = 56
|
||||
static let iconButtonEdgeInsets = UIEdgeInsets(top: 8, left: 0, bottom: 8, right: 0)
|
||||
static let infoLabelFontSize: CGFloat = 16
|
||||
static let infoLabelTopBottomSpacing: CGFloat = 10
|
||||
static let accessoryButtonSize: CGFloat = 44
|
||||
}
|
||||
|
||||
enum Style {
|
||||
case regular
|
||||
case link
|
||||
}
|
||||
|
||||
typealias TapHandler = () -> Void
|
||||
|
||||
let iconButton = UIButton()
|
||||
let infoLabel = UILabel()
|
||||
let accessoryButton = UIButton()
|
||||
|
||||
var infoLabelTapHandler: TapHandler?
|
||||
var infoLabelLongPressHandler: TapHandler?
|
||||
var iconButtonTapHandler: TapHandler?
|
||||
var accessoryImageTapHandler: TapHandler?
|
||||
|
||||
private var style: Style = .regular
|
||||
|
||||
override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
setupView()
|
||||
layout()
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
super.init(coder: coder)
|
||||
setupView()
|
||||
layout()
|
||||
}
|
||||
|
||||
private func setupView() {
|
||||
addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(onInfoLabelTap)))
|
||||
addGestureRecognizer(UILongPressGestureRecognizer(target: self, action: #selector(onInfoLabelLongPress(_:))))
|
||||
|
||||
infoLabel.lineBreakMode = .byTruncatingTail
|
||||
infoLabel.numberOfLines = 1
|
||||
infoLabel.allowsDefaultTighteningForTruncation = true
|
||||
infoLabel.isUserInteractionEnabled = false
|
||||
|
||||
iconButton.imageView?.contentMode = .scaleAspectFit
|
||||
iconButton.addTarget(self, action: #selector(onIconButtonTap), for: .touchUpInside)
|
||||
iconButton.contentEdgeInsets = Constants.iconButtonEdgeInsets
|
||||
|
||||
accessoryButton.addTarget(self, action: #selector(onAccessoryButtonTap), for: .touchUpInside)
|
||||
}
|
||||
|
||||
private func layout() {
|
||||
addSubview(iconButton)
|
||||
addSubview(infoLabel)
|
||||
addSubview(accessoryButton)
|
||||
|
||||
translatesAutoresizingMaskIntoConstraints = false
|
||||
iconButton.translatesAutoresizingMaskIntoConstraints = false
|
||||
infoLabel.translatesAutoresizingMaskIntoConstraints = false
|
||||
accessoryButton.translatesAutoresizingMaskIntoConstraints = false
|
||||
|
||||
NSLayoutConstraint.activate([
|
||||
heightAnchor.constraint(equalToConstant: Constants.viewHeight),
|
||||
|
||||
iconButton.leadingAnchor.constraint(equalTo: leadingAnchor),
|
||||
iconButton.centerYAnchor.constraint(equalTo: centerYAnchor),
|
||||
iconButton.widthAnchor.constraint(equalToConstant: Constants.iconButtonSize),
|
||||
iconButton.topAnchor.constraint(equalTo: topAnchor),
|
||||
iconButton.bottomAnchor.constraint(equalTo: bottomAnchor),
|
||||
|
||||
infoLabel.leadingAnchor.constraint(equalTo: iconButton.trailingAnchor),
|
||||
infoLabel.topAnchor.constraint(equalTo: topAnchor),
|
||||
infoLabel.bottomAnchor.constraint(equalTo: bottomAnchor),
|
||||
infoLabel.trailingAnchor.constraint(equalTo: accessoryButton.leadingAnchor),
|
||||
|
||||
accessoryButton.trailingAnchor.constraint(equalTo: trailingAnchor),
|
||||
accessoryButton.centerYAnchor.constraint(equalTo: centerYAnchor),
|
||||
accessoryButton.widthAnchor.constraint(equalToConstant: Constants.accessoryButtonSize),
|
||||
accessoryButton.topAnchor.constraint(equalTo: topAnchor),
|
||||
accessoryButton.bottomAnchor.constraint(equalTo: bottomAnchor)
|
||||
])
|
||||
}
|
||||
|
||||
@objc
|
||||
private func onInfoLabelTap() {
|
||||
infoLabelTapHandler?()
|
||||
}
|
||||
|
||||
@objc
|
||||
private func onInfoLabelLongPress(_ sender: UILongPressGestureRecognizer) {
|
||||
guard sender.state == .began else { return }
|
||||
infoLabelLongPressHandler?()
|
||||
}
|
||||
|
||||
@objc
|
||||
private func onIconButtonTap() {
|
||||
iconButtonTapHandler?()
|
||||
}
|
||||
|
||||
@objc
|
||||
private func onAccessoryButtonTap() {
|
||||
accessoryImageTapHandler?()
|
||||
}
|
||||
|
||||
func setStyle(_ style: Style) {
|
||||
switch style {
|
||||
case .regular:
|
||||
iconButton.setStyleAndApply(.black)
|
||||
infoLabel.setFontStyleAndApply(.regular16, color: .blackPrimary)
|
||||
case .link:
|
||||
iconButton.setStyleAndApply(.blue)
|
||||
infoLabel.setFontStyleAndApply(.regular16, color: .linkBlue)
|
||||
}
|
||||
accessoryButton.setStyleAndApply(.black)
|
||||
self.style = style
|
||||
}
|
||||
|
||||
func setAccessory(image: UIImage?, tapHandler: TapHandler? = nil) {
|
||||
accessoryButton.setTitle("", for: .normal)
|
||||
accessoryButton.setImage(image, for: .normal)
|
||||
accessoryButton.isHidden = image == nil
|
||||
accessoryImageTapHandler = tapHandler
|
||||
}
|
||||
}
|
||||
|
||||
protocol PlacePageInfoViewControllerDelegate: AnyObject {
|
||||
var shouldShowOpenInApp: Bool { get }
|
||||
|
||||
func didPressCall(to phone: PlacePagePhone)
|
||||
func didPressWebsite()
|
||||
func didPressWebsiteMenu()
|
||||
func didPressWikipedia()
|
||||
func didPressWikimediaCommons()
|
||||
func didPressFediverse()
|
||||
func didPressFacebook()
|
||||
func didPressInstagram()
|
||||
func didPressTwitter()
|
||||
func didPressVk()
|
||||
func didPressLine()
|
||||
func didPressBluesky()
|
||||
func didPressPanoramax()
|
||||
func didPressEmail()
|
||||
func didPressOpenInApp(from sourceView: UIView)
|
||||
func didCopy(_ content: String)
|
||||
}
|
||||
|
||||
class PlacePageInfoViewController: UIViewController {
|
||||
private struct Constants {
|
||||
static let coordFormatIdKey = "PlacePageInfoViewController_coordFormatIdKey"
|
||||
}
|
||||
|
||||
private typealias TapHandler = InfoItemView.TapHandler
|
||||
private typealias Style = InfoItemView.Style
|
||||
|
||||
@IBOutlet var stackView: UIStackView!
|
||||
@IBOutlet var checkDateLabel: UILabel!
|
||||
@IBOutlet var checkDateLabelLayoutConstraint: NSLayoutConstraint!
|
||||
|
||||
private lazy var openingHoursViewController: OpeningHoursViewController = {
|
||||
storyboard!.instantiateViewController(ofType: OpeningHoursViewController.self)
|
||||
}()
|
||||
|
||||
private var rawOpeningHoursView: InfoItemView?
|
||||
private var phoneViews: [InfoItemView] = []
|
||||
private var websiteView: InfoItemView?
|
||||
private var websiteMenuView: InfoItemView?
|
||||
private var wikipediaView: InfoItemView?
|
||||
private var wikimediaCommonsView: InfoItemView?
|
||||
private var emailView: InfoItemView?
|
||||
private var fediverseView: InfoItemView?
|
||||
private var facebookView: InfoItemView?
|
||||
private var instagramView: InfoItemView?
|
||||
private var twitterView: InfoItemView?
|
||||
private var vkView: InfoItemView?
|
||||
private var lineView: InfoItemView?
|
||||
private var blueskyView: InfoItemView?
|
||||
private var panoramaxView: InfoItemView?
|
||||
private var cuisineView: InfoItemView?
|
||||
private var operatorView: InfoItemView?
|
||||
private var wifiView: InfoItemView?
|
||||
private var atmView: InfoItemView?
|
||||
private var addressView: InfoItemView?
|
||||
private var levelView: InfoItemView?
|
||||
private var coordinatesView: InfoItemView?
|
||||
private var openWithAppView: InfoItemView?
|
||||
private var capacityView: InfoItemView?
|
||||
private var wheelchairView: InfoItemView?
|
||||
private var selfServiceView: InfoItemView?
|
||||
private var outdoorSeatingView: InfoItemView?
|
||||
private var driveThroughView: InfoItemView?
|
||||
private var networkView: InfoItemView?
|
||||
|
||||
weak var placePageInfoData: PlacePageInfoData!
|
||||
weak var delegate: PlacePageInfoViewControllerDelegate?
|
||||
var coordinatesFormatId: Int {
|
||||
get { UserDefaults.standard.integer(forKey: Constants.coordFormatIdKey) }
|
||||
set { UserDefaults.standard.set(newValue, forKey: Constants.coordFormatIdKey) }
|
||||
}
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
stackView.axis = .vertical
|
||||
stackView.alignment = .fill
|
||||
stackView.spacing = 0
|
||||
stackView.translatesAutoresizingMaskIntoConstraints = false
|
||||
stackView.addSeparator(.bottom)
|
||||
view.addSubview(stackView)
|
||||
setupViews()
|
||||
}
|
||||
|
||||
// MARK: private
|
||||
private func setupViews() {
|
||||
if let openingHours = placePageInfoData.openingHours {
|
||||
openingHoursViewController.openingHours = openingHours
|
||||
openingHoursViewController.openingHoursCheckDate = placePageInfoData.checkDateOpeningHours
|
||||
addChild(openingHoursViewController)
|
||||
addToStack(openingHoursViewController.view)
|
||||
openingHoursViewController.didMove(toParent: self)
|
||||
} else if let openingHoursString = placePageInfoData.openingHoursString {
|
||||
rawOpeningHoursView = createInfoItem(openingHoursString, icon: UIImage(named: "ic_placepage_open_hours"))
|
||||
rawOpeningHoursView?.infoLabel.numberOfLines = 0
|
||||
}
|
||||
|
||||
if let cuisine = placePageInfoData.cuisine {
|
||||
cuisineView = createInfoItem(cuisine, icon: UIImage(named: "ic_placepage_cuisine"))
|
||||
}
|
||||
|
||||
/// @todo Entrance is missing compared with Android. It's shown in title, but anyway ..
|
||||
|
||||
phoneViews = placePageInfoData.phones.map({ phone in
|
||||
var cellStyle: Style = .regular
|
||||
if let phoneUrl = phone.url, UIApplication.shared.canOpenURL(phoneUrl) {
|
||||
cellStyle = .link
|
||||
}
|
||||
return createInfoItem(phone.phone,
|
||||
icon: UIImage(named: "ic_placepage_phone_number"),
|
||||
style: cellStyle,
|
||||
tapHandler: { [weak self] in
|
||||
self?.delegate?.didPressCall(to: phone)
|
||||
},
|
||||
longPressHandler: { [weak self] in
|
||||
self?.delegate?.didCopy(phone.phone)
|
||||
})
|
||||
})
|
||||
|
||||
if let ppOperator = placePageInfoData.ppOperator {
|
||||
operatorView = createInfoItem(ppOperator, icon: UIImage(named: "ic_placepage_operator"))
|
||||
}
|
||||
|
||||
if let network = placePageInfoData.network {
|
||||
networkView = createInfoItem(network, icon: UIImage(named: "ic_placepage_network"))
|
||||
}
|
||||
|
||||
if let website = placePageInfoData.website {
|
||||
// Strip website url only when the value is displayed, to avoid issues when it's opened or edited.
|
||||
websiteView = createInfoItem(stripUrl(str: website),
|
||||
icon: UIImage(named: "ic_placepage_website"),
|
||||
style: .link,
|
||||
tapHandler: { [weak self] in
|
||||
self?.delegate?.didPressWebsite()
|
||||
},
|
||||
longPressHandler: { [weak self] in
|
||||
self?.delegate?.didCopy(website)
|
||||
})
|
||||
}
|
||||
|
||||
if let websiteMenu = placePageInfoData.websiteMenu {
|
||||
websiteView = createInfoItem(L("website_menu"),
|
||||
icon: UIImage(named: "ic_placepage_website_menu"),
|
||||
style: .link,
|
||||
tapHandler: { [weak self] in
|
||||
self?.delegate?.didPressWebsiteMenu()
|
||||
},
|
||||
longPressHandler: { [weak self] in
|
||||
self?.delegate?.didCopy(websiteMenu)
|
||||
})
|
||||
}
|
||||
|
||||
if let wikipedia = placePageInfoData.wikipedia {
|
||||
wikipediaView = createInfoItem(L("read_in_wikipedia"),
|
||||
icon: UIImage(named: "ic_placepage_wiki"),
|
||||
style: .link,
|
||||
tapHandler: { [weak self] in
|
||||
self?.delegate?.didPressWikipedia()
|
||||
},
|
||||
longPressHandler: { [weak self] in
|
||||
self?.delegate?.didCopy(wikipedia)
|
||||
})
|
||||
}
|
||||
|
||||
if let wikimediaCommons = placePageInfoData.wikimediaCommons {
|
||||
wikimediaCommonsView = createInfoItem(L("wikimedia_commons"),
|
||||
icon: UIImage(named: "ic_placepage_wikimedia_commons"),
|
||||
style: .link,
|
||||
tapHandler: { [weak self] in
|
||||
self?.delegate?.didPressWikimediaCommons()
|
||||
},
|
||||
longPressHandler: { [weak self] in
|
||||
self?.delegate?.didCopy(wikimediaCommons)
|
||||
})
|
||||
}
|
||||
|
||||
if let wifi = placePageInfoData.wifiAvailable {
|
||||
wifiView = createInfoItem(wifi, icon: UIImage(named: "ic_placepage_wifi"))
|
||||
}
|
||||
|
||||
if let atm = placePageInfoData.atm {
|
||||
atmView = createInfoItem(atm, icon: UIImage(named: "ic_placepage_atm"))
|
||||
}
|
||||
|
||||
if let level = placePageInfoData.level {
|
||||
levelView = createInfoItem(level, icon: UIImage(named: "ic_placepage_level"))
|
||||
}
|
||||
|
||||
if let capacity = placePageInfoData.capacity {
|
||||
capacityView = createInfoItem(capacity, icon: UIImage(named: "ic_placepage_capacity"))
|
||||
}
|
||||
|
||||
if let wheelchair = placePageInfoData.wheelchair {
|
||||
wheelchairView = createInfoItem(wheelchair, icon: UIImage(named: "ic_placepage_wheelchair"))
|
||||
}
|
||||
|
||||
if let selfService = placePageInfoData.selfService {
|
||||
selfServiceView = createInfoItem(selfService, icon: UIImage(named: "ic_placepage_self_service"))
|
||||
}
|
||||
|
||||
if let outdoorSeating = placePageInfoData.outdoorSeating {
|
||||
outdoorSeatingView = createInfoItem(outdoorSeating, icon: UIImage(named: "ic_placepage_outdoor_seating"))
|
||||
}
|
||||
|
||||
if let driveThrough = placePageInfoData.driveThrough {
|
||||
driveThroughView = createInfoItem(driveThrough, icon: UIImage(named: "ic_placepage_drive_through"))
|
||||
}
|
||||
|
||||
if let email = placePageInfoData.email {
|
||||
emailView = createInfoItem(email,
|
||||
icon: UIImage(named: "ic_placepage_email"),
|
||||
style: .link,
|
||||
tapHandler: { [weak self] in
|
||||
self?.delegate?.didPressEmail()
|
||||
},
|
||||
longPressHandler: { [weak self] in
|
||||
self?.delegate?.didCopy(email)
|
||||
})
|
||||
}
|
||||
|
||||
if let fediverse = placePageInfoData.fediverse {
|
||||
fediverseView = createInfoItem(fediverse,
|
||||
icon: UIImage(named: "ic_placepage_fediverse"),
|
||||
style: .link,
|
||||
tapHandler: { [weak self] in
|
||||
self?.delegate?.didPressFediverse()
|
||||
},
|
||||
longPressHandler: { [weak self] in
|
||||
self?.delegate?.didCopy(fediverse)
|
||||
})
|
||||
}
|
||||
|
||||
if let facebook = placePageInfoData.facebook {
|
||||
facebookView = createInfoItem(facebook,
|
||||
icon: UIImage(named: "ic_placepage_facebook"),
|
||||
style: .link,
|
||||
tapHandler: { [weak self] in
|
||||
self?.delegate?.didPressFacebook()
|
||||
},
|
||||
longPressHandler: { [weak self] in
|
||||
self?.delegate?.didCopy(facebook)
|
||||
})
|
||||
}
|
||||
|
||||
if let instagram = placePageInfoData.instagram {
|
||||
instagramView = createInfoItem(instagram,
|
||||
icon: UIImage(named: "ic_placepage_instagram"),
|
||||
style: .link,
|
||||
tapHandler: { [weak self] in
|
||||
self?.delegate?.didPressInstagram()
|
||||
},
|
||||
longPressHandler: { [weak self] in
|
||||
self?.delegate?.didCopy(instagram)
|
||||
})
|
||||
}
|
||||
|
||||
if let twitter = placePageInfoData.twitter {
|
||||
twitterView = createInfoItem(twitter,
|
||||
icon: UIImage(named: "ic_placepage_twitter"),
|
||||
style: .link,
|
||||
tapHandler: { [weak self] in
|
||||
self?.delegate?.didPressTwitter()
|
||||
},
|
||||
longPressHandler: { [weak self] in
|
||||
self?.delegate?.didCopy(twitter)
|
||||
})
|
||||
}
|
||||
|
||||
if let vk = placePageInfoData.vk {
|
||||
vkView = createInfoItem(vk,
|
||||
icon: UIImage(named: "ic_placepage_vk"),
|
||||
style: .link,
|
||||
tapHandler: { [weak self] in
|
||||
self?.delegate?.didPressVk()
|
||||
},
|
||||
longPressHandler: { [weak self] in
|
||||
self?.delegate?.didCopy(vk)
|
||||
})
|
||||
}
|
||||
|
||||
if let line = placePageInfoData.line {
|
||||
lineView = createInfoItem(line,
|
||||
icon: UIImage(named: "ic_placepage_line"),
|
||||
style: .link,
|
||||
tapHandler: { [weak self] in
|
||||
self?.delegate?.didPressLine()
|
||||
},
|
||||
longPressHandler: { [weak self] in
|
||||
self?.delegate?.didCopy(line)
|
||||
})
|
||||
}
|
||||
|
||||
if let bluesky = placePageInfoData.bluesky {
|
||||
blueskyView = createInfoItem(bluesky,
|
||||
icon: UIImage(named: "ic_placepage_bluesky"),
|
||||
style: .link,
|
||||
tapHandler: { [weak self] in
|
||||
self?.delegate?.didPressBluesky()
|
||||
},
|
||||
longPressHandler: { [weak self] in
|
||||
self?.delegate?.didCopy(bluesky)
|
||||
})
|
||||
}
|
||||
|
||||
if let panoramax = placePageInfoData.panoramax {
|
||||
panoramaxView = createInfoItem(L("panoramax_picture"),
|
||||
icon: UIImage(named: "ic_placepage_panoramax"),
|
||||
style: .link,
|
||||
tapHandler: { [weak self] in
|
||||
self?.delegate?.didPressPanoramax()
|
||||
},
|
||||
longPressHandler: { [weak self] in
|
||||
self?.delegate?.didCopy(panoramax)
|
||||
})
|
||||
}
|
||||
|
||||
if let address = placePageInfoData.address {
|
||||
addressView = createInfoItem(address,
|
||||
icon: UIImage(named: "ic_placepage_address"),
|
||||
longPressHandler: { [weak self] in
|
||||
self?.delegate?.didCopy(address)
|
||||
})
|
||||
}
|
||||
|
||||
setupCoordinatesView()
|
||||
setupOpenWithAppView()
|
||||
|
||||
if let checkDate = placePageInfoData.checkDate {
|
||||
let checkDateFormatter = RelativeDateTimeFormatter()
|
||||
checkDateFormatter.unitsStyle = .spellOut
|
||||
checkDateFormatter.localizedString(for: checkDate, relativeTo: Date.now)
|
||||
self.checkDateLabel.text = String(format: L("existence_confirmed_time_ago"), checkDateFormatter.localizedString(for: checkDate, relativeTo: Date.now))
|
||||
checkDateLabel.isHidden = false
|
||||
NSLayoutConstraint.activate([checkDateLabelLayoutConstraint])
|
||||
} else {
|
||||
checkDateLabel.text = String()
|
||||
checkDateLabel.isHidden = true
|
||||
NSLayoutConstraint.deactivate([checkDateLabelLayoutConstraint])
|
||||
}
|
||||
}
|
||||
|
||||
private func setupCoordinatesView() {
|
||||
guard let coordFormats = placePageInfoData.coordFormats as? Array<String> else { return }
|
||||
var formatId = coordinatesFormatId
|
||||
if formatId >= coordFormats.count {
|
||||
formatId = 0
|
||||
}
|
||||
coordinatesView = createInfoItem(coordFormats[formatId],
|
||||
icon: UIImage(named: "ic_placepage_coordinate"),
|
||||
accessoryImage: UIImage(named: "ic_placepage_change"),
|
||||
tapHandler: { [weak self] in
|
||||
guard let self else { return }
|
||||
let formatId = (self.coordinatesFormatId + 1) % coordFormats.count
|
||||
self.setCoordinatesSelected(formatId: formatId)
|
||||
},
|
||||
longPressHandler: { [weak self] in
|
||||
self?.copyCoordinatesToPasteboard()
|
||||
})
|
||||
|
||||
let menu = UIMenu(children: coordFormats.enumerated().map { (index, format) in
|
||||
UIAction(title: format, handler: { [weak self] _ in
|
||||
self?.setCoordinatesSelected(formatId: index)
|
||||
self?.copyCoordinatesToPasteboard()
|
||||
})
|
||||
})
|
||||
coordinatesView?.accessoryButton.menu = menu
|
||||
coordinatesView?.accessoryButton.showsMenuAsPrimaryAction = true
|
||||
}
|
||||
|
||||
private func setCoordinatesSelected(formatId: Int) {
|
||||
guard let coordFormats = placePageInfoData.coordFormats as? Array<String> else { return }
|
||||
coordinatesFormatId = formatId
|
||||
let coordinates: String = coordFormats[formatId]
|
||||
coordinatesView?.infoLabel.text = coordinates
|
||||
}
|
||||
|
||||
private func copyCoordinatesToPasteboard() {
|
||||
guard let coordFormats = placePageInfoData.coordFormats as? Array<String> else { return }
|
||||
let coordinates: String = coordFormats[coordinatesFormatId]
|
||||
delegate?.didCopy(coordinates)
|
||||
}
|
||||
|
||||
private func setupOpenWithAppView() {
|
||||
guard let delegate, delegate.shouldShowOpenInApp else { return }
|
||||
openWithAppView = createInfoItem(L("open_in_app"),
|
||||
icon: UIImage(named: "ic_open_in_app"),
|
||||
style: .link,
|
||||
tapHandler: { [weak self] in
|
||||
guard let self, let openWithAppView else { return }
|
||||
self.delegate?.didPressOpenInApp(from: openWithAppView)
|
||||
})
|
||||
}
|
||||
|
||||
private func createInfoItem(_ info: String,
|
||||
icon: UIImage?,
|
||||
tapIconHandler: TapHandler? = nil,
|
||||
style: Style = .regular,
|
||||
accessoryImage: UIImage? = nil,
|
||||
tapHandler: TapHandler? = nil,
|
||||
longPressHandler: TapHandler? = nil,
|
||||
accessoryImageTapHandler: TapHandler? = nil) -> InfoItemView {
|
||||
let view = InfoItemView()
|
||||
addToStack(view)
|
||||
view.iconButton.setImage(icon?.withRenderingMode(.alwaysTemplate), for: .normal)
|
||||
view.iconButtonTapHandler = tapIconHandler
|
||||
view.infoLabel.text = info
|
||||
view.setStyle(style)
|
||||
view.infoLabelTapHandler = tapHandler
|
||||
view.infoLabelLongPressHandler = longPressHandler
|
||||
view.setAccessory(image: accessoryImage, tapHandler: accessoryImageTapHandler)
|
||||
return view
|
||||
}
|
||||
|
||||
private func addToStack(_ view: UIView) {
|
||||
stackView.addArrangedSubviewWithSeparator(view, insets: UIEdgeInsets(top: 0, left: 56, bottom: 0, right: 0))
|
||||
}
|
||||
|
||||
private static let kHttp = "http://"
|
||||
private static let kHttps = "https://"
|
||||
|
||||
private func stripUrl(str: String) -> String {
|
||||
let dropFromStart = str.hasPrefix(PlacePageInfoViewController.kHttps) ? PlacePageInfoViewController.kHttps.count
|
||||
: (str.hasPrefix(PlacePageInfoViewController.kHttp) ? PlacePageInfoViewController.kHttp.count : 0);
|
||||
let dropFromEnd = str.hasSuffix("/") ? 1 : 0;
|
||||
return String(str.dropFirst(dropFromStart).dropLast(dropFromEnd))
|
||||
}
|
||||
}
|
||||
|
||||
private extension UIStackView {
|
||||
func addArrangedSubviewWithSeparator(_ view: UIView, insets: UIEdgeInsets = .zero) {
|
||||
if !arrangedSubviews.isEmpty {
|
||||
view.addSeparator(thickness: CGFloat(1.0), insets: insets)
|
||||
}
|
||||
addArrangedSubview(view)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,269 @@
|
|||
final class PlacePageDirectionView: UIView {
|
||||
@IBOutlet var imageView: UIImageView!
|
||||
@IBOutlet var label: UILabel!
|
||||
}
|
||||
|
||||
final class PlacePagePreviewViewController: UIViewController {
|
||||
@IBOutlet var stackView: UIStackView!
|
||||
@IBOutlet var popularView: UIView!
|
||||
@IBOutlet var subtitleLabel: UILabel! {
|
||||
didSet {
|
||||
subtitleLabel.textColor = UIColor.blackSecondaryText()
|
||||
subtitleLabel.font = UIFont.regular14()
|
||||
}
|
||||
}
|
||||
@IBOutlet var subtitleContainerView: UIStackView!
|
||||
@IBOutlet var scheduleLabel: UILabel!
|
||||
@IBOutlet var reviewsLabel: UILabel!
|
||||
@IBOutlet var addReviewButton: UIButton! {
|
||||
didSet {
|
||||
addReviewButton.setTitle("+ \(L("leave_a_review"))", for: .normal)
|
||||
}
|
||||
}
|
||||
@IBOutlet var addressLabel: UILabel!
|
||||
@IBOutlet var addressContainerView: UIStackView!
|
||||
@IBOutlet var scheduleContainerView: UIStackView!
|
||||
|
||||
@IBOutlet var subtitleDirectionView: PlacePageDirectionView!
|
||||
@IBOutlet var addressDirectionView: PlacePageDirectionView!
|
||||
|
||||
var placePageDirectionView: PlacePageDirectionView?
|
||||
lazy var fullScreenDirectionView: DirectionView = {
|
||||
return Bundle.main.load(viewClass: DirectionView.self)!
|
||||
}()
|
||||
|
||||
var placePagePreviewData: PlacePagePreviewData! {
|
||||
didSet {
|
||||
if isViewLoaded {
|
||||
updateViews()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private var distance: String? = nil
|
||||
private var speedAndAltitude: String? = nil
|
||||
private var heading: CGFloat? = nil
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
|
||||
updateViews()
|
||||
|
||||
if let distance = distance {
|
||||
placePageDirectionView?.isHidden = false
|
||||
placePageDirectionView?.label.text = distance
|
||||
}
|
||||
|
||||
if let heading = heading {
|
||||
updateHeading(heading)
|
||||
} else {
|
||||
placePageDirectionView?.imageView.isHidden = true
|
||||
}
|
||||
}
|
||||
|
||||
override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
|
||||
super.traitCollectionDidChange(previousTraitCollection)
|
||||
guard traitCollection.userInterfaceStyle != previousTraitCollection?.userInterfaceStyle else { return }
|
||||
updateViews()
|
||||
}
|
||||
|
||||
func updateViews() {
|
||||
if placePagePreviewData.isMyPosition {
|
||||
if let speedAndAltitude = speedAndAltitude {
|
||||
subtitleLabel.text = speedAndAltitude
|
||||
}
|
||||
} else {
|
||||
let subtitleString = NSMutableAttributedString()
|
||||
// if placePagePreviewData.isPopular {
|
||||
// subtitleString.append(NSAttributedString(string: L("popular_place"),
|
||||
// attributes: [.foregroundColor : UIColor.linkBlue(),
|
||||
// .font : UIFont.regular14()]))
|
||||
// }
|
||||
|
||||
if let subtitle = placePagePreviewData.subtitle ?? placePagePreviewData.coordinates {
|
||||
subtitleString.append(NSAttributedString(string: !subtitleString.string.isEmpty ? " • " + subtitle : subtitle,
|
||||
attributes: [.foregroundColor : UIColor.blackSecondaryText(),
|
||||
.font : UIFont.regular14()]))
|
||||
subtitleLabel.attributedText = subtitleString
|
||||
subtitleContainerView.isHidden = false
|
||||
} else {
|
||||
subtitleLabel.text = nil
|
||||
subtitleContainerView.isHidden = true
|
||||
}
|
||||
}
|
||||
|
||||
placePageDirectionView = subtitleDirectionView
|
||||
|
||||
if let address = placePagePreviewData.secondarySubtitle {
|
||||
addressLabel.text = address
|
||||
placePageDirectionView = addressDirectionView
|
||||
} else {
|
||||
addressContainerView.isHidden = true
|
||||
}
|
||||
placePageDirectionView?.imageView.changeColoringToOpposite()
|
||||
configSchedule()
|
||||
}
|
||||
|
||||
func updateDistance(_ distance: String) {
|
||||
self.distance = distance
|
||||
placePageDirectionView?.isHidden = false
|
||||
placePageDirectionView?.label.text = distance
|
||||
fullScreenDirectionView.updateDistance(distance)
|
||||
}
|
||||
|
||||
func updateHeading(_ angle: CGFloat) {
|
||||
placePageDirectionView?.imageView.isHidden = false
|
||||
let duration = heading == nil ? .zero : kDefaultAnimationDuration // skip the initial setup animation
|
||||
UIView.animate(withDuration: duration,
|
||||
delay: 0,
|
||||
options: [.beginFromCurrentState, .curveEaseInOut],
|
||||
animations: { [unowned self] in
|
||||
self.placePageDirectionView?.imageView.transform = CGAffineTransform(rotationAngle: CGFloat.pi / 2 - angle)
|
||||
})
|
||||
fullScreenDirectionView.updateHeading(angle)
|
||||
heading = angle
|
||||
}
|
||||
|
||||
func updateSpeedAndAltitude(_ speedAndAltitude: String) {
|
||||
self.speedAndAltitude = speedAndAltitude
|
||||
subtitleLabel?.text = speedAndAltitude
|
||||
}
|
||||
|
||||
@IBAction func onDirectionPressed(_ sender: Any) {
|
||||
guard let heading = heading else {
|
||||
return
|
||||
}
|
||||
|
||||
fullScreenDirectionView.updateTitle(placePagePreviewData.title,
|
||||
subtitle: placePagePreviewData.subtitle ?? placePagePreviewData.coordinates)
|
||||
fullScreenDirectionView.updateHeading(heading)
|
||||
fullScreenDirectionView.updateDistance(distance)
|
||||
fullScreenDirectionView.show()
|
||||
}
|
||||
// MARK: private
|
||||
|
||||
private func configSchedule() {
|
||||
let now = time_t(Date().timeIntervalSince1970)
|
||||
|
||||
func stringFromTime(_ time: Int) -> String {
|
||||
DateTimeFormatter.dateString(from: Date(timeIntervalSince1970: TimeInterval(time)),
|
||||
dateStyle: .none,
|
||||
timeStyle: .short)
|
||||
}
|
||||
|
||||
switch placePagePreviewData.schedule.state {
|
||||
case .unknown:
|
||||
scheduleContainerView.isHidden = true
|
||||
case .allDay:
|
||||
setScheduleLabel(state: L("twentyfour_seven"),
|
||||
stateColor: UIColor.BaseColors.green,
|
||||
details: nil)
|
||||
|
||||
case .open:
|
||||
let nextTimeClosed = placePagePreviewData.schedule.nextTimeClosed
|
||||
let minutesUntilClosed = (nextTimeClosed - now) / 60
|
||||
let stringTimeInterval = getTimeIntervalString(minutes: minutesUntilClosed)
|
||||
let stringTime = stringFromTime(nextTimeClosed)
|
||||
|
||||
var state: String = L("editor_time_open")
|
||||
var stateColor = UIColor.BaseColors.green
|
||||
let details: String?
|
||||
if (minutesUntilClosed < 60) // Less than 1 hour
|
||||
{
|
||||
state = String(format: L("closes_in"), stringTimeInterval)
|
||||
stateColor = UIColor.BaseColors.yellow
|
||||
details = stringTime
|
||||
}
|
||||
else if (minutesUntilClosed < 3 * 60) // Less than 3 hours
|
||||
{
|
||||
details = String(format: L("closes_in"), stringTimeInterval) + " • " + stringTime
|
||||
}
|
||||
else if (minutesUntilClosed < 24 * 60) // Less than 24 hours
|
||||
{
|
||||
details = String(format: L("closes_at"), stringTime)
|
||||
}
|
||||
else
|
||||
{
|
||||
details = nil
|
||||
}
|
||||
|
||||
setScheduleLabel(state: state,
|
||||
stateColor: stateColor,
|
||||
details: details)
|
||||
|
||||
case .closed:
|
||||
let nextTimeOpen = placePagePreviewData.schedule.nextTimeOpen
|
||||
let nextTimeOpenDate = Date(timeIntervalSince1970: TimeInterval(nextTimeOpen))
|
||||
|
||||
let minutesUntilOpen = (nextTimeOpen - now) / 60
|
||||
let stringTimeInterval = getTimeIntervalString(minutes: minutesUntilOpen)
|
||||
let stringTime = stringFromTime(nextTimeOpen)
|
||||
|
||||
var state: String = L("closed_now")
|
||||
var stateColor = UIColor.BaseColors.red
|
||||
|
||||
let details: String?
|
||||
|
||||
if (minutesUntilOpen < 15) { // Less than 15 min
|
||||
state = String(format: L("opens_in"), stringTimeInterval)
|
||||
stateColor = UIColor.BaseColors.yellow
|
||||
details = stringTime
|
||||
}
|
||||
else if (minutesUntilOpen < 3 * 60) // Less than 3 hours
|
||||
{
|
||||
details = String(format: L("opens_in"), stringTimeInterval) + " • " + stringTime
|
||||
}
|
||||
else if (Calendar.current.isDateInToday(nextTimeOpenDate)) // Today
|
||||
{
|
||||
details = String(format: L("opens_at"), stringTime)
|
||||
}
|
||||
else if (minutesUntilOpen < 24 * 60) // Less than 24 hours
|
||||
{
|
||||
details = String(format: L("opens_tomorrow_at"), stringTime)
|
||||
}
|
||||
else if (minutesUntilOpen < 7 * 24 * 60) // Less than 1 week
|
||||
{
|
||||
let dayOfWeek = DateTimeFormatter.dateString(from: nextTimeOpenDate, format: "EEEE")
|
||||
details = String(format: L("opens_dayoftheweek_at"), dayOfWeek, stringTime)
|
||||
}
|
||||
else
|
||||
{
|
||||
details = nil
|
||||
}
|
||||
|
||||
setScheduleLabel(state: state,
|
||||
stateColor: stateColor,
|
||||
details: details)
|
||||
|
||||
@unknown default:
|
||||
fatalError()
|
||||
}
|
||||
}
|
||||
|
||||
private func getTimeIntervalString(minutes: Int) -> String {
|
||||
var str = ""
|
||||
if (minutes >= 60)
|
||||
{
|
||||
str = String(minutes / 60) + " " + L("hour") + " "
|
||||
}
|
||||
str += String(minutes % 60) + " " + L("minute")
|
||||
return str
|
||||
}
|
||||
|
||||
private func setScheduleLabel(state: String, stateColor: UIColor, details: String?) {
|
||||
let attributedString = NSMutableAttributedString()
|
||||
let stateString = NSAttributedString(string: state,
|
||||
attributes: [NSAttributedString.Key.font: UIFont.systemFont(ofSize: 14, weight: .semibold),
|
||||
NSAttributedString.Key.foregroundColor: stateColor])
|
||||
attributedString.append(stateString)
|
||||
if (details != nil)
|
||||
{
|
||||
let detailsString = NSAttributedString(string: " • " + details!,
|
||||
attributes: [NSAttributedString.Key.font: UIFont.regular14(),
|
||||
NSAttributedString.Key.foregroundColor: UIColor.blackSecondaryText()])
|
||||
attributedString.append(detailsString)
|
||||
}
|
||||
|
||||
scheduleLabel.attributedText = attributedString
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,70 @@
|
|||
protocol WikiDescriptionViewControllerDelegate: AnyObject {
|
||||
func didPressMore()
|
||||
}
|
||||
|
||||
class WikiDescriptionViewController: UIViewController {
|
||||
@IBOutlet var descriptionTextView: UITextView!
|
||||
@IBOutlet var moreButton: UIButton!
|
||||
|
||||
var descriptionHtml: String? {
|
||||
didSet{
|
||||
updateDescription()
|
||||
}
|
||||
}
|
||||
weak var delegate: WikiDescriptionViewControllerDelegate?
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
|
||||
descriptionTextView.textContainerInset = .zero
|
||||
updateDescription()
|
||||
}
|
||||
|
||||
override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
|
||||
super.traitCollectionDidChange(previousTraitCollection)
|
||||
guard traitCollection.userInterfaceStyle != previousTraitCollection?.userInterfaceStyle else { return }
|
||||
updateDescription()
|
||||
}
|
||||
|
||||
private func updateDescription() {
|
||||
guard let descriptionHtml = descriptionHtml else { return }
|
||||
|
||||
DispatchQueue.global().async {
|
||||
let font = UIFont.regular14()
|
||||
let color = UIColor.blackPrimaryText()
|
||||
let paragraphStyle = NSMutableParagraphStyle()
|
||||
paragraphStyle.lineSpacing = 4
|
||||
|
||||
let attributedString: NSAttributedString
|
||||
if let str = NSMutableAttributedString(htmlString: descriptionHtml, baseFont: font, paragraphStyle: paragraphStyle) {
|
||||
str.addAttribute(NSAttributedString.Key.foregroundColor,
|
||||
value: color,
|
||||
range: NSRange(location: 0, length: str.length))
|
||||
attributedString = str;
|
||||
} else {
|
||||
attributedString = NSAttributedString(string: descriptionHtml,
|
||||
attributes: [NSAttributedString.Key.font : font,
|
||||
NSAttributedString.Key.foregroundColor: color,
|
||||
NSAttributedString.Key.paragraphStyle: paragraphStyle])
|
||||
}
|
||||
|
||||
DispatchQueue.main.async {
|
||||
if attributedString.length > 500 {
|
||||
self.descriptionTextView.attributedText = attributedString.attributedSubstring(from: NSRange(location: 0,
|
||||
length: 500))
|
||||
} else {
|
||||
self.descriptionTextView.attributedText = attributedString
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@IBAction func onMore(_ sender: UIButton) {
|
||||
delegate?.didPressMore()
|
||||
}
|
||||
|
||||
override func applyTheme() {
|
||||
super.applyTheme()
|
||||
updateDescription()
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue