[ios] replace the modally presented search vc presentation with child

Signed-off-by: Kiryl Kaveryn <kirylkaveryn@gmail.com>
This commit is contained in:
Kiryl Kaveryn 2025-03-08 17:11:32 +04:00
parent 1177842e0f
commit 1f21215391
Signed by: kirylkaveryn
SSH key fingerprint: SHA256:P3swI85Rc1ZoE8vyL28Oa6+FgRIaOwsjkKyqcVZ0NtY
15 changed files with 183 additions and 283 deletions

View file

@ -52,5 +52,6 @@
@property(nonatomic) MWMMyPositionMode currentPositionMode;
@property(strong, nonatomic) IBOutlet EAGLView * _Nonnull mapView;
@property(strong, nonatomic) IBOutlet UIView * _Nonnull controlsView;
@property(nonatomic) UIView * _Nonnull searchContainer;
@end

View file

@ -170,12 +170,18 @@ NSString *const kSettingsSegue = @"Map2Settings";
[self updatePlacePageContainerConstraints];
}
- (void)setupSearchContainer {
self.searchContainer = [[TouchTransparentView alloc] initWithFrame:self.view.bounds];
[self.view addSubview:self.searchContainer];
[self.view bringSubviewToFront:self.searchContainer];
}
- (void)updatePlacePageContainerConstraints {
const BOOL isLimitedWidth = IPAD || self.traitCollection.verticalSizeClass == UIUserInterfaceSizeClassCompact;
[self.placePageWidthConstraint setConstant:kPlacePageCompactWidth];
if (IPAD && self.searchViewContainer != nil) {
NSLayoutConstraint * leadingToSearchConstraint = [self.placePageContainer.leadingAnchor constraintEqualToAnchor:self.searchViewContainer.trailingAnchor constant:kPlacePageLeadingOffset];
if (IPAD && self.searchView != nil) {
NSLayoutConstraint * leadingToSearchConstraint = [self.placePageContainer.leadingAnchor constraintEqualToAnchor:self.searchView.trailingAnchor constant:kPlacePageLeadingOffset];
leadingToSearchConstraint.priority = UILayoutPriorityDefaultHigh;
leadingToSearchConstraint.active = isLimitedWidth;
}
@ -259,9 +265,6 @@ NSString *const kSettingsSegue = @"Map2Settings";
return;
}
if (self.searchManager.isSearching && type == df::TouchEvent::TOUCH_MOVE)
[self.searchManager setMapIsDragging];
NSArray *allTouches = [[event allTouches] allObjects];
if ([allTouches count] < 1)
return;
@ -273,6 +276,10 @@ NSString *const kSettingsSegue = @"Map2Settings";
UITouch *touch = [allTouches objectAtIndex:0];
CGPoint const pt = [touch locationInView:v];
// **Check if the tap is inside searchView**
if (self.searchManager.isSearching && type == df::TouchEvent::TOUCH_MOVE && !CGRectContainsPoint(self.searchView.frame, pt))
[self.searchManager setMapIsDragging];
e.SetTouchType(type);
df::Touch t0;
@ -372,6 +379,7 @@ NSString *const kSettingsSegue = @"Map2Settings";
- (void)viewDidLoad {
[super viewDidLoad];
[self setupPlacePageContainer];
[self setupSearchContainer];
if (@available(iOS 14.0, *))
[self setupTrackPadGestureRecognizers];
@ -726,11 +734,11 @@ NSString *const kSettingsSegue = @"Map2Settings";
- (SearchOnMapManager *)searchManager {
if (!_searchManager)
_searchManager = [[SearchOnMapManager alloc] initWithNavigationController:self.navigationController];
_searchManager = [[SearchOnMapManager alloc] init];
return _searchManager;
}
- (UIView * _Nullable)searchViewContainer {
- (UIView * _Nullable)searchView {
return self.searchManager.viewController.view;
}

View file

@ -1,5 +1,6 @@
enum SearchStyleSheet: String, CaseIterable {
case searchHeader
case searchCancelButton
case searchInstallButton = "SearchInstallButton"
case searchBanner = "SearchBanner"
case searchClosedBackground = "SearchClosedBackground"
@ -103,6 +104,13 @@ extension SearchStyleSheet: IStyleSheet {
return .addFrom(GlobalStyleSheet.tableCell) { s in
s.backgroundColor = colors.transparentGreen
}
case .searchCancelButton:
return .add { s in
s.fontColor = colors.whitePrimaryText
s.fontColorHighlighted = colors.whitePrimaryTextHighlighted
s.font = fonts.regular17
s.backgroundColor = .clear
}
}
}
}

View file

@ -495,16 +495,11 @@
ED70D55C2D5396F300738C1E /* SearchResult.mm in Sources */ = {isa = PBXBuildFile; fileRef = ED70D55A2D5396F300738C1E /* SearchResult.mm */; };
ED70D5892D539A2500738C1E /* SearchOnMapViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = ED70D5872D539A2500738C1E /* SearchOnMapViewController.swift */; };
ED70D58A2D539A2500738C1E /* SearchOnMapModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = ED70D5852D539A2500738C1E /* SearchOnMapModels.swift */; };
ED70D58B2D539A2500738C1E /* SearchOnMapModalTransitionManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = ED70D57E2D539A2500738C1E /* SearchOnMapModalTransitionManager.swift */; };
ED70D58C2D539A2500738C1E /* SearchOnMapPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = ED70D5862D539A2500738C1E /* SearchOnMapPresenter.swift */; };
ED70D58D2D539A2500738C1E /* ModalScreenPresentationStep.swift in Sources */ = {isa = PBXBuildFile; fileRef = ED70D57C2D539A2500738C1E /* ModalScreenPresentationStep.swift */; };
ED70D58E2D539A2500738C1E /* SideMenuPresentationAnimator.swift in Sources */ = {isa = PBXBuildFile; fileRef = ED70D5802D539A2500738C1E /* SideMenuPresentationAnimator.swift */; };
ED70D58F2D539A2500738C1E /* SearchOnMapInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = ED70D5832D539A2500738C1E /* SearchOnMapInteractor.swift */; };
ED70D5902D539A2500738C1E /* SearchOnMapModalPresentationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = ED70D57D2D539A2500738C1E /* SearchOnMapModalPresentationController.swift */; };
ED70D5912D539A2500738C1E /* MapPassthroughView.swift in Sources */ = {isa = PBXBuildFile; fileRef = ED70D57B2D539A2500738C1E /* MapPassthroughView.swift */; };
ED70D5922D539A2500738C1E /* PlaceholderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = ED70D5822D539A2500738C1E /* PlaceholderView.swift */; };
ED70D5932D539A2500738C1E /* SearchOnMapManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = ED70D5842D539A2500738C1E /* SearchOnMapManager.swift */; };
ED70D5942D539A2500738C1E /* SideMenuDismissalAnimator.swift in Sources */ = {isa = PBXBuildFile; fileRef = ED70D57F2D539A2500738C1E /* SideMenuDismissalAnimator.swift */; };
ED77556E2C2C490B0051E656 /* UIAlertController+openInAppActionSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = ED77556D2C2C490B0051E656 /* UIAlertController+openInAppActionSheet.swift */; };
ED79A5AB2BD7AA9C00952D1F /* LoadingOverlayViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = ED79A5AA2BD7AA9C00952D1F /* LoadingOverlayViewController.swift */; };
ED79A5AD2BD7BA0F00952D1F /* UIApplication+LoadingOverlay.swift in Sources */ = {isa = PBXBuildFile; fileRef = ED79A5AC2BD7BA0F00952D1F /* UIApplication+LoadingOverlay.swift */; };
@ -526,6 +521,7 @@
ED9857082C4ED02D00694F6C /* MailComposer.swift in Sources */ = {isa = PBXBuildFile; fileRef = ED9857072C4ED02D00694F6C /* MailComposer.swift */; };
ED9966802B94FBC20083CE55 /* ColorPicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = ED99667D2B94FBC20083CE55 /* ColorPicker.swift */; };
EDA1EAA42CC7ECAD00DBDCAA /* ElevationProfileFormatter.swift in Sources */ = {isa = PBXBuildFile; fileRef = EDA1EAA32CC7ECAD00DBDCAA /* ElevationProfileFormatter.swift */; };
EDB71D5C2D82B4F8004A6A7F /* SearchOnMapPresentationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = EDB71D5B2D82B4F8004A6A7F /* SearchOnMapPresentationController.swift */; };
EDBD68072B625724005DD151 /* LocationServicesDisabledAlert.xib in Resources */ = {isa = PBXBuildFile; fileRef = EDBD68062B625724005DD151 /* LocationServicesDisabledAlert.xib */; };
EDBD680B2B62572E005DD151 /* LocationServicesDisabledAlert.swift in Sources */ = {isa = PBXBuildFile; fileRef = EDBD680A2B62572E005DD151 /* LocationServicesDisabledAlert.swift */; };
EDC3573B2B7B5029001AE9CA /* CALayer+SetCorner.swift in Sources */ = {isa = PBXBuildFile; fileRef = EDC3573A2B7B5029001AE9CA /* CALayer+SetCorner.swift */; };
@ -1472,12 +1468,7 @@
ED70D5592D5396F300738C1E /* SearchResult.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = SearchResult.h; sourceTree = "<group>"; };
ED70D55A2D5396F300738C1E /* SearchResult.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = SearchResult.mm; sourceTree = "<group>"; };
ED70D55B2D5396F300738C1E /* SearchResult+Core.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "SearchResult+Core.h"; sourceTree = "<group>"; };
ED70D57B2D539A2500738C1E /* MapPassthroughView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MapPassthroughView.swift; sourceTree = "<group>"; };
ED70D57C2D539A2500738C1E /* ModalScreenPresentationStep.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ModalScreenPresentationStep.swift; sourceTree = "<group>"; };
ED70D57D2D539A2500738C1E /* SearchOnMapModalPresentationController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchOnMapModalPresentationController.swift; sourceTree = "<group>"; };
ED70D57E2D539A2500738C1E /* SearchOnMapModalTransitionManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchOnMapModalTransitionManager.swift; sourceTree = "<group>"; };
ED70D57F2D539A2500738C1E /* SideMenuDismissalAnimator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SideMenuDismissalAnimator.swift; sourceTree = "<group>"; };
ED70D5802D539A2500738C1E /* SideMenuPresentationAnimator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SideMenuPresentationAnimator.swift; sourceTree = "<group>"; };
ED70D5822D539A2500738C1E /* PlaceholderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlaceholderView.swift; sourceTree = "<group>"; };
ED70D5832D539A2500738C1E /* SearchOnMapInteractor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchOnMapInteractor.swift; sourceTree = "<group>"; };
ED70D5842D539A2500738C1E /* SearchOnMapManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchOnMapManager.swift; sourceTree = "<group>"; };
@ -1548,6 +1539,7 @@
ED9857072C4ED02D00694F6C /* MailComposer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MailComposer.swift; sourceTree = "<group>"; };
ED99667D2B94FBC20083CE55 /* ColorPicker.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ColorPicker.swift; sourceTree = "<group>"; };
EDA1EAA32CC7ECAD00DBDCAA /* ElevationProfileFormatter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ElevationProfileFormatter.swift; sourceTree = "<group>"; };
EDB71D5B2D82B4F8004A6A7F /* SearchOnMapPresentationController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchOnMapPresentationController.swift; sourceTree = "<group>"; };
EDBD68062B625724005DD151 /* LocationServicesDisabledAlert.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = LocationServicesDisabledAlert.xib; sourceTree = "<group>"; };
EDBD680A2B62572E005DD151 /* LocationServicesDisabledAlert.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationServicesDisabledAlert.swift; sourceTree = "<group>"; };
EDC3573A2B7B5029001AE9CA /* CALayer+SetCorner.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CALayer+SetCorner.swift"; sourceTree = "<group>"; };
@ -3291,12 +3283,8 @@
ED70D5812D539A2500738C1E /* Presentation */ = {
isa = PBXGroup;
children = (
ED70D57B2D539A2500738C1E /* MapPassthroughView.swift */,
ED70D57C2D539A2500738C1E /* ModalScreenPresentationStep.swift */,
ED70D57D2D539A2500738C1E /* SearchOnMapModalPresentationController.swift */,
ED70D57E2D539A2500738C1E /* SearchOnMapModalTransitionManager.swift */,
ED70D57F2D539A2500738C1E /* SideMenuDismissalAnimator.swift */,
ED70D5802D539A2500738C1E /* SideMenuPresentationAnimator.swift */,
EDB71D5B2D82B4F8004A6A7F /* SearchOnMapPresentationController.swift */,
);
path = Presentation;
sourceTree = "<group>";
@ -4493,6 +4481,7 @@
99012853244732DB00C72B10 /* BottomTabBarInteractor.swift in Sources */,
6741A9A31BF340DE002C974C /* main.mm in Sources */,
34D3B04F1E38A20C004100F9 /* Bundle+Init.swift in Sources */,
EDB71D5C2D82B4F8004A6A7F /* SearchOnMapPresentationController.swift in Sources */,
34AB666E1FC5AA330078E451 /* TransportTransitStepsCollectionView.swift in Sources */,
993DF11E23F6BDB100AC231A /* UITextViewRenderer.swift in Sources */,
F6E2FF5A1E097BA00083EBEC /* MWMNightModeController.m in Sources */,
@ -4817,16 +4806,11 @@
3404755C1E081A4600C92850 /* MWMLocationManager.mm in Sources */,
ED70D5892D539A2500738C1E /* SearchOnMapViewController.swift in Sources */,
ED70D58A2D539A2500738C1E /* SearchOnMapModels.swift in Sources */,
ED70D58B2D539A2500738C1E /* SearchOnMapModalTransitionManager.swift in Sources */,
ED70D58C2D539A2500738C1E /* SearchOnMapPresenter.swift in Sources */,
ED70D58D2D539A2500738C1E /* ModalScreenPresentationStep.swift in Sources */,
ED70D58E2D539A2500738C1E /* SideMenuPresentationAnimator.swift in Sources */,
ED70D58F2D539A2500738C1E /* SearchOnMapInteractor.swift in Sources */,
ED70D5902D539A2500738C1E /* SearchOnMapModalPresentationController.swift in Sources */,
ED70D5912D539A2500738C1E /* MapPassthroughView.swift in Sources */,
ED70D5922D539A2500738C1E /* PlaceholderView.swift in Sources */,
ED70D5932D539A2500738C1E /* SearchOnMapManager.swift in Sources */,
ED70D5942D539A2500738C1E /* SideMenuDismissalAnimator.swift in Sources */,
3454D7BC1E07F045004AF2AD /* CLLocation+Mercator.mm in Sources */,
47E3C7272111E5A8008B3B27 /* AlertPresentationController.swift in Sources */,
CDCA27812243F59800167D87 /* CarPlayRouter.swift in Sources */,

View file

@ -12,8 +12,7 @@ final class SearchOnMapTests: XCTestCase {
override func setUp() {
super.setUp()
searchManager = SearchManagerMock.self
presenter = SearchOnMapPresenter(transitionManager: SearchOnMapModalTransitionManager(),
isRouting: false,
presenter = SearchOnMapPresenter(isRouting: false,
didChangeState: { [weak self] in self?.currentState = $0 })
interactor = SearchOnMapInteractor(presenter: presenter, searchManager: searchManager)
view = SearchOnMapViewMock()
@ -217,11 +216,14 @@ final class SearchOnMapTests: XCTestCase {
// MARK: - Mocks
private class SearchOnMapViewMock: SearchOnMapView {
var viewModel: SearchOnMap.ViewModel = .initial
var scrollViewDelegate: (any SearchOnMapScrollViewDelegate)?
func render(_ viewModel: SearchOnMap.ViewModel) {
self.viewModel = viewModel
}
func close() {
}
}
private class SearchManagerMock: SearchManager {

View file

@ -1,30 +0,0 @@
/// A transparent view that allows touch events to pass through to the MapViewController's view.
///
/// This view is used to enable interaction with the underlying map while still maintaining a
/// transparent overlay. It does not block touch events but forwards them to the specified `passingView`.
final class MapPassthroughView: UIView {
private weak var passingView: UIView?
init(passingView: UIView) {
self.passingView = passingView
super.init(frame: passingView.bounds)
self.autoresizingMask = [.flexibleWidth, .flexibleHeight]
self.alpha = 0
}
@available(*, unavailable)
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
guard let passingView else { return nil }
let pointInPassthroughView = passingView.convert(point, from: self)
super.hitTest(point, with: event)
if passingView.bounds.contains(pointInPassthroughView) {
return MapViewController.shared()?.view.hitTest(point, with: event)
}
return nil
}
}

View file

@ -48,10 +48,11 @@ extension ModalScreenPresentationStep {
.compact
}
func frame(for viewController: UIViewController, in containerView: UIView) -> CGRect {
func frame() -> CGRect {
let isIPad = UIDevice.current.userInterfaceIdiom == .pad
let containerSize = containerView.bounds.size
let safeAreaInsets = containerView.safeAreaInsets
let containerSize = UIScreen.main.bounds.size
let safeAreaInsets = UIApplication.shared.keyWindow?.safeAreaInsets ?? .zero
let traitCollection = UIScreen.main.traitCollection
var frame = CGRect(origin: .zero, size: containerSize)
if isIPad {
@ -65,7 +66,7 @@ extension ModalScreenPresentationStep {
return frame
}
let isPortraitOrientation = viewController.traitCollection.verticalSizeClass == .regular
let isPortraitOrientation = traitCollection.verticalSizeClass == .regular
if isPortraitOrientation {
switch self {
case .fullScreen:

View file

@ -1,21 +0,0 @@
@objc
final class SearchOnMapModalTransitionManager: NSObject, UIViewControllerTransitioningDelegate {
weak var presentationController: SearchOnMapModalPresentationView?
func animationController(forPresented presented: UIViewController, presenting: UIViewController, source: UIViewController) -> (any UIViewControllerAnimatedTransitioning)? {
isIPad ? SideMenuPresentationAnimator() : nil
}
func animationController(forDismissed dismissed: UIViewController) -> (any UIViewControllerAnimatedTransitioning)? {
isIPad ? SideMenuDismissalAnimator() : nil
}
func presentationController(forPresented presented: UIViewController,
presenting: UIViewController?,
source: UIViewController) -> UIPresentationController? {
let presentationController = SearchOnMapModalPresentationController(presentedViewController: presented, presenting: presenting)
self.presentationController = presentationController
return presentationController
}
}

View file

@ -2,14 +2,10 @@ protocol ModallyPresentedViewController {
func translationYDidUpdate(_ translationY: CGFloat)
}
protocol SearchOnMapModalPresentationView: AnyObject {
func setPresentationStep(_ step: ModalScreenPresentationStep)
func close()
}
final class SearchOnMapModalPresentationController: UIPresentationController {
final class SearchOnMapPresentationController: NSObject {
private enum StepChangeAnimation {
case none
case slide
case slideAndBounce
}
@ -26,83 +22,84 @@ final class SearchOnMapModalPresentationController: UIPresentationController {
static let panGestureThreshold: CGFloat = 5
}
private var initialTranslationY: CGFloat = 0
private weak var interactor: SearchOnMapInteractor? {
(presentedViewController as? SearchOnMapViewController)?.interactor
}
// TODO: replace with set of steps passed from the outside
private var initialTranslationY: CGFloat = .zero
private weak var interactor: SearchOnMapInteractor? { presentedViewController?.interactor }
// TODO: (KK) replace with set of steps passed from the outside
private var presentationStep: ModalScreenPresentationStep = .fullScreen
private var internalScrollViewContentOffset: CGFloat = 0
private var internalScrollViewContentOffset: CGFloat = .zero
private var maxAvailableFrameOfPresentedView: CGRect = .zero
// MARK: - Init
override init(presentedViewController: UIViewController, presenting presentingViewController: UIViewController?) {
super.init(presentedViewController: presentedViewController, presenting: presentingViewController)
private weak var presentedViewController: SearchOnMapViewController?
private weak var parentViewController: UIViewController?
private weak var containerView: UIView?
init(parentViewController: UIViewController,
containerView: UIView) {
self.parentViewController = parentViewController
self.containerView = containerView
}
func setViewController(_ viewController: SearchOnMapViewController) {
self.presentedViewController = viewController
guard let containerView, let parentViewController else { return }
containerView.addSubview(viewController.view)
parentViewController.addChild(viewController)
viewController.view.frame = frameOfPresentedViewInContainerView
viewController.didMove(toParent: parentViewController)
iPhoneSpecific {
let panGestureRecognizer = UIPanGestureRecognizer(target: self, action: #selector(handlePan(_:)))
panGestureRecognizer.delegate = self
presentedViewController.view.addGestureRecognizer(panGestureRecognizer)
if let presentedViewController = presentedViewController as? SearchOnMapView {
presentedViewController.scrollViewDelegate = self
}
viewController.view.addGestureRecognizer(panGestureRecognizer)
viewController.scrollViewDelegate = self
}
animateTo(.hidden, animation: .none)
}
func show() {
interactor?.handle(.openSearch)
}
func close() {
guard let presentedViewController else { return }
presentedViewController.willMove(toParent: nil)
animateTo(.hidden) {
presentedViewController.view.removeFromSuperview()
presentedViewController.removeFromParent()
}
}
// MARK: - Lifecycle
override func containerViewWillLayoutSubviews() {
super.containerViewWillLayoutSubviews()
presentedView?.frame = frameOfPresentedViewInContainerView
}
override func presentationTransitionWillBegin() {
guard let containerView else { return }
containerView.backgroundColor = .clear
let passThroughView = MapPassthroughView(passingView: containerView)
containerView.addSubview(passThroughView)
}
override func presentationTransitionDidEnd(_ completed: Bool) {
translationYDidUpdate(presentedView?.frame.origin.y ?? 0)
}
override func dismissalTransitionDidEnd(_ completed: Bool) {
super.dismissalTransitionDidEnd(completed)
if completed {
presentedView?.removeFromSuperview()
}
}
override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
super.traitCollectionDidChange(previousTraitCollection)
updateMaxAvailableFrameOfPresentedView()
func setPresentationStep(_ step: ModalScreenPresentationStep) {
guard presentationStep != step else { return }
animateTo(step)
}
// MARK: - Layout
override var frameOfPresentedViewInContainerView: CGRect {
guard let containerView else { return .zero }
let frame = presentationStep.frame(for: presentedViewController, in: containerView)
func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
presentedViewController?.view.frame = frameOfPresentedViewInContainerView
presentedViewController?.view.layoutIfNeeded()
}
private var frameOfPresentedViewInContainerView: CGRect {
updateMaxAvailableFrameOfPresentedView()
let frame = presentationStep.frame()
return frame
}
private func updateMaxAvailableFrameOfPresentedView() {
guard let containerView else { return }
maxAvailableFrameOfPresentedView = ModalScreenPresentationStep.fullScreen.frame(for: presentedViewController, in: containerView)
maxAvailableFrameOfPresentedView = ModalScreenPresentationStep.fullScreen.frame()
}
private func updateSideButtonsAvailableArea(_ newY: CGFloat) {
iPhoneSpecific {
guard presentedViewController.traitCollection.verticalSizeClass != .compact else { return }
var sideButtonsAvailableArea = MWMSideButtons.getAvailableArea()
sideButtonsAvailableArea.size.height = newY - sideButtonsAvailableArea.origin.y
MWMSideButtons.updateAvailableArea(sideButtonsAvailableArea)
}
guard presentedViewController?.traitCollection.verticalSizeClass != .compact else { return }
var sideButtonsAvailableArea = MWMSideButtons.getAvailableArea()
sideButtonsAvailableArea.size.height = newY - sideButtonsAvailableArea.origin.y
MWMSideButtons.updateAvailableArea(sideButtonsAvailableArea)
}
// MARK: - Pan gesture handling
@objc private func handlePan(_ gesture: UIPanGestureRecognizer) {
guard let presentedView, maxAvailableFrameOfPresentedView != .zero else { return }
guard let presentedViewController, let presentedView = presentedViewController.view else { return }
interactor?.handle(.didStartDraggingSearch)
let translation = gesture.translation(in: presentedView)
@ -114,7 +111,6 @@ final class SearchOnMapModalPresentationController: UIPresentationController {
case .changed:
let newY = max(max(initialTranslationY + translation.y, 0), maxAvailableFrameOfPresentedView.origin.y)
presentedView.frame.origin.y = newY
updateSideButtonsAvailableArea(newY)
translationYDidUpdate(newY)
case .ended:
let nextStep: ModalScreenPresentationStep
@ -142,15 +138,19 @@ final class SearchOnMapModalPresentationController: UIPresentationController {
}
}
private func animateTo(_ presentationStep: ModalScreenPresentationStep, animation: StepChangeAnimation = .slide) {
guard let presentedView, let containerView else { return }
private func animateTo(_ presentationStep: ModalScreenPresentationStep, animation: StepChangeAnimation = .slide, completion: (() -> Void)? = nil) {
guard let presentedViewController, let presentedView = presentedViewController.view else { return }
self.presentationStep = presentationStep
interactor?.handle(.didUpdatePresentationStep(presentationStep))
let updatedFrame = presentationStep.frame(for: presentedViewController, in: containerView)
let updatedFrame = presentationStep.frame()
let targetYTranslation = updatedFrame.origin.y
switch animation {
case .none:
presentedView.frame = updatedFrame
translationYDidUpdate(targetYTranslation)
completion?()
case .slide:
UIView.animate(withDuration: Constants.animationDuration,
delay: 0,
@ -158,8 +158,9 @@ final class SearchOnMapModalPresentationController: UIPresentationController {
animations: { [weak self] in
presentedView.frame = updatedFrame
self?.translationYDidUpdate(targetYTranslation)
self?.updateSideButtonsAvailableArea(targetYTranslation)
})
}) { _ in
completion?()
}
case .slideAndBounce:
UIView.animate(withDuration: Constants.animationDuration,
delay: 0,
@ -169,37 +170,25 @@ final class SearchOnMapModalPresentationController: UIPresentationController {
animations: { [weak self] in
presentedView.frame = updatedFrame
self?.translationYDidUpdate(targetYTranslation)
self?.updateSideButtonsAvailableArea(targetYTranslation)
})
}) { _ in
completion?()
}
}
}
}
// MARK: - SearchOnMapModalPresentationView
extension SearchOnMapModalPresentationController: SearchOnMapModalPresentationView {
func setPresentationStep(_ step: ModalScreenPresentationStep) {
guard presentationStep != step else { return }
animateTo(step)
}
func close() {
guard let containerView else { return }
updateSideButtonsAvailableArea(containerView.frame.height)
presentedViewController.dismiss(animated: true)
}
}
// MARK: - ModallyPresentedViewController
extension SearchOnMapModalPresentationController: ModallyPresentedViewController {
extension SearchOnMapPresentationController: ModallyPresentedViewController {
func translationYDidUpdate(_ translationY: CGFloat) {
iPhoneSpecific {
(presentedViewController as? SearchOnMapViewController)?.translationYDidUpdate(translationY)
presentedViewController?.translationYDidUpdate(translationY)
updateSideButtonsAvailableArea(translationY)
}
}
}
// MARK: - UIGestureRecognizerDelegate
extension SearchOnMapModalPresentationController: UIGestureRecognizerDelegate {
extension SearchOnMapPresentationController: UIGestureRecognizerDelegate {
func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
true
}
@ -211,9 +200,9 @@ extension SearchOnMapModalPresentationController: UIGestureRecognizerDelegate {
}
// MARK: - SearchOnMapScrollViewDelegate
extension SearchOnMapModalPresentationController: SearchOnMapScrollViewDelegate {
extension SearchOnMapPresentationController: SearchOnMapScrollViewDelegate {
func scrollViewDidScroll(_ scrollView: UIScrollView) {
guard let presentedView else { return }
guard let presentedViewController, let presentedView = presentedViewController.view else { return }
let hasReachedTheTop = Int(presentedView.frame.origin.y) > Int(maxAvailableFrameOfPresentedView.origin.y)
let hasZeroContentOffset = internalScrollViewContentOffset == 0
if hasReachedTheTop && hasZeroContentOffset {

View file

@ -1,22 +0,0 @@
final class SideMenuDismissalAnimator: NSObject, UIViewControllerAnimatedTransitioning {
func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
return kDefaultAnimationDuration / 2
}
func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
guard let fromVC = transitionContext.viewController(forKey: .from) else { return }
let initialFrame = transitionContext.initialFrame(for: fromVC)
let targetFrame = initialFrame.offsetBy(dx: -initialFrame.width, dy: 0)
UIView.animate(withDuration: transitionDuration(using: transitionContext),
delay: .zero,
options: .curveEaseIn,
animations: {
fromVC.view.frame = targetFrame
},
completion: {
fromVC.view.removeFromSuperview()
transitionContext.completeTransition($0)
})
}
}

View file

@ -1,25 +0,0 @@
final class SideMenuPresentationAnimator: NSObject, UIViewControllerAnimatedTransitioning {
func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
return kDefaultAnimationDuration / 2
}
func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
guard let toVC = transitionContext.viewController(forKey: .to) else { return }
let containerView = transitionContext.containerView
let finalFrame = transitionContext.finalFrame(for: toVC)
let originFrame = finalFrame.offsetBy(dx: -finalFrame.width, dy: 0)
containerView.addSubview(toVC.view)
toVC.view.frame = originFrame
toVC.view.autoresizingMask = [.flexibleWidth, .flexibleHeight]
UIView.animate(withDuration: transitionDuration(using: transitionContext),
delay: .zero,
options: .curveEaseOut,
animations: {
toVC.view.frame = finalFrame
},
completion: {
transitionContext.completeTransition($0)
})
}
}

View file

@ -19,6 +19,7 @@ final class SearchOnMapHeaderView: UIView {
private let grabberView = UIView()
private let searchBar = UISearchBar()
private let cancelButton = UIButton()
private let cancelContainer = UIView()
override init(frame: CGRect) {
super.init(frame: frame)
@ -59,8 +60,8 @@ final class SearchOnMapHeaderView: UIView {
}
private func setupCancelButton() {
cancelButton.tintColor = .whitePrimaryText()
cancelButton.setStyle(.clearBackground)
cancelContainer.setStyle(.primaryBackground)
cancelButton.setStyle(.searchCancelButton)
cancelButton.setTitle(L("cancel"), for: .normal)
cancelButton.addTarget(self, action: #selector(cancelButtonTapped), for: .touchUpInside)
}
@ -68,10 +69,12 @@ final class SearchOnMapHeaderView: UIView {
private func layoutView() {
addSubview(grabberView)
addSubview(searchBar)
addSubview(cancelButton)
addSubview(cancelContainer)
cancelContainer.addSubview(cancelButton)
grabberView.translatesAutoresizingMaskIntoConstraints = false
searchBar.translatesAutoresizingMaskIntoConstraints = false
cancelContainer.translatesAutoresizingMaskIntoConstraints = false
cancelButton.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
@ -82,10 +85,16 @@ final class SearchOnMapHeaderView: UIView {
searchBar.topAnchor.constraint(equalTo: grabberView.bottomAnchor),
searchBar.leadingAnchor.constraint(equalTo: leadingAnchor),
searchBar.trailingAnchor.constraint(equalTo: cancelButton.leadingAnchor, constant: -Constants.cancelButtonInsets.left),
searchBar.trailingAnchor.constraint(equalTo: cancelContainer.leadingAnchor),
cancelButton.centerYAnchor.constraint(equalTo: searchBar.centerYAnchor),
cancelButton.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -Constants.cancelButtonInsets.right),
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),
bottomAnchor.constraint(equalTo: searchBar.bottomAnchor)
])

View file

@ -19,16 +19,14 @@ protocol SearchOnMapManagerObserver: AnyObject {
@objcMembers
final class SearchOnMapManager: NSObject {
private let navigationController: UINavigationController
private weak var interactor: SearchOnMapInteractor?
private var interactor: SearchOnMapInteractor? { viewController?.interactor }
private let observers = ListenerContainer<SearchOnMapManagerObserver>()
// MARK: - Public properties
weak var viewController: UIViewController?
weak var viewController: SearchOnMapViewController?
var isSearching: Bool { viewController != nil }
init(navigationController: UINavigationController = MapViewController.shared()!.navigationController!) {
self.navigationController = navigationController
override init() {
super.init()
}
// MARK: - Public methods
@ -38,10 +36,9 @@ final class SearchOnMapManager: NSObject {
return
}
FrameworkHelper.deactivateMapSelection()
let viewController = buildViewController(isRouting: isRouting)
let viewController = SearchOnMapViewControllerBuilder.build(isRouting: isRouting,
didChangeState: notifyObservers)
self.viewController = viewController
self.interactor = viewController.interactor
navigationController.present(viewController, animated: true)
}
func hide() {
@ -77,20 +74,23 @@ final class SearchOnMapManager: NSObject {
observers.removeListener(observer)
}
// MARK: - Private methods
private func buildViewController(isRouting: Bool) -> SearchOnMapViewController {
let transitioningManager = SearchOnMapModalTransitionManager()
let presenter = SearchOnMapPresenter(transitionManager: transitioningManager,
isRouting: isRouting,
didChangeState: { [weak self] state in
guard let self else { return }
self.observers.forEach { observer in observer.searchManager(didChangeState: state) }
})
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 mapViewController = MapViewController.shared()!
let presentationController = SearchOnMapPresentationController(parentViewController: mapViewController,
containerView: mapViewController.searchContainer)
let viewController = SearchOnMapViewController(presentationController: presentationController)
let presenter = SearchOnMapPresenter(isRouting: isRouting,
didChangeState: didChangeState)
let interactor = SearchOnMapInteractor(presenter: presenter)
let viewController = SearchOnMapViewController(interactor: interactor)
presenter.view = viewController
viewController.modalPresentationStyle = .custom
viewController.transitioningDelegate = transitioningManager
viewController.interactor = interactor
presentationController.show()
return viewController
}
}

View file

@ -3,7 +3,6 @@ final class SearchOnMapPresenter {
typealias ViewModel = SearchOnMap.ViewModel
weak var view: SearchOnMapView?
weak var presentationView: SearchOnMapModalPresentationView? { transitionManager.presentationController }
private var searchState: SearchOnMapState = .searching {
didSet {
@ -12,13 +11,11 @@ final class SearchOnMapPresenter {
}
}
private let transitionManager: SearchOnMapModalTransitionManager
private var viewModel: ViewModel = .initial
private var isRouting: Bool
private var didChangeState: ((SearchOnMapState) -> Void)?
init(transitionManager: SearchOnMapModalTransitionManager, isRouting: Bool, didChangeState: ((SearchOnMapState) -> Void)?) {
self.transitionManager = transitionManager
init(isRouting: Bool, didChangeState: ((SearchOnMapState) -> Void)?) {
self.isRouting = isRouting
self.didChangeState = didChangeState
didChangeState?(searchState)
@ -28,8 +25,8 @@ final class SearchOnMapPresenter {
guard response != .none else { return }
if response == .close {
view?.close()
searchState = .closed
presentationView?.close()
return
}
@ -43,7 +40,6 @@ final class SearchOnMapPresenter {
viewModel = newViewModel
view?.render(newViewModel)
searchState = newViewModel.presentationStep.searchState
presentationView?.setPresentationStep(newViewModel.presentationStep)
}
}
@ -97,6 +93,9 @@ final class SearchOnMapPresenter {
viewModel.isTyping = false
viewModel.presentationStep = .compact
case .updatePresentationStep(let step):
if step == .hidden {
viewModel.isTyping = false
}
viewModel.presentationStep = step
case .close, .none:
break

View file

@ -2,6 +2,7 @@ protocol SearchOnMapView: AnyObject {
var scrollViewDelegate: SearchOnMapScrollViewDelegate? { get set }
func render(_ viewModel: SearchOnMap.ViewModel)
func close()
}
@objc
@ -15,16 +16,13 @@ final class SearchOnMapViewController: UIViewController {
typealias SearchText = SearchOnMap.SearchText
fileprivate enum Constants {
static let categoriesHeight: CGFloat = 100
static let filtersHeight: CGFloat = 50
static let keyboardAnimationDuration: CGFloat = 0.3
static let cancelButtonInsets: UIEdgeInsets = UIEdgeInsets(top: 0, left: 6, bottom: 0, right: 8)
static let estimatedRowHeight: CGFloat = 80
}
let interactor: SearchOnMapInteractor
var interactor: SearchOnMapInteractor?
var modalPresentationController: SearchOnMapPresentationController?
weak var scrollViewDelegate: SearchOnMapScrollViewDelegate?
private var searchResults = SearchOnMap.SearchResults([])
// MARK: - UI Elements
@ -32,21 +30,16 @@ final class SearchOnMapViewController: UIViewController {
private let containerView = UIView()
private let resultsTableView = UITableView()
private let historyAndCategoryTabViewController = SearchTabViewController()
// TODO: implement filters
private let filtersCollectionView: UICollectionView = {
let layout = UICollectionViewFlowLayout()
layout.scrollDirection = .horizontal
return UICollectionView(frame: .zero, collectionViewLayout: layout)
}()
private var searchingActivityView = PlaceholderView(hasActivityIndicator: true)
private var containerModalYTranslation: CGFloat = 0
private var searchNoResultsView = PlaceholderView(title: L("search_not_found"),
subtitle: L("search_not_found_query"))
// MARK: - Init
init(interactor: SearchOnMapInteractor) {
self.interactor = interactor
init(presentationController: SearchOnMapPresentationController?) {
self.modalPresentationController = presentationController
super.init(nibName: nil, bundle: nil)
modalPresentationController?.setViewController(self)
}
@available(*, unavailable)
@ -54,16 +47,12 @@ final class SearchOnMapViewController: UIViewController {
fatalError("init(coder:) has not been implemented")
}
deinit {
NotificationCenter.default.removeObserver(self)
}
// MARK: - Lifecycle
override func viewDidLoad() {
super.viewDidLoad()
setupViews()
layoutViews()
interactor.handle(.openSearch)
modalPresentationController?.show()
}
override func viewWillDisappear(_ animated: Bool) {
@ -71,6 +60,11 @@ final class SearchOnMapViewController: UIViewController {
headerView.setIsSearching(false)
}
override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
super.traitCollectionDidChange(previousTraitCollection)
modalPresentationController?.traitCollectionDidChange(traitCollection)
}
// MARK: - Private methods
private func setupViews() {
view.setStyle(.clearBackground)
@ -80,7 +74,6 @@ final class SearchOnMapViewController: UIViewController {
setupResultsTableView()
setupHistoryAndCategoryTabView()
setupResultsTableView()
setupFiltersCollectionView()
}
private func setupTapGestureRecognizer() {
@ -112,12 +105,6 @@ final class SearchOnMapViewController: UIViewController {
historyAndCategoryTabViewController.delegate = self
}
// TODO: (KK) Implement filters collection viewe
private func setupFiltersCollectionView() {
filtersCollectionView.register(UICollectionViewCell.self, forCellWithReuseIdentifier: "FilterCell")
filtersCollectionView.dataSource = self
}
private func layoutViews() {
view.addSubview(headerView)
view.addSubview(containerView)
@ -261,6 +248,16 @@ extension SearchOnMapViewController: SearchOnMapView {
if let searchingText = viewModel.searchingText {
replaceSearchText(with: searchingText)
}
modalPresentationController?.setPresentationStep(viewModel.presentationStep)
}
func close() {
headerView.setIsSearching(false)
guard let modalPresentationController else {
dismiss(animated: true)
return
}
modalPresentationController.close()
}
}
@ -303,12 +300,12 @@ extension SearchOnMapViewController: UITableViewDataSource {
extension SearchOnMapViewController: UITableViewDelegate {
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
let result = searchResults[indexPath.row]
interactor.handle(.didSelectResult(result, withSearchText: headerView.searchText))
interactor?.handle(.didSelectResult(result, withSearchText: headerView.searchText))
tableView.deselectRow(at: indexPath, animated: true)
}
func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
interactor.handle(.didStartDraggingSearch)
interactor?.handle(.didStartDraggingSearch)
}
func scrollViewDidScroll(_ scrollView: UIScrollView) {
@ -332,31 +329,31 @@ extension SearchOnMapViewController: UICollectionViewDataSource {
// MARK: - SearchOnMapHeaderViewDelegate
extension SearchOnMapViewController: SearchOnMapHeaderViewDelegate {
func searchBarTextDidBeginEditing(_ searchBar: UISearchBar) {
interactor.handle(.didStartTyping)
interactor?.handle(.didStartTyping)
}
func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) {
guard !searchText.isEmpty else {
interactor.handle(.clearButtonDidTap)
interactor?.handle(.clearButtonDidTap)
return
}
interactor.handle(.didType(SearchText(searchText, locale: searchBar.textInputMode?.primaryLanguage)))
interactor?.handle(.didType(SearchText(searchText, locale: searchBar.textInputMode?.primaryLanguage)))
}
func searchBarSearchButtonClicked(_ searchBar: UISearchBar) {
guard let searchText = searchBar.text, !searchText.isEmpty else { return }
interactor.handle(.searchButtonDidTap(SearchText(searchText, locale: searchBar.textInputMode?.primaryLanguage)))
interactor?.handle(.searchButtonDidTap(SearchText(searchText, locale: searchBar.textInputMode?.primaryLanguage)))
}
func cancelButtonDidTap() {
interactor.handle(.closeSearch)
interactor?.handle(.closeSearch)
}
}
// MARK: - SearchTabViewControllerDelegate
extension SearchOnMapViewController: SearchTabViewControllerDelegate {
func searchTabController(_ viewController: SearchTabViewController, didSearch text: String, withCategory: Bool) {
interactor.handle(.didSelectText(SearchText(text, locale: nil), isCategory: withCategory))
interactor?.handle(.didSelectText(SearchText(text, locale: nil), isCategory: withCategory))
}
}