Repo created
This commit is contained in:
parent
4af19165ec
commit
68073add76
12458 changed files with 12350765 additions and 2 deletions
5
iphone/Maps/UI/Search/MWMSearchNoResults.h
Normal file
5
iphone/Maps/UI/Search/MWMSearchNoResults.h
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
@interface MWMSearchNoResults : UIView
|
||||
|
||||
+ (instancetype)viewWithImage:(UIImage *)image title:(NSString *)title text:(NSString *)text;
|
||||
|
||||
@end
|
||||
37
iphone/Maps/UI/Search/MWMSearchNoResults.m
Normal file
37
iphone/Maps/UI/Search/MWMSearchNoResults.m
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
#import "MWMSearchNoResults.h"
|
||||
|
||||
static CGFloat const kCompactHeight = 216;
|
||||
static CGFloat const kExtraCompactHeight = 52;
|
||||
|
||||
@interface MWMSearchNoResults ()
|
||||
|
||||
@property(weak, nonatomic) IBOutlet UILabel * title;
|
||||
@property(weak, nonatomic) IBOutlet UILabel * text;
|
||||
@property(weak, nonatomic) IBOutlet NSLayoutConstraint * textCenterY;
|
||||
|
||||
@end
|
||||
|
||||
@implementation MWMSearchNoResults
|
||||
|
||||
+ (instancetype)viewWithImage:(UIImage *)image title:(NSString *)title text:(NSString *)text {
|
||||
MWMSearchNoResults * view =
|
||||
[NSBundle.mainBundle loadNibNamed:[self className] owner:nil options:nil].firstObject;
|
||||
if (title) {
|
||||
view.title.text = title;
|
||||
} else {
|
||||
[view.title removeFromSuperview];
|
||||
}
|
||||
view.text.text = text;
|
||||
return view;
|
||||
}
|
||||
|
||||
- (void)layoutSubviews {
|
||||
[super layoutSubviews];
|
||||
self.frame = self.superview.bounds;
|
||||
BOOL compact = self.height < kCompactHeight;
|
||||
self.textCenterY.priority = compact ? UILayoutPriorityDefaultHigh : UILayoutPriorityFittingSizeLevel;
|
||||
BOOL extraCompact = self.height < kExtraCompactHeight;
|
||||
self.title.hidden = extraCompact;
|
||||
}
|
||||
|
||||
@end
|
||||
58
iphone/Maps/UI/Search/MWMSearchNoResults.xib
Normal file
58
iphone/Maps/UI/Search/MWMSearchNoResults.xib
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="18122" 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="18093"/>
|
||||
<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="iN0-l3-epB" customClass="MWMSearchNoResults" propertyAccessControl="none">
|
||||
<rect key="frame" x="0.0" y="0.0" width="320" height="250"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<subviews>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" horizontalCompressionResistancePriority="1000" verticalCompressionResistancePriority="1000" text="Label" textAlignment="center" lineBreakMode="tailTruncation" numberOfLines="0" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" preferredMaxLayoutWidth="288" translatesAutoresizingMaskIntoConstraints="NO" id="vGi-FH-krh" propertyAccessControl="none">
|
||||
<rect key="frame" x="16" y="82.5" width="288" height="20.5"/>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="17"/>
|
||||
<color key="textColor" systemColor="darkTextColor"/>
|
||||
<nil key="highlightedColor"/>
|
||||
<userDefinedRuntimeAttributes>
|
||||
<userDefinedRuntimeAttribute type="string" keyPath="styleName" value="blackPrimaryText:medium18"/>
|
||||
</userDefinedRuntimeAttributes>
|
||||
</label>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" horizontalCompressionResistancePriority="1000" verticalCompressionResistancePriority="1000" text="Label" textAlignment="center" lineBreakMode="tailTruncation" numberOfLines="0" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" preferredMaxLayoutWidth="288" translatesAutoresizingMaskIntoConstraints="NO" id="2Z7-NG-6sZ" propertyAccessControl="none">
|
||||
<rect key="frame" x="16" y="115" width="288" height="20.5"/>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="17"/>
|
||||
<color key="textColor" systemColor="darkTextColor"/>
|
||||
<nil key="highlightedColor"/>
|
||||
<userDefinedRuntimeAttributes>
|
||||
<userDefinedRuntimeAttribute type="string" keyPath="styleName" value="regular14:blackSecondaryText"/>
|
||||
</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="vGi-FH-krh" secondAttribute="trailing" constant="16" id="1Ye-aw-qVm"/>
|
||||
<constraint firstItem="2Z7-NG-6sZ" firstAttribute="centerY" secondItem="iN0-l3-epB" secondAttribute="centerY" priority="250" id="AK9-c1-mte"/>
|
||||
<constraint firstItem="2Z7-NG-6sZ" firstAttribute="leading" secondItem="iN0-l3-epB" secondAttribute="leading" constant="16" id="Ucg-Kh-I5L"/>
|
||||
<constraint firstItem="2Z7-NG-6sZ" firstAttribute="top" secondItem="vGi-FH-krh" secondAttribute="bottom" constant="12" id="XYx-js-SkS" userLabel="Text.top = centerY"/>
|
||||
<constraint firstAttribute="trailing" secondItem="2Z7-NG-6sZ" secondAttribute="trailing" constant="16" id="ZH9-Do-kc8"/>
|
||||
<constraint firstItem="vGi-FH-krh" firstAttribute="leading" secondItem="iN0-l3-epB" secondAttribute="leading" constant="16" id="lDj-iy-gS7"/>
|
||||
</constraints>
|
||||
<freeformSimulatedSizeMetrics key="simulatedDestinationMetrics"/>
|
||||
<connections>
|
||||
<outlet property="text" destination="2Z7-NG-6sZ" id="eaY-75-TDT"/>
|
||||
<outlet property="textCenterY" destination="AK9-c1-mte" id="XbC-PV-37Y"/>
|
||||
<outlet property="title" destination="vGi-FH-krh" id="11v-W7-Rpl"/>
|
||||
</connections>
|
||||
<point key="canvasLocation" x="139" y="154"/>
|
||||
</view>
|
||||
</objects>
|
||||
<resources>
|
||||
<systemColor name="darkTextColor">
|
||||
<color white="0.0" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
</systemColor>
|
||||
</resources>
|
||||
</document>
|
||||
29
iphone/Maps/UI/Search/SearchNoResultsViewController.swift
Normal file
29
iphone/Maps/UI/Search/SearchNoResultsViewController.swift
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
|
||||
final class SearchNoResultsViewController: MWMViewController {
|
||||
|
||||
static var controller: SearchNoResultsViewController {
|
||||
let storyboard = UIStoryboard.instance(.main)
|
||||
return storyboard.instantiateViewController(withIdentifier: toString(self)) as! SearchNoResultsViewController
|
||||
}
|
||||
|
||||
@IBOutlet private weak var container: UIView!
|
||||
@IBOutlet fileprivate weak var containerBottomOffset: NSLayoutConstraint!
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
|
||||
container.addSubview(MWMSearchNoResults.view(with: nil,
|
||||
title: L("search_not_found"),
|
||||
text: L("search_not_found_query")))
|
||||
MWMKeyboard.add(self)
|
||||
onKeyboardAnimation()
|
||||
}
|
||||
}
|
||||
|
||||
extension SearchNoResultsViewController: MWMKeyboardObserver {
|
||||
|
||||
func onKeyboardAnimation() {
|
||||
containerBottomOffset.constant = MWMKeyboard.keyboardHeight()
|
||||
view.layoutIfNeeded()
|
||||
}
|
||||
}
|
||||
121
iphone/Maps/UI/Search/SearchOnMap/PlaceholderView.swift
Normal file
121
iphone/Maps/UI/Search/SearchOnMap/PlaceholderView.swift
Normal file
|
|
@ -0,0 +1,121 @@
|
|||
final class PlaceholderView: UIView {
|
||||
|
||||
private let activityIndicator: UIActivityIndicatorView?
|
||||
private let titleLabel = UILabel()
|
||||
private let subtitleLabel = UILabel()
|
||||
private let stackView = UIStackView()
|
||||
private var keyboardHeight: CGFloat = 0
|
||||
private var centerYConstraint: NSLayoutConstraint!
|
||||
private var containerModalYTranslation: CGFloat = 0
|
||||
private let minOffsetFromTheKeyboardTop: CGFloat = 20
|
||||
private let maxOffsetFromTheTop: CGFloat = 100
|
||||
|
||||
init(title: String? = nil, subtitle: String? = nil, hasActivityIndicator: Bool = false) {
|
||||
self.activityIndicator = hasActivityIndicator ? UIActivityIndicatorView() : nil
|
||||
super.init(frame: .zero)
|
||||
setupView(title: title, subtitle: subtitle)
|
||||
layoutView()
|
||||
setupKeyboardObservers()
|
||||
}
|
||||
|
||||
@available(*, unavailable)
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
deinit {
|
||||
NotificationCenter.default.removeObserver(self)
|
||||
}
|
||||
|
||||
private func setupKeyboardObservers() {
|
||||
NotificationCenter.default.addObserver(self,
|
||||
selector: #selector(keyboardWillShow(_:)),
|
||||
name: UIResponder.keyboardWillShowNotification,
|
||||
object: nil)
|
||||
NotificationCenter.default.addObserver(self,
|
||||
selector: #selector(keyboardWillHide(_:)),
|
||||
name: UIResponder.keyboardWillHideNotification,
|
||||
object: nil)
|
||||
}
|
||||
|
||||
@objc private func keyboardWillShow(_ notification: Notification) {
|
||||
if let keyboardFrame = notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? CGRect {
|
||||
keyboardHeight = keyboardFrame.height
|
||||
reloadConstraints()
|
||||
}
|
||||
}
|
||||
|
||||
@objc private func keyboardWillHide(_ notification: Notification) {
|
||||
keyboardHeight = 0
|
||||
reloadConstraints()
|
||||
}
|
||||
|
||||
override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
|
||||
super.traitCollectionDidChange(previousTraitCollection)
|
||||
if traitCollection.verticalSizeClass != previousTraitCollection?.verticalSizeClass {
|
||||
reloadConstraints()
|
||||
}
|
||||
}
|
||||
|
||||
private func setupView(title: String?, subtitle: String?) {
|
||||
if let activityIndicator = activityIndicator {
|
||||
activityIndicator.hidesWhenStopped = true
|
||||
activityIndicator.startAnimating()
|
||||
activityIndicator.style = .medium
|
||||
}
|
||||
|
||||
titleLabel.text = title
|
||||
titleLabel.setFontStyle(.medium16, color: .blackPrimary)
|
||||
titleLabel.textAlignment = .center
|
||||
titleLabel.numberOfLines = 0
|
||||
|
||||
subtitleLabel.text = subtitle
|
||||
subtitleLabel.setFontStyle(.regular14, color: .blackSecondary)
|
||||
subtitleLabel.textAlignment = .center
|
||||
subtitleLabel.isHidden = subtitle == nil
|
||||
subtitleLabel.numberOfLines = 0
|
||||
|
||||
stackView.axis = .vertical
|
||||
stackView.alignment = .fill
|
||||
stackView.spacing = 8
|
||||
}
|
||||
|
||||
private func layoutView() {
|
||||
if let activityIndicator = activityIndicator {
|
||||
stackView.addArrangedSubview(activityIndicator)
|
||||
}
|
||||
if let title = titleLabel.text, !title.isEmpty {
|
||||
stackView.addArrangedSubview(titleLabel)
|
||||
}
|
||||
if let subtitle = subtitleLabel.text, !subtitle.isEmpty {
|
||||
stackView.addArrangedSubview(subtitleLabel)
|
||||
}
|
||||
|
||||
addSubview(stackView)
|
||||
stackView.translatesAutoresizingMaskIntoConstraints = false
|
||||
|
||||
centerYConstraint = stackView.centerYAnchor.constraint(equalTo: centerYAnchor)
|
||||
NSLayoutConstraint.activate([
|
||||
stackView.centerXAnchor.constraint(equalTo: centerXAnchor),
|
||||
stackView.widthAnchor.constraint(lessThanOrEqualTo: widthAnchor, multiplier: 0.8),
|
||||
centerYConstraint
|
||||
])
|
||||
}
|
||||
|
||||
private func reloadConstraints() {
|
||||
let offset = keyboardHeight > 0 ? max(bounds.height / 2 - keyboardHeight, minOffsetFromTheKeyboardTop + stackView.frame.height) : containerModalYTranslation / 2
|
||||
let maxOffset = bounds.height / 2 - maxOffsetFromTheTop
|
||||
centerYConstraint.constant = -min(offset, maxOffset)
|
||||
UIView.animate(withDuration: kDefaultAnimationDuration, delay: .zero, options: [.beginFromCurrentState, .curveEaseOut]) {
|
||||
self.layoutIfNeeded()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - ModallyPresentedViewController
|
||||
extension PlaceholderView: ModallyPresentedViewController {
|
||||
func presentationFrameDidChange(_ frame: CGRect) {
|
||||
self.containerModalYTranslation = frame.origin.y
|
||||
reloadConstraints()
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,49 @@
|
|||
enum PresentationStepChangeAnimation {
|
||||
case none
|
||||
case slide
|
||||
case slideAndBounce
|
||||
}
|
||||
|
||||
final class ModalPresentationAnimator {
|
||||
|
||||
private enum Constants {
|
||||
static let animationDuration: TimeInterval = kDefaultAnimationDuration
|
||||
static let springDamping: CGFloat = 0.8
|
||||
static let springVelocity: CGFloat = 0.2
|
||||
static let controlPoint1: CGPoint = CGPoint(x: 0.25, y: 0.1)
|
||||
static let controlPoint2: CGPoint = CGPoint(x: 0.15, y: 1.0)
|
||||
}
|
||||
|
||||
static func animate(with stepAnimation: PresentationStepChangeAnimation = .slide,
|
||||
animations: @escaping (() -> Void),
|
||||
completion: ((Bool) -> Void)?) {
|
||||
switch stepAnimation {
|
||||
case .none:
|
||||
animations()
|
||||
completion?(true)
|
||||
|
||||
case .slide:
|
||||
let timing = UICubicTimingParameters(controlPoint1: Constants.controlPoint1,
|
||||
controlPoint2: Constants.controlPoint2)
|
||||
let animator = UIViewPropertyAnimator(duration: Constants.animationDuration,
|
||||
timingParameters: timing)
|
||||
animator.addAnimations(animations)
|
||||
animator.addCompletion { position in
|
||||
completion?(position == .end)
|
||||
}
|
||||
animator.startAnimation()
|
||||
|
||||
case .slideAndBounce:
|
||||
let velocity = CGVector(dx: Constants.springVelocity, dy: Constants.springVelocity)
|
||||
let timing = UISpringTimingParameters(dampingRatio: Constants.springDamping,
|
||||
initialVelocity: velocity)
|
||||
let animator = UIViewPropertyAnimator(duration: Constants.animationDuration,
|
||||
timingParameters: timing)
|
||||
animator.addAnimations(animations)
|
||||
animator.addCompletion { position in
|
||||
completion?(position == .end)
|
||||
}
|
||||
animator.startAnimation()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,97 @@
|
|||
enum ModalPresentationStep: Int, CaseIterable {
|
||||
case fullScreen
|
||||
case halfScreen
|
||||
case compact
|
||||
case hidden
|
||||
}
|
||||
|
||||
extension ModalPresentationStep {
|
||||
private enum Constants {
|
||||
static let iPadWidth: CGFloat = 350
|
||||
static let compactHeightOffset: CGFloat = 120
|
||||
static let halfScreenHeightFactorPortrait: CGFloat = 0.55
|
||||
static let topInset: CGFloat = 8
|
||||
}
|
||||
|
||||
var upper: ModalPresentationStep {
|
||||
switch self {
|
||||
case .fullScreen:
|
||||
return .fullScreen
|
||||
case .halfScreen:
|
||||
return .fullScreen
|
||||
case .compact:
|
||||
return .halfScreen
|
||||
case .hidden:
|
||||
return .compact
|
||||
}
|
||||
}
|
||||
|
||||
var lower: ModalPresentationStep {
|
||||
switch self {
|
||||
case .fullScreen:
|
||||
return .halfScreen
|
||||
case .halfScreen:
|
||||
return .compact
|
||||
case .compact:
|
||||
return .compact
|
||||
case .hidden:
|
||||
return .hidden
|
||||
}
|
||||
}
|
||||
|
||||
var first: ModalPresentationStep {
|
||||
.fullScreen
|
||||
}
|
||||
|
||||
var last: ModalPresentationStep {
|
||||
.compact
|
||||
}
|
||||
|
||||
func frame(for presentedView: UIView, in containerViewController: UIViewController) -> CGRect {
|
||||
let isIPad = UIDevice.current.userInterfaceIdiom == .pad
|
||||
var containerSize = containerViewController.view.bounds.size
|
||||
if containerSize == .zero {
|
||||
containerSize = UIScreen.main.bounds.size
|
||||
}
|
||||
let safeAreaInsets = containerViewController.view.safeAreaInsets
|
||||
let traitCollection = containerViewController.traitCollection
|
||||
var frame = CGRect(origin: .zero, size: containerSize)
|
||||
|
||||
if isIPad {
|
||||
frame.size.width = Constants.iPadWidth
|
||||
switch self {
|
||||
case .hidden:
|
||||
frame.origin.x = -Constants.iPadWidth
|
||||
default:
|
||||
frame.origin.x = .zero
|
||||
}
|
||||
return frame
|
||||
}
|
||||
|
||||
let isPortraitOrientation = traitCollection.verticalSizeClass == .regular
|
||||
if isPortraitOrientation {
|
||||
switch self {
|
||||
case .fullScreen:
|
||||
frame.origin.y = safeAreaInsets.top + Constants.topInset
|
||||
case .halfScreen:
|
||||
frame.origin.y = containerSize.height * Constants.halfScreenHeightFactorPortrait
|
||||
case .compact:
|
||||
frame.origin.y = containerSize.height - Constants.compactHeightOffset
|
||||
case .hidden:
|
||||
frame.origin.y = containerSize.height
|
||||
}
|
||||
} else {
|
||||
frame.size.width = Constants.iPadWidth
|
||||
frame.origin.x = safeAreaInsets.left
|
||||
switch self {
|
||||
case .fullScreen:
|
||||
frame.origin.y = Constants.topInset
|
||||
case .halfScreen, .compact:
|
||||
frame.origin.y = containerSize.height - Constants.compactHeightOffset
|
||||
case .hidden:
|
||||
frame.origin.y = containerSize.height
|
||||
}
|
||||
}
|
||||
return frame
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,114 @@
|
|||
final class ModalPresentationStepsController {
|
||||
|
||||
enum StepUpdate {
|
||||
case didClose
|
||||
case didUpdateFrame(CGRect)
|
||||
case didUpdateStep(ModalPresentationStep)
|
||||
}
|
||||
|
||||
fileprivate enum Constants {
|
||||
static let slowSwipeVelocity: CGFloat = 500
|
||||
static let fastSwipeDownVelocity: CGFloat = 4000
|
||||
static let fastSwipeUpVelocity: CGFloat = 3000
|
||||
static let translationThreshold: CGFloat = 50
|
||||
}
|
||||
|
||||
private weak var presentedView: UIView?
|
||||
private weak var containerViewController: UIViewController?
|
||||
|
||||
private var initialTranslationY: CGFloat = .zero
|
||||
|
||||
private(set) var currentStep: ModalPresentationStep = .fullScreen
|
||||
private(set) var maxAvailableFrame: CGRect = .zero
|
||||
|
||||
var currentFrame: CGRect { frame(for: currentStep) }
|
||||
var hiddenFrame: CGRect { frame(for: .hidden) }
|
||||
|
||||
var didUpdateHandler: ((StepUpdate) -> Void)?
|
||||
|
||||
func set(presentedView: UIView, containerViewController: UIViewController) {
|
||||
self.presentedView = presentedView
|
||||
self.containerViewController = containerViewController
|
||||
}
|
||||
|
||||
func setInitialState() {
|
||||
setStep(.hidden, animation: .none)
|
||||
}
|
||||
|
||||
func close(completion: (() -> Void)? = nil) {
|
||||
setStep(.hidden, animation: .slide, completion: completion)
|
||||
}
|
||||
|
||||
func updateMaxAvailableFrame() {
|
||||
maxAvailableFrame = frame(for: .fullScreen)
|
||||
}
|
||||
|
||||
func handlePan(_ gesture: UIPanGestureRecognizer) {
|
||||
guard let presentedView else { return }
|
||||
let translation = gesture.translation(in: presentedView)
|
||||
let velocity = gesture.velocity(in: presentedView)
|
||||
var currentFrame = presentedView.frame
|
||||
|
||||
switch gesture.state {
|
||||
case .began:
|
||||
initialTranslationY = presentedView.frame.origin.y
|
||||
case .changed:
|
||||
let newY = max(max(initialTranslationY + translation.y, 0), maxAvailableFrame.origin.y)
|
||||
currentFrame.origin.y = newY
|
||||
presentedView.frame = currentFrame
|
||||
didUpdateHandler?(.didUpdateFrame(currentFrame))
|
||||
case .ended:
|
||||
let nextStep: ModalPresentationStep
|
||||
if velocity.y > Constants.fastSwipeDownVelocity {
|
||||
didUpdateHandler?(.didClose)
|
||||
return
|
||||
} else if velocity.y < -Constants.fastSwipeUpVelocity {
|
||||
nextStep = .fullScreen
|
||||
} else if velocity.y > Constants.slowSwipeVelocity || translation.y > Constants.translationThreshold {
|
||||
if currentStep == .compact {
|
||||
didUpdateHandler?(.didClose)
|
||||
return
|
||||
}
|
||||
nextStep = currentStep.lower
|
||||
} else if velocity.y < -Constants.slowSwipeVelocity || translation.y < -Constants.translationThreshold {
|
||||
nextStep = currentStep.upper
|
||||
} else {
|
||||
nextStep = currentStep
|
||||
}
|
||||
|
||||
let animation: PresentationStepChangeAnimation = abs(velocity.y) > Constants.slowSwipeVelocity ? .slideAndBounce : .slide
|
||||
setStep(nextStep, animation: animation)
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
func setStep(_ step: ModalPresentationStep,
|
||||
completion: (() -> Void)? = nil) {
|
||||
guard currentStep != step else { return }
|
||||
setStep(step, animation: .slide, completion: completion)
|
||||
}
|
||||
|
||||
private func setStep(_ step: ModalPresentationStep,
|
||||
animation: PresentationStepChangeAnimation,
|
||||
completion: (() -> Void)? = nil) {
|
||||
guard let presentedView else { return }
|
||||
currentStep = step
|
||||
updateMaxAvailableFrame()
|
||||
|
||||
let frame = frame(for: step)
|
||||
didUpdateHandler?(.didUpdateStep(step))
|
||||
didUpdateHandler?(.didUpdateFrame(frame))
|
||||
|
||||
ModalPresentationAnimator.animate(with: animation) {
|
||||
presentedView.frame = frame
|
||||
} completion: { _ in
|
||||
completion?()
|
||||
}
|
||||
}
|
||||
|
||||
private func frame(for step: ModalPresentationStep) -> CGRect {
|
||||
guard let presentedView, let containerViewController else { return .zero }
|
||||
return step.frame(for: presentedView, in: containerViewController)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
final class SearchOnMapAreaView: UIView {
|
||||
override var sideButtonsAreaAffectDirections: MWMAvailableAreaAffectDirections {
|
||||
alternative(iPhone: .bottom, iPad: [])
|
||||
}
|
||||
|
||||
override var trafficButtonAreaAffectDirections: MWMAvailableAreaAffectDirections {
|
||||
alternative(iPhone: .bottom, iPad: [])
|
||||
}
|
||||
}
|
||||
151
iphone/Maps/UI/Search/SearchOnMap/SearchOnMapHeaderView.swift
Normal file
151
iphone/Maps/UI/Search/SearchOnMap/SearchOnMapHeaderView.swift
Normal file
|
|
@ -0,0 +1,151 @@
|
|||
protocol SearchOnMapHeaderViewDelegate: UISearchBarDelegate {
|
||||
func cancelButtonDidTap()
|
||||
func grabberDidTap()
|
||||
}
|
||||
|
||||
final class SearchOnMapHeaderView: UIView {
|
||||
weak var delegate: SearchOnMapHeaderViewDelegate? {
|
||||
didSet {
|
||||
searchBar.delegate = delegate
|
||||
}
|
||||
}
|
||||
|
||||
private enum Constants {
|
||||
static let searchBarHeight: CGFloat = 36
|
||||
static let searchBarInsets: UIEdgeInsets = UIEdgeInsets(top: 8, left: 10, bottom: 10, right: 0)
|
||||
static let grabberHeight: CGFloat = 5
|
||||
static let grabberWidth: CGFloat = 36
|
||||
static let grabberTopMargin: CGFloat = 5
|
||||
static let cancelButtonInsets: UIEdgeInsets = UIEdgeInsets(top: 0, left: 6, bottom: 0, right: 16)
|
||||
}
|
||||
|
||||
private let grabberView = UIView()
|
||||
private let grabberTapHandlerView = UIView()
|
||||
private let searchBar = UISearchBar()
|
||||
private let cancelButton = UIButton()
|
||||
private let cancelContainer = UIView()
|
||||
private var separator: UIView?
|
||||
|
||||
override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
setupView()
|
||||
layoutView()
|
||||
}
|
||||
|
||||
@available(*, unavailable)
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
private func setupView() {
|
||||
setStyle(.background)
|
||||
|
||||
setupGrabberView()
|
||||
setupGrabberTapHandlerView()
|
||||
setupSearchBar()
|
||||
setupCancelButton()
|
||||
}
|
||||
|
||||
private func setupGrabberView() {
|
||||
grabberView.setStyle(.grabber)
|
||||
iPadSpecific { [weak self] in
|
||||
self?.grabberView.isHidden = true
|
||||
}
|
||||
}
|
||||
|
||||
private func setupGrabberTapHandlerView() {
|
||||
grabberTapHandlerView.backgroundColor = .clear
|
||||
iPhoneSpecific {
|
||||
let tapGesture = UITapGestureRecognizer(target: self, action: #selector(grabberDidTap))
|
||||
grabberTapHandlerView.addGestureRecognizer(tapGesture)
|
||||
}
|
||||
}
|
||||
|
||||
private func setupSearchBar() {
|
||||
searchBar.setStyle(.defaultSearchBar)
|
||||
searchBar.placeholder = L("search")
|
||||
searchBar.showsCancelButton = false
|
||||
searchBar.searchTextField.clearButtonMode = .always
|
||||
searchBar.returnKeyType = .search
|
||||
searchBar.searchTextField.enablesReturnKeyAutomatically = true
|
||||
searchBar.backgroundImage = UIImage()
|
||||
}
|
||||
|
||||
private func setupCancelButton() {
|
||||
cancelContainer.setStyle(.background)
|
||||
cancelButton.setStyle(.searchCancelButton)
|
||||
cancelButton.setTitle(L("cancel"), for: .normal)
|
||||
cancelButton.addTarget(self, action: #selector(cancelButtonDidTap), for: .touchUpInside)
|
||||
}
|
||||
|
||||
private func layoutView() {
|
||||
addSubview(grabberView)
|
||||
addSubview(grabberTapHandlerView)
|
||||
addSubview(searchBar)
|
||||
addSubview(cancelContainer)
|
||||
cancelContainer.addSubview(cancelButton)
|
||||
separator = addSeparator(.bottom)
|
||||
|
||||
grabberView.translatesAutoresizingMaskIntoConstraints = false
|
||||
grabberTapHandlerView.translatesAutoresizingMaskIntoConstraints = false
|
||||
grabberTapHandlerView.setContentHuggingPriority(.defaultLow, for: .vertical)
|
||||
searchBar.translatesAutoresizingMaskIntoConstraints = false
|
||||
cancelContainer.translatesAutoresizingMaskIntoConstraints = false
|
||||
cancelButton.translatesAutoresizingMaskIntoConstraints = false
|
||||
|
||||
NSLayoutConstraint.activate([
|
||||
grabberView.topAnchor.constraint(equalTo: topAnchor, constant: Constants.grabberTopMargin),
|
||||
grabberView.centerXAnchor.constraint(equalTo: centerXAnchor),
|
||||
grabberView.widthAnchor.constraint(equalToConstant: Constants.grabberWidth),
|
||||
grabberView.heightAnchor.constraint(equalToConstant: Constants.grabberHeight),
|
||||
|
||||
grabberTapHandlerView.topAnchor.constraint(equalTo: grabberView.bottomAnchor),
|
||||
grabberTapHandlerView.leadingAnchor.constraint(equalTo: leadingAnchor),
|
||||
grabberTapHandlerView.trailingAnchor.constraint(equalTo: trailingAnchor),
|
||||
grabberTapHandlerView.bottomAnchor.constraint(equalTo: searchBar.topAnchor),
|
||||
|
||||
searchBar.topAnchor.constraint(equalTo: grabberView.bottomAnchor, constant: Constants.searchBarInsets.top),
|
||||
searchBar.leadingAnchor.constraint(equalTo: leadingAnchor, constant: Constants.searchBarInsets.left),
|
||||
searchBar.trailingAnchor.constraint(equalTo: cancelContainer.leadingAnchor),
|
||||
searchBar.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -Constants.searchBarInsets.bottom),
|
||||
searchBar.heightAnchor.constraint(equalToConstant: Constants.searchBarHeight),
|
||||
|
||||
cancelContainer.trailingAnchor.constraint(equalTo: trailingAnchor),
|
||||
cancelContainer.topAnchor.constraint(equalTo: searchBar.topAnchor),
|
||||
cancelContainer.bottomAnchor.constraint(equalTo: searchBar.bottomAnchor),
|
||||
|
||||
cancelButton.topAnchor.constraint(equalTo: cancelContainer.topAnchor),
|
||||
cancelButton.leadingAnchor.constraint(equalTo: cancelContainer.leadingAnchor, constant: Constants.cancelButtonInsets.left),
|
||||
cancelButton.trailingAnchor.constraint(equalTo: cancelContainer.trailingAnchor, constant: -Constants.cancelButtonInsets.right),
|
||||
cancelButton.bottomAnchor.constraint(equalTo: cancelContainer.bottomAnchor),
|
||||
])
|
||||
}
|
||||
|
||||
@objc private func grabberDidTap() {
|
||||
delegate?.grabberDidTap()
|
||||
}
|
||||
|
||||
@objc private func cancelButtonDidTap() {
|
||||
delegate?.cancelButtonDidTap()
|
||||
}
|
||||
|
||||
func setSearchText(_ text: String) {
|
||||
searchBar.text = text
|
||||
}
|
||||
|
||||
func setIsSearching(_ isSearching: Bool) {
|
||||
if isSearching {
|
||||
searchBar.becomeFirstResponder()
|
||||
} else if searchBar.isFirstResponder {
|
||||
searchBar.resignFirstResponder()
|
||||
}
|
||||
}
|
||||
|
||||
var searchQuery: SearchQuery {
|
||||
SearchQuery(searchBar.text ?? "", locale: searchBar.textInputMode?.primaryLanguage, source: .typedText)
|
||||
}
|
||||
|
||||
func setSeparatorHidden(_ hidden: Bool) {
|
||||
separator?.isHidden = hidden
|
||||
}
|
||||
}
|
||||
176
iphone/Maps/UI/Search/SearchOnMap/SearchOnMapInteractor.swift
Normal file
176
iphone/Maps/UI/Search/SearchOnMap/SearchOnMapInteractor.swift
Normal file
|
|
@ -0,0 +1,176 @@
|
|||
final class SearchOnMapInteractor: NSObject {
|
||||
|
||||
private let presenter: SearchOnMapPresenter
|
||||
private let searchManager: SearchManager.Type
|
||||
private let routeManager: MWMRouter.Type
|
||||
private var isUpdatesDisabled = false
|
||||
|
||||
var routingTooltipSearch: SearchOnMapRoutingTooltipSearch = .none
|
||||
|
||||
init(presenter: SearchOnMapPresenter,
|
||||
searchManager: SearchManager.Type = Search.self,
|
||||
routeManager: MWMRouter.Type = MWMRouter.self) {
|
||||
self.presenter = presenter
|
||||
self.searchManager = searchManager
|
||||
self.routeManager = routeManager
|
||||
super.init()
|
||||
searchManager.add(self)
|
||||
}
|
||||
|
||||
deinit {
|
||||
searchManager.remove(self)
|
||||
}
|
||||
|
||||
func handle(_ event: SearchOnMap.Request) {
|
||||
let response = resolve(event)
|
||||
presenter.process(response)
|
||||
}
|
||||
|
||||
private func resolve(_ event: SearchOnMap.Request) -> SearchOnMap.Response {
|
||||
switch event {
|
||||
case .openSearch:
|
||||
return .showHistoryAndCategory
|
||||
|
||||
case .hideSearch:
|
||||
return .setSearchScreenHidden(true)
|
||||
|
||||
case .didStartDraggingSearch:
|
||||
return .setIsTyping(false)
|
||||
|
||||
case .didStartTyping:
|
||||
return .setIsTyping(true)
|
||||
|
||||
case .didType(let searchText):
|
||||
return processTypedText(searchText)
|
||||
|
||||
case .clearButtonDidTap:
|
||||
return processClearButtonDidTap()
|
||||
|
||||
case .didSelect(let searchText):
|
||||
return processSelectedText(searchText)
|
||||
|
||||
case .searchButtonDidTap(let searchText):
|
||||
return processSearchButtonDidTap(searchText)
|
||||
|
||||
case .didSelectResult(let result, let query):
|
||||
return processSelectedResult(result, query: query)
|
||||
|
||||
case .didSelectPlaceOnMap:
|
||||
return isiPad ? .none : .setSearchScreenHidden(true)
|
||||
|
||||
case .didDeselectPlaceOnMap:
|
||||
return deselectPlaceOnMap()
|
||||
|
||||
case .didStartDraggingMap:
|
||||
return .setSearchScreenCompact
|
||||
|
||||
case .didUpdatePresentationStep(let step):
|
||||
searchManager.setSearchMode(searchModeForPresentationStep(step))
|
||||
return .updatePresentationStep(step)
|
||||
|
||||
case .closeSearch:
|
||||
return closeSearch()
|
||||
}
|
||||
}
|
||||
|
||||
private func processClearButtonDidTap() -> SearchOnMap.Response {
|
||||
isUpdatesDisabled = true
|
||||
searchManager.clear()
|
||||
return .clearSearch
|
||||
}
|
||||
|
||||
private func processSearchButtonDidTap(_ query: SearchQuery) -> SearchOnMap.Response {
|
||||
searchManager.save(query)
|
||||
return .showOnTheMap
|
||||
}
|
||||
|
||||
private func processTypedText(_ query: SearchQuery) -> SearchOnMap.Response {
|
||||
isUpdatesDisabled = false
|
||||
searchManager.searchQuery(query)
|
||||
return .startSearching
|
||||
}
|
||||
|
||||
private func processSelectedText(_ query: SearchQuery) -> SearchOnMap.Response {
|
||||
isUpdatesDisabled = false
|
||||
if query.source != .history {
|
||||
searchManager.save(query)
|
||||
}
|
||||
searchManager.searchQuery(query)
|
||||
return .selectQuery(query)
|
||||
}
|
||||
|
||||
private func processSelectedResult(_ result: SearchResult, query: SearchQuery) -> SearchOnMap.Response {
|
||||
switch result.itemType {
|
||||
case .regular:
|
||||
searchManager.save(query)
|
||||
switch routingTooltipSearch {
|
||||
case .none:
|
||||
searchManager.showResult(at: result.index)
|
||||
case .start:
|
||||
let point = MWMRoutePoint(cgPoint: result.point,
|
||||
title: result.titleText,
|
||||
subtitle: result.addressText,
|
||||
type: .start,
|
||||
intermediateIndex: 0)
|
||||
routeManager.build(from: point, bestRouter: false)
|
||||
case .finish:
|
||||
let point = MWMRoutePoint(cgPoint: result.point,
|
||||
title: result.titleText,
|
||||
subtitle: result.addressText,
|
||||
type: .finish,
|
||||
intermediateIndex: 0)
|
||||
routeManager.build(to: point, bestRouter: false)
|
||||
@unknown default:
|
||||
fatalError("Unsupported routingTooltipSearch")
|
||||
}
|
||||
return isiPad ? .none : .setSearchScreenHidden(true)
|
||||
case .suggestion:
|
||||
let suggestionQuery = SearchQuery(result.suggestion,
|
||||
locale: query.locale,
|
||||
source: result.isPureSuggest ? .suggestion : .typedText)
|
||||
searchManager.searchQuery(suggestionQuery)
|
||||
return .selectQuery(suggestionQuery)
|
||||
@unknown default:
|
||||
fatalError("Unsupported result type")
|
||||
}
|
||||
}
|
||||
|
||||
private func deselectPlaceOnMap() -> SearchOnMap.Response {
|
||||
routingTooltipSearch = .none
|
||||
return .setSearchScreenHidden(false)
|
||||
}
|
||||
|
||||
private func closeSearch() -> SearchOnMap.Response {
|
||||
routingTooltipSearch = .none
|
||||
isUpdatesDisabled = true
|
||||
searchManager.clear()
|
||||
return .close
|
||||
}
|
||||
|
||||
private func searchModeForPresentationStep(_ step: ModalPresentationStep) -> SearchMode {
|
||||
switch step {
|
||||
case .fullScreen:
|
||||
return isiPad ? .everywhereAndViewport : .everywhere
|
||||
case .halfScreen, .compact:
|
||||
return .everywhereAndViewport
|
||||
case .hidden:
|
||||
return .viewport
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - MWMSearchObserver
|
||||
extension SearchOnMapInteractor: MWMSearchObserver {
|
||||
func onSearchCompleted() {
|
||||
guard !isUpdatesDisabled, searchManager.searchMode() != .viewport else { return }
|
||||
let results = searchManager.getResults()
|
||||
presenter.process(.showResults(SearchOnMap.SearchResults(results), isSearchCompleted: true))
|
||||
}
|
||||
|
||||
func onSearchResultsUpdated() {
|
||||
guard !isUpdatesDisabled, searchManager.searchMode() != .viewport else { return }
|
||||
let results = searchManager.getResults()
|
||||
guard !results.isEmpty else { return }
|
||||
presenter.process(.showResults(SearchOnMap.SearchResults(results), isSearchCompleted: false))
|
||||
}
|
||||
}
|
||||
92
iphone/Maps/UI/Search/SearchOnMap/SearchOnMapManager.swift
Normal file
92
iphone/Maps/UI/Search/SearchOnMap/SearchOnMapManager.swift
Normal file
|
|
@ -0,0 +1,92 @@
|
|||
@objc
|
||||
enum SearchOnMapState: Int {
|
||||
case searching
|
||||
case hidden
|
||||
case closed
|
||||
}
|
||||
|
||||
@objc
|
||||
enum SearchOnMapRoutingTooltipSearch: Int {
|
||||
case none
|
||||
case start
|
||||
case finish
|
||||
}
|
||||
|
||||
@objc
|
||||
protocol SearchOnMapManagerObserver: AnyObject {
|
||||
func searchManager(didChangeState state: SearchOnMapState)
|
||||
}
|
||||
|
||||
@objcMembers
|
||||
final class SearchOnMapManager: NSObject {
|
||||
private var interactor: SearchOnMapInteractor? { viewController?.interactor }
|
||||
private let observers = ListenerContainer<SearchOnMapManagerObserver>()
|
||||
|
||||
weak var viewController: SearchOnMapViewController?
|
||||
var isSearching: Bool { viewController != nil }
|
||||
|
||||
override init() {
|
||||
super.init()
|
||||
}
|
||||
|
||||
// MARK: - Public methods
|
||||
func startSearching(isRouting: Bool) {
|
||||
if viewController != nil {
|
||||
interactor?.handle(.openSearch)
|
||||
return
|
||||
}
|
||||
FrameworkHelper.deactivateMapSelection()
|
||||
let viewController = SearchOnMapViewControllerBuilder.build(isRouting: isRouting,
|
||||
didChangeState: notifyObservers)
|
||||
self.viewController = viewController
|
||||
}
|
||||
|
||||
func hide() {
|
||||
interactor?.handle(.hideSearch)
|
||||
}
|
||||
|
||||
func close() {
|
||||
interactor?.handle(.closeSearch)
|
||||
}
|
||||
|
||||
func setRoutingTooltip(_ tooltip: SearchOnMapRoutingTooltipSearch) {
|
||||
interactor?.routingTooltipSearch = tooltip
|
||||
}
|
||||
|
||||
func setPlaceOnMapSelected(_ isSelected: Bool) {
|
||||
interactor?.handle(isSelected ? .didSelectPlaceOnMap : .didDeselectPlaceOnMap)
|
||||
}
|
||||
|
||||
func setMapIsDragging() {
|
||||
interactor?.handle(.didStartDraggingMap)
|
||||
}
|
||||
|
||||
func searchText(_ searchText: SearchQuery) {
|
||||
interactor?.handle(.didSelect(searchText))
|
||||
}
|
||||
|
||||
func addObserver(_ observer: SearchOnMapManagerObserver) {
|
||||
observers.addListener(observer)
|
||||
}
|
||||
|
||||
func removeObserver(_ observer: SearchOnMapManagerObserver) {
|
||||
observers.removeListener(observer)
|
||||
}
|
||||
|
||||
private func notifyObservers(_ state: SearchOnMapState) {
|
||||
observers.forEach { observer in observer.searchManager(didChangeState: state) }
|
||||
}
|
||||
}
|
||||
|
||||
private struct SearchOnMapViewControllerBuilder {
|
||||
static func build(isRouting: Bool, didChangeState: @escaping ((SearchOnMapState) -> Void)) -> SearchOnMapViewController {
|
||||
let viewController = SearchOnMapViewController()
|
||||
let presenter = SearchOnMapPresenter(isRouting: isRouting,
|
||||
didChangeState: didChangeState)
|
||||
let interactor = SearchOnMapInteractor(presenter: presenter)
|
||||
presenter.view = viewController
|
||||
viewController.interactor = interactor
|
||||
viewController.show()
|
||||
return viewController
|
||||
}
|
||||
}
|
||||
97
iphone/Maps/UI/Search/SearchOnMap/SearchOnMapModels.swift
Normal file
97
iphone/Maps/UI/Search/SearchOnMap/SearchOnMapModels.swift
Normal file
|
|
@ -0,0 +1,97 @@
|
|||
@objcMembers
|
||||
final class SearchQuery: NSObject {
|
||||
let text: String
|
||||
let locale: String
|
||||
let source: SearchTextSource
|
||||
|
||||
init(_ text: String, locale: String? = nil, source: SearchTextSource) {
|
||||
self.text = text
|
||||
self.locale = locale ?? AppInfo.shared().languageId
|
||||
self.source = source
|
||||
}
|
||||
}
|
||||
|
||||
enum SearchOnMap {
|
||||
struct ViewModel: Equatable {
|
||||
enum Content: Equatable {
|
||||
case historyAndCategory
|
||||
case results(SearchResults)
|
||||
case noResults
|
||||
case searching
|
||||
}
|
||||
|
||||
var isTyping: Bool
|
||||
var skipSuggestions: Bool
|
||||
var searchingText: String?
|
||||
var contentState: Content
|
||||
var presentationStep: ModalPresentationStep
|
||||
}
|
||||
|
||||
struct SearchResults: Equatable {
|
||||
let results: [SearchResult]
|
||||
let hasPartialMatch: Bool
|
||||
let isEmpty: Bool
|
||||
let count: Int
|
||||
let suggestionsCount: Int
|
||||
|
||||
init(_ results: [SearchResult]) {
|
||||
self.results = results
|
||||
self.hasPartialMatch = !results.allSatisfy { $0.highlightRanges.isEmpty }
|
||||
self.isEmpty = results.isEmpty
|
||||
self.count = results.count
|
||||
self.suggestionsCount = results.filter { $0.itemType == .suggestion }.count
|
||||
}
|
||||
}
|
||||
|
||||
enum Request {
|
||||
case openSearch
|
||||
case hideSearch
|
||||
case closeSearch
|
||||
case didStartDraggingSearch
|
||||
case didStartDraggingMap
|
||||
case didStartTyping
|
||||
case didType(SearchQuery)
|
||||
case didSelect(SearchQuery)
|
||||
case didSelectResult(SearchResult, withQuery: SearchQuery)
|
||||
case searchButtonDidTap(SearchQuery)
|
||||
case clearButtonDidTap
|
||||
case didSelectPlaceOnMap
|
||||
case didDeselectPlaceOnMap
|
||||
case didUpdatePresentationStep(ModalPresentationStep)
|
||||
}
|
||||
|
||||
enum Response: Equatable {
|
||||
case startSearching
|
||||
case showOnTheMap
|
||||
case setIsTyping(Bool)
|
||||
case showHistoryAndCategory
|
||||
case showResults(SearchResults, isSearchCompleted: Bool = false)
|
||||
case selectQuery(SearchQuery)
|
||||
case clearSearch
|
||||
case setSearchScreenHidden(Bool)
|
||||
case setSearchScreenCompact
|
||||
case updatePresentationStep(ModalPresentationStep)
|
||||
case close
|
||||
case none
|
||||
}
|
||||
}
|
||||
|
||||
extension SearchOnMap.SearchResults {
|
||||
static let empty = SearchOnMap.SearchResults([])
|
||||
|
||||
subscript(index: Int) -> SearchResult {
|
||||
results[index]
|
||||
}
|
||||
|
||||
mutating func skipSuggestions() {
|
||||
self = SearchOnMap.SearchResults(results.filter { $0.itemType != .suggestion })
|
||||
}
|
||||
}
|
||||
|
||||
extension SearchOnMap.ViewModel {
|
||||
static let initial = SearchOnMap.ViewModel(isTyping: false,
|
||||
skipSuggestions: false,
|
||||
searchingText: nil,
|
||||
contentState: .historyAndCategory,
|
||||
presentationStep: .fullScreen)
|
||||
}
|
||||
124
iphone/Maps/UI/Search/SearchOnMap/SearchOnMapPresenter.swift
Normal file
124
iphone/Maps/UI/Search/SearchOnMap/SearchOnMapPresenter.swift
Normal file
|
|
@ -0,0 +1,124 @@
|
|||
final class SearchOnMapPresenter {
|
||||
typealias Response = SearchOnMap.Response
|
||||
typealias ViewModel = SearchOnMap.ViewModel
|
||||
|
||||
weak var view: SearchOnMapView?
|
||||
|
||||
private var searchState: SearchOnMapState = .searching {
|
||||
didSet {
|
||||
guard searchState != oldValue else { return }
|
||||
didChangeState?(searchState)
|
||||
}
|
||||
}
|
||||
|
||||
private var viewModel: ViewModel = .initial
|
||||
private var isRouting: Bool
|
||||
private var didChangeState: ((SearchOnMapState) -> Void)?
|
||||
|
||||
init(isRouting: Bool, didChangeState: ((SearchOnMapState) -> Void)?) {
|
||||
self.isRouting = isRouting
|
||||
self.didChangeState = didChangeState
|
||||
didChangeState?(searchState)
|
||||
}
|
||||
|
||||
func process(_ response: SearchOnMap.Response) {
|
||||
guard response != .none else { return }
|
||||
|
||||
if response == .close {
|
||||
view?.close()
|
||||
searchState = .closed
|
||||
return
|
||||
}
|
||||
|
||||
let showSearch = response == .setSearchScreenHidden(false) || response == .showHistoryAndCategory
|
||||
guard viewModel.presentationStep != .hidden || showSearch else {
|
||||
return
|
||||
}
|
||||
|
||||
let newViewModel = resolve(action: response, with: viewModel)
|
||||
if viewModel != newViewModel {
|
||||
viewModel = newViewModel
|
||||
view?.render(newViewModel)
|
||||
searchState = newViewModel.presentationStep.searchState
|
||||
}
|
||||
}
|
||||
|
||||
private func resolve(action: Response, with previousViewModel: ViewModel) -> ViewModel {
|
||||
var viewModel = previousViewModel
|
||||
viewModel.searchingText = nil // should not be nil only when the text is passed to the search field
|
||||
|
||||
switch action {
|
||||
case .startSearching:
|
||||
viewModel.isTyping = true
|
||||
viewModel.skipSuggestions = false
|
||||
viewModel.contentState = .searching
|
||||
case .showOnTheMap:
|
||||
viewModel.isTyping = false
|
||||
viewModel.skipSuggestions = true
|
||||
viewModel.presentationStep = isRouting ? .hidden : .halfScreen
|
||||
if case .results(var results) = viewModel.contentState, !results.isEmpty {
|
||||
results.skipSuggestions()
|
||||
viewModel.contentState = .results(results)
|
||||
}
|
||||
case .setIsTyping(let isSearching):
|
||||
viewModel.isTyping = isSearching
|
||||
if isSearching {
|
||||
viewModel.presentationStep = .fullScreen
|
||||
}
|
||||
case .showHistoryAndCategory:
|
||||
viewModel.isTyping = true
|
||||
viewModel.contentState = .historyAndCategory
|
||||
viewModel.presentationStep = .fullScreen
|
||||
case .showResults(var searchResults, let isSearchCompleted):
|
||||
if (viewModel.skipSuggestions) {
|
||||
searchResults.skipSuggestions()
|
||||
}
|
||||
viewModel.contentState = searchResults.isEmpty && isSearchCompleted ? .noResults : .results(searchResults)
|
||||
case .selectQuery(let query):
|
||||
viewModel.skipSuggestions = false
|
||||
viewModel.searchingText = query.text
|
||||
viewModel.contentState = .searching
|
||||
|
||||
switch query.source {
|
||||
case .typedText, .suggestion:
|
||||
viewModel.isTyping = true
|
||||
case .category, .history, .deeplink:
|
||||
viewModel.isTyping = false
|
||||
viewModel.presentationStep = isRouting ? .hidden : .halfScreen
|
||||
@unknown default:
|
||||
fatalError("Unknown search text source")
|
||||
}
|
||||
case .clearSearch:
|
||||
viewModel.searchingText = ""
|
||||
viewModel.isTyping = true
|
||||
viewModel.skipSuggestions = false
|
||||
viewModel.contentState = .historyAndCategory
|
||||
viewModel.presentationStep = .fullScreen
|
||||
case .setSearchScreenHidden(let isHidden):
|
||||
viewModel.isTyping = false
|
||||
viewModel.presentationStep = isHidden ? .hidden : (isRouting ? .fullScreen : .halfScreen)
|
||||
case .setSearchScreenCompact:
|
||||
viewModel.isTyping = false
|
||||
viewModel.presentationStep = .compact
|
||||
case .updatePresentationStep(let step):
|
||||
if step == .hidden {
|
||||
viewModel.isTyping = false
|
||||
}
|
||||
viewModel.presentationStep = step
|
||||
case .close, .none:
|
||||
break
|
||||
}
|
||||
return viewModel
|
||||
}
|
||||
}
|
||||
|
||||
private extension ModalPresentationStep {
|
||||
var searchState: SearchOnMapState {
|
||||
switch self {
|
||||
case .fullScreen, .halfScreen, .compact:
|
||||
return .searching
|
||||
case .hidden:
|
||||
return .hidden
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,498 @@
|
|||
protocol SearchOnMapView: AnyObject {
|
||||
func render(_ viewModel: SearchOnMap.ViewModel)
|
||||
func show()
|
||||
func close()
|
||||
}
|
||||
|
||||
@objc
|
||||
protocol SearchOnMapScrollViewDelegate: AnyObject {
|
||||
func scrollViewDidScroll(_ scrollView: UIScrollView)
|
||||
func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>)
|
||||
}
|
||||
|
||||
@objc
|
||||
protocol ModallyPresentedViewController: AnyObject {
|
||||
@objc func presentationFrameDidChange(_ frame: CGRect)
|
||||
}
|
||||
|
||||
final class SearchOnMapViewController: UIViewController {
|
||||
typealias ViewModel = SearchOnMap.ViewModel
|
||||
typealias Content = SearchOnMap.ViewModel.Content
|
||||
|
||||
fileprivate enum Constants {
|
||||
static let estimatedRowHeight: CGFloat = 80
|
||||
static let panGestureThreshold: CGFloat = 5
|
||||
static let dimAlpha: CGFloat = 0.3
|
||||
static let dimViewThreshold: CGFloat = 50
|
||||
}
|
||||
|
||||
var interactor: SearchOnMapInteractor?
|
||||
|
||||
@objc let availableAreaView = SearchOnMapAreaView()
|
||||
private let contentView = UIView()
|
||||
private let headerView = SearchOnMapHeaderView()
|
||||
private let searchResultsView = UIView()
|
||||
private let resultsTableView = UITableView()
|
||||
private let historyAndCategoryTabViewController = SearchTabViewController()
|
||||
private var searchingActivityView = PlaceholderView(hasActivityIndicator: true)
|
||||
private var searchNoResultsView = PlaceholderView(title: L("search_not_found"),
|
||||
subtitle: L("search_not_found_query"))
|
||||
private var dimView: UIView?
|
||||
|
||||
private var internalScrollViewContentOffset: CGFloat = .zero
|
||||
private let presentationStepsController = ModalPresentationStepsController()
|
||||
private var searchResults = SearchOnMap.SearchResults([])
|
||||
|
||||
// MARK: - Init
|
||||
init() {
|
||||
super.init(nibName: nil, bundle: nil)
|
||||
configureModalPresentation()
|
||||
}
|
||||
|
||||
private func configureModalPresentation() {
|
||||
guard let mapViewController = MapViewController.shared() else {
|
||||
fatalError("MapViewController is not available")
|
||||
}
|
||||
presentationStepsController.set(presentedView: availableAreaView, containerViewController: self)
|
||||
presentationStepsController.didUpdateHandler = presentationUpdateHandler
|
||||
|
||||
mapViewController.searchContainer.addSubview(view)
|
||||
mapViewController.addChild(self)
|
||||
view.frame = mapViewController.searchContainer.bounds
|
||||
view.autoresizingMask = [.flexibleWidth, .flexibleHeight]
|
||||
didMove(toParent: mapViewController)
|
||||
|
||||
let affectedAreaViews = [
|
||||
mapViewController.sideButtonsArea,
|
||||
mapViewController.trafficButtonArea,
|
||||
]
|
||||
affectedAreaViews.forEach { $0?.addAffectingView(availableAreaView) }
|
||||
}
|
||||
|
||||
@available(*, unavailable)
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
// MARK: - Lifecycle
|
||||
override func loadView() {
|
||||
view = TouchTransparentView()
|
||||
}
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
setupViews()
|
||||
layoutViews()
|
||||
presentationStepsController.setInitialState()
|
||||
}
|
||||
|
||||
override func viewWillDisappear(_ animated: Bool) {
|
||||
super.viewWillDisappear(animated)
|
||||
headerView.setIsSearching(false)
|
||||
}
|
||||
|
||||
override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
|
||||
super.traitCollectionDidChange(previousTraitCollection)
|
||||
updateFrameOfPresentedViewInContainerView()
|
||||
updateDimView(for: availableAreaView.frame)
|
||||
}
|
||||
|
||||
override func viewWillTransition(to size: CGSize, with coordinator: any UIViewControllerTransitionCoordinator) {
|
||||
super.viewWillTransition(to: size, with: coordinator)
|
||||
if ProcessInfo.processInfo.isiOSAppOnMac {
|
||||
updateFrameOfPresentedViewInContainerView()
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Private methods
|
||||
private func setupViews() {
|
||||
availableAreaView.setStyleAndApply(.modalSheetBackground)
|
||||
contentView.setStyleAndApply(.modalSheetContent)
|
||||
|
||||
setupGestureRecognizers()
|
||||
setupDimView()
|
||||
setupHeaderView()
|
||||
setupSearchResultsView()
|
||||
setupResultsTableView()
|
||||
setupHistoryAndCategoryTabView()
|
||||
}
|
||||
|
||||
private func setupDimView() {
|
||||
iPhoneSpecific {
|
||||
dimView = UIView()
|
||||
dimView?.backgroundColor = .black
|
||||
dimView?.frame = view.bounds
|
||||
}
|
||||
}
|
||||
|
||||
private func setupGestureRecognizers() {
|
||||
let tapGesture = UITapGestureRecognizer(target: self, action: #selector(handleTapOutside))
|
||||
tapGesture.cancelsTouchesInView = false
|
||||
contentView.addGestureRecognizer(tapGesture)
|
||||
|
||||
iPhoneSpecific {
|
||||
let panGestureRecognizer = UIPanGestureRecognizer(target: self, action: #selector(handlePan(_:)))
|
||||
panGestureRecognizer.delegate = self
|
||||
contentView.addGestureRecognizer(panGestureRecognizer)
|
||||
}
|
||||
}
|
||||
|
||||
private func setupHeaderView() {
|
||||
headerView.delegate = self
|
||||
}
|
||||
|
||||
private func setupSearchResultsView() {
|
||||
searchResultsView.setStyle(.background)
|
||||
}
|
||||
|
||||
private func setupResultsTableView() {
|
||||
resultsTableView.setStyle(.background)
|
||||
resultsTableView.estimatedRowHeight = Constants.estimatedRowHeight
|
||||
resultsTableView.rowHeight = UITableView.automaticDimension
|
||||
resultsTableView.registerNib(cellClass: SearchSuggestionCell.self)
|
||||
resultsTableView.registerNib(cellClass: SearchCommonCell.self)
|
||||
resultsTableView.dataSource = self
|
||||
resultsTableView.delegate = self
|
||||
resultsTableView.keyboardDismissMode = .onDrag
|
||||
}
|
||||
|
||||
private func setupHistoryAndCategoryTabView() {
|
||||
historyAndCategoryTabViewController.delegate = self
|
||||
}
|
||||
|
||||
private func layoutViews() {
|
||||
if let dimView {
|
||||
view.addSubview(dimView)
|
||||
dimView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
|
||||
}
|
||||
view.addSubview(availableAreaView)
|
||||
availableAreaView.addSubview(contentView)
|
||||
contentView.addSubview(searchResultsView)
|
||||
contentView.addSubview(headerView)
|
||||
|
||||
contentView.translatesAutoresizingMaskIntoConstraints = false
|
||||
headerView.translatesAutoresizingMaskIntoConstraints = false
|
||||
searchResultsView.translatesAutoresizingMaskIntoConstraints = false
|
||||
|
||||
NSLayoutConstraint.activate([
|
||||
contentView.topAnchor.constraint(equalTo: availableAreaView.topAnchor),
|
||||
contentView.leadingAnchor.constraint(equalTo: availableAreaView.leadingAnchor),
|
||||
contentView.trailingAnchor.constraint(equalTo: availableAreaView.trailingAnchor),
|
||||
contentView.bottomAnchor.constraint(equalTo: availableAreaView.bottomAnchor),
|
||||
|
||||
headerView.topAnchor.constraint(equalTo: contentView.topAnchor),
|
||||
headerView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor),
|
||||
headerView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor),
|
||||
|
||||
searchResultsView.topAnchor.constraint(equalTo: headerView.bottomAnchor),
|
||||
searchResultsView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor),
|
||||
searchResultsView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor),
|
||||
searchResultsView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor),
|
||||
])
|
||||
|
||||
layoutResultsView()
|
||||
layoutHistoryAndCategoryTabView()
|
||||
layoutSearchNoResultsView()
|
||||
layoutSearchingView()
|
||||
updateFrameOfPresentedViewInContainerView()
|
||||
}
|
||||
|
||||
private func layoutResultsView() {
|
||||
searchResultsView.addSubview(resultsTableView)
|
||||
resultsTableView.translatesAutoresizingMaskIntoConstraints = false
|
||||
|
||||
NSLayoutConstraint.activate([
|
||||
resultsTableView.topAnchor.constraint(equalTo: searchResultsView.topAnchor),
|
||||
resultsTableView.leadingAnchor.constraint(equalTo: searchResultsView.leadingAnchor),
|
||||
resultsTableView.trailingAnchor.constraint(equalTo: searchResultsView.trailingAnchor),
|
||||
resultsTableView.bottomAnchor.constraint(equalTo: searchResultsView.bottomAnchor)
|
||||
])
|
||||
}
|
||||
|
||||
private func layoutHistoryAndCategoryTabView() {
|
||||
searchResultsView.addSubview(historyAndCategoryTabViewController.view)
|
||||
historyAndCategoryTabViewController.view.translatesAutoresizingMaskIntoConstraints = false
|
||||
|
||||
NSLayoutConstraint.activate([
|
||||
historyAndCategoryTabViewController.view.topAnchor.constraint(equalTo: searchResultsView.topAnchor),
|
||||
historyAndCategoryTabViewController.view.leadingAnchor.constraint(equalTo: searchResultsView.leadingAnchor),
|
||||
historyAndCategoryTabViewController.view.trailingAnchor.constraint(equalTo: searchResultsView.trailingAnchor),
|
||||
historyAndCategoryTabViewController.view.bottomAnchor.constraint(equalTo: searchResultsView.bottomAnchor)
|
||||
])
|
||||
}
|
||||
|
||||
private func layoutSearchNoResultsView() {
|
||||
searchNoResultsView.translatesAutoresizingMaskIntoConstraints = false
|
||||
searchResultsView.addSubview(searchNoResultsView)
|
||||
NSLayoutConstraint.activate([
|
||||
searchNoResultsView.topAnchor.constraint(equalTo: searchResultsView.topAnchor),
|
||||
searchNoResultsView.leadingAnchor.constraint(equalTo: searchResultsView.leadingAnchor),
|
||||
searchNoResultsView.trailingAnchor.constraint(equalTo: searchResultsView.trailingAnchor),
|
||||
searchNoResultsView.bottomAnchor.constraint(equalTo: searchResultsView.bottomAnchor)
|
||||
])
|
||||
}
|
||||
|
||||
private func layoutSearchingView() {
|
||||
searchResultsView.insertSubview(searchingActivityView, at: 0)
|
||||
searchingActivityView.translatesAutoresizingMaskIntoConstraints = false
|
||||
NSLayoutConstraint.activate([
|
||||
searchingActivityView.leadingAnchor.constraint(equalTo: searchResultsView.leadingAnchor),
|
||||
searchingActivityView.trailingAnchor.constraint(equalTo: searchResultsView.trailingAnchor),
|
||||
searchingActivityView.topAnchor.constraint(equalTo: searchResultsView.topAnchor),
|
||||
searchingActivityView.bottomAnchor.constraint(equalTo: searchResultsView.bottomAnchor)
|
||||
])
|
||||
}
|
||||
|
||||
// MARK: - Handle Presentation Steps
|
||||
private func updateFrameOfPresentedViewInContainerView() {
|
||||
presentationStepsController.updateMaxAvailableFrame()
|
||||
availableAreaView.frame = presentationStepsController.currentFrame
|
||||
view.layoutIfNeeded()
|
||||
}
|
||||
|
||||
@objc
|
||||
private func handleTapOutside(_ gesture: UITapGestureRecognizer) {
|
||||
let location = gesture.location(in: view)
|
||||
if resultsTableView.frame.contains(location) && searchResults.isEmpty {
|
||||
headerView.setIsSearching(false)
|
||||
}
|
||||
}
|
||||
|
||||
@objc
|
||||
private func handlePan(_ gesture: UIPanGestureRecognizer) {
|
||||
interactor?.handle(.didStartDraggingSearch)
|
||||
presentationStepsController.handlePan(gesture)
|
||||
}
|
||||
|
||||
private var presentationUpdateHandler: (ModalPresentationStepsController.StepUpdate) -> Void {
|
||||
{ [weak self] update in
|
||||
guard let self else { return }
|
||||
switch update {
|
||||
case .didClose:
|
||||
self.interactor?.handle(.closeSearch)
|
||||
case .didUpdateFrame(let frame):
|
||||
self.presentationFrameDidChange(frame)
|
||||
self.updateDimView(for: frame)
|
||||
case .didUpdateStep(let step):
|
||||
self.interactor?.handle(.didUpdatePresentationStep(step))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func updateDimView(for frame: CGRect) {
|
||||
guard let dimView else { return }
|
||||
let currentTop = frame.origin.y
|
||||
let maxTop = presentationStepsController.maxAvailableFrame.origin.y
|
||||
let alpha = (1 - (currentTop - maxTop) / Constants.dimViewThreshold) * Constants.dimAlpha
|
||||
let isCloseToTop = currentTop - maxTop < Constants.dimViewThreshold
|
||||
let isPortrait = UIApplication.shared.statusBarOrientation.isPortrait
|
||||
let shouldDim = isCloseToTop && isPortrait
|
||||
UIView.animate(withDuration: kDefaultAnimationDuration / 2) {
|
||||
dimView.alpha = shouldDim ? alpha : 0
|
||||
dimView.isHidden = !shouldDim
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Handle Content Updates
|
||||
private func setContent(_ content: Content) {
|
||||
switch content {
|
||||
case .historyAndCategory:
|
||||
historyAndCategoryTabViewController.reloadSearchHistory()
|
||||
case let .results(results):
|
||||
if searchResults != results {
|
||||
searchResults = results
|
||||
resultsTableView.reloadData()
|
||||
}
|
||||
case .noResults:
|
||||
searchResults = .empty
|
||||
resultsTableView.reloadData()
|
||||
case .searching:
|
||||
break
|
||||
}
|
||||
headerView.setSeparatorHidden(content == .historyAndCategory)
|
||||
showView(viewToShow(for: content))
|
||||
}
|
||||
|
||||
private func viewToShow(for content: Content) -> UIView {
|
||||
switch content {
|
||||
case .historyAndCategory:
|
||||
return historyAndCategoryTabViewController.view
|
||||
case .results:
|
||||
return resultsTableView
|
||||
case .noResults:
|
||||
return searchNoResultsView
|
||||
case .searching:
|
||||
return searchingActivityView
|
||||
}
|
||||
}
|
||||
|
||||
private func showView(_ view: UIView) {
|
||||
let viewsToHide: [UIView] = [resultsTableView,
|
||||
historyAndCategoryTabViewController.view,
|
||||
searchNoResultsView,
|
||||
searchingActivityView].filter { $0 != view }
|
||||
UIView.animate(withDuration: kDefaultAnimationDuration / 2,
|
||||
delay: 0,
|
||||
options: .curveEaseInOut,
|
||||
animations: {
|
||||
viewsToHide.forEach { $0.alpha = 0 }
|
||||
view.alpha = 1
|
||||
}) { _ in
|
||||
viewsToHide.forEach { $0.isHidden = true }
|
||||
view.isHidden = false
|
||||
}
|
||||
}
|
||||
|
||||
private func setIsSearching(_ isSearching: Bool) {
|
||||
headerView.setIsSearching(isSearching)
|
||||
}
|
||||
|
||||
private func setSearchText(_ text: String?) {
|
||||
if let text {
|
||||
headerView.setSearchText(text)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Public methods
|
||||
extension SearchOnMapViewController: SearchOnMapView {
|
||||
func render(_ viewModel: ViewModel) {
|
||||
setContent(viewModel.contentState)
|
||||
setIsSearching(viewModel.isTyping)
|
||||
setSearchText(viewModel.searchingText)
|
||||
presentationStepsController.setStep(viewModel.presentationStep)
|
||||
}
|
||||
|
||||
func show() {
|
||||
interactor?.handle(.openSearch)
|
||||
}
|
||||
|
||||
func close() {
|
||||
headerView.setIsSearching(false)
|
||||
updateDimView(for: presentationStepsController.hiddenFrame)
|
||||
willMove(toParent: nil)
|
||||
presentationStepsController.close { [weak self] in
|
||||
self?.view.removeFromSuperview()
|
||||
self?.removeFromParent()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - ModallyPresentedViewController
|
||||
extension SearchOnMapViewController: ModallyPresentedViewController {
|
||||
func presentationFrameDidChange(_ frame: CGRect) {
|
||||
let translationY = frame.origin.y
|
||||
resultsTableView.contentInset.bottom = translationY
|
||||
historyAndCategoryTabViewController.presentationFrameDidChange(frame)
|
||||
searchNoResultsView.presentationFrameDidChange(frame)
|
||||
searchingActivityView.presentationFrameDidChange(frame)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - UITableViewDataSource
|
||||
extension SearchOnMapViewController: UITableViewDataSource {
|
||||
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
|
||||
searchResults.count
|
||||
}
|
||||
|
||||
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
|
||||
let result = searchResults[indexPath.row]
|
||||
switch result.itemType {
|
||||
case .regular:
|
||||
let cell = tableView.dequeueReusableCell(cell: SearchCommonCell.self, indexPath: indexPath)
|
||||
cell.configure(with: result, isPartialMatching: searchResults.hasPartialMatch)
|
||||
return cell
|
||||
case .suggestion:
|
||||
let cell = tableView.dequeueReusableCell(cell: SearchSuggestionCell.self, indexPath: indexPath)
|
||||
cell.configure(with: result, isPartialMatching: true)
|
||||
cell.isLastCell = indexPath.row == searchResults.suggestionsCount - 1
|
||||
return cell
|
||||
@unknown default:
|
||||
fatalError("Unknown item type")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - UITableViewDelegate
|
||||
extension SearchOnMapViewController: UITableViewDelegate {
|
||||
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
|
||||
let result = searchResults[indexPath.row]
|
||||
interactor?.handle(.didSelectResult(result, withQuery: headerView.searchQuery))
|
||||
tableView.deselectRow(at: indexPath, animated: true)
|
||||
}
|
||||
|
||||
func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
|
||||
interactor?.handle(.didStartDraggingSearch)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - SearchOnMapHeaderViewDelegate
|
||||
extension SearchOnMapViewController: SearchOnMapHeaderViewDelegate {
|
||||
func searchBarTextDidBeginEditing(_ searchBar: UISearchBar) {
|
||||
interactor?.handle(.didStartTyping)
|
||||
}
|
||||
|
||||
func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) {
|
||||
guard !searchText.isEmpty else {
|
||||
interactor?.handle(.clearButtonDidTap)
|
||||
return
|
||||
}
|
||||
interactor?.handle(.didType(SearchQuery(searchText, locale: searchBar.textInputMode?.primaryLanguage, source: .typedText)))
|
||||
}
|
||||
|
||||
func searchBarSearchButtonClicked(_ searchBar: UISearchBar) {
|
||||
guard let searchText = searchBar.text, !searchText.isEmpty else { return }
|
||||
interactor?.handle(.searchButtonDidTap(SearchQuery(searchText, locale: searchBar.textInputMode?.primaryLanguage, source: .typedText)))
|
||||
}
|
||||
|
||||
func cancelButtonDidTap() {
|
||||
interactor?.handle(.closeSearch)
|
||||
}
|
||||
|
||||
func grabberDidTap() {
|
||||
interactor?.handle(.didUpdatePresentationStep(.fullScreen))
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - SearchTabViewControllerDelegate
|
||||
extension SearchOnMapViewController: SearchTabViewControllerDelegate {
|
||||
func searchTabController(_ viewController: SearchTabViewController, didSearch query: SearchQuery) {
|
||||
interactor?.handle(.didSelect(query))
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - UIGestureRecognizerDelegate
|
||||
extension SearchOnMapViewController: UIGestureRecognizerDelegate {
|
||||
func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
|
||||
true
|
||||
}
|
||||
|
||||
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
|
||||
if gestureRecognizer is UIPanGestureRecognizer && otherGestureRecognizer is UIPanGestureRecognizer {
|
||||
// threshold is used to soften transition from the internal scroll zero content offset
|
||||
return internalScrollViewContentOffset < Constants.panGestureThreshold
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - SearchOnMapScrollViewDelegate
|
||||
extension SearchOnMapViewController: SearchOnMapScrollViewDelegate {
|
||||
func scrollViewDidScroll(_ scrollView: UIScrollView) {
|
||||
let hasReachedTheTop = Int(availableAreaView.frame.origin.y) > Int(presentationStepsController.maxAvailableFrame.origin.y)
|
||||
let hasZeroContentOffset = internalScrollViewContentOffset == 0
|
||||
if hasReachedTheTop && hasZeroContentOffset {
|
||||
// prevent the internal scroll view scrolling
|
||||
scrollView.contentOffset.y = internalScrollViewContentOffset
|
||||
return
|
||||
}
|
||||
internalScrollViewContentOffset = scrollView.contentOffset.y
|
||||
}
|
||||
|
||||
func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>) {
|
||||
// lock internal scroll view when the user fast scrolls screen to the top
|
||||
if internalScrollViewContentOffset == 0 {
|
||||
targetContentOffset.pointee = .zero
|
||||
}
|
||||
}
|
||||
}
|
||||
11
iphone/Maps/UI/Search/TableView/MWMSearchCell.h
Normal file
11
iphone/Maps/UI/Search/TableView/MWMSearchCell.h
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
#import "MWMTableViewCell.h"
|
||||
|
||||
@class SearchResult;
|
||||
|
||||
static CGFloat const kSearchCellSeparatorInset = 48;
|
||||
|
||||
@interface MWMSearchCell : MWMTableViewCell
|
||||
|
||||
- (void)configureWith:(SearchResult * _Nonnull)result isPartialMatching:(BOOL)isPartialMatching;
|
||||
|
||||
@end
|
||||
68
iphone/Maps/UI/Search/TableView/MWMSearchCell.mm
Normal file
68
iphone/Maps/UI/Search/TableView/MWMSearchCell.mm
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
#import "MWMSearchCell.h"
|
||||
#import "SearchResult.h"
|
||||
|
||||
@interface MWMSearchCell ()
|
||||
|
||||
@property (weak, nonatomic) IBOutlet UILabel * titleLabel;
|
||||
|
||||
@end
|
||||
|
||||
@implementation MWMSearchCell
|
||||
|
||||
- (void)configureWith:(SearchResult * _Nonnull)result isPartialMatching:(BOOL)isPartialMatching {
|
||||
NSString * title = result.titleText;
|
||||
|
||||
if (title.length == 0)
|
||||
{
|
||||
self.titleLabel.text = @"";
|
||||
return;
|
||||
}
|
||||
|
||||
bool hasBranchName = (result.branchText && result.branchText.length > 0 && ![title containsString:result.branchText]);
|
||||
|
||||
NSDictionary * selectedTitleAttributes = [self selectedTitleAttributes];
|
||||
NSDictionary * unselectedTitleAttributes = [self unselectedTitleAttributes];
|
||||
if ((!selectedTitleAttributes || !unselectedTitleAttributes) && !hasBranchName)
|
||||
{
|
||||
self.titleLabel.text = title;
|
||||
return;
|
||||
}
|
||||
|
||||
NSMutableAttributedString * attributedTitle =
|
||||
[[NSMutableAttributedString alloc] initWithString:title];
|
||||
|
||||
NSArray<NSValue *> *highlightRanges = result.highlightRanges;
|
||||
[attributedTitle addAttributes:unselectedTitleAttributes range:NSMakeRange(0, title.length)];
|
||||
|
||||
// Add branch with thinner font weight if present and not already in title
|
||||
if (hasBranchName) {
|
||||
NSMutableDictionary * branchAttributes = [unselectedTitleAttributes mutableCopy];
|
||||
[branchAttributes setValue:[UIFont regular17] forKey:NSFontAttributeName];
|
||||
NSAttributedString * branchString = [[NSAttributedString alloc] initWithString:[NSString stringWithFormat:@" %@", result.branchText] attributes:branchAttributes];
|
||||
[attributedTitle appendAttributedString:branchString];
|
||||
}
|
||||
|
||||
for (NSValue *rangeValue in highlightRanges) {
|
||||
NSRange range = [rangeValue rangeValue];
|
||||
if (NSMaxRange(range) <= attributedTitle.string.length) {
|
||||
[attributedTitle setAttributes:selectedTitleAttributes range:range];
|
||||
} else {
|
||||
NSLog(@"Incorrect range: %@ for string: %@", NSStringFromRange(range), attributedTitle.string);
|
||||
}
|
||||
}
|
||||
|
||||
self.titleLabel.attributedText = attributedTitle;
|
||||
[self.titleLabel sizeToFit];
|
||||
}
|
||||
|
||||
- (NSDictionary *)selectedTitleAttributes
|
||||
{
|
||||
return nil;
|
||||
}
|
||||
|
||||
- (NSDictionary *)unselectedTitleAttributes
|
||||
{
|
||||
return nil;
|
||||
}
|
||||
|
||||
@end
|
||||
10
iphone/Maps/UI/Search/TableView/MWMSearchCommonCell.h
Normal file
10
iphone/Maps/UI/Search/TableView/MWMSearchCommonCell.h
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
#import "MWMSearchCell.h"
|
||||
|
||||
@class SearchResult;
|
||||
|
||||
NS_SWIFT_NAME(SearchCommonCell)
|
||||
@interface MWMSearchCommonCell : MWMSearchCell
|
||||
|
||||
- (void)configureWith:(SearchResult * _Nonnull)result isPartialMatching:(BOOL)isPartialMatching;
|
||||
|
||||
@end
|
||||
59
iphone/Maps/UI/Search/TableView/MWMSearchCommonCell.mm
Normal file
59
iphone/Maps/UI/Search/TableView/MWMSearchCommonCell.mm
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
#import "MWMSearchCommonCell.h"
|
||||
#import "CLLocation+Mercator.h"
|
||||
#import "MWMLocationManager.h"
|
||||
#import "SwiftBridge.h"
|
||||
#import "SearchResult.h"
|
||||
|
||||
@interface MWMSearchCommonCell ()
|
||||
|
||||
@property(weak, nonatomic) IBOutlet UILabel * distanceLabel;
|
||||
@property(weak, nonatomic) IBOutlet UILabel * infoLabel;
|
||||
@property(weak, nonatomic) IBOutlet UILabel * locationLabel;
|
||||
@property(weak, nonatomic) IBOutlet UILabel * openLabel;
|
||||
@property(weak, nonatomic) IBOutlet UIView * popularView;
|
||||
@property(weak, nonatomic) IBOutlet UIImageView * iconImageView;
|
||||
|
||||
@end
|
||||
|
||||
@implementation MWMSearchCommonCell
|
||||
|
||||
- (void)configureWith:(SearchResult * _Nonnull)result isPartialMatching:(BOOL)isPartialMatching {
|
||||
[super configureWith:result isPartialMatching:isPartialMatching];
|
||||
self.locationLabel.text = result.addressText;
|
||||
[self.locationLabel sizeToFit];
|
||||
self.infoLabel.text = result.infoText;
|
||||
self.distanceLabel.text = result.distanceText;
|
||||
self.distanceLabel.textColor = [UIColor.labelColor colorWithAlphaComponent:0.7];
|
||||
self.popularView.hidden = YES;
|
||||
self.openLabel.text = result.openStatusText;
|
||||
self.openLabel.textColor = result.openStatusColor;
|
||||
[self.openLabel setHidden:result.openStatusText.length == 0];
|
||||
[self setStyleNameAndApply:@"Background"];
|
||||
if (result.iconImageName != nil) {
|
||||
self.iconImageView.image = [[UIImage imageNamed:result.iconImageName] imageWithTintColor:UIColor.white];
|
||||
}
|
||||
self.iconImageView.backgroundColor = [UIColor colorNamed:@"Base Colors/Blue Color"];
|
||||
self.separatorInset = UIEdgeInsetsMake(0, kSearchCellSeparatorInset, 0, 0);
|
||||
}
|
||||
|
||||
- (void)layoutSubviews {
|
||||
[super layoutSubviews];
|
||||
self.iconImageView.image = [self.iconImageView.image imageWithTintColor:UIColor.white];
|
||||
[self.iconImageView.layer setCornerRadius:self.iconImageView.height / 2];
|
||||
}
|
||||
|
||||
- (NSDictionary *)selectedTitleAttributes {
|
||||
return @{
|
||||
NSForegroundColorAttributeName : [UIColor black],
|
||||
NSFontAttributeName : [UIFont bold17]
|
||||
};
|
||||
}
|
||||
|
||||
- (NSDictionary *)unselectedTitleAttributes {
|
||||
return @{
|
||||
NSForegroundColorAttributeName : [UIColor blackPrimaryText],
|
||||
NSFontAttributeName : [UIFont medium17]
|
||||
};
|
||||
}
|
||||
|
||||
@end
|
||||
145
iphone/Maps/UI/Search/TableView/MWMSearchCommonCell.xib
Normal file
145
iphone/Maps/UI/Search/TableView/MWMSearchCommonCell.xib
Normal file
|
|
@ -0,0 +1,145 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="23727" 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="23721"/>
|
||||
<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"/>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="-2" customClass="UIResponder"/>
|
||||
<tableViewCell contentMode="scaleToFill" selectionStyle="default" indentationWidth="10" reuseIdentifier="MWMSearchCommonCell" rowHeight="85" id="KGk-i7-Jjw" customClass="MWMSearchCommonCell">
|
||||
<rect key="frame" x="0.0" y="0.0" width="321" height="85"/>
|
||||
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
|
||||
<tableViewCellContentView key="contentView" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" tableViewCell="KGk-i7-Jjw" id="H2p-sc-9uM">
|
||||
<rect key="frame" x="0.0" y="0.0" width="321" height="85"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<subviews>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="252" verticalCompressionResistancePriority="752" text="New York Cafe" textAlignment="natural" lineBreakMode="wordWrap" numberOfLines="0" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="4FD-RE-ffF">
|
||||
<rect key="frame" x="56" y="12" width="181" height="20.5"/>
|
||||
<accessibility key="accessibilityConfiguration" identifier="searchTitle"/>
|
||||
<fontDescription key="fontDescription" type="system" weight="medium" pointSize="17"/>
|
||||
<color key="textColor" red="0.12941176470588234" green="0.12941176470588234" blue="0.12941176470588234" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
<nil key="highlightedColor"/>
|
||||
<userDefinedRuntimeAttributes>
|
||||
<userDefinedRuntimeAttribute type="string" keyPath="styleName" value="blackPrimaryText"/>
|
||||
</userDefinedRuntimeAttributes>
|
||||
</label>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="" textAlignment="right" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="P8X-Xp-AaE">
|
||||
<rect key="frame" x="237" y="57" width="68" height="0.0"/>
|
||||
<fontDescription key="fontDescription" type="system" weight="medium" pointSize="13"/>
|
||||
<color key="textColor" red="0.12549019607843137" green="0.58823529411764708" blue="0.95294117647058818" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
<view hidden="YES" contentMode="scaleToFill" ambiguous="YES" translatesAutoresizingMaskIntoConstraints="NO" id="uWz-7m-GUu">
|
||||
<rect key="frame" x="262.5" y="16" width="41.5" height="20"/>
|
||||
<subviews>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="TOP" textAlignment="center" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="yjT-ah-SWQ">
|
||||
<rect key="frame" x="8" y="0.0" width="25.5" height="20"/>
|
||||
<fontDescription key="fontDescription" type="boldSystem" pointSize="12"/>
|
||||
<color key="textColor" red="0.1176470588" green="0.58823529409999997" blue="0.93983289930000002" alpha="1" colorSpace="calibratedRGB"/>
|
||||
<nil key="highlightedColor"/>
|
||||
<userDefinedRuntimeAttributes>
|
||||
<userDefinedRuntimeAttribute type="string" keyPath="styleName" value="bold12:linkBlueText"/>
|
||||
<userDefinedRuntimeAttribute type="string" keyPath="localizedText" value="popular_place"/>
|
||||
</userDefinedRuntimeAttributes>
|
||||
</label>
|
||||
</subviews>
|
||||
<color key="backgroundColor" red="0.14117647059999999" green="0.61176470589999998" blue="0.94901960780000005" alpha="0.16424978600000001" colorSpace="calibratedRGB"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="height" constant="20" id="g2X-lA-DE3"/>
|
||||
<constraint firstAttribute="bottom" secondItem="yjT-ah-SWQ" secondAttribute="bottom" id="ghe-TD-Ni1"/>
|
||||
<constraint firstItem="yjT-ah-SWQ" firstAttribute="top" secondItem="uWz-7m-GUu" secondAttribute="top" id="rK0-T0-I3J"/>
|
||||
<constraint firstItem="yjT-ah-SWQ" firstAttribute="leading" secondItem="uWz-7m-GUu" secondAttribute="leading" constant="8" id="ucR-R6-Fyw"/>
|
||||
<constraint firstAttribute="trailing" secondItem="yjT-ah-SWQ" secondAttribute="trailing" constant="8" id="vYZ-sk-v7u"/>
|
||||
</constraints>
|
||||
<userDefinedRuntimeAttributes>
|
||||
<userDefinedRuntimeAttribute type="string" keyPath="styleName" value="SearchPopularView"/>
|
||||
</userDefinedRuntimeAttributes>
|
||||
</view>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Сafe" textAlignment="natural" lineBreakMode="tailTruncation" numberOfLines="0" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="5UO-MD-Hgx">
|
||||
<rect key="frame" x="56" y="36.5" width="29" height="1"/>
|
||||
<accessibility key="accessibilityConfiguration" identifier="searchType"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="width" relation="greaterThanOrEqual" constant="26" id="O31-Vq-Bsz"/>
|
||||
</constraints>
|
||||
<fontDescription key="fontDescription" type="system" weight="medium" pointSize="13"/>
|
||||
<color key="textColor" red="0.0" green="0.0" blue="0.0" alpha="0.54000000000000004" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
<nil key="highlightedColor"/>
|
||||
<userDefinedRuntimeAttributes>
|
||||
<userDefinedRuntimeAttribute type="string" keyPath="styleName" value="medium13:blackSecondaryText"/>
|
||||
</userDefinedRuntimeAttributes>
|
||||
</label>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalCompressionResistancePriority="751" verticalCompressionResistancePriority="751" text="Open" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="SDd-3c-YeL">
|
||||
<rect key="frame" x="271.5" y="21.5" width="33.5" height="16"/>
|
||||
<accessibility key="accessibilityConfiguration" identifier="searchType"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="width" relation="greaterThanOrEqual" constant="26" id="tqr-8N-JwN"/>
|
||||
</constraints>
|
||||
<fontDescription key="fontDescription" type="system" weight="medium" pointSize="13"/>
|
||||
<color key="textColor" red="0.0" green="0.0" blue="0.0" alpha="0.54000000000000004" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" verticalCompressionResistancePriority="751" text="Russia, Moscow & Central, Moscow" textAlignment="natural" lineBreakMode="tailTruncation" numberOfLines="0" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="6pc-4s-GyP">
|
||||
<rect key="frame" x="56" y="41.5" width="181" height="31.5"/>
|
||||
<accessibility key="accessibilityConfiguration" identifier="searchSubTitle"/>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="13"/>
|
||||
<color key="textColor" red="0.0" green="0.0" blue="0.0" alpha="0.54000000000000004" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
<nil key="highlightedColor"/>
|
||||
<userDefinedRuntimeAttributes>
|
||||
<userDefinedRuntimeAttribute type="string" keyPath="styleName" value="regular13:blackSecondaryText"/>
|
||||
</userDefinedRuntimeAttributes>
|
||||
</label>
|
||||
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="center" horizontalHuggingPriority="251" verticalHuggingPriority="251" translatesAutoresizingMaskIntoConstraints="NO" id="YIa-Hb-PGV" userLabel="Icon ImageView">
|
||||
<rect key="frame" x="16" y="28.5" width="28" height="28"/>
|
||||
<color key="backgroundColor" systemColor="linkColor"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="height" constant="28" id="fPn-mC-8Je"/>
|
||||
<constraint firstAttribute="width" constant="28" id="sJ9-6S-sMG"/>
|
||||
</constraints>
|
||||
</imageView>
|
||||
</subviews>
|
||||
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
<constraints>
|
||||
<constraint firstItem="P8X-Xp-AaE" firstAttribute="leading" secondItem="6pc-4s-GyP" secondAttribute="trailing" id="0hr-QT-t0D"/>
|
||||
<constraint firstItem="5UO-MD-Hgx" firstAttribute="top" secondItem="4FD-RE-ffF" secondAttribute="bottom" constant="4" id="5dn-ca-dCn"/>
|
||||
<constraint firstItem="6pc-4s-GyP" firstAttribute="top" secondItem="5UO-MD-Hgx" secondAttribute="bottom" constant="4" id="7pm-XZ-vLK"/>
|
||||
<constraint firstAttribute="bottom" secondItem="6pc-4s-GyP" secondAttribute="bottom" constant="12" id="HWe-cz-8hm"/>
|
||||
<constraint firstItem="5UO-MD-Hgx" firstAttribute="trailing" relation="lessThanOrEqual" secondItem="SDd-3c-YeL" secondAttribute="leading" id="SJj-b5-T2k"/>
|
||||
<constraint firstAttribute="trailing" secondItem="4FD-RE-ffF" secondAttribute="trailing" constant="84" id="Ugu-lP-b9G"/>
|
||||
<constraint firstAttribute="trailing" secondItem="P8X-Xp-AaE" secondAttribute="trailing" constant="16" id="VJE-wo-TBb"/>
|
||||
<constraint firstItem="YIa-Hb-PGV" firstAttribute="leading" secondItem="H2p-sc-9uM" secondAttribute="leading" constant="16" id="WRp-Ac-GmE"/>
|
||||
<constraint firstItem="YIa-Hb-PGV" firstAttribute="centerY" secondItem="H2p-sc-9uM" secondAttribute="centerY" id="g6c-sx-War"/>
|
||||
<constraint firstItem="5UO-MD-Hgx" firstAttribute="leading" secondItem="4FD-RE-ffF" secondAttribute="leading" id="gNK-ED-O8W"/>
|
||||
<constraint firstItem="4FD-RE-ffF" firstAttribute="top" secondItem="H2p-sc-9uM" secondAttribute="top" constant="12" id="hM6-br-iKE"/>
|
||||
<constraint firstItem="6pc-4s-GyP" firstAttribute="leading" secondItem="5UO-MD-Hgx" secondAttribute="leading" id="jvQ-jd-XUJ"/>
|
||||
<constraint firstItem="4FD-RE-ffF" firstAttribute="leading" secondItem="YIa-Hb-PGV" secondAttribute="trailing" constant="12" id="nKc-RK-2zd"/>
|
||||
<constraint firstAttribute="trailing" secondItem="6pc-4s-GyP" secondAttribute="trailing" constant="84" id="nfE-NI-LX9"/>
|
||||
<constraint firstItem="6pc-4s-GyP" firstAttribute="baseline" secondItem="P8X-Xp-AaE" secondAttribute="firstBaseline" id="q7E-Jg-MYT"/>
|
||||
<constraint firstAttribute="trailing" secondItem="SDd-3c-YeL" secondAttribute="trailing" constant="16" id="vay-ux-6dA"/>
|
||||
<constraint firstItem="SDd-3c-YeL" firstAttribute="bottom" secondItem="5UO-MD-Hgx" secondAttribute="bottom" id="wXg-ce-SnG"/>
|
||||
</constraints>
|
||||
</tableViewCellContentView>
|
||||
<userDefinedRuntimeAttributes>
|
||||
<userDefinedRuntimeAttribute type="string" keyPath="styleName" value="Background"/>
|
||||
</userDefinedRuntimeAttributes>
|
||||
<connections>
|
||||
<outlet property="distanceLabel" destination="P8X-Xp-AaE" id="Kaw-aR-8uJ"/>
|
||||
<outlet property="iconImageView" destination="YIa-Hb-PGV" id="oQP-F5-9hw"/>
|
||||
<outlet property="infoLabel" destination="5UO-MD-Hgx" id="lgJ-zE-omX"/>
|
||||
<outlet property="locationLabel" destination="6pc-4s-GyP" id="Te0-y3-sVQ"/>
|
||||
<outlet property="openLabel" destination="SDd-3c-YeL" id="5Rv-fO-g4x"/>
|
||||
<outlet property="popularView" destination="uWz-7m-GUu" id="LAK-NA-Fea"/>
|
||||
<outlet property="titleLabel" destination="4FD-RE-ffF" id="OQm-o8-LUd"/>
|
||||
</connections>
|
||||
<point key="canvasLocation" x="315.21739130434787" y="296.98660714285711"/>
|
||||
</tableViewCell>
|
||||
</objects>
|
||||
<resources>
|
||||
<systemColor name="linkColor">
|
||||
<color red="0.0" green="0.47843137250000001" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
</systemColor>
|
||||
</resources>
|
||||
</document>
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
#import "MWMSearchCell.h"
|
||||
|
||||
NS_SWIFT_NAME(SearchSuggestionCell)
|
||||
@interface MWMSearchSuggestionCell : MWMSearchCell
|
||||
|
||||
@property (nonatomic) BOOL isLastCell;
|
||||
|
||||
@end
|
||||
36
iphone/Maps/UI/Search/TableView/MWMSearchSuggestionCell.mm
Normal file
36
iphone/Maps/UI/Search/TableView/MWMSearchSuggestionCell.mm
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
#import "MWMSearchSuggestionCell.h"
|
||||
|
||||
@interface MWMSearchSuggestionCell ()
|
||||
|
||||
@property (weak, nonatomic) IBOutlet UIImageView * icon;
|
||||
|
||||
@end
|
||||
|
||||
@implementation MWMSearchSuggestionCell
|
||||
|
||||
- (void)awakeFromNib
|
||||
{
|
||||
[super awakeFromNib];
|
||||
if (IPAD)
|
||||
self.contentView.backgroundColor = [UIColor white];
|
||||
}
|
||||
|
||||
- (NSDictionary *)selectedTitleAttributes
|
||||
{
|
||||
return @{NSForegroundColorAttributeName : UIColor.linkBlue, NSFontAttributeName : UIFont.bold16};
|
||||
}
|
||||
|
||||
- (NSDictionary *)unselectedTitleAttributes
|
||||
{
|
||||
return @{NSForegroundColorAttributeName : UIColor.linkBlue, NSFontAttributeName : UIFont.regular16};
|
||||
}
|
||||
|
||||
#pragma mark - Properties
|
||||
|
||||
- (void)setIsLastCell:(BOOL)isLastCell
|
||||
{
|
||||
_isLastCell = isLastCell;
|
||||
self.separatorInset = UIEdgeInsetsMake(0, isLastCell ? 0 : kSearchCellSeparatorInset, 0, 0);
|
||||
}
|
||||
|
||||
@end
|
||||
58
iphone/Maps/UI/Search/TableView/MWMSearchSuggestionCell.xib
Normal file
58
iphone/Maps/UI/Search/TableView/MWMSearchSuggestionCell.xib
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="23504" 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="23506"/>
|
||||
<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="default" indentationWidth="10" id="KGk-i7-Jjw" customClass="MWMSearchSuggestionCell" propertyAccessControl="none">
|
||||
<rect key="frame" x="0.0" y="0.0" width="320" height="44"/>
|
||||
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
|
||||
<tableViewCellContentView key="contentView" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" ambiguous="YES" tableViewCell="KGk-i7-Jjw" id="H2p-sc-9uM">
|
||||
<rect key="frame" x="0.0" y="0.0" width="320" height="44"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<subviews>
|
||||
<imageView userInteractionEnabled="NO" contentMode="center" horizontalHuggingPriority="251" verticalHuggingPriority="251" image="ic_search" translatesAutoresizingMaskIntoConstraints="NO" id="1IA-T9-KOb">
|
||||
<rect key="frame" x="16" y="8" width="24" height="24"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="width" constant="24" id="TPv-Js-EXq"/>
|
||||
<constraint firstAttribute="height" constant="24" id="bse-BN-jT9"/>
|
||||
</constraints>
|
||||
<userDefinedRuntimeAttributes>
|
||||
<userDefinedRuntimeAttribute type="string" keyPath="styleName" value="MWMBlue"/>
|
||||
</userDefinedRuntimeAttributes>
|
||||
</imageView>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="New Arbat Avenue" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="gWP-Zj-GCt">
|
||||
<rect key="frame" x="52" y="9.5" width="252" height="21"/>
|
||||
<fontDescription key="fontDescription" type="system" weight="medium" pointSize="17"/>
|
||||
<color key="textColor" red="0.12549019610000001" green="0.58823529409999997" blue="0.95294117649999999" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
</subviews>
|
||||
<constraints>
|
||||
<constraint firstAttribute="bottom" secondItem="1IA-T9-KOb" secondAttribute="bottom" constant="8" id="0He-dX-gNg"/>
|
||||
<constraint firstAttribute="trailing" secondItem="gWP-Zj-GCt" secondAttribute="trailing" constant="16" id="c1H-CR-1P7"/>
|
||||
<constraint firstItem="gWP-Zj-GCt" firstAttribute="centerY" secondItem="1IA-T9-KOb" secondAttribute="centerY" id="iJZ-pk-VCW"/>
|
||||
<constraint firstItem="1IA-T9-KOb" firstAttribute="leading" secondItem="H2p-sc-9uM" secondAttribute="leading" constant="16" id="l2F-lx-j3x"/>
|
||||
<constraint firstItem="gWP-Zj-GCt" firstAttribute="leading" secondItem="1IA-T9-KOb" secondAttribute="trailing" constant="12" id="xfc-Vx-bth"/>
|
||||
<constraint firstItem="1IA-T9-KOb" firstAttribute="top" secondItem="H2p-sc-9uM" secondAttribute="top" constant="8" id="y24-rC-y5X"/>
|
||||
</constraints>
|
||||
</tableViewCellContentView>
|
||||
<userDefinedRuntimeAttributes>
|
||||
<userDefinedRuntimeAttribute type="string" keyPath="styleName" value="Background"/>
|
||||
</userDefinedRuntimeAttributes>
|
||||
<connections>
|
||||
<outlet property="icon" destination="1IA-T9-KOb" id="eBB-Wj-yU5"/>
|
||||
<outlet property="titleLabel" destination="gWP-Zj-GCt" id="P6N-C2-ce6"/>
|
||||
</connections>
|
||||
<point key="canvasLocation" x="139" y="154"/>
|
||||
</tableViewCell>
|
||||
</objects>
|
||||
<resources>
|
||||
<image name="ic_search" width="28" height="28"/>
|
||||
</resources>
|
||||
</document>
|
||||
|
|
@ -0,0 +1,65 @@
|
|||
protocol SearchCategoriesViewControllerDelegate: SearchOnMapScrollViewDelegate {
|
||||
func categoriesViewController(_ viewController: SearchCategoriesViewController,
|
||||
didSelect category: String)
|
||||
}
|
||||
|
||||
final class SearchCategoriesViewController: MWMTableViewController {
|
||||
private weak var delegate: SearchCategoriesViewControllerDelegate?
|
||||
private let categories: [String]
|
||||
|
||||
init(frameworkHelper: MWMSearchFrameworkHelper.Type, delegate: SearchCategoriesViewControllerDelegate?) {
|
||||
self.delegate = delegate
|
||||
categories = frameworkHelper.searchCategories()
|
||||
super.init(nibName: nil, bundle: nil)
|
||||
}
|
||||
|
||||
@available(*, unavailable)
|
||||
required init?(coder aDecoder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
tableView.setStyle(.background)
|
||||
tableView.register(cell: SearchCategoryCell.self)
|
||||
tableView.keyboardDismissMode = .onDrag
|
||||
let footerHeight = (UIApplication.shared.connectedScenes.filter { $0.activationState == .foregroundActive }.first(where: { $0 is UIWindowScene }) as? UIWindowScene)?.keyWindow?.safeAreaInsets.bottom ?? 1
|
||||
tableView.tableFooterView = UIView(frame: CGRect(x: 0, y: 0, width: 400, height: footerHeight))
|
||||
}
|
||||
|
||||
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
|
||||
return categories.count
|
||||
}
|
||||
|
||||
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
|
||||
let cell = tableView.dequeueReusableCell(cell: SearchCategoryCell.self, indexPath: indexPath)
|
||||
cell.configure(with: category(at: indexPath))
|
||||
return cell
|
||||
}
|
||||
|
||||
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
|
||||
let selectedCategory = category(at: indexPath)
|
||||
delegate?.categoriesViewController(self, didSelect: selectedCategory)
|
||||
tableView.deselectRow(at: indexPath, animated: true)
|
||||
}
|
||||
|
||||
override func scrollViewDidScroll(_ scrollView: UIScrollView) {
|
||||
delegate?.scrollViewDidScroll(scrollView)
|
||||
}
|
||||
|
||||
override func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>) {
|
||||
delegate?.scrollViewWillEndDragging(scrollView, withVelocity: velocity, targetContentOffset: targetContentOffset)
|
||||
}
|
||||
|
||||
func category(at indexPath: IndexPath) -> String {
|
||||
let index = indexPath.row
|
||||
return categories[index]
|
||||
}
|
||||
}
|
||||
|
||||
extension SearchCategoriesViewController: ModallyPresentedViewController {
|
||||
func presentationFrameDidChange(_ frame: CGRect) {
|
||||
guard isViewLoaded else { return }
|
||||
tableView.contentInset.bottom = frame.origin.y + view.safeAreaInsets.bottom
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
final class SearchCategoryCell: UITableViewCell {
|
||||
|
||||
private var categoryName: String = ""
|
||||
|
||||
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
|
||||
super.init(style: .default, reuseIdentifier: reuseIdentifier)
|
||||
setStyle(.defaultTableViewCell)
|
||||
}
|
||||
|
||||
@available(*, unavailable)
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
func configure(with categoryName: String) {
|
||||
self.categoryName = categoryName
|
||||
textLabel?.text = L(categoryName)
|
||||
imageView?.image = UIImage(named: String(format: "Search/Categories/%@", categoryName.replacingOccurrences(of: "category_", with: "")))
|
||||
}
|
||||
|
||||
override func applyTheme() {
|
||||
super.applyTheme()
|
||||
imageView?.image = UIImage(named: String(format: "Search/Categories/%@", categoryName.replacingOccurrences(of: "category_", with: "")))
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,34 @@
|
|||
final class SearchHistoryCell: MWMTableViewCell {
|
||||
enum Content {
|
||||
case query(String)
|
||||
case clear
|
||||
}
|
||||
|
||||
static private let placeholderImage = UIImage.filled(with: .clear, size: CGSize(width: 28, height: 28))
|
||||
|
||||
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
|
||||
super.init(style: .default, reuseIdentifier: reuseIdentifier)
|
||||
setStyle(.defaultTableViewCell)
|
||||
}
|
||||
|
||||
@available(*, unavailable)
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
func configure(for content: Content) {
|
||||
switch content {
|
||||
case .query(let query):
|
||||
textLabel?.text = query
|
||||
textLabel?.setFontStyleAndApply(.regular17, color: .blackSecondary)
|
||||
imageView?.image = UIImage(resource: .icSearch)
|
||||
imageView?.setStyleAndApply(.black)
|
||||
isSeparatorHidden = false
|
||||
case .clear:
|
||||
textLabel?.text = L("clear_search")
|
||||
textLabel?.setFontStyleAndApply(.regular14, color: .linkBlue)
|
||||
imageView?.image = Self.placeholderImage
|
||||
isSeparatorHidden = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,131 @@
|
|||
protocol SearchHistoryViewControllerDelegate: SearchOnMapScrollViewDelegate {
|
||||
func searchHistoryViewController(_ viewController: SearchHistoryViewController,
|
||||
didSelect query: String)
|
||||
}
|
||||
|
||||
final class SearchHistoryViewController: MWMViewController {
|
||||
private weak var delegate: SearchHistoryViewControllerDelegate?
|
||||
private var lastQueries: [String] = []
|
||||
private let frameworkHelper: MWMSearchFrameworkHelper.Type
|
||||
private let emptyHistoryView = PlaceholderView(title: L("search_history_title"),
|
||||
subtitle: L("search_history_text"))
|
||||
|
||||
private let tableView = UITableView()
|
||||
|
||||
// MARK: - Init
|
||||
init(frameworkHelper: MWMSearchFrameworkHelper.Type, delegate: SearchHistoryViewControllerDelegate?) {
|
||||
self.delegate = delegate
|
||||
self.frameworkHelper = frameworkHelper
|
||||
super.init(nibName: nil, bundle: nil)
|
||||
}
|
||||
|
||||
@available(*, unavailable)
|
||||
required init?(coder aDecoder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
// MARK: - Lifecycle
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
setupTableView()
|
||||
setupNoResultsView()
|
||||
}
|
||||
|
||||
override func viewDidAppear(_ animated: Bool) {
|
||||
super.viewDidAppear(animated)
|
||||
reload()
|
||||
}
|
||||
|
||||
// MARK: - Private methods
|
||||
private func setupTableView() {
|
||||
tableView.setStyle(.background)
|
||||
tableView.register(cell: SearchHistoryCell.self)
|
||||
tableView.keyboardDismissMode = .onDrag
|
||||
tableView.delegate = self
|
||||
tableView.dataSource = self
|
||||
let footerHeight = (UIApplication.shared.connectedScenes.filter { $0.activationState == .foregroundActive }.first(where: { $0 is UIWindowScene }) as? UIWindowScene)?.keyWindow?.safeAreaInsets.bottom ?? 1
|
||||
tableView.tableFooterView = UIView(frame: CGRect(x: 0, y: 0, width: 400, height: footerHeight))
|
||||
|
||||
view.addSubview(tableView)
|
||||
tableView.translatesAutoresizingMaskIntoConstraints = false
|
||||
NSLayoutConstraint.activate([
|
||||
tableView.topAnchor.constraint(equalTo: view.topAnchor),
|
||||
tableView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
|
||||
tableView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
|
||||
tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor)
|
||||
])
|
||||
}
|
||||
|
||||
private func setupNoResultsView() {
|
||||
view.addSubview(emptyHistoryView)
|
||||
emptyHistoryView.translatesAutoresizingMaskIntoConstraints = false
|
||||
NSLayoutConstraint.activate([
|
||||
emptyHistoryView.topAnchor.constraint(equalTo: view.topAnchor),
|
||||
emptyHistoryView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
|
||||
emptyHistoryView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
|
||||
emptyHistoryView.bottomAnchor.constraint(equalTo: view.bottomAnchor)
|
||||
])
|
||||
}
|
||||
|
||||
private func showEmptyHistoryView(_ isVisible: Bool = true, animated: Bool = true) {
|
||||
UIView.transition(with: emptyHistoryView,
|
||||
duration: animated ? kDefaultAnimationDuration : 0,
|
||||
options: [.transitionCrossDissolve, .curveEaseInOut]) {
|
||||
self.emptyHistoryView.alpha = isVisible ? 1.0 : 0.0
|
||||
self.emptyHistoryView.isHidden = !isVisible
|
||||
}
|
||||
}
|
||||
|
||||
private func clearSearchHistory() {
|
||||
frameworkHelper.clearSearchHistory()
|
||||
reload()
|
||||
}
|
||||
|
||||
// MARK: - Public methods
|
||||
func reload() {
|
||||
guard isViewLoaded else { return }
|
||||
lastQueries = frameworkHelper.lastSearchQueries()
|
||||
showEmptyHistoryView(lastQueries.isEmpty ? true : false)
|
||||
tableView.reloadData()
|
||||
}
|
||||
}
|
||||
|
||||
extension SearchHistoryViewController: UITableViewDataSource {
|
||||
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
|
||||
return lastQueries.isEmpty ? 0 : lastQueries.count + 1
|
||||
}
|
||||
|
||||
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
|
||||
let cell = tableView.dequeueReusableCell(cell: SearchHistoryCell.self, indexPath: indexPath)
|
||||
if indexPath.row == lastQueries.count {
|
||||
cell.configure(for: .clear)
|
||||
} else {
|
||||
cell.configure(for: .query(lastQueries[indexPath.row]))
|
||||
}
|
||||
return cell
|
||||
}
|
||||
}
|
||||
|
||||
extension SearchHistoryViewController: UITableViewDelegate {
|
||||
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
|
||||
if indexPath.row == lastQueries.count {
|
||||
clearSearchHistory()
|
||||
} else {
|
||||
let query = lastQueries[indexPath.row]
|
||||
delegate?.searchHistoryViewController(self, didSelect: query)
|
||||
}
|
||||
tableView.deselectRow(at: indexPath, animated: true)
|
||||
}
|
||||
|
||||
func scrollViewDidScroll(_ scrollView: UIScrollView) {
|
||||
delegate?.scrollViewDidScroll(scrollView)
|
||||
}
|
||||
}
|
||||
|
||||
extension SearchHistoryViewController: ModallyPresentedViewController {
|
||||
func presentationFrameDidChange(_ frame: CGRect) {
|
||||
guard isViewLoaded else { return }
|
||||
tableView.contentInset.bottom = frame.origin.y
|
||||
emptyHistoryView.presentationFrameDidChange(frame)
|
||||
}
|
||||
}
|
||||
87
iphone/Maps/UI/Search/Tabs/SearchTabViewController.swift
Normal file
87
iphone/Maps/UI/Search/Tabs/SearchTabViewController.swift
Normal file
|
|
@ -0,0 +1,87 @@
|
|||
@objc(MWMSearchTabViewControllerDelegate)
|
||||
protocol SearchTabViewControllerDelegate: SearchOnMapScrollViewDelegate {
|
||||
func searchTabController(_ viewController: SearchTabViewController, didSearch: SearchQuery)
|
||||
}
|
||||
|
||||
@objc(MWMSearchTabViewController)
|
||||
final class SearchTabViewController: TabViewController {
|
||||
private enum SearchActiveTab: Int {
|
||||
case history = 0
|
||||
case categories
|
||||
}
|
||||
|
||||
private static let selectedIndexKey = "SearchTabViewController_selectedIndexKey"
|
||||
@objc weak var delegate: SearchTabViewControllerDelegate?
|
||||
|
||||
private var frameworkHelper = MWMSearchFrameworkHelper.self
|
||||
|
||||
private var activeTab: SearchActiveTab = SearchActiveTab.init(rawValue:
|
||||
UserDefaults.standard.integer(forKey: SearchTabViewController.selectedIndexKey)) ?? .categories {
|
||||
didSet {
|
||||
UserDefaults.standard.set(activeTab.rawValue, forKey: SearchTabViewController.selectedIndexKey)
|
||||
}
|
||||
}
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
|
||||
let history = SearchHistoryViewController(frameworkHelper: frameworkHelper,
|
||||
delegate: self)
|
||||
history.title = L("history")
|
||||
|
||||
let categories = SearchCategoriesViewController(frameworkHelper: frameworkHelper,
|
||||
delegate: self)
|
||||
categories.title = L("categories")
|
||||
viewControllers = [history, categories]
|
||||
|
||||
if frameworkHelper.isSearchHistoryEmpty() {
|
||||
tabView.selectedIndex = SearchActiveTab.categories.rawValue
|
||||
} else {
|
||||
tabView.selectedIndex = activeTab.rawValue
|
||||
}
|
||||
}
|
||||
|
||||
override func viewDidDisappear(_ animated: Bool) {
|
||||
super.viewDidDisappear(animated)
|
||||
activeTab = SearchActiveTab.init(rawValue: tabView.selectedIndex ?? 0) ?? .categories
|
||||
}
|
||||
|
||||
func reloadSearchHistory() {
|
||||
(viewControllers[SearchActiveTab.history.rawValue] as? SearchHistoryViewController)?.reload()
|
||||
}
|
||||
}
|
||||
|
||||
extension SearchTabViewController: ModallyPresentedViewController {
|
||||
func presentationFrameDidChange(_ frame: CGRect) {
|
||||
viewControllers.forEach { ($0 as? ModallyPresentedViewController)?.presentationFrameDidChange(frame) }
|
||||
}
|
||||
}
|
||||
|
||||
extension SearchTabViewController: SearchOnMapScrollViewDelegate {
|
||||
func scrollViewDidScroll(_ scrollView: UIScrollView) {
|
||||
delegate?.scrollViewDidScroll(scrollView)
|
||||
}
|
||||
|
||||
func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>) {
|
||||
delegate?.scrollViewWillEndDragging(scrollView, withVelocity: velocity, targetContentOffset: targetContentOffset)
|
||||
}
|
||||
}
|
||||
|
||||
extension SearchTabViewController: SearchCategoriesViewControllerDelegate {
|
||||
func categoriesViewController(_ viewController: SearchCategoriesViewController,
|
||||
didSelect category: String) {
|
||||
let preferredLang = AppInfo.shared().languageId
|
||||
let supportedBySearchLang = MWMSearchFrameworkHelper.isLanguageSupported(preferredLang) ? preferredLang : "en"
|
||||
let searchText = L(category, languageCode: supportedBySearchLang) + " "
|
||||
let query = SearchQuery(searchText, locale: supportedBySearchLang, source: .category)
|
||||
delegate?.searchTabController(self, didSearch: query)
|
||||
}
|
||||
}
|
||||
|
||||
extension SearchTabViewController: SearchHistoryViewControllerDelegate {
|
||||
func searchHistoryViewController(_ viewController: SearchHistoryViewController,
|
||||
didSelect query: String) {
|
||||
let query = SearchQuery(query.trimmingCharacters(in: .whitespacesAndNewlines) + " ", source: .history)
|
||||
delegate?.searchTabController(self, didSearch: query)
|
||||
}
|
||||
}
|
||||
84
iphone/Maps/UI/Search/ValueStepperView.swift
Normal file
84
iphone/Maps/UI/Search/ValueStepperView.swift
Normal file
|
|
@ -0,0 +1,84 @@
|
|||
final class ValueStepperView: UIControl {
|
||||
var minValue = 0 {
|
||||
didSet {
|
||||
guard minValue <= maxValue else { fatalError() }
|
||||
value = max(value, minValue)
|
||||
}
|
||||
}
|
||||
|
||||
var maxValue = 100 {
|
||||
didSet {
|
||||
guard maxValue >= minValue else { fatalError() }
|
||||
value = min(value, maxValue)
|
||||
}
|
||||
}
|
||||
|
||||
var value = 0 {
|
||||
didSet {
|
||||
guard value <= maxValue && value >= minValue else { fatalError() }
|
||||
minusButton.isEnabled = value > minValue
|
||||
plusButton.isEnabled = value < maxValue
|
||||
valueLabel.text = "\(value)"
|
||||
}
|
||||
}
|
||||
|
||||
let minusButton = MWMButton(type: .custom)
|
||||
let plusButton = MWMButton(type: .custom)
|
||||
let valueLabel = UILabel()
|
||||
|
||||
private var viewConstraints: [NSLayoutConstraint]!
|
||||
|
||||
override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
configure()
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
super.init(coder: coder)
|
||||
configure()
|
||||
}
|
||||
|
||||
private func configure() {
|
||||
addSubview(minusButton)
|
||||
addSubview(valueLabel)
|
||||
addSubview(plusButton)
|
||||
minusButton.translatesAutoresizingMaskIntoConstraints = false
|
||||
valueLabel.translatesAutoresizingMaskIntoConstraints = false
|
||||
plusButton.translatesAutoresizingMaskIntoConstraints = false
|
||||
|
||||
valueLabel.textAlignment = .center
|
||||
minusButton.isEnabled = false
|
||||
|
||||
minusButton.setImage(UIImage(named: "ic_booking_minus"), for: .normal)
|
||||
plusButton.setImage(UIImage(named: "ic_booking_plus"), for: .normal)
|
||||
valueLabel.text = "\(value)"
|
||||
|
||||
minusButton.addTarget(self, action: #selector(onMinus(_:)), for: .touchUpInside)
|
||||
plusButton.addTarget(self, action: #selector(onPlus(_:)), for: .touchUpInside)
|
||||
|
||||
NSLayoutConstraint.activate([
|
||||
minusButton.leadingAnchor.constraint(equalTo: leadingAnchor),
|
||||
valueLabel.leadingAnchor.constraint(equalTo: minusButton.trailingAnchor),
|
||||
plusButton.leadingAnchor.constraint(equalTo: valueLabel.trailingAnchor),
|
||||
plusButton.trailingAnchor.constraint(equalTo: trailingAnchor),
|
||||
valueLabel.topAnchor.constraint(equalTo: topAnchor),
|
||||
valueLabel.bottomAnchor.constraint(equalTo: bottomAnchor),
|
||||
minusButton.topAnchor.constraint(equalTo: topAnchor),
|
||||
minusButton.bottomAnchor.constraint(equalTo: bottomAnchor),
|
||||
plusButton.topAnchor.constraint(equalTo: topAnchor),
|
||||
plusButton.bottomAnchor.constraint(equalTo: bottomAnchor),
|
||||
minusButton.widthAnchor.constraint(equalTo: minusButton.heightAnchor, multiplier: 1),
|
||||
plusButton.widthAnchor.constraint(equalTo: plusButton.heightAnchor, multiplier: 1)
|
||||
])
|
||||
}
|
||||
|
||||
@objc func onMinus(_ sender: UIButton) {
|
||||
value -= 1
|
||||
sendActions(for: .valueChanged)
|
||||
}
|
||||
|
||||
@objc func onPlus(_ sender: UIButton) {
|
||||
value += 1
|
||||
sendActions(for: .valueChanged)
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue