Repo created

This commit is contained in:
Fr4nz D13trich 2025-11-22 13:58:55 +01:00
parent 4af19165ec
commit 68073add76
12458 changed files with 12350765 additions and 2 deletions

View file

@ -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)
}
}
}

View file

@ -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
}
}

View file

@ -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()
}
}

View file

@ -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)
}
}

View file

@ -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
}
}

View file

@ -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>

View file

@ -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
}
}

View file

@ -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
}
}

View file

@ -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)
}
}

View file

@ -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 }
}

View file

@ -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()
}
}

View file

@ -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)"
}
}
}

View file

@ -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
}
}

View file

@ -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)
}
}

View file

@ -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()
}
}

View file

@ -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
}
}

View file

@ -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)
}
}

View file

@ -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
}
}

View file

@ -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
}
}

View file

@ -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)
}
}

View file

@ -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
}
}

View file

@ -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])
})
}
}

View file

@ -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)
}
}

View file

@ -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
}
}

View file

@ -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()
}
}

View file

@ -0,0 +1,72 @@
import UIKit
class DirectionView: SolidTouchView {
@IBOutlet private var titleLabel: UILabel!
@IBOutlet private var typeLabel: UILabel!
@IBOutlet private var distanceLabel :UILabel!
@IBOutlet private var directionArrow: UIImageView!
@IBOutlet private var contentView: UIView!
override func awakeFromNib() {
distanceLabel.font = alternative(iPhone: .regular32(), iPad: .regular52())
typeLabel.font = alternative(iPhone: .regular16(), iPad: .regular24())
}
override func didMoveToSuperview() {
super.didMoveToSuperview()
let manager = MWMMapViewControlsManager.manager()
let app = MapsAppDelegate.theApp()
if superview != nil {
app.disableStandby()
manager?.isDirectionViewHidden = false
} else {
app.enableStandby()
manager?.isDirectionViewHidden = true
}
app.mapViewController.updateStatusBarStyle()
}
override func layoutSubviews() {
var textAlignment = NSTextAlignment.center
if UIDevice.current.orientation == .landscapeLeft || UIDevice.current.orientation == .landscapeRight {
textAlignment = alternative(iPhone: .left, iPad: .center)
}
titleLabel.textAlignment = textAlignment
typeLabel.textAlignment = textAlignment
distanceLabel.textAlignment = textAlignment
super.layoutSubviews()
}
func show() {
guard let superview = MapViewController.shared()?.view else {
assertionFailure()
return
}
superview.addSubview(self)
self.alignToSuperview()
setNeedsLayout()
}
func updateTitle(_ title: String?, subtitle: String?) {
self.titleLabel.text = title
self.typeLabel.text = subtitle
}
func updateDistance(_ distance: String?) {
distanceLabel?.text = distance
}
func updateHeading(_ angle: CGFloat) {
UIView.animate(withDuration: kDefaultAnimationDuration,
delay: 0,
options: [.beginFromCurrentState, .curveEaseInOut],
animations: { [unowned self] in
self.directionArrow?.transform = CGAffineTransform(rotationAngle: CGFloat.pi / 2 - angle)
})
}
@IBAction func onTap(_ sender: Any) {
removeFromSuperview()
}
}

View file

@ -0,0 +1,176 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="15705" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
<device id="retina6_5" orientation="portrait" appearance="light"/>
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="15706"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<objects>
<placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner"/>
<placeholder placeholderIdentifier="IBFirstResponder" id="-2" customClass="UIResponder"/>
<view contentMode="scaleToFill" insetsLayoutMarginsFromSafeArea="NO" translatesAutoresizingMaskIntoConstraints="NO" id="iN0-l3-epB" customClass="DirectionView" customModule="CoMaps" customModuleProvider="target">
<rect key="frame" x="0.0" y="0.0" width="414" height="896"/>
<subviews>
<view autoresizesSubviews="NO" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="yXW-1V-PEL">
<rect key="frame" x="134.33333333333337" y="284" width="145.66666666666663" height="328"/>
<subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Аэропорт " textAlignment="center" lineBreakMode="tailTruncation" numberOfLines="0" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="mMN-to-Xnd" userLabel="Аэропорт" propertyAccessControl="none">
<rect key="frame" x="0.0" y="0.0" width="145.66666666666666" height="38.333333333333336"/>
<fontDescription key="fontDescription" type="system" weight="light" pointSize="32"/>
<color key="textColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<nil key="highlightedColor"/>
</label>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="метро" textAlignment="center" lineBreakMode="tailTruncation" numberOfLines="0" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="AnD-rO-bOm">
<rect key="frame" x="0.0" y="46.333333333333314" width="145.66666666666666" height="19.333333333333329"/>
<fontDescription key="fontDescription" type="system" pointSize="16"/>
<color key="textColor" red="1" green="1" blue="1" alpha="0.54000000000000004" colorSpace="custom" customColorSpace="sRGB"/>
<nil key="highlightedColor"/>
</label>
<imageView userInteractionEnabled="NO" contentMode="scaleToFill" horizontalHuggingPriority="251" verticalHuggingPriority="251" image="direction_mini" translatesAutoresizingMaskIntoConstraints="NO" id="yQx-g1-ALQ">
<rect key="frame" x="-7.3333333333333428" y="97.666666666666686" width="160" height="160"/>
<constraints>
<constraint firstAttribute="height" constant="260" id="DPQ-m6-2rE"/>
<constraint firstAttribute="width" constant="160" id="Itv-S8-i8O"/>
<constraint firstAttribute="height" constant="160" id="Lqf-lc-Q24"/>
<constraint firstAttribute="width" constant="260" id="mLL-K1-KUx"/>
</constraints>
<variation key="default">
<mask key="constraints">
<exclude reference="DPQ-m6-2rE"/>
<exclude reference="mLL-K1-KUx"/>
</mask>
</variation>
<variation key="heightClass=regular-widthClass=regular" image="direction_big">
<mask key="constraints">
<include reference="DPQ-m6-2rE"/>
<exclude reference="Itv-S8-i8O"/>
<exclude reference="Lqf-lc-Q24"/>
<include reference="mLL-K1-KUx"/>
</mask>
</variation>
</imageView>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" verticalCompressionResistancePriority="749" text="200" textAlignment="center" lineBreakMode="tailTruncation" numberOfLines="0" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="Dty-4K-t4Q" propertyAccessControl="none">
<rect key="frame" x="44" y="289.66666666666663" width="57.333333333333343" height="38.333333333333314"/>
<fontDescription key="fontDescription" type="system" weight="light" pointSize="32"/>
<color key="textColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<nil key="highlightedColor"/>
</label>
</subviews>
<color key="backgroundColor" red="0.0" green="0.0" blue="0.0" alpha="0.0" colorSpace="custom" customColorSpace="sRGB"/>
<constraints>
<constraint firstItem="AnD-rO-bOm" firstAttribute="leading" secondItem="yXW-1V-PEL" secondAttribute="leading" id="31W-qL-IEa"/>
<constraint firstItem="yQx-g1-ALQ" firstAttribute="top" secondItem="AnD-rO-bOm" secondAttribute="bottom" constant="32" id="3OY-Ra-BkM"/>
<constraint firstItem="mMN-to-Xnd" firstAttribute="top" secondItem="yXW-1V-PEL" secondAttribute="top" id="8kg-GW-rzb"/>
<constraint firstAttribute="trailing" secondItem="mMN-to-Xnd" secondAttribute="trailing" id="GfH-sE-U19"/>
<constraint firstItem="Dty-4K-t4Q" firstAttribute="centerX" secondItem="yXW-1V-PEL" secondAttribute="centerX" id="Htg-sT-i06"/>
<constraint firstItem="mMN-to-Xnd" firstAttribute="leading" secondItem="yQx-g1-ALQ" secondAttribute="trailing" constant="40" id="Ien-Fu-Zsx"/>
<constraint firstItem="Dty-4K-t4Q" firstAttribute="top" secondItem="yQx-g1-ALQ" secondAttribute="bottom" constant="80" id="Ode-yk-1hC"/>
<constraint firstAttribute="trailing" secondItem="mMN-to-Xnd" secondAttribute="trailing" id="PS8-J1-Ejw"/>
<constraint firstItem="Dty-4K-t4Q" firstAttribute="leading" secondItem="AnD-rO-bOm" secondAttribute="leading" id="RXz-0g-mBe"/>
<constraint firstItem="AnD-rO-bOm" firstAttribute="centerX" secondItem="yXW-1V-PEL" secondAttribute="centerX" id="SNX-wh-gXF"/>
<constraint firstItem="yQx-g1-ALQ" firstAttribute="centerY" secondItem="yXW-1V-PEL" secondAttribute="centerY" id="TRr-fs-ngi"/>
<constraint firstItem="Dty-4K-t4Q" firstAttribute="top" secondItem="yQx-g1-ALQ" secondAttribute="bottom" constant="32" id="YwQ-Oo-qlI"/>
<constraint firstAttribute="trailing" secondItem="AnD-rO-bOm" secondAttribute="trailing" id="ax9-hz-vY8"/>
<constraint firstItem="yQx-g1-ALQ" firstAttribute="top" secondItem="AnD-rO-bOm" secondAttribute="bottom" constant="80" id="c6v-le-4cJ"/>
<constraint firstItem="yQx-g1-ALQ" firstAttribute="centerX" secondItem="yXW-1V-PEL" secondAttribute="centerX" id="nch-yT-8QH"/>
<constraint firstItem="AnD-rO-bOm" firstAttribute="top" secondItem="mMN-to-Xnd" secondAttribute="bottom" constant="12" id="q2a-dg-BlX"/>
<constraint firstAttribute="bottom" secondItem="Dty-4K-t4Q" secondAttribute="bottom" id="s21-Ou-6RT"/>
<constraint firstItem="AnD-rO-bOm" firstAttribute="top" secondItem="mMN-to-Xnd" secondAttribute="bottom" constant="8" id="vHI-bP-i2E"/>
<constraint firstItem="Dty-4K-t4Q" firstAttribute="top" secondItem="AnD-rO-bOm" secondAttribute="bottom" constant="24" id="vc5-pN-Pif"/>
<constraint firstItem="yQx-g1-ALQ" firstAttribute="leading" secondItem="yXW-1V-PEL" secondAttribute="leading" id="wZx-W2-VNr"/>
<constraint firstAttribute="trailing" secondItem="AnD-rO-bOm" secondAttribute="trailing" id="xD1-Og-f6n"/>
<constraint firstItem="AnD-rO-bOm" firstAttribute="leading" secondItem="mMN-to-Xnd" secondAttribute="leading" id="xyV-hl-wt9"/>
<constraint firstItem="mMN-to-Xnd" firstAttribute="leading" secondItem="yXW-1V-PEL" secondAttribute="leading" id="zHl-6e-hKr"/>
</constraints>
<variation key="default">
<mask key="constraints">
<exclude reference="Ien-Fu-Zsx"/>
<exclude reference="PS8-J1-Ejw"/>
<exclude reference="zHl-6e-hKr"/>
<exclude reference="31W-qL-IEa"/>
<exclude reference="SNX-wh-gXF"/>
<exclude reference="ax9-hz-vY8"/>
<exclude reference="q2a-dg-BlX"/>
<exclude reference="xD1-Og-f6n"/>
<exclude reference="xyV-hl-wt9"/>
<exclude reference="TRr-fs-ngi"/>
<exclude reference="c6v-le-4cJ"/>
<exclude reference="wZx-W2-VNr"/>
<exclude reference="Htg-sT-i06"/>
<exclude reference="Ode-yk-1hC"/>
<exclude reference="RXz-0g-mBe"/>
<exclude reference="vc5-pN-Pif"/>
</mask>
</variation>
<variation key="heightClass=compact">
<mask key="constraints">
<include reference="Ien-Fu-Zsx"/>
<include reference="xD1-Og-f6n"/>
<include reference="xyV-hl-wt9"/>
<exclude reference="3OY-Ra-BkM"/>
<include reference="TRr-fs-ngi"/>
<exclude reference="nch-yT-8QH"/>
<include reference="wZx-W2-VNr"/>
<include reference="RXz-0g-mBe"/>
<exclude reference="YwQ-Oo-qlI"/>
<include reference="vc5-pN-Pif"/>
</mask>
</variation>
<variation key="heightClass=regular">
<mask key="constraints">
<include reference="PS8-J1-Ejw"/>
<include reference="zHl-6e-hKr"/>
<include reference="SNX-wh-gXF"/>
<include reference="ax9-hz-vY8"/>
<include reference="Htg-sT-i06"/>
</mask>
</variation>
<variation key="heightClass=regular-widthClass=compact">
<mask key="constraints">
<include reference="31W-qL-IEa"/>
</mask>
</variation>
<variation key="heightClass=regular-widthClass=regular">
<mask key="constraints">
<include reference="q2a-dg-BlX"/>
<exclude reference="vHI-bP-i2E"/>
<exclude reference="3OY-Ra-BkM"/>
<include reference="c6v-le-4cJ"/>
<include reference="Ode-yk-1hC"/>
<exclude reference="YwQ-Oo-qlI"/>
</mask>
</variation>
</view>
</subviews>
<color key="backgroundColor" white="0.0" alpha="0.89824493838028174" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<gestureRecognizers/>
<constraints>
<constraint firstItem="yXW-1V-PEL" firstAttribute="centerY" secondItem="iN0-l3-epB" secondAttribute="centerY" id="4E6-bS-m07"/>
<constraint firstAttribute="trailing" relation="greaterThanOrEqual" secondItem="yXW-1V-PEL" secondAttribute="trailing" constant="40" id="99u-oH-BKQ"/>
<constraint firstItem="yXW-1V-PEL" firstAttribute="centerX" secondItem="iN0-l3-epB" secondAttribute="centerX" id="DQR-jC-ais"/>
<constraint firstItem="yXW-1V-PEL" firstAttribute="top" relation="greaterThanOrEqual" secondItem="iN0-l3-epB" secondAttribute="top" constant="40" id="naI-CL-ySn"/>
<constraint firstItem="yXW-1V-PEL" firstAttribute="leading" relation="greaterThanOrEqual" secondItem="iN0-l3-epB" secondAttribute="leading" constant="40" id="phe-dZ-nRc"/>
<constraint firstAttribute="bottom" relation="greaterThanOrEqual" secondItem="yXW-1V-PEL" secondAttribute="bottom" constant="40" id="q7Y-mD-MX3"/>
</constraints>
<connections>
<outlet property="contentView" destination="yXW-1V-PEL" id="0aM-CO-ndr"/>
<outlet property="directionArrow" destination="yQx-g1-ALQ" id="60a-Lr-OyJ"/>
<outlet property="distanceLabel" destination="Dty-4K-t4Q" id="CZu-VQ-R6X"/>
<outlet property="titleLabel" destination="mMN-to-Xnd" id="M5A-9k-lvB"/>
<outlet property="typeLabel" destination="AnD-rO-bOm" id="r3h-vk-AKk"/>
<outletCollection property="gestureRecognizers" destination="jon-c0-OXb" appends="YES" id="rIw-dp-vZt"/>
</connections>
<point key="canvasLocation" x="140.625" y="153.62318840579712"/>
</view>
<tapGestureRecognizer id="jon-c0-OXb">
<connections>
<action selector="onTap:" destination="iN0-l3-epB" id="oCQ-ok-hyw"/>
</connections>
</tapGestureRecognizer>
</objects>
<resources>
<image name="direction_big" width="260" height="260"/>
<image name="direction_mini" width="160" height="160"/>
</resources>
</document>

View file

@ -0,0 +1,15 @@
#import "MWMMapViewControlsManager.h"
struct FeatureID;
@protocol MWMFeatureHolder<NSObject>
- (FeatureID const &)featureId;
@end
@protocol MWMPlacePageProtocol<MWMFeatureHolder>
- (BOOL)isPPShown;
@end

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,58 @@
@objc class PlacePageBuilder: NSObject {
@objc static func build(for data: PlacePageData) -> PlacePageViewController {
let storyboard = UIStoryboard.instance(.placePage)
guard let viewController = storyboard.instantiateInitialViewController() as? PlacePageViewController else {
fatalError()
}
viewController.isPreviewPlus = data.isPreviewPlus
let interactor = PlacePageInteractor(viewController: viewController,
data: data,
mapViewController: MapViewController.shared()!)
let layout: IPlacePageLayout
switch data.objectType {
case .POI, .bookmark:
layout = PlacePageCommonLayout(interactor: interactor, storyboard: storyboard, data: data)
case .track:
let trackLayout = PlacePageTrackLayout(interactor: interactor, storyboard: storyboard, data: data)
interactor.trackActivePointPresenter = trackLayout.elevationMapViewController?.presenter
layout = trackLayout
case .trackRecording:
layout = PlacePageTrackRecordingLayout(interactor: interactor, storyboard: storyboard, data: data)
@unknown default:
fatalError()
}
let presenter = PlacePagePresenter(view: viewController, headerView: layout.headerViewController)
viewController.setLayout(layout)
viewController.interactor = interactor
interactor.presenter = presenter
layout.presenter = presenter
return viewController
}
@objc static func update(_ viewController: PlacePageViewController, with data: PlacePageData) {
viewController.isPreviewPlus = data.isPreviewPlus
let interactor = PlacePageInteractor(viewController: viewController,
data: data,
mapViewController: MapViewController.shared()!)
let layout: IPlacePageLayout
let storyboard = viewController.storyboard!
switch data.objectType {
case .POI, .bookmark:
layout = PlacePageCommonLayout(interactor: interactor, storyboard: storyboard, data: data)
case .track:
let trackLayout = PlacePageTrackLayout(interactor: interactor, storyboard: storyboard, data: data)
interactor.trackActivePointPresenter = trackLayout.elevationMapViewController?.presenter
layout = trackLayout
case .trackRecording:
layout = PlacePageTrackRecordingLayout(interactor: interactor, storyboard: storyboard, data: data)
@unknown default:
fatalError()
}
let presenter = PlacePagePresenter(view: viewController, headerView: layout.headerViewController)
viewController.interactor = interactor
interactor.presenter = presenter
layout.presenter = presenter
viewController.updateWithLayout(layout)
viewController.updatePreviewOffset()
}
}

View file

@ -0,0 +1,438 @@
protocol PlacePageInteractorProtocol: AnyObject {
func viewWillAppear()
func viewWillDisappear()
func updateTopBound(_ bound: CGFloat, duration: TimeInterval)
}
class PlacePageInteractor: NSObject {
var presenter: PlacePagePresenterProtocol?
weak var viewController: UIViewController?
weak var mapViewController: MapViewController?
weak var trackActivePointPresenter: TrackActivePointPresenter?
private let bookmarksManager = BookmarksManager.shared()
private var placePageData: PlacePageData
private var viewWillAppearIsCalledForTheFirstTime = false
init(viewController: UIViewController, data: PlacePageData, mapViewController: MapViewController) {
self.placePageData = data
self.viewController = viewController
self.mapViewController = mapViewController
super.init()
addToBookmarksManagerObserverList()
subscribeOnTrackActivePointUpdatesIfNeeded()
}
deinit {
removeFromBookmarksManagerObserverList()
}
private func updatePlacePageIfNeeded() {
func updatePlacePage() {
FrameworkHelper.updatePlacePageData()
placePageData.updateBookmarkStatus()
}
switch placePageData.objectType {
case .POI, .trackRecording:
break
case .bookmark:
guard let bookmarkData = placePageData.bookmarkData, bookmarksManager.hasBookmark(bookmarkData.bookmarkId) else {
presenter?.closeAnimated()
return
}
updatePlacePage()
case .track:
guard let trackData = placePageData.trackData, bookmarksManager.hasTrack(trackData.trackId) else {
presenter?.closeAnimated()
return
}
updatePlacePage()
@unknown default:
fatalError("Unknown object type")
}
}
private func subscribeOnTrackActivePointUpdatesIfNeeded() {
unsubscribeFromTrackActivePointUpdates()
guard placePageData.objectType == .track, let trackData = placePageData.trackData else { return }
bookmarksManager.setElevationActivePointChanged(trackData.trackId) { [weak self] distance in
self?.trackActivePointPresenter?.updateActivePointDistance(distance)
trackData.updateActivePointDistance(distance)
}
bookmarksManager.setElevationMyPositionChanged(trackData.trackId) { [weak self] distance in
self?.trackActivePointPresenter?.updateMyPositionDistance(distance)
}
}
private func unsubscribeFromTrackActivePointUpdates() {
bookmarksManager.resetElevationActivePointChanged()
bookmarksManager.resetElevationMyPositionChanged()
}
private func addToBookmarksManagerObserverList() {
bookmarksManager.add(self)
}
private func removeFromBookmarksManagerObserverList() {
bookmarksManager.remove(self)
}
}
extension PlacePageInteractor: PlacePageInteractorProtocol {
func viewWillAppear() {
// Skip data reloading on the first appearance, to avoid unnecessary updates.
guard viewWillAppearIsCalledForTheFirstTime else {
viewWillAppearIsCalledForTheFirstTime = true
return
}
updatePlacePageIfNeeded()
}
func viewWillDisappear() {
unsubscribeFromTrackActivePointUpdates()
}
func updateTopBound(_ bound: CGFloat, duration: TimeInterval) {
mapViewController?.setPlacePageTopBound(bound, duration: duration)
}
}
// MARK: - PlacePageInfoViewControllerDelegate
extension PlacePageInteractor: PlacePageInfoViewControllerDelegate {
var shouldShowOpenInApp: Bool {
!OpenInApplication.availableApps.isEmpty
}
func didPressCall(to phone: PlacePagePhone) {
MWMPlacePageManagerHelper.call(phone)
}
func didPressWebsite() {
MWMPlacePageManagerHelper.openWebsite(placePageData)
}
func didPressWebsiteMenu() {
MWMPlacePageManagerHelper.openWebsiteMenu(placePageData)
}
func didPressWikipedia() {
MWMPlacePageManagerHelper.openWikipedia(placePageData)
}
func didPressWikimediaCommons() {
MWMPlacePageManagerHelper.openWikimediaCommons(placePageData)
}
func didPressFediverse() {
MWMPlacePageManagerHelper.openFediverse(placePageData)
}
func didPressFacebook() {
MWMPlacePageManagerHelper.openFacebook(placePageData)
}
func didPressInstagram() {
MWMPlacePageManagerHelper.openInstagram(placePageData)
}
func didPressTwitter() {
MWMPlacePageManagerHelper.openTwitter(placePageData)
}
func didPressVk() {
MWMPlacePageManagerHelper.openVk(placePageData)
}
func didPressLine() {
MWMPlacePageManagerHelper.openLine(placePageData)
}
func didPressBluesky() {
MWMPlacePageManagerHelper.openBluesky(placePageData)
}
func didPressPanoramax() {
MWMPlacePageManagerHelper.openPanoramax(placePageData)
}
func didPressEmail() {
MWMPlacePageManagerHelper.openEmail(placePageData)
}
func didCopy(_ content: String) {
UIPasteboard.general.string = content
let message = String(format: L("copied_to_clipboard"), content)
UIImpactFeedbackGenerator(style: .medium).impactOccurred()
Toast.show(withText: message, alignment: .bottom)
}
func didPressOpenInApp(from sourceView: UIView) {
let availableApps = OpenInApplication.availableApps
guard !availableApps.isEmpty else {
LOG(.warning, "Applications selection sheet should not be presented when the list of available applications is empty.")
return
}
let openInAppActionSheet = UIAlertController.presentInAppActionSheet(from: sourceView, apps: availableApps) { [weak self] selectedApp in
guard let self else { return }
let link = selectedApp.linkWith(coordinates: self.placePageData.locationCoordinate, destinationName: self.placePageData.previewData.title)
self.mapViewController?.openUrl(link, externally: true)
}
presenter?.showAlert(openInAppActionSheet)
}
}
// MARK: - WikiDescriptionViewControllerDelegate
extension PlacePageInteractor: WikiDescriptionViewControllerDelegate {
func didPressMore() {
MWMPlacePageManagerHelper.showPlaceDescription(placePageData.wikiDescriptionHtml)
}
}
// MARK: - PlacePageButtonsViewControllerDelegate
extension PlacePageInteractor: PlacePageButtonsViewControllerDelegate {
func didPressHotels() {
MWMPlacePageManagerHelper.openDescriptionUrl(placePageData)
}
func didPressAddPlace() {
MWMPlacePageManagerHelper.addPlace(placePageData.locationCoordinate)
}
func didPressEditPlace() {
MWMPlacePageManagerHelper.editPlace()
}
func didPressAddBusiness() {
MWMPlacePageManagerHelper.addBusiness()
}
}
// MARK: - PlacePageEditBookmarkOrTrackViewControllerDelegate
extension PlacePageInteractor: PlacePageEditBookmarkOrTrackViewControllerDelegate {
func didUpdate(color: UIColor, category: MWMMarkGroupID, for data: PlacePageEditData) {
switch data {
case .bookmark(let bookmarkData):
let bookmarkColor = BookmarkColor.bookmarkColor(from: color) ?? bookmarkData.color
MWMPlacePageManagerHelper.updateBookmark(placePageData, color: bookmarkColor, category: category)
case .track:
MWMPlacePageManagerHelper.updateTrack(placePageData, color: color, category: category)
}
}
func didPressEdit(_ data: PlacePageEditData) {
switch data {
case .bookmark:
MWMPlacePageManagerHelper.editBookmark(placePageData)
case .track:
MWMPlacePageManagerHelper.editTrack(placePageData)
}
}
}
// MARK: - ActionBarViewControllerDelegate
extension PlacePageInteractor: ActionBarViewControllerDelegate {
func actionBar(_ actionBar: ActionBarViewController, didPressButton type: ActionBarButtonType) {
switch type {
case .booking:
MWMPlacePageManagerHelper.book(placePageData)
case .bookingSearch:
MWMPlacePageManagerHelper.searchBookingHotels(placePageData)
case .bookmark:
if placePageData.bookmarkData != nil {
MWMPlacePageManagerHelper.removeBookmark(placePageData)
} else {
MWMPlacePageManagerHelper.addBookmark(placePageData)
}
case .call:
// since `.call` is a case in an obj-c enum, it can't have associated data, so there is no easy way to
// pass the exact phone, and we have to ask the user here which one to use, if there are multiple ones
let phones = placePageData.infoData?.phones ?? []
let hasOnePhoneNumber = phones.count == 1
if hasOnePhoneNumber {
MWMPlacePageManagerHelper.call(phones[0])
} else if (phones.count > 1) {
showPhoneNumberPicker(phones, handler: MWMPlacePageManagerHelper.call)
}
case .download:
guard let mapNodeAttributes = placePageData.mapNodeAttributes else {
fatalError("Download button can't be displayed if mapNodeAttributes is empty")
}
switch mapNodeAttributes.nodeStatus {
case .downloading, .inQueue, .applying:
Storage.shared().cancelDownloadNode(mapNodeAttributes.countryId)
case .notDownloaded, .partly, .error:
Storage.shared().downloadNode(mapNodeAttributes.countryId)
case .undefined, .onDiskOutOfDate, .onDisk:
fatalError("Download button shouldn't be displayed when node is in these states")
@unknown default:
fatalError()
}
case .opentable:
fatalError("Opentable is not supported and will be deleted")
case .routeAddStop:
MWMPlacePageManagerHelper.routeAddStop(placePageData)
case .routeFrom:
MWMPlacePageManagerHelper.route(from: placePageData)
case .routeRemoveStop:
MWMPlacePageManagerHelper.routeRemoveStop(placePageData)
case .routeTo:
MWMPlacePageManagerHelper.route(to: placePageData)
case .avoidToll:
MWMPlacePageManagerHelper.avoidToll()
case .avoidDirty:
MWMPlacePageManagerHelper.avoidDirty()
case .avoidFerry:
MWMPlacePageManagerHelper.avoidFerry()
case .more:
fatalError("More button should've been handled in ActionBarViewContoller")
case .track:
guard placePageData.trackData != nil else { return }
// TODO: (KK) This is temporary solution. Remove the dialog and use the MWMPlacePageManagerHelper.removeTrack
// directly here when the track recovery mechanism will be implemented.
showTrackDeletionConfirmationDialog()
case .saveTrackRecording:
// TODO: (KK) pass name typed by user
TrackRecordingManager.shared.stopAndSave() { [weak self] result in
switch result {
case .success:
break
case .trackIsEmpty:
self?.presenter?.closeAnimated()
}
}
case .notSaveTrackRecording:
TrackRecordingManager.shared.stop() { [weak self] result in
self?.presenter?.closeAnimated()
}
@unknown default:
fatalError()
}
}
private func showTrackDeletionConfirmationDialog() {
let alert = UIAlertController(title: nil, message: L("placepage_delete_track_confirmation_alert_message"), preferredStyle: .actionSheet)
let deleteAction = UIAlertAction(title: L("delete"), style: .destructive) { [weak self] _ in
guard let self = self else { return }
guard self.placePageData.trackData != nil else {
fatalError("The track data should not be nil during the track deletion")
}
MWMPlacePageManagerHelper.removeTrack(self.placePageData)
self.presenter?.closeAnimated()
}
let cancelAction = UIAlertAction(title: L("cancel"), style: .cancel)
alert.addAction(deleteAction)
alert.addAction(cancelAction)
guard let viewController else { return }
iPadSpecific {
alert.popoverPresentationController?.sourceView = viewController.view
alert.popoverPresentationController?.sourceRect = viewController.view.frame
}
viewController.present(alert, animated: true)
}
private func showPhoneNumberPicker(_ phones: [PlacePagePhone], handler: @escaping (PlacePagePhone) -> Void) {
guard let viewController else { return }
let alert = UIAlertController(title: nil, message: nil, preferredStyle: .actionSheet)
phones.forEach({phone in
alert.addAction(UIAlertAction(title: phone.phone, style: .default, handler: { _ in
handler(phone)
}))
})
alert.addAction(UIAlertAction(title: L("cancel"), style: .cancel))
viewController.present(alert, animated: true)
}
}
// MARK: - ElevationProfileViewControllerDelegate
extension PlacePageInteractor: ElevationProfileViewControllerDelegate {
func openDifficultyPopup() {
MWMPlacePageManagerHelper.openElevationDifficultPopup(placePageData)
}
func updateMapPoint(_ point: CLLocationCoordinate2D, distance: Double) {
guard let trackData = placePageData.trackData, trackData.elevationProfileData?.isTrackRecording == false else { return }
bookmarksManager.setElevationActivePoint(point, distance: distance, trackId: trackData.trackId)
placePageData.trackData?.updateActivePointDistance(distance)
}
}
// MARK: - PlacePageHeaderViewController
extension PlacePageInteractor: PlacePageHeaderViewControllerDelegate {
func previewDidPressClose() {
presenter?.closeAnimated()
}
func previewDidPressExpand() {
presenter?.showNextStop()
}
func previewDidPressShare(from sourceView: UIView) {
guard let mapViewController else { return }
switch placePageData.objectType {
case .POI, .bookmark:
let shareViewController = ActivityViewController.share(forPlacePage: placePageData)
shareViewController.present(inParentViewController: mapViewController, anchorView: sourceView)
case .track:
presenter?.showShareTrackMenu()
default:
guard let coordinates = LocationManager.lastLocation()?.coordinate else {
viewController?.present(UIAlertController.unknownCurrentPosition(), animated: true, completion: nil)
return
}
let activity = ActivityViewController.share(forMyPosition: coordinates)
activity.present(inParentViewController: mapViewController, anchorView: sourceView)
}
}
func previewDidPressExportTrack(_ type: KmlFileType, from sourceView: UIView) {
guard let trackId = placePageData.trackData?.trackId else {
fatalError("Track data should not be nil during the track export")
}
bookmarksManager.shareTrack(trackId, fileType: type) { [weak self] status, url in
guard let self, let mapViewController else { return }
switch status {
case .success:
guard let url else { fatalError("Invalid sharing url") }
let shareViewController = ActivityViewController.share(for: url, message: self.placePageData.previewData.title!) { _,_,_,_ in
self.bookmarksManager.finishSharing()
}
shareViewController.present(inParentViewController: mapViewController, anchorView: sourceView)
case .emptyCategory:
self.showAlert(withTitle: L("bookmarks_error_title_share_empty"),
message: L("bookmarks_error_message_share_empty"))
case .archiveError, .fileError:
self.showAlert(withTitle: L("dialog_routing_system_error"),
message: L("bookmarks_error_message_share_general"))
}
}
}
private func showAlert(withTitle title: String, message: String) {
MWMAlertViewController.activeAlert().presentInfoAlert(title, text: message)
}
}
// MARK: - BookmarksObserver
extension PlacePageInteractor: BookmarksObserver {
func onBookmarksLoadFinished() {
updatePlacePageIfNeeded()
}
func onBookmarksCategoryDeleted(_ groupId: MWMMarkGroupID) {
guard let bookmarkGroupId = placePageData.bookmarkData?.bookmarkGroupId else { return }
if bookmarkGroupId == groupId {
presenter?.closeAnimated()
}
}
}

View file

@ -0,0 +1,62 @@
typedef NS_ENUM(NSInteger, MWMActionBarButtonType) {
MWMActionBarButtonTypeBooking,
MWMActionBarButtonTypeBookingSearch,
MWMActionBarButtonTypeBookmark,
MWMActionBarButtonTypeTrack,
MWMActionBarButtonTypeSaveTrackRecording,
MWMActionBarButtonTypeNotSaveTrackRecording,
MWMActionBarButtonTypeCall,
MWMActionBarButtonTypeDownload,
MWMActionBarButtonTypeMore,
MWMActionBarButtonTypeOpentable,
MWMActionBarButtonTypeRouteAddStop,
MWMActionBarButtonTypeRouteFrom,
MWMActionBarButtonTypeRouteRemoveStop,
MWMActionBarButtonTypeRouteTo,
MWMActionBarButtonTypeAvoidToll,
MWMActionBarButtonTypeAvoidDirty,
MWMActionBarButtonTypeAvoidFerry
} NS_SWIFT_NAME(ActionBarButtonType);
typedef NS_ENUM(NSInteger, MWMBookmarksButtonState) {
MWMBookmarksButtonStateSave,
MWMBookmarksButtonStateDelete,
MWMBookmarksButtonStateRecover,
};
NS_ASSUME_NONNULL_BEGIN
#ifdef __cplusplus
extern "C" {
#endif
NSString * titleForButton(MWMActionBarButtonType type, BOOL isSelected);
#ifdef __cplusplus
}
#endif
@class MWMActionBarButton;
@class MWMCircularProgress;
NS_SWIFT_NAME(ActionBarButtonDelegate)
@protocol MWMActionBarButtonDelegate <NSObject>
- (void)tapOnButtonWithType:(MWMActionBarButtonType)type;
@end
NS_SWIFT_NAME(ActionBarButton)
@interface MWMActionBarButton : UIView
@property(nonatomic, readonly) MWMActionBarButtonType type;
@property(nonatomic, readonly, nullable) MWMCircularProgress *mapDownloadProgress;
+ (MWMActionBarButton *)buttonWithDelegate:(id<MWMActionBarButtonDelegate>)delegate
buttonType:(MWMActionBarButtonType)type
isSelected:(BOOL)isSelected
isEnabled:(BOOL)isEnabled;
- (void)setBookmarkButtonState:(MWMBookmarksButtonState)state;
@end
NS_ASSUME_NONNULL_END

View file

@ -0,0 +1,236 @@
#import "MWMActionBarButton.h"
#import "MWMButton.h"
#import "MWMCircularProgress.h"
#import "SwiftBridge.h"
static NSString * const kUDDidHighlightRouteToButton = @"kUDDidHighlightPoint2PointButton";
NSString *titleForButton(MWMActionBarButtonType type, BOOL isSelected) {
switch (type) {
case MWMActionBarButtonTypeDownload:
return L(@"download");
case MWMActionBarButtonTypeBooking:
case MWMActionBarButtonTypeOpentable:
return L(@"book_button");
case MWMActionBarButtonTypeBookingSearch:
return L(@"booking_search");
case MWMActionBarButtonTypeCall:
return L(@"placepage_call_button");
case MWMActionBarButtonTypeBookmark:
case MWMActionBarButtonTypeTrack:
return L(isSelected ? @"delete" : @"save");
case MWMActionBarButtonTypeSaveTrackRecording:
return L(@"save");
case MWMActionBarButtonTypeNotSaveTrackRecording:
return L(@"delete");
case MWMActionBarButtonTypeRouteFrom:
return L(@"p2p_from_here");
case MWMActionBarButtonTypeRouteTo:
return L(@"p2p_to_here");
case MWMActionBarButtonTypeMore:
return L(@"placepage_more_button");
case MWMActionBarButtonTypeRouteAddStop:
return L(@"placepage_add_stop");
case MWMActionBarButtonTypeRouteRemoveStop:
return L(@"placepage_remove_stop");
case MWMActionBarButtonTypeAvoidToll:
return L(@"avoid_tolls");
case MWMActionBarButtonTypeAvoidDirty:
return L(@"avoid_unpaved");
case MWMActionBarButtonTypeAvoidFerry:
return L(@"avoid_ferry");
}
}
@interface MWMActionBarButton () <MWMCircularProgressProtocol>
@property(nonatomic) MWMActionBarButtonType type;
@property(nonatomic) MWMCircularProgress *mapDownloadProgress;
@property(weak, nonatomic) IBOutlet MWMButton *button;
@property(weak, nonatomic) IBOutlet UILabel *label;
@property(weak, nonatomic) IBOutlet UIView *extraBackground;
@property(weak, nonatomic) id<MWMActionBarButtonDelegate> delegate;
@end
@implementation MWMActionBarButton
- (void)configButton:(BOOL)isSelected enabled:(BOOL)isEnabled {
self.label.text = titleForButton(self.type, isSelected);
self.extraBackground.hidden = YES;
self.button.coloring = MWMButtonColoringBlack;
[self.button.imageView setContentMode:UIViewContentModeScaleAspectFit];
switch (self.type) {
case MWMActionBarButtonTypeDownload: {
if (self.mapDownloadProgress)
return;
self.mapDownloadProgress = [MWMCircularProgress downloaderProgressForParentView:self.button];
self.mapDownloadProgress.delegate = self;
MWMCircularProgressStateVec affectedStates =
@[@(MWMCircularProgressStateNormal), @(MWMCircularProgressStateSelected)];
[self.mapDownloadProgress setImageName:@"ic_download" forStates:affectedStates];
[self.mapDownloadProgress setColoring:MWMButtonColoringBlue forStates:affectedStates];
break;
}
case MWMActionBarButtonTypeBooking:
[self.button setImage:[UIImage imageNamed:@"ic_booking_logo"] forState:UIControlStateNormal];
self.label.styleName = @"PPActionBarTitlePartner";
self.backgroundColor = [UIColor bookingBackground];
if (!IPAD) {
self.extraBackground.backgroundColor = [UIColor bookingBackground];
self.extraBackground.hidden = NO;
}
break;
case MWMActionBarButtonTypeBookingSearch:
[self.button setImage:[UIImage imageNamed:@"ic_booking_search"] forState:UIControlStateNormal];
self.label.styleName = @"PPActionBarTitlePartner";
self.backgroundColor = [UIColor bookingBackground];
if (!IPAD) {
self.extraBackground.backgroundColor = [UIColor bookingBackground];
self.extraBackground.hidden = NO;
}
break;
case MWMActionBarButtonTypeOpentable:
[self.button setImage:[UIImage imageNamed:@"ic_opentable"] forState:UIControlStateNormal];
self.label.styleName = @"PPActionBarTitlePartner";
self.backgroundColor = [UIColor opentableBackground];
if (!IPAD) {
self.extraBackground.backgroundColor = [UIColor opentableBackground];
self.extraBackground.hidden = NO;
}
break;
case MWMActionBarButtonTypeCall:
[self.button setImage:[UIImage imageNamed:@"ic_placepage_phone_number"] forState:UIControlStateNormal];
break;
case MWMActionBarButtonTypeBookmark:
[self setupBookmarkButton:isSelected];
break;
case MWMActionBarButtonTypeTrack:
[self.button setImage:[[UIImage imageNamed:@"ic_route_manager_trash"] imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate] forState:UIControlStateNormal];
self.button.coloring = MWMButtonColoringRed;
break;
case MWMActionBarButtonTypeSaveTrackRecording:
[self.button setImage:[UIImage systemImageNamed:@"square.and.arrow.down" withConfiguration:[UIImageSymbolConfiguration configurationWithPointSize:22 weight:UIImageSymbolWeightSemibold]] forState:UIControlStateNormal];
break;
case MWMActionBarButtonTypeNotSaveTrackRecording:
[self.button setImage:[UIImage systemImageNamed:@"trash.fill" withConfiguration:[UIImageSymbolConfiguration configurationWithPointSize:22 weight:UIImageSymbolWeightSemibold]] forState:UIControlStateNormal];
self.button.coloring = MWMButtonColoringRed;
break;
case MWMActionBarButtonTypeRouteFrom:
[self.button setImage:[UIImage imageNamed:@"ic_route_from"] forState:UIControlStateNormal];
break;
case MWMActionBarButtonTypeRouteTo:
[self.button setImage:[UIImage imageNamed:@"ic_route_to"] forState:UIControlStateNormal];
if ([self needsToHighlightRouteToButton])
self.button.coloring = MWMButtonColoringBlue;
break;
case MWMActionBarButtonTypeMore:
[self.button setImage:[UIImage imageNamed:@"ic_placepage_more"] forState:UIControlStateNormal];
break;
case MWMActionBarButtonTypeRouteAddStop:
[self.button setImage:[UIImage imageNamed:@"ic_add_route_point"] forState:UIControlStateNormal];
break;
case MWMActionBarButtonTypeRouteRemoveStop:
[self.button setImage:[UIImage imageNamed:@"ic_remove_route_point"] forState:UIControlStateNormal];
break;
case MWMActionBarButtonTypeAvoidToll:
[self.button setImage:[UIImage imageNamed:@"ic_avoid_tolls"] forState:UIControlStateNormal];
break;
case MWMActionBarButtonTypeAvoidDirty:
[self.button setImage:[UIImage imageNamed:@"ic_avoid_dirty"] forState:UIControlStateNormal];
break;
case MWMActionBarButtonTypeAvoidFerry:
[self.button setImage:[UIImage imageNamed:@"ic_avoid_ferry"] forState:UIControlStateNormal];
break;
}
self.button.enabled = isEnabled;
}
+ (MWMActionBarButton *)buttonWithDelegate:(id<MWMActionBarButtonDelegate>)delegate
buttonType:(MWMActionBarButtonType)type
isSelected:(BOOL)isSelected
isEnabled:(BOOL)isEnabled {
MWMActionBarButton *button = [NSBundle.mainBundle loadNibNamed:[self className] owner:nil options:nil].firstObject;
button.delegate = delegate;
button.type = type;
[button configButton:isSelected enabled:isEnabled];
return button;
}
- (void)progressButtonPressed:(MWMCircularProgress *)progress {
[self.delegate tapOnButtonWithType:self.type];
}
- (IBAction)tap {
if (self.type == MWMActionBarButtonTypeRouteTo)
[self disableRouteToButtonHighlight];
[self.delegate tapOnButtonWithType:self.type];
}
- (void)setBookmarkButtonState:(MWMBookmarksButtonState)state {
switch (state) {
case MWMBookmarksButtonStateSave:
self.label.text = L(@"save");
self.button.selected = false;
break;
case MWMBookmarksButtonStateDelete:
self.label.text = L(@"delete");
if (!self.button.selected)
[self.button.imageView startAnimating];
self.button.selected = true;
break;
case MWMBookmarksButtonStateRecover:
self.label.text = L(@"restore");
self.button.selected = false;
break;
}
}
- (void)setupBookmarkButton:(BOOL)isSelected {
MWMButton *btn = self.button;
[btn setImage:[UIImage imageNamed:@"ic_bookmarks_off"] forState:UIControlStateNormal];
[btn setImage:[UIImage imageNamed:@"ic_bookmarks_on"] forState:UIControlStateSelected];
[btn setImage:[UIImage imageNamed:@"ic_bookmarks_on"] forState:UIControlStateHighlighted];
[btn setImage:[UIImage imageNamed:@"ic_bookmarks_on"] forState:UIControlStateDisabled];
[btn setSelected:isSelected];
NSUInteger const animationImagesCount = 11;
NSMutableArray *animationImages = [NSMutableArray arrayWithCapacity:animationImagesCount];
for (NSUInteger i = 0; i < animationImagesCount; ++i) {
UIImage *image = [UIImage imageNamed:[NSString stringWithFormat:@"ic_bookmarks_%@", @(i + 1)]];
animationImages[i] = image;
}
UIImageView *animationIV = btn.imageView;
animationIV.animationImages = animationImages;
animationIV.animationRepeatCount = 1;
}
- (BOOL)needsToHighlightRouteToButton {
return ![NSUserDefaults.standardUserDefaults boolForKey:kUDDidHighlightRouteToButton];
}
- (void)disableRouteToButtonHighlight {
[NSUserDefaults.standardUserDefaults setBool:true forKey:kUDDidHighlightRouteToButton];
}
- (void)traitCollectionDidChange:(UITraitCollection *)previousTraitCollection {
[super traitCollectionDidChange:previousTraitCollection];
if (@available(iOS 13.0, *)) {
if ([self.traitCollection hasDifferentColorAppearanceComparedToTraitCollection:previousTraitCollection])
// Update button for the current selection state.
[self.button setSelected:self.button.isSelected];
}
}
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
return [self pointInside:point withEvent:event] ? self.button : nil;
}
@end

View file

@ -0,0 +1,63 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="21701" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" colorMatched="YES">
<device id="retina6_1" orientation="portrait" appearance="light"/>
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="21679"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<objects>
<placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner" customClass="MWMActionBarButton"/>
<placeholder placeholderIdentifier="IBFirstResponder" id="-2" customClass="UIResponder"/>
<view contentMode="scaleToFill" id="iN0-l3-epB" customClass="MWMActionBarButton" propertyAccessControl="none">
<rect key="frame" x="0.0" y="0.0" width="80" height="48"/>
<autoresizingMask key="autoresizingMask" flexibleMinX="YES" flexibleMaxX="YES" flexibleMinY="YES" flexibleMaxY="YES"/>
<subviews>
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="R50-Tj-X0W">
<rect key="frame" x="0.0" y="0.0" width="80" height="48"/>
<color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
</view>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="W07-Hz-J60" customClass="MWMButton">
<rect key="frame" x="0.0" y="2" width="80" height="33"/>
<connections>
<action selector="tap" destination="iN0-l3-epB" eventType="touchUpInside" id="yKY-7K-Wyl"/>
</connections>
</button>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Label" textAlignment="center" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" preferredMaxLayoutWidth="80" translatesAutoresizingMaskIntoConstraints="NO" id="rrI-0A-w3s">
<rect key="frame" x="0.0" y="32" width="80" height="14"/>
<constraints>
<constraint firstAttribute="height" constant="14" id="BBl-pC-RJq"/>
</constraints>
<fontDescription key="fontDescription" type="system" pointSize="17"/>
<color key="textColor" red="0.0" green="0.0" blue="0.0" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<nil key="highlightedColor"/>
<userDefinedRuntimeAttributes>
<userDefinedRuntimeAttribute type="string" keyPath="styleName" value="PPActionBarTitle"/>
</userDefinedRuntimeAttributes>
</label>
</subviews>
<color key="backgroundColor" red="0.0" green="0.0" blue="0.0" alpha="0.0" colorSpace="custom" customColorSpace="sRGB"/>
<constraints>
<constraint firstAttribute="trailing" secondItem="R50-Tj-X0W" secondAttribute="trailing" id="2jF-U9-Gei"/>
<constraint firstAttribute="trailing" secondItem="rrI-0A-w3s" secondAttribute="trailing" id="LPs-Yx-xz6"/>
<constraint firstItem="rrI-0A-w3s" firstAttribute="top" secondItem="W07-Hz-J60" secondAttribute="bottom" constant="-3" id="SMD-s3-Tz5"/>
<constraint firstItem="W07-Hz-J60" firstAttribute="top" secondItem="iN0-l3-epB" secondAttribute="top" constant="2" id="UJy-Ef-B7E"/>
<constraint firstItem="rrI-0A-w3s" firstAttribute="leading" secondItem="iN0-l3-epB" secondAttribute="leading" id="X6f-tU-o9a"/>
<constraint firstItem="R50-Tj-X0W" firstAttribute="top" secondItem="iN0-l3-epB" secondAttribute="top" id="ZHQ-XD-E90"/>
<constraint firstAttribute="bottom" secondItem="rrI-0A-w3s" secondAttribute="bottom" constant="2" id="Zsi-G2-yc8"/>
<constraint firstAttribute="bottom" secondItem="R50-Tj-X0W" secondAttribute="bottom" id="adE-76-Hab"/>
<constraint firstItem="R50-Tj-X0W" firstAttribute="leading" secondItem="iN0-l3-epB" secondAttribute="leading" id="qid-13-F5b"/>
<constraint firstItem="W07-Hz-J60" firstAttribute="leading" secondItem="iN0-l3-epB" secondAttribute="leading" id="rBR-of-5Ha"/>
<constraint firstAttribute="trailing" secondItem="W07-Hz-J60" secondAttribute="trailing" id="teM-gm-CX7"/>
</constraints>
<nil key="simulatedStatusBarMetrics"/>
<freeformSimulatedSizeMetrics key="simulatedDestinationMetrics"/>
<connections>
<outlet property="button" destination="W07-Hz-J60" id="dAN-CS-btL"/>
<outlet property="extraBackground" destination="R50-Tj-X0W" id="c90-1d-BSU"/>
<outlet property="label" destination="rrI-0A-w3s" id="LMD-pz-agZ"/>
</connections>
<point key="canvasLocation" x="139" y="154"/>
</view>
</objects>
</document>

View file

@ -0,0 +1,45 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="13771" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" colorMatched="YES">
<device id="retina4_7" orientation="portrait">
<adaptation id="fullscreen"/>
</device>
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="13772"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<objects>
<placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner"/>
<placeholder placeholderIdentifier="IBFirstResponder" id="-2" customClass="UIResponder"/>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" selectionStyle="default" indentationWidth="10" reuseIdentifier="MWMOpeningHoursCell" id="Ive-iN-SIs" customClass="MWMOpeningHoursCell" propertyAccessControl="all">
<rect key="frame" x="0.0" y="0.0" width="375" height="44"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" tableViewCell="Ive-iN-SIs" id="ab9-am-bMR">
<rect key="frame" x="0.0" y="0.0" width="375" height="43.5"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<tableView clipsSubviews="YES" contentMode="scaleToFill" alwaysBounceVertical="YES" scrollEnabled="NO" style="plain" separatorStyle="none" allowsSelection="NO" rowHeight="44" sectionHeaderHeight="28" sectionFooterHeight="28" translatesAutoresizingMaskIntoConstraints="NO" id="JR5-1q-ZuW">
<rect key="frame" x="0.0" y="0.0" width="375" height="43.5"/>
<color key="backgroundColor" white="1" alpha="1" colorSpace="calibratedWhite"/>
<constraints>
<constraint firstAttribute="height" priority="750" constant="43" id="JLa-25-thU"/>
</constraints>
<color key="separatorColor" white="0.0" alpha="0.0" colorSpace="calibratedWhite"/>
<inset key="separatorInset" minX="60" minY="0.0" maxX="0.0" maxY="0.0"/>
</tableView>
</subviews>
<constraints>
<constraint firstAttribute="trailing" secondItem="JR5-1q-ZuW" secondAttribute="trailing" id="59Z-Q5-25r"/>
<constraint firstAttribute="bottom" secondItem="JR5-1q-ZuW" secondAttribute="bottom" id="P8D-kD-kMF"/>
<constraint firstItem="JR5-1q-ZuW" firstAttribute="leading" secondItem="ab9-am-bMR" secondAttribute="leading" id="Xox-6I-JsG"/>
<constraint firstItem="JR5-1q-ZuW" firstAttribute="top" secondItem="ab9-am-bMR" secondAttribute="top" id="hgZ-Jr-yDd"/>
</constraints>
</tableViewCellContentView>
<connections>
<outlet property="tableView" destination="JR5-1q-ZuW" id="Qyu-x3-lTv"/>
<outlet property="tableViewHeight" destination="JLa-25-thU" id="dtQ-TV-gso"/>
</connections>
<point key="canvasLocation" x="-872.5" y="-141"/>
</tableViewCell>
</objects>
</document>

View file

@ -0,0 +1,22 @@
#import "MWMTableViewCell.h"
@protocol MWMPlacePageOpeningHoursCellProtocol <NSObject>
- (BOOL)forcedButton;
- (BOOL)isPlaceholder;
- (BOOL)isEditor;
- (BOOL)openingHoursCellExpanded;
- (void)setOpeningHoursCellExpanded:(BOOL)openingHoursCellExpanded;
@end
@interface MWMPlacePageOpeningHoursCell : MWMTableViewCell
@property (nonatomic, readonly) BOOL isClosed;
- (void)configWithDelegate:(id<MWMPlacePageOpeningHoursCellProtocol>)delegate
info:(NSString *)info;
- (CGFloat)cellHeight;
@end

View file

@ -0,0 +1,272 @@
#import "MWMPlacePageOpeningHoursCell.h"
#import <CoreApi/MWMCommon.h>
#import <CoreApi/MWMOpeningHoursCommon.h>
#import "MWMPlacePageOpeningHoursDayView.h"
#import "SwiftBridge.h"
#include "editor/ui2oh.hpp"
using namespace editor;
using namespace osmoh;
using WeekDayView = MWMPlacePageOpeningHoursDayView *;
@interface MWMPlacePageOpeningHoursCell ()
@property(weak, nonatomic) IBOutlet WeekDayView currentDay;
@property(weak, nonatomic) IBOutlet UIView * middleSeparator;
@property(weak, nonatomic) IBOutlet UIView * weekDaysView;
@property(weak, nonatomic) IBOutlet UIImageView * expandImage;
@property(weak, nonatomic) IBOutlet UIButton * toggleButton;
@property(weak, nonatomic) IBOutlet UILabel * openTime;
@property(weak, nonatomic) IBOutlet NSLayoutConstraint * openTimeLeadingOffset;
@property(weak, nonatomic) IBOutlet NSLayoutConstraint * openTimeTrailingOffset;
@property(weak, nonatomic) IBOutlet NSLayoutConstraint * weekDaysViewHeight;
@property(nonatomic) CGFloat weekDaysViewEstimatedHeight;
@property(weak, nonatomic) id<MWMPlacePageOpeningHoursCellProtocol> delegate;
@property(nonatomic, readwrite) BOOL isClosed;
@property(nonatomic) BOOL haveExpandSchedule;
@end
NSString * stringFromTimeSpan(Timespan const & timeSpan)
{
return [NSString stringWithFormat:@"%@-%@", stringFromTime(timeSpan.GetStart()),
stringFromTime(timeSpan.GetEnd())];
}
NSArray<NSString *> * arrayFromClosedTimes(TTimespans const & closedTimes)
{
NSMutableArray<NSString *> * breaks = [NSMutableArray arrayWithCapacity:closedTimes.size()];
for (auto & ct : closedTimes)
{
[breaks addObject:stringFromTimeSpan(ct)];
}
return [breaks copy];
}
WeekDayView getWeekDayView()
{
return [NSBundle.mainBundle loadNibNamed:@"MWMPlacePageOpeningHoursWeekDayView"
owner:nil
options:nil]
.firstObject;
}
@implementation MWMPlacePageOpeningHoursCell
{
ui::TimeTableSet timeTableSet;
}
- (void)configWithDelegate:(id<MWMPlacePageOpeningHoursCellProtocol>)delegate info:(NSString *)info
{
self.delegate = delegate;
WeekDayView cd = self.currentDay;
cd.currentDay = YES;
self.toggleButton.hidden = !delegate.forcedButton;
self.expandImage.hidden = !delegate.forcedButton;
self.expandImage.image = [UIImage imageNamed:@"ic_arrow_gray_right"];
self.expandImage.styleName = @"MWMGray";
if (isInterfaceRightToLeft())
self.expandImage.transform = CGAffineTransformMakeScale(-1, 1);
NSAssert(info, @"Schedule can not be empty");
osmoh::OpeningHours oh(info.UTF8String);
if (MakeTimeTableSet(oh, timeTableSet))
{
cd.isCompatibility = NO;
if (delegate.isEditor)
self.isClosed = NO;
else
self.isClosed = oh.IsClosed(time(nullptr));
[self processSchedule];
}
else
{
cd.isCompatibility = YES;
[cd setCompatibilityText:info isPlaceholder:delegate.isPlaceholder];
}
BOOL const isHidden = !self.isExpanded;
self.middleSeparator.hidden = isHidden;
self.weekDaysView.hidden = isHidden;
[cd invalidate];
}
- (void)processSchedule
{
NSCalendar * cal = NSCalendar.currentCalendar;
cal.locale = NSLocale.currentLocale;
Weekday currentDay =
static_cast<Weekday>([cal components:NSCalendarUnitWeekday fromDate:[NSDate date]].weekday);
BOOL haveCurrentDay = NO;
size_t timeTablesCount = timeTableSet.Size();
self.haveExpandSchedule = (timeTablesCount > 1 || !timeTableSet.GetUnhandledDays().empty());
self.weekDaysViewEstimatedHeight = 0.0;
[self.weekDaysView.subviews makeObjectsPerformSelector:@selector(removeFromSuperview)];
for (size_t idx = 0; idx < timeTablesCount; ++idx)
{
auto tt = timeTableSet.Get(idx);
ui::OpeningDays const & workingDays = tt.GetOpeningDays();
if (workingDays.find(currentDay) != workingDays.end())
{
haveCurrentDay = YES;
[self addCurrentDay:tt];
}
if (self.isExpanded)
[self addWeekDays:tt];
}
if (!haveCurrentDay)
[self addEmptyCurrentDay];
id<MWMPlacePageOpeningHoursCellProtocol> delegate = self.delegate;
if (self.haveExpandSchedule)
{
self.toggleButton.hidden = NO;
self.expandImage.hidden = NO;
if (delegate.forcedButton)
self.expandImage.image = [UIImage imageNamed:@"ic_arrow_gray_right"];
else if (self.isExpanded)
self.expandImage.image = [UIImage imageNamed:@"ic_arrow_gray_up"];
else
self.expandImage.image = [UIImage imageNamed:@"ic_arrow_gray_down"];
self.expandImage.styleName = @"MWMGray";
if (isInterfaceRightToLeft())
self.expandImage.transform = CGAffineTransformMakeScale(-1, 1);
if (self.isExpanded)
[self addClosedDays];
}
self.openTimeTrailingOffset.priority =
delegate.forcedButton ? UILayoutPriorityDefaultHigh : UILayoutPriorityDefaultLow;
self.weekDaysViewHeight.constant = ceil(self.weekDaysViewEstimatedHeight);
[self alignTimeOffsets];
}
- (void)addCurrentDay:(ui::TimeTableSet::Proxy)timeTable
{
WeekDayView cd = self.currentDay;
NSString * label;
NSString * openTime;
NSArray<NSString *> * breaks;
BOOL const everyDay = isEveryDay(timeTable);
if (timeTable.IsTwentyFourHours())
{
label = everyDay ? L(@"twentyfour_seven") : L(@"editor_time_allday");
openTime = @"";
breaks = @[];
}
else
{
self.haveExpandSchedule |= !everyDay;
label = everyDay ? L(@"daily") : L(@"today");
openTime = stringFromTimeSpan(timeTable.GetOpeningTime());
breaks = arrayFromClosedTimes(timeTable.GetExcludeTime());
}
[cd setLabelText:label isRed:NO];
[cd setOpenTimeText:openTime];
[cd setBreaks:breaks];
[cd setClosed:self.isClosed];
}
- (void)addEmptyCurrentDay
{
WeekDayView cd = self.currentDay;
[cd setLabelText:L(@"day_off_today") isRed:YES];
[cd setOpenTimeText:@""];
[cd setBreaks:@[]];
[cd setClosed:NO];
}
- (void)addWeekDays:(ui::TimeTableSet::Proxy)timeTable
{
WeekDayView wd = getWeekDayView();
wd.currentDay = NO;
wd.frame = {{0, self.weekDaysViewEstimatedHeight}, {self.weekDaysView.width, 0}};
[wd setLabelText:stringFromOpeningDays(timeTable.GetOpeningDays()) isRed:NO];
if (timeTable.IsTwentyFourHours())
{
BOOL const everyDay = isEveryDay(timeTable);
[wd setOpenTimeText:everyDay ? L(@"twentyfour_seven") : L(@"editor_time_allday")];
[wd setBreaks:@[]];
}
else
{
[wd setOpenTimeText:stringFromTimeSpan(timeTable.GetOpeningTime())];
[wd setBreaks:arrayFromClosedTimes(timeTable.GetExcludeTime())];
}
[wd invalidate];
[self.weekDaysView addSubview:wd];
self.weekDaysViewEstimatedHeight += wd.viewHeight;
}
- (void)addClosedDays
{
editor::ui::OpeningDays closedDays = timeTableSet.GetUnhandledDays();
if (closedDays.empty())
return;
WeekDayView wd = getWeekDayView();
wd.currentDay = NO;
wd.frame = {{0, self.weekDaysViewEstimatedHeight}, {self.weekDaysView.width, 0}};
[wd setLabelText:stringFromOpeningDays(closedDays) isRed:NO];
[wd setOpenTimeText:L(@"day_off")];
[wd setBreaks:@[]];
[wd invalidate];
[self.weekDaysView addSubview:wd];
self.weekDaysViewEstimatedHeight += wd.viewHeight;
}
- (void)alignTimeOffsets
{
CGFloat offset = self.openTime.minX;
for (WeekDayView wd in self.weekDaysView.subviews)
offset = MAX(offset, wd.openTimeLeadingOffset);
for (WeekDayView wd in self.weekDaysView.subviews)
wd.openTimeLeadingOffset = offset;
}
- (CGFloat)cellHeight
{
CGFloat height = self.currentDay.viewHeight;
if (self.isExpanded)
{
CGFloat const bottomOffset = 4.0;
height += bottomOffset;
if (!self.currentDay.isCompatibility)
height += self.weekDaysViewHeight.constant;
}
return ceil(height);
}
#pragma mark - Actions
- (IBAction)toggleButtonTap
{
id<MWMPlacePageOpeningHoursCellProtocol> delegate = self.delegate;
[delegate setOpeningHoursCellExpanded:!delegate.openingHoursCellExpanded];
// Workaround for slow devices.
// Major QA can tap multiple times before first segue call is performed.
// This leads to multiple identical controllers to be pushed.
self.toggleButton.enabled = NO;
dispatch_async(dispatch_get_main_queue(), ^{
self.toggleButton.enabled = YES;
});
}
#pragma mark - Properties
- (BOOL)isExpanded
{
if (self.currentDay.isCompatibility || !self.haveExpandSchedule)
return NO;
return self.delegate.openingHoursCellExpanded;
}
@end

View file

@ -0,0 +1,215 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="15702" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" colorMatched="YES">
<device id="retina4_7" orientation="portrait" appearance="light"/>
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="15704"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<objects>
<placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner"/>
<placeholder placeholderIdentifier="IBFirstResponder" id="-2" customClass="UIResponder"/>
<tableViewCell contentMode="scaleToFill" selectionStyle="none" indentationWidth="10" id="KGk-i7-Jjw" customClass="MWMPlacePageOpeningHoursCell" propertyAccessControl="none">
<rect key="frame" x="0.0" y="0.0" width="320" height="249"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" tableViewCell="KGk-i7-Jjw" id="H2p-sc-9uM">
<rect key="frame" x="0.0" y="0.0" width="320" height="249"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="swk-um-XzG" customClass="MWMPlacePageOpeningHoursDayView">
<rect key="frame" x="0.0" y="0.0" width="320" height="126"/>
<subviews>
<label hidden="YES" opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Mo-Su 11:00-24:00" textAlignment="natural" lineBreakMode="tailTruncation" numberOfLines="0" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" preferredMaxLayoutWidth="228" translatesAutoresizingMaskIntoConstraints="NO" id="ZdV-4y-cz4">
<rect key="frame" x="60" y="53.5" width="228" height="19"/>
<fontDescription key="fontDescription" type="system" pointSize="16"/>
<color key="textColor" red="0.0" green="0.0" blue="0.0" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<nil key="highlightedColor"/>
<userDefinedRuntimeAttributes>
<userDefinedRuntimeAttribute type="string" keyPath="styleName" value="regular16:blackPrimaryText"/>
</userDefinedRuntimeAttributes>
</label>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" horizontalCompressionResistancePriority="300" text="Сегодня" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" minimumFontSize="14" preferredMaxLayoutWidth="68" translatesAutoresizingMaskIntoConstraints="NO" id="Ot5-QJ-jhp">
<rect key="frame" x="60" y="12" width="68" height="20"/>
<constraints>
<constraint firstAttribute="width" relation="greaterThanOrEqual" priority="750" constant="68" id="5G1-mL-J4T"/>
<constraint firstAttribute="height" constant="20" id="Uo2-AE-U2v"/>
</constraints>
<fontDescription key="fontDescription" type="system" pointSize="16"/>
<color key="textColor" red="0.0" green="0.0" blue="0.0" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<nil key="highlightedColor"/>
<userDefinedRuntimeAttributes>
<userDefinedRuntimeAttribute type="string" keyPath="styleName" value="regular16:blackPrimaryText"/>
</userDefinedRuntimeAttributes>
</label>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="10:00—20:00" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" preferredMaxLayoutWidth="140" translatesAutoresizingMaskIntoConstraints="NO" id="oTF-IZ-Un1">
<rect key="frame" x="140" y="12" width="140" height="19"/>
<constraints>
<constraint firstAttribute="width" relation="greaterThanOrEqual" priority="300" constant="140" id="up3-Kv-Z1P"/>
</constraints>
<fontDescription key="fontDescription" type="system" pointSize="16"/>
<color key="textColor" red="0.0" green="0.0" blue="0.0" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<nil key="highlightedColor"/>
<userDefinedRuntimeAttributes>
<userDefinedRuntimeAttribute type="string" keyPath="styleName" value="regular16:blackPrimaryText"/>
</userDefinedRuntimeAttributes>
</label>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Перерыв" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" preferredMaxLayoutWidth="94" translatesAutoresizingMaskIntoConstraints="NO" id="hpw-oR-ZSb">
<rect key="frame" x="60" y="36" width="68" height="16"/>
<constraints>
<constraint firstAttribute="width" relation="greaterThanOrEqual" priority="750" constant="68" id="Aji-QM-nRY"/>
</constraints>
<fontDescription key="fontDescription" type="system" pointSize="14"/>
<color key="textColor" red="0.0" green="0.0" blue="0.0" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<nil key="highlightedColor"/>
<userDefinedRuntimeAttributes>
<userDefinedRuntimeAttribute type="string" keyPath="styleName" value="regular14:blackSecondaryText"/>
<userDefinedRuntimeAttribute type="string" keyPath="localizedText" value="editor_hours_closed"/>
</userDefinedRuntimeAttributes>
</label>
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="7Oa-hg-icC">
<rect key="frame" x="140" y="36" width="140" height="58"/>
<color key="backgroundColor" red="0.0" green="0.0" blue="0.0" alpha="0.0" colorSpace="custom" customColorSpace="sRGB"/>
<constraints>
<constraint firstAttribute="height" constant="58" id="RWf-JS-tim"/>
</constraints>
</view>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Сейчас закрыто" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" preferredMaxLayoutWidth="107" translatesAutoresizingMaskIntoConstraints="NO" id="EcD-Q8-7zu">
<rect key="frame" x="60" y="98" width="106" height="16"/>
<constraints>
<constraint firstAttribute="height" constant="16" id="LKy-Dc-veQ"/>
</constraints>
<fontDescription key="fontDescription" type="system" pointSize="14"/>
<color key="textColor" red="1" green="0.0" blue="0.0" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<nil key="highlightedColor"/>
<userDefinedRuntimeAttributes>
<userDefinedRuntimeAttribute type="string" keyPath="styleName" value="redText"/>
<userDefinedRuntimeAttribute type="string" keyPath="localizedText" value="closed_now"/>
</userDefinedRuntimeAttributes>
</label>
<imageView userInteractionEnabled="NO" contentMode="center" horizontalHuggingPriority="251" verticalHuggingPriority="251" image="ic_arrow_gray_down" translatesAutoresizingMaskIntoConstraints="NO" id="mGc-k4-uvQ">
<rect key="frame" x="288" y="51" width="24" height="24"/>
<constraints>
<constraint firstAttribute="width" constant="24" id="514-4Z-QO3"/>
<constraint firstAttribute="height" constant="24" id="OJ4-4J-1uv"/>
</constraints>
<userDefinedRuntimeAttributes>
<userDefinedRuntimeAttribute type="string" keyPath="styleName" value="MWMGray"/>
</userDefinedRuntimeAttributes>
</imageView>
<imageView userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" image="ic_placepage_open_hours" translatesAutoresizingMaskIntoConstraints="NO" id="pa0-fe-w8W">
<rect key="frame" x="16" y="49" width="28" height="28"/>
<constraints>
<constraint firstAttribute="width" constant="28" id="2AI-Zc-VlL"/>
<constraint firstAttribute="height" constant="28" id="gd5-OU-PDF"/>
</constraints>
<userDefinedRuntimeAttributes>
<userDefinedRuntimeAttribute type="string" keyPath="styleName" value="MWMBlack"/>
</userDefinedRuntimeAttributes>
</imageView>
<button opaque="NO" contentMode="scaleToFill" fixedFrame="YES" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="3Fa-V6-tC5" userLabel="Toggle Button">
<rect key="frame" x="0.0" y="0.0" width="320" height="248"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<color key="backgroundColor" red="0.0" green="0.0" blue="0.0" alpha="0.0" colorSpace="custom" customColorSpace="sRGB"/>
<state key="normal">
<color key="titleColor" red="0.0" green="0.0" blue="0.0" alpha="0.0" colorSpace="custom" customColorSpace="sRGB"/>
<color key="titleShadowColor" red="0.0" green="0.0" blue="0.0" alpha="0.0" colorSpace="custom" customColorSpace="sRGB"/>
</state>
<connections>
<action selector="toggleButtonTap" destination="KGk-i7-Jjw" eventType="touchUpInside" id="XDD-YV-Lea"/>
</connections>
</button>
</subviews>
<color key="backgroundColor" red="0.0" green="0.0" blue="0.0" alpha="0.0" colorSpace="custom" customColorSpace="sRGB"/>
<constraints>
<constraint firstItem="ZdV-4y-cz4" firstAttribute="centerY" secondItem="swk-um-XzG" secondAttribute="centerY" id="6hD-qt-buS"/>
<constraint firstItem="oTF-IZ-Un1" firstAttribute="top" secondItem="swk-um-XzG" secondAttribute="top" constant="12" id="93j-yG-wF2"/>
<constraint firstItem="hpw-oR-ZSb" firstAttribute="top" secondItem="Ot5-QJ-jhp" secondAttribute="bottom" constant="4" id="9GL-pC-tse"/>
<constraint firstItem="oTF-IZ-Un1" firstAttribute="leading" relation="greaterThanOrEqual" secondItem="Ot5-QJ-jhp" secondAttribute="trailing" priority="310" constant="4" id="9QL-6i-a0L"/>
<constraint firstItem="EcD-Q8-7zu" firstAttribute="leading" secondItem="swk-um-XzG" secondAttribute="leading" constant="60" id="Bhv-rl-AUe"/>
<constraint firstAttribute="trailing" relation="greaterThanOrEqual" secondItem="oTF-IZ-Un1" secondAttribute="trailing" constant="16" id="C9i-5K-AVk"/>
<constraint firstAttribute="trailing" secondItem="mGc-k4-uvQ" secondAttribute="trailing" constant="8" id="CS2-Y7-odx"/>
<constraint firstItem="pa0-fe-w8W" firstAttribute="centerY" secondItem="swk-um-XzG" secondAttribute="centerY" id="CXB-0i-wxj"/>
<constraint firstItem="mGc-k4-uvQ" firstAttribute="centerY" secondItem="swk-um-XzG" secondAttribute="centerY" id="DQP-gP-Lv1"/>
<constraint firstItem="pa0-fe-w8W" firstAttribute="leading" secondItem="swk-um-XzG" secondAttribute="leading" constant="16" id="Dgv-BZ-60x"/>
<constraint firstItem="7Oa-hg-icC" firstAttribute="top" secondItem="hpw-oR-ZSb" secondAttribute="top" id="J7A-De-gPK"/>
<constraint firstItem="oTF-IZ-Un1" firstAttribute="leading" secondItem="swk-um-XzG" secondAttribute="leading" priority="250" id="KGY-T0-s0P"/>
<constraint firstItem="7Oa-hg-icC" firstAttribute="leading" secondItem="oTF-IZ-Un1" secondAttribute="leading" priority="300" id="RBf-8S-CYY"/>
<constraint firstItem="7Oa-hg-icC" firstAttribute="trailing" secondItem="oTF-IZ-Un1" secondAttribute="trailing" priority="300" id="XqV-8v-RkG"/>
<constraint firstItem="ZdV-4y-cz4" firstAttribute="leading" secondItem="swk-um-XzG" secondAttribute="leading" constant="60" id="YId-0j-rvE"/>
<constraint firstItem="Ot5-QJ-jhp" firstAttribute="leading" secondItem="swk-um-XzG" secondAttribute="leading" constant="60" id="ZcQ-Ll-hiP"/>
<constraint firstItem="hpw-oR-ZSb" firstAttribute="leading" secondItem="Ot5-QJ-jhp" secondAttribute="leading" id="dpq-9g-Kme"/>
<constraint firstItem="7Oa-hg-icC" firstAttribute="leading" relation="greaterThanOrEqual" secondItem="hpw-oR-ZSb" secondAttribute="trailing" priority="310" constant="12" id="efF-u9-Cqb"/>
<constraint firstAttribute="trailing" secondItem="ZdV-4y-cz4" secondAttribute="trailing" constant="32" id="hnz-zM-TYH"/>
<constraint firstAttribute="height" constant="126" id="hsa-4B-Df0"/>
<constraint firstItem="mGc-k4-uvQ" firstAttribute="leading" relation="greaterThanOrEqual" secondItem="oTF-IZ-Un1" secondAttribute="trailing" priority="750" id="iiH-Tm-ybN"/>
<constraint firstItem="Ot5-QJ-jhp" firstAttribute="top" secondItem="swk-um-XzG" secondAttribute="top" constant="12" id="n1a-Y2-Nfo"/>
<constraint firstAttribute="bottom" secondItem="EcD-Q8-7zu" secondAttribute="bottom" constant="12" id="qwA-KB-e9x"/>
</constraints>
<connections>
<outlet property="breakLabel" destination="hpw-oR-ZSb" id="3Rc-7X-y7r"/>
<outlet property="breakLabelWidth" destination="Aji-QM-nRY" id="uKp-5M-qg3"/>
<outlet property="breaksHolder" destination="7Oa-hg-icC" id="ttI-ww-abo"/>
<outlet property="breaksHolderHeight" destination="RWf-JS-tim" id="Pgt-3Q-cEG"/>
<outlet property="closedLabel" destination="EcD-Q8-7zu" id="hhh-Q4-9vo"/>
<outlet property="compatibilityLabel" destination="ZdV-4y-cz4" id="Jwu-Ux-lqO"/>
<outlet property="height" destination="hsa-4B-Df0" id="he7-ZL-kOE"/>
<outlet property="label" destination="Ot5-QJ-jhp" id="zOc-Fe-grg"/>
<outlet property="labelOpenTimeLabelSpacing" destination="9QL-6i-a0L" id="ShG-5V-lOy"/>
<outlet property="labelTopSpacing" destination="n1a-Y2-Nfo" id="upg-Ua-PDE"/>
<outlet property="labelWidth" destination="5G1-mL-J4T" id="esU-tO-DPm"/>
<outlet property="openTime" destination="oTF-IZ-Un1" id="oSf-bK-Va1"/>
</connections>
</view>
<imageView userInteractionEnabled="NO" contentMode="scaleToFill" horizontalHuggingPriority="251" verticalHuggingPriority="251" image="separator_image" translatesAutoresizingMaskIntoConstraints="NO" id="0kQ-hh-2Cy">
<rect key="frame" x="60" y="126" width="228" height="1"/>
<constraints>
<constraint firstAttribute="height" constant="1" id="5lV-kq-jGM"/>
</constraints>
<userDefinedRuntimeAttributes>
<userDefinedRuntimeAttribute type="string" keyPath="styleName" value="MWMSeparator"/>
</userDefinedRuntimeAttributes>
</imageView>
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="fNU-1q-AiR">
<rect key="frame" x="0.0" y="127" width="320" height="122"/>
<color key="backgroundColor" red="0.0" green="0.0" blue="0.0" alpha="0.0" colorSpace="custom" customColorSpace="sRGB"/>
<constraints>
<constraint firstAttribute="height" constant="122" id="Ifb-EB-LIb"/>
</constraints>
</view>
</subviews>
<constraints>
<constraint firstItem="fNU-1q-AiR" firstAttribute="leading" secondItem="H2p-sc-9uM" secondAttribute="leading" id="08I-np-9jr"/>
<constraint firstAttribute="trailing" secondItem="fNU-1q-AiR" secondAttribute="trailing" id="2Hz-cA-KuN"/>
<constraint firstItem="0kQ-hh-2Cy" firstAttribute="leading" secondItem="H2p-sc-9uM" secondAttribute="leading" constant="60" id="KwF-TF-PmH"/>
<constraint firstAttribute="trailing" secondItem="0kQ-hh-2Cy" secondAttribute="trailing" constant="32" id="RqH-0b-AyG"/>
<constraint firstItem="swk-um-XzG" firstAttribute="top" secondItem="H2p-sc-9uM" secondAttribute="top" id="VsQ-qI-dIi"/>
<constraint firstItem="0kQ-hh-2Cy" firstAttribute="top" secondItem="swk-um-XzG" secondAttribute="bottom" id="Xrh-Vg-VYg"/>
<constraint firstItem="swk-um-XzG" firstAttribute="leading" secondItem="H2p-sc-9uM" secondAttribute="leading" id="p14-Mi-kcR"/>
<constraint firstItem="fNU-1q-AiR" firstAttribute="top" secondItem="0kQ-hh-2Cy" secondAttribute="bottom" id="uKD-bb-yHT"/>
<constraint firstAttribute="trailing" secondItem="swk-um-XzG" secondAttribute="trailing" id="zir-No-59Q"/>
</constraints>
<userDefinedRuntimeAttributes>
<userDefinedRuntimeAttribute type="string" keyPath="styleName" value="Background"/>
</userDefinedRuntimeAttributes>
</tableViewCellContentView>
<inset key="separatorInset" minX="60" minY="0.0" maxX="0.0" maxY="0.0"/>
<connections>
<outlet property="currentDay" destination="swk-um-XzG" id="CJG-LQ-Pu8"/>
<outlet property="expandImage" destination="mGc-k4-uvQ" id="ohq-Yq-hLX"/>
<outlet property="middleSeparator" destination="0kQ-hh-2Cy" id="TJM-Ch-7E0"/>
<outlet property="openTime" destination="oTF-IZ-Un1" id="hsY-uV-wWE"/>
<outlet property="openTimeLeadingOffset" destination="KGY-T0-s0P" id="rHG-56-gHg"/>
<outlet property="openTimeTrailingOffset" destination="iiH-Tm-ybN" id="Abp-Yd-ZCk"/>
<outlet property="toggleButton" destination="3Fa-V6-tC5" id="8FI-Je-uFy"/>
<outlet property="weekDaysView" destination="fNU-1q-AiR" id="zM3-OD-vBA"/>
<outlet property="weekDaysViewHeight" destination="Ifb-EB-LIb" id="sEe-Y1-ubY"/>
</connections>
<point key="canvasLocation" x="139" y="155"/>
</tableViewCell>
</objects>
<resources>
<image name="ic_arrow_gray_down" width="28" height="28"/>
<image name="ic_placepage_open_hours" width="28" height="28"/>
<image name="separator_image" width="1" height="1"/>
</resources>
</document>

View file

@ -0,0 +1,17 @@
@interface MWMPlacePageOpeningHoursDayView : UIView
@property (nonatomic) BOOL currentDay;
@property (nonatomic) CGFloat viewHeight;
@property (nonatomic) BOOL isCompatibility;
@property (nonatomic) CGFloat openTimeLeadingOffset;
- (void)setLabelText:(NSString *)text isRed:(BOOL)isRed;
- (void)setOpenTimeText:(NSString *)text;
- (void)setBreaks:(NSArray<NSString *> *)breaks;
- (void)setClosed:(BOOL)closed;
- (void)setCompatibilityText:(NSString *)text isPlaceholder:(BOOL)isPlaceholder;
- (void)invalidate;
@end

View file

@ -0,0 +1,164 @@
#import "MWMPlacePageOpeningHoursDayView.h"
#import "SwiftBridge.h"
@interface MWMPlacePageOpeningHoursDayView ()
@property (weak, nonatomic) IBOutlet UILabel * label;
@property (weak, nonatomic) IBOutlet UILabel * openTime;
@property (weak, nonatomic) IBOutlet UILabel * compatibilityLabel;
@property (weak, nonatomic) IBOutlet UILabel * breakLabel;
@property (weak, nonatomic) IBOutlet UIView * breaksHolder;
@property (weak, nonatomic) IBOutlet UILabel * closedLabel;
@property (weak, nonatomic) IBOutlet NSLayoutConstraint * height;
@property (weak, nonatomic) IBOutlet NSLayoutConstraint * labelTopSpacing;
@property (weak, nonatomic) IBOutlet NSLayoutConstraint * labelWidth;
@property (weak, nonatomic) IBOutlet NSLayoutConstraint * breakLabelWidth;
@property (weak, nonatomic) IBOutlet NSLayoutConstraint * breaksHolderHeight;
@property (weak, nonatomic) IBOutlet NSLayoutConstraint * openTimeLabelLeadingOffset;
@property (weak, nonatomic) IBOutlet NSLayoutConstraint * labelOpenTimeLabelSpacing;
@end
@implementation MWMPlacePageOpeningHoursDayView
- (void)setLabelText:(NSString *)text isRed:(BOOL)isRed
{
UILabel * label = self.label;
label.text = text;
if (isRed)
[label setStyleNameAndApply:@"redText"];
else if (self.currentDay)
[label setStyleNameAndApply:@"blackPrimaryText"];
else
[label setStyleNameAndApply:@"blackSecondaryText"];
}
- (void)setOpenTimeText:(NSString *)text
{
self.openTime.hidden = (text.length == 0);
self.openTime.text = text;
}
- (void)setBreaks:(NSArray<NSString *> *)breaks
{
NSUInteger breaksCount = breaks.count;
BOOL haveBreaks = breaksCount != 0;
[self.breaksHolder.subviews makeObjectsPerformSelector:@selector(removeFromSuperview)];
if (haveBreaks)
{
CGFloat breakSpacerHeight = 4.0;
self.breakLabel.hidden = NO;
self.breaksHolder.hidden = NO;
CGFloat labelY = 0.0;
for (NSString * br in breaks)
{
UILabel * label = [[UILabel alloc] initWithFrame:CGRectMake(0, labelY, 0, 0)];
label.text = br;
label.font = self.currentDay ? [UIFont regular14] : [UIFont light12];
label.textColor = [UIColor blackSecondaryText];
[label sizeToIntegralFit];
[self.breaksHolder addSubview:label];
labelY += label.height + breakSpacerHeight;
}
self.breaksHolderHeight.constant = ceil(labelY - breakSpacerHeight);
}
else
{
self.breakLabel.hidden = YES;
self.breaksHolder.hidden = YES;
self.breaksHolderHeight.constant = 0.0;
}
}
- (void)setClosed:(BOOL)closed
{
self.closedLabel.hidden = !closed;
}
- (void)setCompatibilityText:(NSString *)text isPlaceholder:(BOOL)isPlaceholder
{
self.compatibilityLabel.text = text;
self.compatibilityLabel.textColor = isPlaceholder ? [UIColor blackHintText] : [UIColor blackPrimaryText];
}
- (void)invalidate
{
CGFloat viewHeight;
if (self.isCompatibility)
{
[self.compatibilityLabel sizeToIntegralFit];
CGFloat compatibilityLabelVerticalOffsets = 24.0;
viewHeight = self.compatibilityLabel.height + compatibilityLabelVerticalOffsets;
}
else
{
UILabel * label = self.label;
UILabel * openTime = self.openTime;
CGFloat labelOpenTimeLabelSpacing = self.labelOpenTimeLabelSpacing.constant;
[label sizeToIntegralFit];
self.labelWidth.constant = MIN(label.width, openTime.minX - label.minX - labelOpenTimeLabelSpacing);
[self.breakLabel sizeToIntegralFit];
self.breakLabelWidth.constant = self.breakLabel.width;
CGFloat verticalSuperviewSpacing = self.labelTopSpacing.constant;
CGFloat minHeight = label.height + 2 * verticalSuperviewSpacing;
CGFloat breaksHolderHeight = self.breaksHolderHeight.constant;
CGFloat additionalHeight = (breaksHolderHeight > 0 ? 4.0 : 0.0);
viewHeight = minHeight + breaksHolderHeight + additionalHeight;
if (self.closedLabel && !self.closedLabel.hidden)
{
CGFloat heightForClosedLabel = 20.0;
viewHeight += heightForClosedLabel;
}
}
self.viewHeight = ceil(viewHeight);
[self setNeedsLayout];
[self layoutIfNeeded];
}
#pragma mark - Properties
- (void)setViewHeight:(CGFloat)viewHeight
{
_viewHeight = viewHeight;
if (self.currentDay)
{
self.height.constant = viewHeight;
}
else
{
CGRect frame = self.frame;
frame.size.height = viewHeight;
self.frame = frame;
}
}
- (void)setIsCompatibility:(BOOL)isCompatibility
{
_isCompatibility = isCompatibility;
self.compatibilityLabel.hidden = !isCompatibility;
self.label.hidden = isCompatibility;
self.openTime.hidden = isCompatibility;
self.breakLabel.hidden = isCompatibility;
self.breaksHolder.hidden = isCompatibility;
self.closedLabel.hidden = isCompatibility;
}
- (CGFloat)openTimeLeadingOffset
{
return self.openTime.minX;
}
- (void)setOpenTimeLeadingOffset:(CGFloat)openTimeLeadingOffset
{
self.openTimeLabelLeadingOffset.constant = openTimeLeadingOffset;
}
@end

View file

@ -0,0 +1,95 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="15702" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" colorMatched="YES">
<device id="retina4_7" orientation="portrait" appearance="light"/>
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="15704"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<objects>
<placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner"/>
<placeholder placeholderIdentifier="IBFirstResponder" id="-2" customClass="UIResponder"/>
<view contentMode="scaleToFill" id="SUx-BN-Qk1" customClass="MWMPlacePageOpeningHoursDayView" propertyAccessControl="none">
<rect key="frame" x="0.0" y="0.0" width="375" height="102"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Пн-Пт" textAlignment="natural" lineBreakMode="wordWrap" numberOfLines="0" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" preferredMaxLayoutWidth="68" translatesAutoresizingMaskIntoConstraints="NO" id="DqS-ds-oj4">
<rect key="frame" x="60" y="4" width="68" height="16.5"/>
<constraints>
<constraint firstAttribute="width" relation="greaterThanOrEqual" priority="750" constant="68" id="TSk-hp-vXl"/>
</constraints>
<fontDescription key="fontDescription" type="system" pointSize="14"/>
<color key="textColor" red="0.0" green="0.0" blue="0.0" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<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" text="10:00—20:00" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" preferredMaxLayoutWidth="180" translatesAutoresizingMaskIntoConstraints="NO" id="Pzb-84-nVN">
<rect key="frame" x="136" y="4.5" width="100" height="16"/>
<constraints>
<constraint firstAttribute="width" relation="greaterThanOrEqual" constant="100" id="HSs-ZO-QYt"/>
</constraints>
<fontDescription key="fontDescription" type="system" pointSize="13"/>
<color key="textColor" red="0.0" green="0.0" blue="0.0" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<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" text="Перерыв" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" preferredMaxLayoutWidth="68" translatesAutoresizingMaskIntoConstraints="NO" id="LY3-Eu-ESE">
<rect key="frame" x="60" y="24.5" width="68" height="14"/>
<constraints>
<constraint firstAttribute="width" relation="greaterThanOrEqual" priority="750" constant="68" id="zcl-0l-OMI"/>
</constraints>
<fontDescription key="fontDescription" type="system" pointSize="12"/>
<color key="textColor" red="0.0" green="0.0" blue="0.0" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<nil key="highlightedColor"/>
<userDefinedRuntimeAttributes>
<userDefinedRuntimeAttribute type="string" keyPath="styleName" value="regular12:blackSecondaryText"/>
<userDefinedRuntimeAttribute type="string" keyPath="localizedText" value="editor_hours_closed"/>
</userDefinedRuntimeAttributes>
</label>
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="skl-yW-xDB">
<rect key="frame" x="136" y="24.5" width="100" height="58"/>
<color key="backgroundColor" red="0.0" green="0.0" blue="0.0" alpha="0.0" colorSpace="custom" customColorSpace="sRGB"/>
<constraints>
<constraint firstAttribute="height" constant="58" id="JQd-xS-lP1"/>
</constraints>
</view>
</subviews>
<color key="backgroundColor" red="0.0" green="0.0" blue="0.0" alpha="0.0" colorSpace="custom" customColorSpace="sRGB"/>
<constraints>
<constraint firstItem="skl-yW-xDB" firstAttribute="trailing" secondItem="Pzb-84-nVN" secondAttribute="trailing" id="2Qa-dX-CKF"/>
<constraint firstItem="skl-yW-xDB" firstAttribute="leading" relation="greaterThanOrEqual" secondItem="LY3-Eu-ESE" secondAttribute="trailing" priority="310" constant="8" id="4HL-o3-GlG"/>
<constraint firstItem="Pzb-84-nVN" firstAttribute="centerY" secondItem="DqS-ds-oj4" secondAttribute="centerY" id="D6k-RM-Cx8"/>
<constraint firstItem="skl-yW-xDB" firstAttribute="leading" secondItem="Pzb-84-nVN" secondAttribute="leading" id="H3w-tN-bFd"/>
<constraint firstItem="LY3-Eu-ESE" firstAttribute="top" secondItem="DqS-ds-oj4" secondAttribute="bottom" constant="4" id="MKr-pT-3ET"/>
<constraint firstItem="LY3-Eu-ESE" firstAttribute="leading" secondItem="DqS-ds-oj4" secondAttribute="leading" id="OFa-uz-HMs"/>
<constraint firstItem="DqS-ds-oj4" firstAttribute="top" secondItem="SUx-BN-Qk1" secondAttribute="top" constant="4" id="gGO-xk-DeA"/>
<constraint firstItem="skl-yW-xDB" firstAttribute="top" secondItem="LY3-Eu-ESE" secondAttribute="top" id="iut-jl-BrW"/>
<constraint firstAttribute="trailing" relation="greaterThanOrEqual" secondItem="Pzb-84-nVN" secondAttribute="trailing" priority="750" constant="32" id="rPc-Jd-cW6"/>
<constraint firstItem="DqS-ds-oj4" firstAttribute="leading" secondItem="SUx-BN-Qk1" secondAttribute="leading" constant="60" id="sqM-DI-KUl"/>
<constraint firstItem="Pzb-84-nVN" firstAttribute="leading" relation="greaterThanOrEqual" secondItem="DqS-ds-oj4" secondAttribute="trailing" priority="310" constant="8" id="vtQ-YR-qDv"/>
<constraint firstItem="Pzb-84-nVN" firstAttribute="leading" secondItem="SUx-BN-Qk1" secondAttribute="leading" priority="300" id="xiL-Qi-HQc"/>
</constraints>
<nil key="simulatedStatusBarMetrics"/>
<nil key="simulatedTopBarMetrics"/>
<nil key="simulatedBottomBarMetrics"/>
<freeformSimulatedSizeMetrics key="simulatedDestinationMetrics"/>
<connections>
<outlet property="breakLabel" destination="LY3-Eu-ESE" id="n3q-d9-OCx"/>
<outlet property="breakLabelWidth" destination="zcl-0l-OMI" id="OQq-QW-fVw"/>
<outlet property="breaksHolder" destination="skl-yW-xDB" id="O5A-vk-D8U"/>
<outlet property="breaksHolderHeight" destination="JQd-xS-lP1" id="yD5-Y7-gDM"/>
<outlet property="label" destination="DqS-ds-oj4" id="6RA-e6-H3w"/>
<outlet property="labelOpenTimeLabelSpacing" destination="vtQ-YR-qDv" id="Ya6-Gr-qHm"/>
<outlet property="labelTopSpacing" destination="gGO-xk-DeA" id="8qk-bN-ayf"/>
<outlet property="labelWidth" destination="TSk-hp-vXl" id="r1R-Cy-QXI"/>
<outlet property="openTime" destination="Pzb-84-nVN" id="3YU-3c-hyz"/>
<outlet property="openTimeLabelLeadingOffset" destination="xiL-Qi-HQc" id="vIE-5M-9Q0"/>
</connections>
<point key="canvasLocation" x="332" y="512"/>
</view>
</objects>
</document>

View file

@ -0,0 +1,80 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="15702" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" colorMatched="YES">
<device id="retina4_7" orientation="portrait" appearance="light"/>
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="15704"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<objects>
<placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner"/>
<placeholder placeholderIdentifier="IBFirstResponder" id="-2" customClass="UIResponder"/>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" selectionStyle="default" indentationWidth="10" reuseIdentifier="_MWMOHHeaderCell" id="LXG-cP-akO" customClass="_MWMOHHeaderCell" propertyAccessControl="none">
<rect key="frame" x="0.0" y="0.0" width="375" height="48"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" tableViewCell="LXG-cP-akO" id="WuT-dc-opP">
<rect key="frame" x="0.0" y="0.0" width="375" height="48"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<imageView userInteractionEnabled="NO" contentMode="scaleToFill" horizontalHuggingPriority="251" verticalHuggingPriority="251" image="ic_placepage_open_hours" translatesAutoresizingMaskIntoConstraints="NO" id="nwH-Nj-buF">
<rect key="frame" x="16" y="12" width="24" height="24"/>
<constraints>
<constraint firstAttribute="height" constant="24" id="AU3-h8-Jf3"/>
<constraint firstAttribute="width" constant="24" id="RuT-UD-d6E"/>
</constraints>
<userDefinedRuntimeAttributes>
<userDefinedRuntimeAttribute type="string" keyPath="styleName" value="MWMBlack"/>
</userDefinedRuntimeAttributes>
</imageView>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Сегодня 8:00 18:00" textAlignment="natural" lineBreakMode="tailTruncation" numberOfLines="0" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="qDh-SU-MHG" userLabel="Text">
<rect key="frame" x="60" y="14" width="271" height="20"/>
<fontDescription key="fontDescription" type="system" pointSize="17"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
<imageView userInteractionEnabled="NO" contentMode="scaleToFill" horizontalHuggingPriority="251" verticalHuggingPriority="251" image="ic_arrow_gray_down" translatesAutoresizingMaskIntoConstraints="NO" id="VHY-FB-giE">
<rect key="frame" x="339" y="10" width="28" height="28"/>
<constraints>
<constraint firstAttribute="height" constant="28" id="GMF-Az-vGZ"/>
<constraint firstAttribute="width" constant="28" id="qap-Cz-ia0"/>
</constraints>
<userDefinedRuntimeAttributes>
<userDefinedRuntimeAttribute type="string" keyPath="styleName" value="MWMBlack"/>
</userDefinedRuntimeAttributes>
</imageView>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="Mvv-gY-euE">
<rect key="frame" x="0.0" y="0.0" width="375" height="48"/>
<connections>
<action selector="extendTap" destination="LXG-cP-akO" eventType="touchUpInside" id="yMy-04-GCs"/>
</connections>
</button>
</subviews>
<constraints>
<constraint firstAttribute="bottom" secondItem="qDh-SU-MHG" secondAttribute="bottom" constant="14" id="0DZ-az-Lhs"/>
<constraint firstItem="nwH-Nj-buF" firstAttribute="leading" secondItem="WuT-dc-opP" secondAttribute="leading" constant="16" id="6Pu-RX-aEk"/>
<constraint firstItem="Mvv-gY-euE" firstAttribute="leading" secondItem="WuT-dc-opP" secondAttribute="leading" id="8P4-c4-MZD"/>
<constraint firstItem="VHY-FB-giE" firstAttribute="leading" secondItem="qDh-SU-MHG" secondAttribute="trailing" constant="8" id="9t0-oW-VAo"/>
<constraint firstItem="qDh-SU-MHG" firstAttribute="top" secondItem="WuT-dc-opP" secondAttribute="top" constant="14" id="EEt-O9-WgL"/>
<constraint firstAttribute="bottom" secondItem="Mvv-gY-euE" secondAttribute="bottom" id="Gbe-0R-FgZ"/>
<constraint firstAttribute="trailing" secondItem="Mvv-gY-euE" secondAttribute="trailing" id="fbo-mH-Bi6"/>
<constraint firstAttribute="trailing" secondItem="VHY-FB-giE" secondAttribute="trailing" constant="8" id="jQX-bd-gBc"/>
<constraint firstItem="qDh-SU-MHG" firstAttribute="leading" secondItem="nwH-Nj-buF" secondAttribute="trailing" constant="20" id="rcw-Eo-aHO"/>
<constraint firstItem="Mvv-gY-euE" firstAttribute="top" secondItem="WuT-dc-opP" secondAttribute="top" id="zyW-ZM-u0d"/>
</constraints>
</tableViewCellContentView>
<constraints>
<constraint firstItem="nwH-Nj-buF" firstAttribute="centerY" secondItem="LXG-cP-akO" secondAttribute="centerY" id="Fk1-cA-KAh"/>
<constraint firstItem="VHY-FB-giE" firstAttribute="centerY" secondItem="LXG-cP-akO" secondAttribute="centerY" id="x8c-sP-BJ3"/>
</constraints>
<connections>
<outlet property="arrowIcon" destination="VHY-FB-giE" id="j5m-f3-Wv6"/>
<outlet property="text" destination="qDh-SU-MHG" id="OTq-0N-S6b"/>
</connections>
<point key="canvasLocation" x="65.94202898550725" y="28.794642857142854"/>
</tableViewCell>
</objects>
<resources>
<image name="ic_arrow_gray_down" width="28" height="28"/>
<image name="ic_placepage_open_hours" width="28" height="28"/>
</resources>
</document>

View file

@ -0,0 +1,68 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="15702" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" colorMatched="YES">
<device id="retina4_7" orientation="portrait" appearance="light"/>
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="15704"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<objects>
<placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner"/>
<placeholder placeholderIdentifier="IBFirstResponder" id="-2" customClass="UIResponder"/>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" selectionStyle="none" indentationWidth="10" reuseIdentifier="_MWMOHSubCell" id="QJs-zE-xfN" customClass="_MWMOHSubCell" propertyAccessControl="none">
<rect key="frame" x="0.0" y="0.0" width="375" height="75"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" tableViewCell="QJs-zE-xfN" id="n1O-q5-zmj">
<rect key="frame" x="0.0" y="0.0" width="375" height="75"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="248" verticalHuggingPriority="251" text="Sun-Wed, Fri-Sat" lineBreakMode="tailTruncation" numberOfLines="0" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="YIs-LL-j77" userLabel="Days">
<rect key="frame" x="60" y="14" width="132.5" height="20.5"/>
<fontDescription key="fontDescription" type="system" pointSize="17"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
<userDefinedRuntimeAttributes>
<userDefinedRuntimeAttribute type="string" keyPath="styleName" value="regular15:blackPrimaryText"/>
</userDefinedRuntimeAttributes>
</label>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="All Day (24 hours)" textAlignment="right" lineBreakMode="tailTruncation" numberOfLines="0" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="vjg-UU-FVu" userLabel="Schedule">
<rect key="frame" x="221.5" y="14" width="137.5" height="20.5"/>
<fontDescription key="fontDescription" type="system" pointSize="17"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
<userDefinedRuntimeAttributes>
<userDefinedRuntimeAttribute type="string" keyPath="styleName" value="regular15:blackPrimaryText"/>
</userDefinedRuntimeAttributes>
</label>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Перерыв 12:00 13:00" textAlignment="right" lineBreakMode="tailTruncation" numberOfLines="0" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="iYw-fc-mKi">
<rect key="frame" x="60" y="38.5" width="299" height="21.5"/>
<fontDescription key="fontDescription" type="system" pointSize="17"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
<userDefinedRuntimeAttributes>
<userDefinedRuntimeAttribute type="string" keyPath="styleName" value="regular13:blackSecondaryText"/>
</userDefinedRuntimeAttributes>
</label>
</subviews>
<constraints>
<constraint firstItem="iYw-fc-mKi" firstAttribute="top" secondItem="vjg-UU-FVu" secondAttribute="bottom" constant="4" id="LaC-Fj-SGw"/>
<constraint firstItem="YIs-LL-j77" firstAttribute="leading" secondItem="n1O-q5-zmj" secondAttribute="leading" constant="60" id="Pqu-1L-AJT"/>
<constraint firstItem="vjg-UU-FVu" firstAttribute="leading" relation="greaterThanOrEqual" secondItem="YIs-LL-j77" secondAttribute="trailing" constant="10" id="S0j-ZD-Yzm"/>
<constraint firstAttribute="trailing" secondItem="vjg-UU-FVu" secondAttribute="trailing" constant="16" id="Y3R-u7-9vy"/>
<constraint firstItem="iYw-fc-mKi" firstAttribute="top" secondItem="YIs-LL-j77" secondAttribute="bottom" constant="4" id="c2Y-bD-pvv"/>
<constraint firstAttribute="trailing" secondItem="iYw-fc-mKi" secondAttribute="trailing" constant="16" id="fDs-dk-Fok"/>
<constraint firstItem="YIs-LL-j77" firstAttribute="top" secondItem="n1O-q5-zmj" secondAttribute="top" constant="14" id="hto-60-gvp"/>
<constraint firstAttribute="bottom" secondItem="iYw-fc-mKi" secondAttribute="bottom" constant="15" id="j14-1H-U4E"/>
<constraint firstItem="iYw-fc-mKi" firstAttribute="leading" secondItem="n1O-q5-zmj" secondAttribute="leading" constant="60" id="qgW-Wu-maW"/>
<constraint firstItem="vjg-UU-FVu" firstAttribute="top" secondItem="n1O-q5-zmj" secondAttribute="top" constant="14" id="qzM-8s-sDR"/>
</constraints>
</tableViewCellContentView>
<connections>
<outlet property="breaks" destination="iYw-fc-mKi" id="2ez-hw-xaw"/>
<outlet property="days" destination="YIs-LL-j77" id="dqM-WD-2NN"/>
<outlet property="schedule" destination="vjg-UU-FVu" id="Xdv-dH-NPZ"/>
</connections>
<point key="canvasLocation" x="-96.376811594202906" y="16.40625"/>
</tableViewCell>
</objects>
</document>

View file

@ -0,0 +1,15 @@
@objc(MWMUGCSelectImpressionCell)
final class UGCSelectImpressionCell: MWMTableViewCell {
@IBOutlet private var buttons: [UIButton]!
private weak var delegate: MWMPlacePageButtonsProtocol?
@objc func configWith(delegate: MWMPlacePageButtonsProtocol?) {
self.delegate = delegate
}
@IBAction private func tap(on: UIButton) {
buttons.forEach { $0.isSelected = false }
on.isSelected = true
delegate?.review(on: on.tag)
}
}

View file

@ -0,0 +1,32 @@
@objc(MWMUGCSpecificReviewDelegate)
protocol UGCSpecificReviewDelegate: NSObjectProtocol {
func changeReviewRate(_ rate: Int, atIndexPath: NSIndexPath)
}
@objc(MWMUGCSpecificReviewCell)
final class UGCSpecificReviewCell: MWMTableViewCell {
@IBOutlet private weak var specification: UILabel!
@IBOutlet private var stars: [UIButton]!
private var indexPath: NSIndexPath = NSIndexPath()
private var delegate: UGCSpecificReviewDelegate?
@objc func configWith(specification: String, rate: Int, atIndexPath: NSIndexPath, delegate: UGCSpecificReviewDelegate?) {
self.specification.text = specification
self.delegate = delegate
indexPath = atIndexPath
stars.forEach { $0.isSelected = $0.tag <= rate }
}
@IBAction private func tap(on: UIButton) {
stars.forEach { $0.isSelected = $0.tag <= on.tag }
delegate?.changeReviewRate(on.tag, atIndexPath: indexPath)
}
// TODO: Make highlighting and dragging.
@IBAction private func highlight(on _: UIButton) {}
@IBAction private func touchingCanceled(on _: UIButton) {}
@IBAction private func drag(inside _: UIButton) {}
}

View file

@ -0,0 +1,38 @@
@objc(MWMUGCTextReviewDelegate)
protocol UGCTextReviewDelegate: NSObjectProtocol {
func changeReviewText(_ text: String)
}
@objc(MWMUGCTextReviewCell)
final class UGCTextReviewCell: MWMTableViewCell, UITextViewDelegate {
private enum Consts {
static let kMaxNumberOfSymbols = 400
}
@IBOutlet private weak var textView: MWMTextView!
@IBOutlet private weak var countLabel: UILabel!
private weak var delegate: UGCTextReviewDelegate?
private var indexPath: NSIndexPath = NSIndexPath()
@objc func configWith(delegate: UGCTextReviewDelegate?) {
self.delegate = delegate
setCount(textView.text.characters.count)
}
private func setCount(_ count: Int) {
countLabel.text = "\(count)/\(Consts.kMaxNumberOfSymbols)"
}
// MARK: UITextViewDelegate
func textView(_ textView: UITextView, shouldChangeTextIn _: NSRange, replacementText _: String) -> Bool {
return textView.text.characters.count <= Consts.kMaxNumberOfSymbols
}
func textViewDidChange(_ textView: UITextView) {
setCount(textView.text.characters.count)
}
func textViewDidEndEditing(_ textView: UITextView) {
delegate?.changeReviewText(textView.text)
}
}

View file

@ -0,0 +1,13 @@
final class CopyLabel: UILabel {
override var canBecomeFirstResponder: Bool {
true
}
override func canPerformAction(_ action: Selector, withSender sender: Any?) -> Bool {
action == #selector(copy(_:))
}
override func copy(_ sender: Any?) {
UIPasteboard.general.string = text
}
}

View file

@ -0,0 +1,38 @@
enum PlacePageState {
case closed(CGFloat)
case preview(CGFloat)
case previewPlus(CGFloat)
case expanded(CGFloat)
case full(CGFloat)
var offset: CGFloat {
switch self {
case .closed(let value):
return value
case .preview(let value):
return value
case .previewPlus(let value):
return value
case .expanded(let value):
return value
case .full(let value):
return value
}
}
}
protocol IPlacePageLayout: AnyObject {
var presenter: PlacePagePresenterProtocol? { get set }
var headerViewControllers: [UIViewController] { get }
var headerViewController: PlacePageHeaderViewController { get }
var bodyViewControllers: [UIViewController] { get }
var actionBar: ActionBarViewController? { get }
var navigationBar: UIViewController? { get }
var sectionSpacing: CGFloat { get }
func calculateSteps(inScrollView scrollView: UIScrollView, compact: Bool) -> [PlacePageState]
}
extension IPlacePageLayout {
var sectionSpacing: CGFloat { return 24.0 }
}

View file

@ -0,0 +1,230 @@
class PlacePageCommonLayout: NSObject, IPlacePageLayout {
private let distanceFormatter = DistanceFormatter.self
private let altitudeFormatter = AltitudeFormatter.self
private var placePageData: PlacePageData
private var interactor: PlacePageInteractor
private let storyboard: UIStoryboard
private var lastLocation: CLLocation?
weak var presenter: PlacePagePresenterProtocol?
var headerViewControllers: [UIViewController] {
[headerViewController, previewViewController]
}
lazy var bodyViewControllers: [UIViewController] = {
configureViewControllers()
}()
var actionBar: ActionBarViewController? {
actionBarViewController
}
var navigationBar: UIViewController? {
placePageNavigationViewController
}
lazy var headerViewController: PlacePageHeaderViewController = {
PlacePageHeaderBuilder.build(data: placePageData, delegate: interactor, headerType: .flexible)
}()
private lazy var previewViewController: PlacePagePreviewViewController = {
let vc = storyboard.instantiateViewController(ofType: PlacePagePreviewViewController.self)
vc.placePagePreviewData = placePageData.previewData
return vc
}()
private lazy var wikiDescriptionViewController: WikiDescriptionViewController = {
let vc = storyboard.instantiateViewController(ofType: WikiDescriptionViewController.self)
vc.view.isHidden = true
vc.delegate = interactor
return vc
}()
private lazy var editBookmarkViewController: PlacePageEditBookmarkOrTrackViewController = {
let vc = storyboard.instantiateViewController(ofType: PlacePageEditBookmarkOrTrackViewController.self)
vc.view.isHidden = true
vc.delegate = interactor
return vc
}()
private lazy var infoViewController: PlacePageInfoViewController = {
let vc = storyboard.instantiateViewController(ofType: PlacePageInfoViewController.self)
vc.placePageInfoData = placePageData.infoData
vc.delegate = interactor
return vc
}()
private lazy var buttonsViewController: PlacePageButtonsViewController = {
let vc = storyboard.instantiateViewController(ofType: PlacePageButtonsViewController.self)
vc.buttonsData = placePageData.buttonsData!
vc.delegate = interactor
return vc
}()
private lazy var actionBarViewController: ActionBarViewController = {
let vc = storyboard.instantiateViewController(ofType: ActionBarViewController.self)
vc.placePageData = placePageData
vc.canAddStop = MWMRouter.canAddIntermediatePoint()
vc.isRoutePlanning = MWMNavigationDashboardManager.shared().state != .hidden
vc.delegate = interactor
return vc
}()
private lazy var placePageNavigationViewController: PlacePageHeaderViewController = {
return PlacePageHeaderBuilder.build(data: placePageData, delegate: interactor, headerType: .fixed)
}()
init(interactor: PlacePageInteractor, storyboard: UIStoryboard, data: PlacePageData) {
self.interactor = interactor
self.storyboard = storyboard
self.placePageData = data
}
private func configureViewControllers() -> [UIViewController] {
var viewControllers = [UIViewController]()
viewControllers.append(editBookmarkViewController)
if let bookmarkData = placePageData.bookmarkData {
editBookmarkViewController.data = .bookmark(bookmarkData)
editBookmarkViewController.view.isHidden = false
}
viewControllers.append(wikiDescriptionViewController)
if let wikiDescriptionHtml = placePageData.wikiDescriptionHtml {
wikiDescriptionViewController.descriptionHtml = wikiDescriptionHtml
wikiDescriptionViewController.view.isHidden = false
}
if placePageData.infoData != nil {
viewControllers.append(infoViewController)
}
if placePageData.buttonsData != nil {
viewControllers.append(buttonsViewController)
}
placePageData.onBookmarkStatusUpdate = { [weak self] in
guard let self = self else { return }
self.actionBarViewController.updateBookmarkButtonState(isSelected: self.placePageData.bookmarkData != nil)
self.previewViewController.placePagePreviewData = self.placePageData.previewData
self.updateBookmarkRelatedSections()
}
LocationManager.add(observer: self)
if let lastLocation = LocationManager.lastLocation() {
onLocationUpdate(lastLocation)
self.lastLocation = lastLocation
}
if let lastHeading = LocationManager.lastHeading() {
onHeadingUpdate(lastHeading)
}
placePageData.onMapNodeStatusUpdate = { [weak self] in
guard let self = self else { return }
self.actionBarViewController.updateDownloadButtonState(self.placePageData.mapNodeAttributes!.nodeStatus)
switch self.placePageData.mapNodeAttributes!.nodeStatus {
case .onDisk, .onDiskOutOfDate, .undefined:
self.actionBarViewController.resetButtons()
if self.placePageData.buttonsData != nil {
self.buttonsViewController.buttonsEnabled = true
}
default:
break
}
}
placePageData.onMapNodeProgressUpdate = { [weak self] (downloadedBytes, totalBytes) in
guard let self = self, let downloadButton = self.actionBarViewController.downloadButton else { return }
downloadButton.mapDownloadProgress?.progress = CGFloat(downloadedBytes) / CGFloat(totalBytes)
}
return viewControllers
}
func calculateSteps(inScrollView scrollView: UIScrollView, compact: Bool) -> [PlacePageState] {
var steps: [PlacePageState] = []
let scrollHeight = scrollView.height
steps.append(.closed(-scrollHeight))
guard let preview = previewViewController.view else {
return steps
}
let previewFrame = scrollView.convert(preview.bounds, from: preview)
steps.append(.preview(previewFrame.maxY - scrollHeight))
if !compact {
if placePageData.isPreviewPlus {
steps.append(.previewPlus(-scrollHeight * 0.55))
}
steps.append(.expanded(-scrollHeight * 0.3))
}
steps.append(.full(0))
return steps
}
}
// MARK: - PlacePageData async callbacks for loaders
extension PlacePageCommonLayout {
func updateBookmarkRelatedSections() {
var isBookmark = false
if let bookmarkData = placePageData.bookmarkData {
editBookmarkViewController.data = .bookmark(bookmarkData)
isBookmark = true
}
if let title = placePageData.previewData.title, let headerViewController = headerViewControllers.compactMap({ $0 as? PlacePageHeaderViewController }).first {
let secondaryTitle = placePageData.previewData.secondaryTitle
headerViewController.setTitle(title, secondaryTitle: secondaryTitle)
placePageNavigationViewController.setTitle(title, secondaryTitle: secondaryTitle)
}
presenter?.layoutIfNeeded()
UIView.animate(withDuration: kDefaultAnimationDuration) { [unowned self] in
self.editBookmarkViewController.view.isHidden = !isBookmark
}
}
}
// MARK: - MWMLocationObserver
extension PlacePageCommonLayout: MWMLocationObserver {
func onHeadingUpdate(_ heading: CLHeading) {
if !placePageData.isMyPosition {
updateHeading(heading.trueHeading)
}
}
func onLocationUpdate(_ location: CLLocation) {
if placePageData.isMyPosition {
let altString = "\(altitudeFormatter.altitudeString(fromMeters: location.altitude))"
if location.speed > 0 && location.timestamp.timeIntervalSinceNow >= -2 {
let speedMeasure = Measure.init(asSpeed: location.speed)
let speedString = "\(LocationManager.speedSymbolFor(location.speed))\(speedMeasure.valueAsString) \(speedMeasure.unit)"
previewViewController.updateSpeedAndAltitude("\(altString) \(speedString)")
} else {
previewViewController.updateSpeedAndAltitude(altString)
}
} else {
let ppLocation = CLLocation(latitude: placePageData.locationCoordinate.latitude,
longitude: placePageData.locationCoordinate.longitude)
let distance = location.distance(from: ppLocation)
let formattedDistance = distanceFormatter.distanceString(fromMeters: distance)
previewViewController.updateDistance(formattedDistance)
lastLocation = location
}
}
func onLocationError(_ locationError: MWMLocationStatus) {
}
private func updateHeading(_ heading: CLLocationDirection) {
guard let location = lastLocation, heading > 0 else {
return
}
let rad = heading * Double.pi / 180
let angle = GeoUtil.angle(atPoint: location.coordinate, toPoint: placePageData.locationCoordinate)
previewViewController.updateHeading(CGFloat(angle) + CGFloat(rad))
}
}

View file

@ -0,0 +1,120 @@
class PlacePageTrackLayout: IPlacePageLayout {
private var placePageData: PlacePageData
private var trackData: PlacePageTrackData
private var interactor: PlacePageInteractor
private let storyboard: UIStoryboard
weak var presenter: PlacePagePresenterProtocol?
lazy var bodyViewControllers: [UIViewController] = {
return configureViewControllers()
}()
var actionBar: ActionBarViewController? {
actionBarViewController
}
var navigationBar: UIViewController? {
placePageNavigationViewController
}
var headerViewControllers: [UIViewController] {
[headerViewController, previewViewController]
}
lazy var headerViewController: PlacePageHeaderViewController = {
PlacePageHeaderBuilder.build(data: placePageData, delegate: interactor, headerType: .flexible)
}()
private lazy var previewViewController: PlacePagePreviewViewController = {
let vc = storyboard.instantiateViewController(ofType: PlacePagePreviewViewController.self)
vc.placePagePreviewData = placePageData.previewData
return vc
}()
private lazy var placePageNavigationViewController: PlacePageHeaderViewController = {
return PlacePageHeaderBuilder.build(data: placePageData, delegate: interactor, headerType: .fixed)
}()
private lazy var editTrackViewController: PlacePageEditBookmarkOrTrackViewController = {
let vc = storyboard.instantiateViewController(ofType: PlacePageEditBookmarkOrTrackViewController.self)
vc.view.isHidden = true
vc.delegate = interactor
return vc
}()
lazy var elevationMapViewController: ElevationProfileViewController? = {
guard trackData.trackInfo.hasElevationInfo, trackData.elevationProfileData != nil else {
return nil
}
return ElevationProfileBuilder.build(trackData: trackData, delegate: interactor)
}()
private lazy var actionBarViewController: ActionBarViewController = {
let vc = storyboard.instantiateViewController(ofType: ActionBarViewController.self)
vc.placePageData = placePageData
vc.canAddStop = MWMRouter.canAddIntermediatePoint()
vc.isRoutePlanning = MWMNavigationDashboardManager.shared().state != .hidden
vc.delegate = interactor
return vc
}()
init(interactor: PlacePageInteractor, storyboard: UIStoryboard, data: PlacePageData) {
self.interactor = interactor
self.storyboard = storyboard
self.placePageData = data
guard let trackData = data.trackData else {
fatalError("PlacePageData must contain trackData for the PlacePageTrackLayout")
}
self.trackData = trackData
}
private func configureViewControllers() -> [UIViewController] {
var viewControllers = [UIViewController]()
viewControllers.append(editTrackViewController)
if let trackData = placePageData.trackData {
editTrackViewController.view.isHidden = false
editTrackViewController.data = .track(trackData)
}
placePageData.onBookmarkStatusUpdate = { [weak self] in
guard let self = self else { return }
self.previewViewController.placePagePreviewData = self.placePageData.previewData
self.updateTrackRelatedSections()
}
if let elevationMapViewController {
viewControllers.append(elevationMapViewController)
}
return viewControllers
}
func calculateSteps(inScrollView scrollView: UIScrollView, compact: Bool) -> [PlacePageState] {
var steps: [PlacePageState] = []
let scrollHeight = scrollView.height
steps.append(.closed(-scrollHeight))
steps.append(.full(0))
return steps
}
}
private extension PlacePageTrackLayout {
func updateTrackRelatedSections() {
guard let trackData = placePageData.trackData else {
presenter?.closeAnimated()
return
}
editTrackViewController.data = .track(trackData)
let previewData = placePageData.previewData
if let headerViewController = headerViewControllers.compactMap({ $0 as? PlacePageHeaderViewController }).first {
headerViewController.setTitle(previewData.title, secondaryTitle: previewData.secondaryTitle)
placePageNavigationViewController.setTitle(previewData.title, secondaryTitle: previewData.secondaryTitle)
}
if let previewViewController = headerViewControllers.compactMap({ $0 as? PlacePagePreviewViewController }).first {
previewViewController.placePagePreviewData = previewData
previewViewController.updateViews()
}
presenter?.layoutIfNeeded()
}
}

View file

@ -0,0 +1,86 @@
final class PlacePageTrackRecordingLayout: IPlacePageLayout {
private var placePageData: PlacePageData
private var interactor: PlacePageInteractor
private let storyboard: UIStoryboard
weak var presenter: PlacePagePresenterProtocol?
lazy var bodyViewControllers: [UIViewController] = {
configureViewControllers()
}()
var actionBar: ActionBarViewController? {
actionBarViewController
}
var navigationBar: UIViewController? {
placePageNavigationViewController
}
var headerViewControllers: [UIViewController] {
[headerViewController]
}
lazy var headerViewController: PlacePageHeaderViewController = {
PlacePageHeaderBuilder.build(data: placePageData, delegate: interactor, headerType: .flexible)
}()
private lazy var placePageNavigationViewController: PlacePageHeaderViewController = {
PlacePageHeaderBuilder.build(data: placePageData, delegate: interactor, headerType: .fixed)
}()
private lazy var elevationProfileViewController: ElevationProfileViewController? = {
guard let trackData = placePageData.trackData else {
return nil
}
return ElevationProfileBuilder.build(trackData: trackData,
delegate: interactor)
}()
private lazy var actionBarViewController: ActionBarViewController = {
let vc = storyboard.instantiateViewController(ofType: ActionBarViewController.self)
vc.placePageData = placePageData
vc.canAddStop = MWMRouter.canAddIntermediatePoint()
vc.isRoutePlanning = MWMNavigationDashboardManager.shared().state != .hidden
vc.delegate = interactor
return vc
}()
var sectionSpacing: CGFloat { 0.0 }
init(interactor: PlacePageInteractor, storyboard: UIStoryboard, data: PlacePageData) {
self.interactor = interactor
self.storyboard = storyboard
self.placePageData = data
}
private func configureViewControllers() -> [UIViewController] {
var viewControllers = [UIViewController]()
if let elevationProfileViewController {
viewControllers.append(elevationProfileViewController)
}
placePageData.onTrackRecordingProgressUpdate = { [weak self] in
self?.updateTrackRecordingRelatedSections()
}
return viewControllers
}
func calculateSteps(inScrollView scrollView: UIScrollView, compact: Bool) -> [PlacePageState] {
var steps: [PlacePageState] = []
let scrollHeight = scrollView.height
steps.append(.closed(-scrollHeight))
steps.append(.full(0))
return steps
}
}
private extension PlacePageTrackRecordingLayout {
func updateTrackRecordingRelatedSections() {
guard let elevationProfileViewController, let trackData = placePageData.trackData else { return }
headerViewController.setTitle(placePageData.previewData.title, secondaryTitle: nil)
elevationProfileViewController.presenter?.update(with: trackData)
presenter?.layoutIfNeeded()
}
}

View file

@ -0,0 +1,5 @@
#import "MWMPlacePageProtocol.h"
@interface MWMPlacePageManager : NSObject<MWMPlacePageProtocol>
@end

View file

@ -0,0 +1,344 @@
#import "MWMPlacePageManager.h"
#import "CLLocation+Mercator.h"
#import "MWMActivityViewController.h"
#import "MWMLocationHelpers.h"
#import "MWMLocationObserver.h"
#import "MWMRoutePoint+CPP.h"
#import "MWMStorage+UI.h"
#import "SwiftBridge.h"
#import "MWMMapViewControlsManager+AddPlace.h"
#import <CoreApi/Framework.h>
#import <CoreApi/StringUtils.h>
#include "platform/downloader_defines.hpp"
#include "indexer/validate_and_format_contacts.hpp"
using namespace storage;
@interface MWMPlacePageManager ()
@property(nonatomic) storage::NodeStatus currentDownloaderStatus;
@end
@implementation MWMPlacePageManager
- (BOOL)isPPShown {
return GetFramework().HasPlacePageInfo();
}
- (void)closePlacePage { GetFramework().DeactivateMapSelection(); }
- (void)routeFrom:(PlacePageData *)data {
MWMRoutePoint *point = [self routePoint:data withType:MWMRoutePointTypeStart intermediateIndex:0];
[MWMRouter buildFromPoint:point bestRouter:YES];
[self closePlacePage];
}
- (void)routeTo:(PlacePageData *)data {
if ([MWMRouter isOnRoute]) {
[MWMRouter stopRouting];
}
[MWMSearch clear];
[[[MapViewController sharedController] searchManager] close];
if ([MWMMapOverlayManager transitEnabled]) {
[MWMRouter setType:MWMRouterTypePublicTransport];
}
MWMRoutePoint *point = [self routePoint:data withType:MWMRoutePointTypeFinish intermediateIndex:0];
[MWMRouter buildToPoint:point bestRouter:YES];
[self closePlacePage];
}
- (void)routeAddStop:(PlacePageData *)data {
MWMRoutePoint *point = [self routePoint:data withType:MWMRoutePointTypeIntermediate intermediateIndex:0];
[MWMRouter addPointAndRebuild:point];
[self closePlacePage];
}
- (void)routeRemoveStop:(PlacePageData *)data {
MWMRoutePoint *point = nil;
auto const intermediateIndex = GetFramework().GetCurrentPlacePageInfo().GetIntermediateIndex();
switch (GetFramework().GetCurrentPlacePageInfo().GetRouteMarkType()) {
case RouteMarkType::Start:
point = [self routePoint:data withType:MWMRoutePointTypeStart intermediateIndex:intermediateIndex];
break;
case RouteMarkType::Finish:
point = [self routePoint:data withType:MWMRoutePointTypeFinish intermediateIndex:intermediateIndex];
break;
case RouteMarkType::Intermediate:
point = [self routePoint:data withType:MWMRoutePointTypeIntermediate intermediateIndex:intermediateIndex];
break;
}
[MWMRouter removePointAndRebuild:point];
[self closePlacePage];
}
- (MWMRoutePoint *)routePointWithData:(PlacePageData *)data
pointType:(MWMRoutePointType)type
intermediateIndex:(size_t)intermediateIndex
{
if (data.isMyPosition) {
return [[MWMRoutePoint alloc] initWithLastLocationAndType:type intermediateIndex:intermediateIndex];
}
NSString *title = nil;
if (data.previewData.title.length > 0) {
title = data.previewData.title;
} else if (data.previewData.secondarySubtitle.length > 0) {
title = data.previewData.secondarySubtitle;
} else if (data.previewData.subtitle.length > 0) {
title = data.previewData.subtitle;
} else if (data.bookmarkData != nil) {
title = data.bookmarkData.externalTitle;
} else {
title = L(@"core_placepage_unknown_place");
}
NSString * subtitle = nil;
if (data.previewData.subtitle.length > 0 && ![title isEqualToString:data.previewData.subtitle]) {
subtitle = data.previewData.subtitle;
}
return [[MWMRoutePoint alloc] initWithPoint:location_helpers::ToMercator(data.locationCoordinate)
title:title
subtitle:subtitle
type:type
intermediateIndex:intermediateIndex];
}
- (MWMRoutePoint *)routePoint:(PlacePageData *)data
withType:(MWMRoutePointType)type
intermediateIndex:(size_t)intermediateIndex
{
if (data.isMyPosition) {
return [[MWMRoutePoint alloc] initWithLastLocationAndType:type
intermediateIndex:intermediateIndex];
}
NSString *title = nil;
if (data.previewData.title.length > 0) {
title = data.previewData.title;
} else if (data.previewData.secondarySubtitle.length > 0) {
title = data.previewData.secondarySubtitle;
} else if (data.previewData.subtitle.length > 0) {
title = data.previewData.subtitle;
} else if (data.bookmarkData != nil) {
title = data.bookmarkData.externalTitle;
} else {
title = L(@"core_placepage_unknown_place");
}
NSString * subtitle = nil;
if (data.previewData.subtitle.length > 0 && ![title isEqualToString:data.previewData.subtitle]) {
subtitle = data.previewData.subtitle;
}
return [[MWMRoutePoint alloc] initWithPoint:location_helpers::ToMercator(data.locationCoordinate)
title:title
subtitle:subtitle
type:type
intermediateIndex:intermediateIndex];
}
- (void)editPlace
{
[self.ownerViewController openEditor];
}
- (void)addBusiness
{
[[MWMMapViewControlsManager manager] addPlace:YES position:nullptr];
}
- (void)addPlace:(CLLocationCoordinate2D)coordinate
{
auto const position = location_helpers::ToMercator(coordinate);
[[MWMMapViewControlsManager manager] addPlace:NO position:&position];
}
- (void)addBookmark:(PlacePageData *)data {
auto &f = GetFramework();
auto &bmManager = f.GetBookmarkManager();
auto &info = f.GetCurrentPlacePageInfo();
auto const categoryId = f.LastEditedBMCategory();
kml::BookmarkData bmData;
bmData.m_name = info.FormatNewBookmarkName();
bmData.m_color.m_predefinedColor = f.LastEditedBMColor();
bmData.m_point = info.GetMercator();
if (info.IsFeature()) {
SaveFeatureTypes(info.GetTypes(), bmData);
}
auto editSession = bmManager.GetEditSession();
auto const *bookmark = editSession.CreateBookmark(std::move(bmData), categoryId);
auto buildInfo = info.GetBuildInfo();
buildInfo.m_match = place_page::BuildInfo::Match::Everything;
buildInfo.m_userMarkId = bookmark->GetId();
f.UpdatePlacePageInfoForCurrentSelection(buildInfo);
[data updateBookmarkStatus];
}
- (void)updateBookmark:(PlacePageData *)data color:(MWMBookmarkColor)color category:(MWMMarkGroupID)category {
MWMBookmarksManager * bookmarksManager = [MWMBookmarksManager sharedManager];
[bookmarksManager updateBookmark:data.bookmarkData.bookmarkId setGroupId:category title:data.previewData.title color:color description:data.bookmarkData.bookmarkDescription];
[MWMFrameworkHelper updatePlacePageData];
[data updateBookmarkStatus];
}
- (void)removeBookmark:(PlacePageData *)data
{
auto &f = GetFramework();
f.GetBookmarkManager().GetEditSession().DeleteBookmark(data.bookmarkData.bookmarkId);
[MWMFrameworkHelper updateAfterDeleteBookmark];
[data updateBookmarkStatus];
}
- (void)updateTrack:(PlacePageData *)data color:(UIColor *)color category:(MWMMarkGroupID)category {
MWMBookmarksManager * bookmarksManager = [MWMBookmarksManager sharedManager];
[bookmarksManager updateTrack:data.trackData.trackId setGroupId:category color:color title:data.previewData.title];
[MWMFrameworkHelper updatePlacePageData];
[data updateBookmarkStatus];
}
- (void)removeTrack:(PlacePageData *)data
{
auto &f = GetFramework();
f.GetBookmarkManager().GetEditSession().DeleteTrack(data.trackData.trackId);
}
- (void)call:(PlacePagePhone *)phone {
NSURL * _Nullable phoneURL = phone.url;
if (phoneURL && [UIApplication.sharedApplication canOpenURL:phoneURL]) {
[UIApplication.sharedApplication openURL:phoneURL options:@{} completionHandler:nil];
}
}
- (void)editBookmark:(PlacePageData *)data {
MWMEditBookmarkController *editBookmarkController = [[UIStoryboard instance:MWMStoryboardMain]
instantiateViewControllerWithIdentifier:@"MWMEditBookmarkController"];
[editBookmarkController configureWithPlacePageData:data];
[[MapViewController sharedController].navigationController pushViewController:editBookmarkController animated:YES];
}
- (void)editTrack:(PlacePageData *)data
{
if (data.objectType != PlacePageObjectTypeTrack)
{
LOG(LERROR, ("editTrack called for non-track object"));
return;
}
EditTrackViewController * editTrackController = [[EditTrackViewController alloc] initWithTrackId:data.trackData.trackId editCompletion:^(BOOL edited) {
if (!edited)
return;
[MWMFrameworkHelper updatePlacePageData];
[data updateBookmarkStatus];
}];
[[MapViewController sharedController].navigationController pushViewController:editTrackController animated:YES];
}
- (void)showPlaceDescription:(NSString *)htmlString
{
[self.ownerViewController openFullPlaceDescriptionWithHtml:htmlString];
}
- (void)avoidDirty {
[MWMRouter avoidRoadTypeAndRebuild:MWMRoadTypeDirty];
[self closePlacePage];
}
- (void)avoidFerry {
[MWMRouter avoidRoadTypeAndRebuild:MWMRoadTypeFerry];
[self closePlacePage];
}
- (void)avoidToll {
[MWMRouter avoidRoadTypeAndRebuild:MWMRoadTypeToll];
[self closePlacePage];
}
- (void)openWebsite:(PlacePageData *)data {
[self.ownerViewController openUrl:data.infoData.website externally:YES];
}
- (void)openWebsiteMenu:(PlacePageData *)data {
[self.ownerViewController openUrl:data.infoData.websiteMenu externally:YES];
}
- (void)openWikipedia:(PlacePageData *)data {
[self.ownerViewController openUrl:data.infoData.wikipedia externally:YES];
}
- (void)openWikimediaCommons:(PlacePageData *)data {
[self.ownerViewController openUrl:data.infoData.wikimediaCommons externally:YES];
}
- (void)openFediverse:(PlacePageData *)data {
std::string const fullUrl = osm::socialContactToURL(osm::MapObject::MetadataID::FMD_CONTACT_FEDIVERSE, [data.infoData.fediverse UTF8String]);
[self.ownerViewController openUrl:ToNSString(fullUrl) externally:YES];
}
- (void)openFacebook:(PlacePageData *)data {
std::string const fullUrl = osm::socialContactToURL(osm::MapObject::MetadataID::FMD_CONTACT_FACEBOOK, [data.infoData.facebook UTF8String]);
[self.ownerViewController openUrl:ToNSString(fullUrl) externally:YES];
}
- (void)openInstagram:(PlacePageData *)data {
std::string const fullUrl = osm::socialContactToURL(osm::MapObject::MetadataID::FMD_CONTACT_INSTAGRAM, [data.infoData.instagram UTF8String]);
[self.ownerViewController openUrl:ToNSString(fullUrl) externally:YES];
}
- (void)openTwitter:(PlacePageData *)data {
std::string const fullUrl = osm::socialContactToURL(osm::MapObject::MetadataID::FMD_CONTACT_TWITTER, [data.infoData.twitter UTF8String]);
[self.ownerViewController openUrl:ToNSString(fullUrl) externally:YES];
}
- (void)openVk:(PlacePageData *)data {
std::string const fullUrl = osm::socialContactToURL(osm::MapObject::MetadataID::FMD_CONTACT_VK, [data.infoData.vk UTF8String]);
[self.ownerViewController openUrl:ToNSString(fullUrl) externally:YES];
}
- (void)openLine:(PlacePageData *)data {
std::string const fullUrl = osm::socialContactToURL(osm::MapObject::MetadataID::FMD_CONTACT_LINE, [data.infoData.line UTF8String]);
[self.ownerViewController openUrl:ToNSString(fullUrl) externally:YES];
}
- (void)openBluesky:(PlacePageData *)data {
std::string const fullUrl = osm::socialContactToURL(osm::MapObject::MetadataID::FMD_CONTACT_BLUESKY, [data.infoData.bluesky UTF8String]);
[self.ownerViewController openUrl:ToNSString(fullUrl) externally:YES];
}
- (void)openPanoramax:(PlacePageData *)data {
std::string const fullUrl = osm::socialContactToURL(osm::MapObject::MetadataID::FMD_PANORAMAX, [data.infoData.panoramax UTF8String]);
[self.ownerViewController openUrl:ToNSString(fullUrl) externally:YES];
}
- (void)openEmail:(PlacePageData *)data {
[MailComposer sendEmailWithSubject:nil body:nil toRecipients:@[data.infoData.email] attachmentFileURL:nil];
}
- (void)openElevationDifficultPopup:(PlacePageData *)data {
auto difficultyPopup = [ElevationDetailsBuilder buildWithData:data];
[[MapViewController sharedController] presentViewController:difficultyPopup animated:YES completion:nil];
}
#pragma mark - AvailableArea / PlacePageArea
- (void)updateAvailableArea:(CGRect)frame
{
// auto data = self.data;
// if (data)
// [self.layout updateAvailableArea:frame];
}
#pragma mark - MWMFeatureHolder
- (FeatureID const &)featureId { return GetFramework().GetCurrentPlacePageInfo().GetID(); }
- (MapViewController *)ownerViewController { return [MapViewController sharedController]; }
@end

View file

@ -0,0 +1,50 @@
@class PlacePageData;
@class PlacePagePhone;
@class ElevationProfileData;
@interface MWMPlacePageManagerHelper : NSObject
+ (void)updateAvailableArea:(CGRect)frame;
+ (void)editPlace;
+ (void)addBusiness;
+ (void)addPlace:(CLLocationCoordinate2D)coordinate;
+ (void)openWebsite:(PlacePageData *)data;
+ (void)openWebsiteMenu:(PlacePageData *)data;
+ (void)openWikipedia:(PlacePageData *)data;
+ (void)openWikimediaCommons:(PlacePageData *)data;
+ (void)openEmail:(PlacePageData *)data;
+ (void)openFediverse:(PlacePageData *)data;
+ (void)openFacebook:(PlacePageData *)data;
+ (void)openInstagram:(PlacePageData *)data;
+ (void)openTwitter:(PlacePageData *)data;
+ (void)openVk:(PlacePageData *)data;
+ (void)openLine:(PlacePageData *)data;
+ (void)openBluesky:(PlacePageData *)data;
+ (void)openPanoramax:(PlacePageData *)data;
+ (void)call:(PlacePagePhone *)phone;
+ (void)showAllFacilities:(PlacePageData *)data;
+ (void)showPlaceDescription:(NSString *)htmlString;
+ (void)openMoreUrl:(PlacePageData *)data;
+ (void)openReviewUrl:(PlacePageData *)data;
+ (void)openDescriptionUrl:(PlacePageData *)data;
+ (void)openCatalogSingleItem:(PlacePageData *)data atIndex:(NSInteger)index;
+ (void)openCatalogMoreItems:(PlacePageData *)data;
+ (void)addBookmark:(PlacePageData *)data;
+ (void)updateBookmark:(PlacePageData *)data color:(MWMBookmarkColor)color category:(MWMMarkGroupID)category;
+ (void)removeBookmark:(PlacePageData *)data;
+ (void)updateTrack:(PlacePageData *)data color:(UIColor *)color category:(MWMMarkGroupID)category;
+ (void)removeTrack:(PlacePageData *)data;
+ (void)editBookmark:(PlacePageData *)data;
+ (void)editTrack:(PlacePageData *)data;
+ (void)searchBookingHotels:(PlacePageData *)data;
+ (void)book:(PlacePageData *)data;
+ (void)routeFrom:(PlacePageData *)data;
+ (void)routeTo:(PlacePageData *)data;
+ (void)routeAddStop:(PlacePageData *)data;
+ (void)routeRemoveStop:(PlacePageData *)data;
+ (void)avoidDirty;
+ (void)avoidFerry;
+ (void)avoidToll;
+ (void)openElevationDifficultPopup:(PlacePageData *)data;
@end

View file

@ -0,0 +1,229 @@
#import "MWMPlacePageManagerHelper.h"
#import "MWMMapViewControlsManager.h"
#import "MWMPlacePageManager.h"
@interface MWMMapViewControlsManager ()
@property(nonatomic) MWMPlacePageManager * placePageManager;
@end
@interface MWMPlacePageManager ()
- (void)updateAvailableArea:(CGRect)frame;
- (void)editPlace;
- (void)addBusiness;
- (void)addPlace:(CLLocationCoordinate2D)coordinate;
- (void)openWebsite:(PlacePageData *)data;
- (void)openWebsiteMenu:(PlacePageData *)data;
- (void)openWikipedia:(PlacePageData *)data;
- (void)openWikimediaCommons:(PlacePageData *)data;
- (void)openEmail:(PlacePageData *)data;
- (void)openFediverse:(PlacePageData *)data;
- (void)openFacebook:(PlacePageData *)data;
- (void)openInstagram:(PlacePageData *)data;
- (void)openTwitter:(PlacePageData *)data;
- (void)openVk:(PlacePageData *)data;
- (void)openLine:(PlacePageData *)data;
- (void)openBluesky:(PlacePageData *)data;
- (void)openPanoramax:(PlacePageData *)data;
- (void)call:(PlacePagePhone *)phone;
- (void)showAllFacilities:(PlacePageData *)data;
- (void)showPlaceDescription:(NSString *)htmlString;
- (void)openMoreUrl:(PlacePageData *)data;
- (void)openReviewUrl:(PlacePageData *)data;
- (void)openDescriptionUrl:(PlacePageData *)data;
- (void)openCatalogSingleItem:(PlacePageData *)data atIndex:(NSInteger)index;
- (void)openCatalogMoreItems:(PlacePageData *)data;
- (void)addBookmark:(PlacePageData *)data;
- (void)updateBookmark:(PlacePageData *)data color:(MWMBookmarkColor)color category:(MWMMarkGroupID)category;
- (void)removeBookmark:(PlacePageData *)data;
- (void)updateTrack:(PlacePageData *)data color:(UIColor *)color category:(MWMMarkGroupID)category;
- (void)removeTrack:(PlacePageData *)data;
- (void)editBookmark:(PlacePageData *)data;
- (void)editTrack:(PlacePageData *)data;
- (void)searchBookingHotels:(PlacePageData *)data;
- (void)book:(PlacePageData *)data;
- (void)routeFrom:(PlacePageData *)data;
- (void)routeTo:(PlacePageData *)data;
- (void)routeAddStop:(PlacePageData *)data;
- (void)routeRemoveStop:(PlacePageData *)data;
- (void)avoidDirty;
- (void)avoidFerry;
- (void)avoidToll;
- (void)openElevationDifficultPopup:(PlacePageData *)data;
@end
@implementation MWMPlacePageManagerHelper
+ (void)updateAvailableArea:(CGRect)frame
{
[[MWMMapViewControlsManager manager].placePageManager updateAvailableArea:frame];
}
+ (void)editPlace {
[[MWMMapViewControlsManager manager].placePageManager editPlace];
}
+ (void)addBusiness {
[[MWMMapViewControlsManager manager].placePageManager addBusiness];
}
+ (void)addPlace:(CLLocationCoordinate2D)coordinate {
[[MWMMapViewControlsManager manager].placePageManager addPlace:coordinate];
}
+ (void)openWebsite:(PlacePageData *)data {
[[MWMMapViewControlsManager manager].placePageManager openWebsite:data];
}
+ (void)openWebsiteMenu:(PlacePageData *)data {
[[MWMMapViewControlsManager manager].placePageManager openWebsiteMenu:data];
}
+ (void)openEmail:(PlacePageData *)data {
[[MWMMapViewControlsManager manager].placePageManager openEmail:data];
}
+ (void)openWikipedia:(PlacePageData *)data {
[[MWMMapViewControlsManager manager].placePageManager openWikipedia:data];
}
+ (void)openWikimediaCommons:(PlacePageData *)data {
[[MWMMapViewControlsManager manager].placePageManager openWikimediaCommons:data];
}
+ (void)openFediverse:(PlacePageData *)data {
[[MWMMapViewControlsManager manager].placePageManager openFediverse:data];
}
+ (void)openFacebook:(PlacePageData *)data {
[[MWMMapViewControlsManager manager].placePageManager openFacebook:data];
}
+ (void)openInstagram:(PlacePageData *)data {
[[MWMMapViewControlsManager manager].placePageManager openInstagram:data];
}
+ (void)openTwitter:(PlacePageData *)data {
[[MWMMapViewControlsManager manager].placePageManager openTwitter:data];
}
+ (void)openVk:(PlacePageData *)data {
[[MWMMapViewControlsManager manager].placePageManager openVk:data];
}
+ (void)openLine:(PlacePageData *)data {
[[MWMMapViewControlsManager manager].placePageManager openLine:data];
}
+ (void)openBluesky:(PlacePageData *)data {
[[MWMMapViewControlsManager manager].placePageManager openBluesky:data];
}
+ (void)openPanoramax:(PlacePageData *)data {
[[MWMMapViewControlsManager manager].placePageManager openPanoramax:data];
}
+ (void)call:(PlacePagePhone *)phone {
[[MWMMapViewControlsManager manager].placePageManager call:phone];
}
+ (void)showAllFacilities:(PlacePageData *)data {
[[MWMMapViewControlsManager manager].placePageManager showAllFacilities:data];
}
+ (void)showPlaceDescription:(NSString *)htmlString {
[[MWMMapViewControlsManager manager].placePageManager showPlaceDescription:htmlString];
}
+ (void)openMoreUrl:(PlacePageData *)data {
[[MWMMapViewControlsManager manager].placePageManager openMoreUrl:data];
}
+ (void)openReviewUrl:(PlacePageData *)data {
[[MWMMapViewControlsManager manager].placePageManager openReviewUrl:data];
}
+ (void)openDescriptionUrl:(PlacePageData *)data {
[[MWMMapViewControlsManager manager].placePageManager openDescriptionUrl:data];
}
+ (void)openCatalogSingleItem:(PlacePageData *)data atIndex:(NSInteger)index {
[[MWMMapViewControlsManager manager].placePageManager openCatalogSingleItem:data atIndex:index];
}
+ (void)openCatalogMoreItems:(PlacePageData *)data {
[[MWMMapViewControlsManager manager].placePageManager openCatalogMoreItems:data];
}
+ (void)addBookmark:(PlacePageData *)data {
[[MWMMapViewControlsManager manager].placePageManager addBookmark:data];
}
+ (void)updateBookmark:(PlacePageData *)data color:(MWMBookmarkColor)color category:(MWMMarkGroupID)category {
[[MWMMapViewControlsManager manager].placePageManager updateBookmark:data color:color category:category];
}
+ (void)removeBookmark:(PlacePageData *)data {
[[MWMMapViewControlsManager manager].placePageManager removeBookmark:data];
}
+ (void)updateTrack:(PlacePageData *)data color:(UIColor *)color category:(MWMMarkGroupID)category {
[[MWMMapViewControlsManager manager].placePageManager updateTrack:data color:color category:category];
}
+ (void)removeTrack:(PlacePageData *)data {
[[MWMMapViewControlsManager manager].placePageManager removeTrack:data];
}
+ (void)editBookmark:(PlacePageData *)data {
[[MWMMapViewControlsManager manager].placePageManager editBookmark:data];
}
+ (void)editTrack:(PlacePageData *)data {
[[MWMMapViewControlsManager manager].placePageManager editTrack:data];
}
+ (void)searchBookingHotels:(PlacePageData *)data {
[[MWMMapViewControlsManager manager].placePageManager searchBookingHotels:data];
}
+ (void)book:(PlacePageData *)data {
[[MWMMapViewControlsManager manager].placePageManager book:data];
}
+ (void)routeFrom:(PlacePageData *)data {
[[MWMMapViewControlsManager manager].placePageManager routeFrom:data];
}
+ (void)routeTo:(PlacePageData *)data {
[[MWMMapViewControlsManager manager].placePageManager routeTo:data];
}
+ (void)routeAddStop:(PlacePageData *)data {
[[MWMMapViewControlsManager manager].placePageManager routeAddStop:data];
}
+ (void)routeRemoveStop:(PlacePageData *)data {
[[MWMMapViewControlsManager manager].placePageManager routeRemoveStop:data];
}
+ (void)avoidDirty {
[[MWMMapViewControlsManager manager].placePageManager avoidDirty];
}
+ (void)avoidFerry {
[[MWMMapViewControlsManager manager].placePageManager avoidFerry];
}
+ (void)avoidToll {
[[MWMMapViewControlsManager manager].placePageManager avoidToll];
}
+ (void)openElevationDifficultPopup:(PlacePageData *)data {
[[MWMMapViewControlsManager manager].placePageManager openElevationDifficultPopup:data];
}
@end

View file

@ -0,0 +1,46 @@
protocol PlacePagePresenterProtocol: AnyObject {
func updatePreviewOffset()
func layoutIfNeeded()
func showNextStop()
func closeAnimated()
func showAlert(_ alert: UIAlertController)
func showShareTrackMenu()
}
final class PlacePagePresenter: NSObject {
private weak var view: PlacePageViewProtocol!
private weak var headerView: PlacePageHeaderViewProtocol!
init(view: PlacePageViewProtocol, headerView: PlacePageHeaderViewProtocol) {
self.view = view
self.headerView = headerView
}
}
// MARK: - PlacePagePresenterProtocol
extension PlacePagePresenter: PlacePagePresenterProtocol {
func updatePreviewOffset() {
view.updatePreviewOffset()
}
func layoutIfNeeded() {
view.layoutIfNeeded()
}
func showNextStop() {
view.showNextStop()
}
func closeAnimated() {
view.closeAnimated(completion: nil)
}
func showAlert(_ alert: UIAlertController) {
view.showAlert(alert)
}
func showShareTrackMenu() {
headerView.showShareTrackMenu()
}
}

View file

@ -0,0 +1,417 @@
protocol PlacePageViewProtocol: AnyObject {
var interactor: PlacePageInteractorProtocol? { get set }
func setLayout(_ layout: IPlacePageLayout)
func closeAnimated(completion: (() -> Void)?)
func updatePreviewOffset()
func showNextStop()
func layoutIfNeeded()
func updateWithLayout(_ layout: IPlacePageLayout)
func showAlert(_ alert: UIAlertController)
}
final class PlacePageScrollView: UIScrollView {
override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
return point.y > 0
}
}
@objc final class PlacePageViewController: UIViewController {
private enum Constants {
static let actionBarHeight: CGFloat = 50
static let additionalPreviewOffset: CGFloat = 80
}
@IBOutlet var scrollView: UIScrollView!
@IBOutlet var stackView: UIStackView!
@IBOutlet var actionBarContainerView: UIView!
@IBOutlet var actionBarHeightConstraint: NSLayoutConstraint!
@IBOutlet var panGesture: UIPanGestureRecognizer!
var headerStackView: UIStackView = {
let stackView = UIStackView()
stackView.axis = .vertical
stackView.distribution = .fill
return stackView
}()
var interactor: PlacePageInteractorProtocol?
var beginDragging = false
var rootViewController: MapViewController {
MapViewController.shared()!
}
private var previousTraitCollection: UITraitCollection?
private var layout: IPlacePageLayout!
private var scrollSteps: [PlacePageState] = []
var isPreviewPlus: Bool = false
private var isNavigationBarVisible = false
// MARK: - VC Lifecycle
override func viewDidLoad() {
super.viewDidLoad()
setupView()
setupLayout(layout)
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
panGesture.isEnabled = alternativeSizeClass(iPhone: false, iPad: true)
previousTraitCollection = traitCollection
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
interactor?.viewWillAppear()
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
updatePreviewOffset()
}
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
interactor?.viewWillDisappear()
}
override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
super.traitCollectionDidChange(previousTraitCollection)
// Update layout when the device was rotated but skip when the appearance was changed.
if self.previousTraitCollection != nil, previousTraitCollection?.userInterfaceStyle == traitCollection.userInterfaceStyle, previousTraitCollection?.verticalSizeClass != traitCollection.verticalSizeClass {
DispatchQueue.main.async {
self.updateSteps()
self.showLastStop()
self.scrollView.contentInset = self.alternativeSizeClass(iPhone: UIEdgeInsets(top: self.scrollView.height, left: 0, bottom: 0, right: 0),
iPad: UIEdgeInsets.zero)
}
}
}
// MARK: - Actions
@IBAction func onPan(gesture: UIPanGestureRecognizer) {
let xOffset = gesture.translation(in: view.superview).x
gesture.setTranslation(CGPoint.zero, in: view.superview)
view.minX += xOffset
view.minX = min(view.minX, 0)
let alpha = view.maxX / view.width
view.alpha = alpha
let state = gesture.state
if state == .ended || state == .cancelled {
if alpha < 0.8 {
closeAnimated()
} else {
UIView.animate(withDuration: kDefaultAnimationDuration) {
self.view.minX = 0
self.view.alpha = 1
}
}
}
}
// MARK: - Private methods
private func updateSteps() {
layoutIfNeeded()
scrollSteps = layout.calculateSteps(inScrollView: scrollView,
compact: traitCollection.verticalSizeClass == .compact)
}
private func findNextStop(_ offset: CGFloat, velocity: CGFloat) -> PlacePageState {
if velocity == 0 {
return findNearestStop(offset)
}
var result: PlacePageState
if velocity < 0 {
guard let first = scrollSteps.first else { return .closed(-scrollView.height) }
result = first
scrollSteps.suffix(from: 1).forEach {
if offset > $0.offset {
result = $0
}
}
} else {
guard let last = scrollSteps.last else { return .closed(-scrollView.height) }
result = last
scrollSteps.reversed().suffix(from: 1).forEach {
if offset < $0.offset {
result = $0
}
}
}
return result
}
private func setupView() {
let bgView = UIView()
stackView.insertSubview(bgView, at: 0)
bgView.alignToSuperview()
scrollView.decelerationRate = .fast
scrollView.backgroundColor = .clear
stackView.backgroundColor = .clear
let cornersToMask: CACornerMask = alternativeSizeClass(iPhone: [], iPad: [.layerMinXMaxYCorner, .layerMaxXMaxYCorner])
actionBarContainerView.layer.setCornerRadius(.modalSheet, maskedCorners: cornersToMask)
actionBarContainerView.layer.masksToBounds = true
if previousTraitCollection == nil {
scrollView.contentInset = alternativeSizeClass(iPhone: UIEdgeInsets(top: view.height, left: 0, bottom: 0, right: 0),
iPad: UIEdgeInsets.zero)
scrollView.layoutIfNeeded()
}
}
private func setupLayout(_ layout: IPlacePageLayout) {
setLayout(layout)
let showSeparator = layout.sectionSpacing > 0
stackView.spacing = layout.sectionSpacing
fillHeader(with: layout.headerViewControllers, showSeparator: showSeparator)
fillBody(with: layout.bodyViewControllers, showSeparator: showSeparator)
beginDragging = false
if let actionBar = layout.actionBar {
hideActionBar(false)
addActionBar(actionBar)
} else {
hideActionBar(true)
}
}
private func fillHeader(with viewControllers: [UIViewController], showSeparator: Bool = true) {
viewControllers.forEach { [self] viewController in
if !stackView.arrangedSubviews.contains(headerStackView) {
stackView.addArrangedSubview(headerStackView)
}
headerStackView.addArrangedSubview(viewController.view)
}
if showSeparator {
headerStackView.addSeparator(.bottom)
}
}
private func fillBody(with viewControllers: [UIViewController], showSeparator: Bool = true) {
viewControllers.forEach { [self] viewController in
addChild(viewController)
stackView.addArrangedSubview(viewController.view)
viewController.didMove(toParent: self)
if showSeparator {
viewController.view.addSeparator(.top)
if !(viewController is PlacePageInfoViewController) {
viewController.view.addSeparator(.bottom)
}
}
}
}
private func cleanupLayout() {
guard let layout else { return }
let childViewControllers = [layout.actionBar, layout.navigationBar] + layout.headerViewControllers + layout.bodyViewControllers
childViewControllers.forEach {
$0?.willMove(toParent: nil)
$0?.view.removeFromSuperview()
$0?.removeFromParent()
}
headerStackView.arrangedSubviews.forEach { $0.removeFromSuperview() }
stackView.arrangedSubviews.forEach { $0.removeFromSuperview() }
}
private func findNearestStop(_ offset: CGFloat) -> PlacePageState {
var result = scrollSteps[0]
scrollSteps.suffix(from: 1).forEach { ppState in
if abs(result.offset - offset) > abs(ppState.offset - offset) {
result = ppState
}
}
return result
}
private func addActionBar(_ actionBarViewController: UIViewController) {
addChild(actionBarViewController)
actionBarViewController.view.translatesAutoresizingMaskIntoConstraints = false
actionBarContainerView.addSubview(actionBarViewController.view)
actionBarViewController.didMove(toParent: self)
NSLayoutConstraint.activate([
actionBarViewController.view.leadingAnchor.constraint(equalTo: actionBarContainerView.leadingAnchor),
actionBarViewController.view.topAnchor.constraint(equalTo: actionBarContainerView.topAnchor),
actionBarViewController.view.trailingAnchor.constraint(equalTo: actionBarContainerView.trailingAnchor),
actionBarViewController.view.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor)
])
}
private func addNavigationBar(_ header: UIViewController) {
header.view.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(header.view)
addChild(header)
NSLayoutConstraint.activate([
header.view.leadingAnchor.constraint(equalTo: view.leadingAnchor),
header.view.topAnchor.constraint(equalTo: view.topAnchor),
header.view.trailingAnchor.constraint(equalTo: view.trailingAnchor)
])
}
private func scrollTo(_ point: CGPoint, animated: Bool = true, forced: Bool = false, completion: (() -> Void)? = nil) {
if alternativeSizeClass(iPhone: beginDragging, iPad: true) && !forced {
return
}
if forced {
beginDragging = true
}
let scrollPosition = CGPoint(x: point.x, y: min(scrollView.contentSize.height - scrollView.height, point.y))
let bound = view.height + scrollPosition.y
if animated {
updateTopBound(bound, duration: kDefaultAnimationDuration)
UIView.animate(withDuration: kDefaultAnimationDuration, animations: { [weak scrollView] in
scrollView?.contentOffset = scrollPosition
self.layoutIfNeeded()
}) { complete in
if complete {
completion?()
}
}
} else {
scrollView?.contentOffset = scrollPosition
completion?()
}
}
private func showLastStop() {
if let lastStop = scrollSteps.last {
scrollTo(CGPoint(x: 0, y: lastStop.offset), forced: true)
}
}
private func updateTopBound(_ bound: CGFloat, duration: TimeInterval) {
alternativeSizeClass(iPhone: {
interactor?.updateTopBound(bound, duration: duration)
}, iPad: {})
}
}
// MARK: - PlacePageViewProtocol
extension PlacePageViewController: PlacePageViewProtocol {
func layoutIfNeeded() {
guard layout != nil else { return }
view.layoutIfNeeded()
}
func updateWithLayout(_ layout: IPlacePageLayout) {
setupLayout(layout)
}
func setLayout(_ layout: IPlacePageLayout) {
if self.layout != nil {
cleanupLayout()
}
self.layout = layout
}
func hideActionBar(_ value: Bool) {
actionBarHeightConstraint.constant = !value ? Constants.actionBarHeight : .zero
}
func updatePreviewOffset() {
updateSteps()
if !beginDragging {
let stateOffset = isPreviewPlus ? scrollSteps[2].offset : scrollSteps[1].offset + Constants.additionalPreviewOffset
scrollTo(CGPoint(x: 0, y: stateOffset))
}
}
func showNextStop() {
if let nextStop = scrollSteps.last(where: { $0.offset > scrollView.contentOffset.y }) {
scrollTo(CGPoint(x: 0, y: nextStop.offset), forced: true)
}
}
@objc
func closeAnimated(completion: (() -> Void)? = nil) {
view.isUserInteractionEnabled = false
alternativeSizeClass(iPhone: {
self.scrollTo(CGPoint(x: 0, y: -self.scrollView.height + 1),
forced: true) {
self.rootViewController.dismissPlacePage()
completion?()
}
}, iPad: {
UIView.animate(withDuration: kDefaultAnimationDuration,
animations: {
let frame = self.view.frame
self.view.minX = frame.minX - frame.width
self.view.alpha = 0
}) { complete in
self.rootViewController.dismissPlacePage()
completion?()
}
})
}
func showAlert(_ alert: UIAlertController) {
present(alert, animated: true)
}
}
// MARK: - UIScrollViewDelegate
extension PlacePageViewController: UIScrollViewDelegate {
func scrollViewDidScroll(_ scrollView: UIScrollView) {
if scrollView.contentOffset.y < -scrollView.height + 1 && beginDragging {
closeAnimated()
}
onOffsetChanged(scrollView.contentOffset.y)
let bound = view.height + scrollView.contentOffset.y
updateTopBound(bound, duration: 0)
}
func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
beginDragging = true
}
func scrollViewWillEndDragging(_ scrollView: UIScrollView,
withVelocity velocity: CGPoint,
targetContentOffset: UnsafeMutablePointer<CGPoint>) {
let maxOffset = scrollSteps.last?.offset ?? 0
if targetContentOffset.pointee.y > maxOffset {
return
}
let targetState = findNextStop(scrollView.contentOffset.y, velocity: velocity.y)
if targetState.offset > scrollView.contentSize.height - scrollView.contentInset.top {
return
}
updateSteps()
let nextStep = findNextStop(scrollView.contentOffset.y, velocity: velocity.y)
targetContentOffset.pointee = CGPoint(x: 0, y: nextStep.offset)
}
func onOffsetChanged(_ offset: CGFloat) {
if offset > 0 && !isNavigationBarVisible {
setNavigationBarVisible(true)
} else if offset <= 0 && isNavigationBarVisible {
setNavigationBarVisible(false)
}
}
private func setNavigationBarVisible(_ visible: Bool) {
guard visible != isNavigationBarVisible, let navigationBar = layout?.navigationBar else { return }
isNavigationBarVisible = visible
if isNavigationBarVisible {
addNavigationBar(navigationBar)
} else {
navigationBar.removeFromParent()
navigationBar.view.removeFromSuperview()
}
}
}

View file

@ -0,0 +1,32 @@
import Foundation
@objcMembers
class OpeinigHoursLocalization: NSObject, IOpeningHoursLocalization {
var closedString: String {
L("closed")
}
var breakString: String {
L("editor_hours_closed")
}
var twentyFourSevenString: String {
L("twentyfour_seven")
}
var allDayString: String {
L("editor_time_allday")
}
var dailyString: String {
L("daily")
}
var todayString: String {
L("today")
}
var dayOffString: String {
L("day_off_today")
}
}

View file

@ -0,0 +1,65 @@
class DifficultyView: UIView {
private let stackView = UIStackView()
private var views:[UIView] = []
var difficulty: ElevationDifficulty = .easy {
didSet {
updateView()
}
}
var colors: [UIColor] = [.gray, .green, .orange, .red]
{
didSet {
updateView()
}
}
var emptyColor: UIColor = UIColor.gray {
didSet {
updateView()
}
}
private let bulletSize = CGSize(width: 10, height: 10)
private let bulletSpacing: CGFloat = 5
private let difficultyLevelCount = 3
override init(frame: CGRect) {
super.init(frame: frame)
initComponent()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
initComponent()
}
private func initComponent() {
self.addSubview(stackView)
stackView.frame = bounds
stackView.distribution = .fillEqually
stackView.axis = .horizontal
stackView.spacing = bulletSpacing
stackView.alignment = .fill
for _ in 0..<difficultyLevelCount {
let view = UIView()
stackView.addArrangedSubview(view)
view.layer.setCornerRadius(.custom(bulletSize.height / 2))
views.append(view)
}
}
private func updateView() {
guard colors.count > difficulty.rawValue else {
assertionFailure("No fill color")
return
}
let fillColor = colors[difficulty.rawValue]
for (idx, view) in views.enumerated() {
if idx < difficulty.rawValue {
view.backgroundColor = fillColor
} else {
view.backgroundColor = emptyColor
}
}
}
}

View file

@ -0,0 +1,167 @@
final class ExpandableLabel: UIView {
typealias OnExpandClosure = (() -> Void) -> Void
private let stackView = UIStackView()
private let textView = UITextView()
private let expandLabel = UILabel()
var onExpandClosure: OnExpandClosure?
var font = UIFont.systemFont(ofSize: 16) {
didSet {
textView.font = font
expandLabel.font = font
}
}
var textColor = UIColor.black {
didSet {
textView.textColor = textColor
}
}
var expandColor = UIColor.systemBlue {
didSet {
expandLabel.textColor = expandColor
}
}
var text: String? {
didSet {
containerText = text
textView.text = text
if let text = text {
isHidden = text.isEmpty
} else {
isHidden = true
}
}
}
var attributedText: NSAttributedString? {
didSet {
containerText = attributedText?.string
textView.attributedText = attributedText
if let attributedText = attributedText {
isHidden = attributedText.length == 0
} else {
isHidden = true
}
}
}
var expandText = "More" {
didSet {
expandLabel.text = expandText
}
}
var numberOfLines = 2 {
didSet {
containerMaximumNumberOfLines = numberOfLines > 0 ? numberOfLines + 1 : 0
}
}
private var containerText: String?
private var containerMaximumNumberOfLines = 2 {
didSet {
textView.textContainer.maximumNumberOfLines = containerMaximumNumberOfLines
textView.invalidateIntrinsicContentSize()
}
}
private var oldWidth: CGFloat = 0
override func setContentHuggingPriority(_ priority: UILayoutPriority, for axis: NSLayoutConstraint.Axis) {
super.setContentHuggingPriority(priority, for: axis)
textView.setContentHuggingPriority(priority, for: axis)
expandLabel.setContentHuggingPriority(priority, for: axis)
}
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
commonInit()
}
private func commonInit() {
stackView.axis = .vertical
stackView.alignment = .leading
containerMaximumNumberOfLines = numberOfLines > 0 ? numberOfLines + 1 : 0
textView.textContainer.lineFragmentPadding = 0
textView.isScrollEnabled = false
textView.isEditable = false
textView.textContainerInset = .zero
textView.contentMode = .topLeft
textView.font = font
textView.textColor = textColor
textView.text = text
textView.attributedText = attributedText
textView.setContentHuggingPriority(contentHuggingPriority(for: .vertical), for: .vertical)
textView.backgroundColor = .clear
textView.dataDetectorTypes = [.link, .phoneNumber]
expandLabel.setContentHuggingPriority(contentHuggingPriority(for: .vertical), for: .vertical)
expandLabel.font = font
expandLabel.textColor = expandColor
expandLabel.text = expandText
expandLabel.isHidden = true
addSubview(stackView)
stackView.addArrangedSubview(textView)
stackView.addArrangedSubview(expandLabel)
stackView.alignToSuperview()
let gr = UITapGestureRecognizer(target: self, action: #selector(onExpand(_:)))
addGestureRecognizer(gr)
}
@objc func onExpand(_ sender: UITapGestureRecognizer) {
if expandLabel.isHidden { return }
let expandClosure = {
UIView.animate(withDuration: kDefaultAnimationDuration) {
self.containerMaximumNumberOfLines = 0
self.expandLabel.isHidden = true
self.stackView.layoutIfNeeded()
}
}
if let onExpandClosure = onExpandClosure {
onExpandClosure(expandClosure)
} else {
expandClosure()
}
}
override func layoutSubviews() {
super.layoutSubviews()
if oldWidth != bounds.width, let attributedText = attributedText?.mutableCopy() as? NSMutableAttributedString {
attributedText.enumerateAttachments(estimatedWidth: bounds.width)
self.attributedText = attributedText
oldWidth = bounds.width
}
guard containerMaximumNumberOfLines > 0,
containerMaximumNumberOfLines != numberOfLines,
let s = containerText,
!s.isEmpty else {
return
}
let textRect = s.boundingRect(with: CGSize(width: width, height: CGFloat.greatestFiniteMagnitude),
options: .usesLineFragmentOrigin,
attributes: [.font: font],
context: nil)
let lineHeight = font.lineHeight
if Int(lineHeight * CGFloat(numberOfLines + 1)) >= Int(textRect.height) {
expandLabel.isHidden = true
containerMaximumNumberOfLines = 0
} else {
expandLabel.isHidden = false
containerMaximumNumberOfLines = numberOfLines
}
layoutIfNeeded()
}
}

View file

@ -0,0 +1,80 @@
final class InfoView: UIView {
private let stackView = UIStackView()
private let imageView = UIImageView()
private let titleLabel = UILabel()
private lazy var imageViewWidthConstrain = imageView.widthAnchor.constraint(equalToConstant: 0)
init() {
super.init(frame: .zero)
self.setupView()
self.layoutViews()
}
convenience init(image: UIImage?, title: String) {
self.init()
self.set(image: image, title: title)
}
override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
super.traitCollectionDidChange(previousTraitCollection)
if #available(iOS 13.0, *), traitCollection.hasDifferentColorAppearance(comparedTo: previousTraitCollection) {
imageView.applyTheme()
}
}
@available(*, unavailable)
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
private func setupView() {
setStyle(.clearBackground)
stackView.axis = .horizontal
stackView.distribution = .fill
stackView.alignment = .center
stackView.spacing = 16
titleLabel.setFontStyle(.regular16, color: .blackPrimary)
titleLabel.lineBreakMode = .byWordWrapping
titleLabel.numberOfLines = .zero
imageView.setStyle(.black)
imageView.contentMode = .scaleAspectFit
}
private func layoutViews() {
addSubview(stackView)
stackView.addArrangedSubview(imageView)
stackView.addArrangedSubview(titleLabel)
stackView.translatesAutoresizingMaskIntoConstraints = false
imageView.translatesAutoresizingMaskIntoConstraints = false
titleLabel.translatesAutoresizingMaskIntoConstraints = false
imageView.setContentHuggingPriority(.defaultHigh, for: .vertical)
imageView.setContentHuggingPriority(.defaultHigh, for: .horizontal)
titleLabel.setContentHuggingPriority(.defaultLow, for: .horizontal)
NSLayoutConstraint.activate([
stackView.leadingAnchor.constraint(equalTo: leadingAnchor),
stackView.trailingAnchor.constraint(equalTo: trailingAnchor),
stackView.topAnchor.constraint(equalTo: topAnchor),
stackView.bottomAnchor.constraint(equalTo: bottomAnchor),
imageView.heightAnchor.constraint(equalToConstant: 24),
imageViewWidthConstrain
])
updateImageWidth()
}
private func updateImageWidth() {
imageViewWidthConstrain.constant = imageView.image == nil ? 0 : 24
imageView.isHidden = imageView.image == nil
}
// MARK: - Public
func set(image: UIImage?, title: String) {
imageView.image = image
titleLabel.text = title
updateImageWidth()
}
}

View file

@ -0,0 +1,9 @@
final class TouchTransparentView: UIView {
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
let view = super.hitTest(point, with: event)
if view === self {
return nil
}
return view
}
}