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