forked from organicmaps/organicmaps
Compare commits
3 commits
master
...
ios/search
Author | SHA1 | Date | |
---|---|---|---|
200b433f71 | |||
a2b642d601 | |||
f53ed1a9ef |
17 changed files with 214 additions and 303 deletions
|
@ -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
|
||||
|
|
|
@ -148,34 +148,45 @@ NSString *const kSettingsSegue = @"Map2Settings";
|
|||
|
||||
- (void)setupPlacePageContainer {
|
||||
self.placePageContainer = [[TouchTransparentView alloc] initWithFrame:self.view.bounds];
|
||||
self.placePageContainer.translatesAutoresizingMaskIntoConstraints = NO;
|
||||
[self.view addSubview:self.placePageContainer];
|
||||
[self.view bringSubviewToFront:self.placePageContainer];
|
||||
|
||||
self.placePageContainer.translatesAutoresizingMaskIntoConstraints = NO;
|
||||
self.placePageLeadingConstraint = [self.placePageContainer.leadingAnchor constraintEqualToAnchor:self.view.safeAreaLayoutGuide.leadingAnchor constant:kPlacePageLeadingOffset];
|
||||
self.placePageLeadingConstraint.active = YES;
|
||||
if (IPAD)
|
||||
self.placePageLeadingConstraint.priority = UILayoutPriorityDefaultLow;
|
||||
|
||||
self.placePageWidthConstraint = [self.placePageContainer.widthAnchor constraintEqualToConstant:0];
|
||||
self.placePageWidthConstraint = [self.placePageContainer.widthAnchor constraintEqualToConstant:kPlacePageCompactWidth];
|
||||
self.placePageTrailingConstraint = [self.placePageContainer.trailingAnchor constraintEqualToAnchor:self.view.safeAreaLayoutGuide.trailingAnchor];
|
||||
|
||||
[self.placePageContainer.topAnchor constraintEqualToAnchor:self.view.safeAreaLayoutGuide.topAnchor].active = YES;
|
||||
if (IPAD) {
|
||||
self.placePageLeadingConstraint.priority = UILayoutPriorityDefaultLow;
|
||||
[self.placePageContainer.bottomAnchor constraintLessThanOrEqualToAnchor:self.view.bottomAnchor].active = YES;
|
||||
}
|
||||
else {
|
||||
[self.placePageContainer.bottomAnchor constraintEqualToAnchor:self.view.bottomAnchor].active = YES;
|
||||
}
|
||||
NSLayoutConstraint * topConstraint = [self.placePageContainer.topAnchor constraintEqualToAnchor:self.view.safeAreaLayoutGuide.topAnchor];
|
||||
|
||||
NSLayoutConstraint * bottomConstraint;
|
||||
if (IPAD)
|
||||
bottomConstraint = [self.placePageContainer.bottomAnchor constraintLessThanOrEqualToAnchor:self.view.bottomAnchor];
|
||||
else
|
||||
bottomConstraint = [self.placePageContainer.bottomAnchor constraintEqualToAnchor:self.view.bottomAnchor];
|
||||
|
||||
[NSLayoutConstraint activateConstraints:@[
|
||||
self.placePageLeadingConstraint,
|
||||
topConstraint,
|
||||
bottomConstraint,
|
||||
]];
|
||||
|
||||
[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 +270,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 +281,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 +384,7 @@ NSString *const kSettingsSegue = @"Map2Settings";
|
|||
- (void)viewDidLoad {
|
||||
[super viewDidLoad];
|
||||
[self setupPlacePageContainer];
|
||||
[self setupSearchContainer];
|
||||
|
||||
if (@available(iOS 14.0, *))
|
||||
[self setupTrackPadGestureRecognizers];
|
||||
|
@ -726,11 +739,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;
|
||||
}
|
||||
|
||||
|
|
|
@ -61,7 +61,7 @@ using Observers = NSHashTable<Observer>;
|
|||
- (void)searchEverywhere {
|
||||
self.lastSearchTimestamp += 1;
|
||||
NSUInteger const timestamp = self.lastSearchTimestamp;
|
||||
|
||||
|
||||
search::EverywhereSearchParams params{
|
||||
m_query, m_locale, {} /* default timeout */, m_isCategory,
|
||||
// m_onResults
|
||||
|
@ -156,6 +156,7 @@ using Observers = NSHashTable<Observer>;
|
|||
|
||||
+ (void)showResultAtIndex:(NSUInteger)index {
|
||||
auto const & result = [MWMSearch manager]->m_everywhereResults[index];
|
||||
GetFramework().StopLocationFollow();
|
||||
GetFramework().SelectSearchResult(result, true);
|
||||
}
|
||||
|
||||
|
@ -168,8 +169,13 @@ using Observers = NSHashTable<Observer>;
|
|||
|
||||
+ (void)showEverywhereSearchResultsOnMap {
|
||||
MWMSearch * manager = [MWMSearch manager];
|
||||
if (![MWMRouter isRoutingActive])
|
||||
GetFramework().ShowSearchResults(manager->m_everywhereResults);
|
||||
if (![MWMRouter isRoutingActive]) {
|
||||
auto const & results = manager->m_everywhereResults;
|
||||
if (results.GetCount() == 1)
|
||||
[self showResultAtIndex:0];
|
||||
else
|
||||
GetFramework().ShowSearchResults(manager->m_everywhereResults);
|
||||
}
|
||||
}
|
||||
|
||||
+ (void)showViewportSearchResultsOnMap {
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -495,16 +495,12 @@
|
|||
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 */; };
|
||||
ED70D5902D539A2500738C1E /* SearchOnMapPresentationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = ED70D57D2D539A2500738C1E /* SearchOnMapPresentationController.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 */; };
|
||||
|
@ -1390,6 +1386,10 @@
|
|||
A630D205207CAA3A00976DEA /* zh-Hant */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = "zh-Hant"; path = "zh-Hant.lproj/Localizable.stringsdict"; sourceTree = "<group>"; };
|
||||
A630D206207CAA5800976DEA /* zh-Hans */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = "zh-Hans"; path = "zh-Hans.lproj/Localizable.stringsdict"; sourceTree = "<group>"; };
|
||||
AA1C7E3D269A2DD600BAADF2 /* EditTrackViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditTrackViewController.swift; sourceTree = "<group>"; };
|
||||
AC4209FF2D79BCEC00A64AA9 /* af */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = af; path = af.lproj/InfoPlist.strings; sourceTree = "<group>"; };
|
||||
AC420A002D79BCED00A64AA9 /* af */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = af; path = af.lproj/Localizable.strings; sourceTree = "<group>"; };
|
||||
AC420A012D79BCED00A64AA9 /* af */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = af; path = af.lproj/Localizable.stringsdict; sourceTree = "<group>"; };
|
||||
AC420A022D79BCEE00A64AA9 /* af */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = af; path = af.lproj/LocalizableTypes.strings; sourceTree = "<group>"; };
|
||||
AC420A082D79BDDA00A64AA9 /* lt */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = lt; path = lt.lproj/InfoPlist.strings; sourceTree = "<group>"; };
|
||||
AC420A092D79BDDA00A64AA9 /* lt */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = lt; path = lt.lproj/Localizable.strings; sourceTree = "<group>"; };
|
||||
AC420A0A2D79BDDB00A64AA9 /* lt */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = lt; path = lt.lproj/Localizable.stringsdict; sourceTree = "<group>"; };
|
||||
|
@ -1398,10 +1398,6 @@
|
|||
AC420A142D79C2EC00A64AA9 /* mt */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = mt; path = mt.lproj/Localizable.strings; sourceTree = "<group>"; };
|
||||
AC420A152D79C2EC00A64AA9 /* mt */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = mt; path = mt.lproj/Localizable.stringsdict; sourceTree = "<group>"; };
|
||||
AC420A162D79C2ED00A64AA9 /* mt */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = mt; path = mt.lproj/LocalizableTypes.strings; sourceTree = "<group>"; };
|
||||
AC4209FF2D79BCEC00A64AA9 /* af */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = af; path = af.lproj/InfoPlist.strings; sourceTree = "<group>"; };
|
||||
AC420A002D79BCED00A64AA9 /* af */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = af; path = af.lproj/Localizable.strings; sourceTree = "<group>"; };
|
||||
AC420A012D79BCED00A64AA9 /* af */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = af; path = af.lproj/Localizable.stringsdict; sourceTree = "<group>"; };
|
||||
AC420A022D79BCEE00A64AA9 /* af */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = af; path = af.lproj/LocalizableTypes.strings; sourceTree = "<group>"; };
|
||||
AC79C8912A65AB9500594C24 /* UIColor+hexString.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIColor+hexString.swift"; sourceTree = "<group>"; };
|
||||
B33D21AE20DAF9F000BAD749 /* Toast.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Toast.swift; sourceTree = "<group>"; };
|
||||
B3E3B4FC20D463B700DA8C13 /* BMCCategoriesHeader.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = BMCCategoriesHeader.xib; sourceTree = "<group>"; };
|
||||
|
@ -1472,12 +1468,8 @@
|
|||
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>"; };
|
||||
ED70D57D2D539A2500738C1E /* SearchOnMapPresentationController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchOnMapPresentationController.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>"; };
|
||||
|
@ -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 */,
|
||||
ED70D57D2D539A2500738C1E /* SearchOnMapPresentationController.swift */,
|
||||
);
|
||||
path = Presentation;
|
||||
sourceTree = "<group>";
|
||||
|
@ -4817,16 +4805,12 @@
|
|||
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 */,
|
||||
ED70D5902D539A2500738C1E /* SearchOnMapPresentationController.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 */,
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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:
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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 {
|
|
@ -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)
|
||||
})
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
})
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
])
|
||||
|
|
|
@ -70,12 +70,12 @@ final class SearchOnMapInteractor: NSObject {
|
|||
searchManager.saveQuery(searchText.text,
|
||||
forInputLocale: searchText.locale)
|
||||
showResultsOnMap = true
|
||||
searchManager.showEverywhereSearchResultsOnMap()
|
||||
return .showOnTheMap
|
||||
}
|
||||
|
||||
private func processTypedText(_ searchText: SearchOnMap.SearchText) -> SearchOnMap.Response {
|
||||
isUpdatesDisabled = false
|
||||
showResultsOnMap = true
|
||||
searchManager.searchQuery(searchText.text,
|
||||
forInputLocale: searchText.locale,
|
||||
withCategory: false)
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
Loading…
Add table
Reference in a new issue