forked from organicmaps/organicmaps-tmp
[ios] implement modal search screen SearchOnMap
Signed-off-by: Kiryl Kaveryn <kirylkaveryn@gmail.com>
This commit is contained in:
parent
4eb7bf3f73
commit
5db61f0498
21 changed files with 1956 additions and 105 deletions
|
@ -5,9 +5,7 @@ NS_ASSUME_NONNULL_BEGIN
|
|||
|
||||
@class SearchResult;
|
||||
|
||||
NS_SWIFT_NAME(Search)
|
||||
@interface MWMSearch : NSObject
|
||||
|
||||
@protocol SearchManager
|
||||
+ (void)addObserver:(id<MWMSearchObserver>)observer;
|
||||
+ (void)removeObserver:(id<MWMSearchObserver>)observer;
|
||||
|
||||
|
@ -18,12 +16,17 @@ NS_SWIFT_NAME(Search)
|
|||
+ (void)showEverywhereSearchResultsOnMap;
|
||||
+ (void)showViewportSearchResultsOnMap;
|
||||
|
||||
+ (SearchItemType)resultTypeWithRow:(NSUInteger)row;
|
||||
+ (NSUInteger)containerIndexWithRow:(NSUInteger)row;
|
||||
+ (SearchResult *)resultWithContainerIndex:(NSUInteger)index;
|
||||
+ (NSArray<SearchResult *> *)getResults;
|
||||
|
||||
+ (void)clear;
|
||||
@end
|
||||
|
||||
NS_SWIFT_NAME(Search)
|
||||
@interface MWMSearch : NSObject<SearchManager>
|
||||
|
||||
+ (SearchItemType)resultTypeWithRow:(NSUInteger)row;
|
||||
+ (NSUInteger)containerIndexWithRow:(NSUInteger)row;
|
||||
+ (SearchResult *)resultWithContainerIndex:(NSUInteger)index;
|
||||
|
||||
+ (void)setSearchOnMap:(BOOL)searchOnMap;
|
||||
|
||||
|
|
|
@ -176,15 +176,15 @@ using Observers = NSHashTable<Observer>;
|
|||
}
|
||||
|
||||
+ (void)showEverywhereSearchResultsOnMap {
|
||||
MWMSearch *manager = [MWMSearch manager];
|
||||
MWMSearch * manager = [MWMSearch manager];
|
||||
if (![MWMRouter isRoutingActive])
|
||||
GetFramework().ShowSearchResults(manager->m_everywhereResults);
|
||||
}
|
||||
|
||||
+ (void)showViewportSearchResultsOnMap {
|
||||
MWMSearch *manager = [MWMSearch manager];
|
||||
MWMSearch * manager = [MWMSearch manager];
|
||||
if (![MWMRouter isRoutingActive])
|
||||
GetFramework().ShowSearchResults(manager->m_viewportResults);
|
||||
[manager processViewportChangedEvent];
|
||||
}
|
||||
|
||||
+ (NSArray<SearchResult *> *)getResults {
|
||||
|
|
|
@ -493,8 +493,21 @@
|
|||
ED4DC7772CAEDECC0029B338 /* ProductsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = ED4DC7752CAEDECC0029B338 /* ProductsViewModel.swift */; };
|
||||
ED4DC7782CAEDECC0029B338 /* ProductButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = ED4DC7732CAEDECC0029B338 /* ProductButton.swift */; };
|
||||
ED4DC7792CAEDECC0029B338 /* ProductsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = ED4DC7742CAEDECC0029B338 /* ProductsViewController.swift */; };
|
||||
ED5BAF4B2D688F5B0088D7B1 /* SearchOnMapHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = ED5BAF4A2D688F5A0088D7B1 /* SearchOnMapHeaderView.swift */; };
|
||||
ED63CEB92BDF8F9D006155C4 /* SettingsTableViewiCloudSwitchCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = ED63CEB62BDF8F9C006155C4 /* SettingsTableViewiCloudSwitchCell.swift */; };
|
||||
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 */; };
|
||||
|
@ -506,6 +519,7 @@
|
|||
ED79A5D82BDF8D6100952D1F /* LocalDirectoryMonitor.swift in Sources */ = {isa = PBXBuildFile; fileRef = ED79A5D02BDF8D6100952D1F /* LocalDirectoryMonitor.swift */; };
|
||||
ED7CCC4F2C1362E300E2A737 /* FileType.swift in Sources */ = {isa = PBXBuildFile; fileRef = ED7CCC4E2C1362E300E2A737 /* FileType.swift */; };
|
||||
ED808D0F2C38407800D52585 /* CircleImageButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = ED808D0E2C38407800D52585 /* CircleImageButton.swift */; };
|
||||
ED810EC52D566E9B00ECDE2C /* SearchOnMapTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = ED810EC42D566E9B00ECDE2C /* SearchOnMapTests.swift */; };
|
||||
ED8270F02C2071A3005966DA /* SettingsTableViewDetailedSwitchCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = ED8270EF2C2071A3005966DA /* SettingsTableViewDetailedSwitchCell.swift */; };
|
||||
ED83880F2D54DEB3002A0536 /* UIImage+FilledWithColor.swift in Sources */ = {isa = PBXBuildFile; fileRef = ED83880E2D54DEA4002A0536 /* UIImage+FilledWithColor.swift */; };
|
||||
ED914AB22D35063A00973C45 /* TextColorStyleSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = ED914AB12D35063A00973C45 /* TextColorStyleSheet.swift */; };
|
||||
|
@ -523,7 +537,6 @@
|
|||
EDC4E3612C5E2576009286A2 /* RecentlyDeletedCategoriesViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EDC4E3412C5D1BD3009286A2 /* RecentlyDeletedCategoriesViewModelTests.swift */; };
|
||||
EDC4E3692C5E6F5B009286A2 /* MockRecentlyDeletedCategoriesManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = EDC4E3402C5D1BD3009286A2 /* MockRecentlyDeletedCategoriesManager.swift */; };
|
||||
EDCA7CDF2D317DF9003366CE /* StyleSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = EDCA7CDE2D317DF9003366CE /* StyleSheet.swift */; };
|
||||
EDDE060E2D6CAEAF000C328A /* SearchHistoryViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = EDDE060D2D6CAEAF000C328A /* SearchHistoryViewController.xib */; };
|
||||
EDE243DD2B6D2E640057369B /* AboutController.swift in Sources */ = {isa = PBXBuildFile; fileRef = EDE243D52B6CF3980057369B /* AboutController.swift */; };
|
||||
EDE243E52B6D3F400057369B /* OSMView.swift in Sources */ = {isa = PBXBuildFile; fileRef = EDE243E42B6D3F400057369B /* OSMView.swift */; };
|
||||
EDE243E72B6D55610057369B /* InfoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = EDE243E02B6D3EA00057369B /* InfoView.swift */; };
|
||||
|
@ -1455,11 +1468,24 @@
|
|||
ED4DC7732CAEDECC0029B338 /* ProductButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProductButton.swift; sourceTree = "<group>"; };
|
||||
ED4DC7742CAEDECC0029B338 /* ProductsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProductsViewController.swift; sourceTree = "<group>"; };
|
||||
ED4DC7752CAEDECC0029B338 /* ProductsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProductsViewModel.swift; sourceTree = "<group>"; };
|
||||
ED5BAF4A2D688F5A0088D7B1 /* SearchOnMapHeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchOnMapHeaderView.swift; sourceTree = "<group>"; };
|
||||
ED63CEB62BDF8F9C006155C4 /* SettingsTableViewiCloudSwitchCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SettingsTableViewiCloudSwitchCell.swift; sourceTree = "<group>"; };
|
||||
ED70D5582D5396F300738C1E /* SearchItemType.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = SearchItemType.h; sourceTree = "<group>"; };
|
||||
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>"; };
|
||||
ED70D5852D539A2500738C1E /* SearchOnMapModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchOnMapModels.swift; sourceTree = "<group>"; };
|
||||
ED70D5862D539A2500738C1E /* SearchOnMapPresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchOnMapPresenter.swift; sourceTree = "<group>"; };
|
||||
ED70D5872D539A2500738C1E /* SearchOnMapViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchOnMapViewController.swift; sourceTree = "<group>"; };
|
||||
ED77556D2C2C490B0051E656 /* UIAlertController+openInAppActionSheet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIAlertController+openInAppActionSheet.swift"; sourceTree = "<group>"; };
|
||||
ED79A5AA2BD7AA9C00952D1F /* LoadingOverlayViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoadingOverlayViewController.swift; sourceTree = "<group>"; };
|
||||
ED79A5AC2BD7BA0F00952D1F /* UIApplication+LoadingOverlay.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIApplication+LoadingOverlay.swift"; sourceTree = "<group>"; };
|
||||
|
@ -1471,6 +1497,7 @@
|
|||
ED79A5D02BDF8D6100952D1F /* LocalDirectoryMonitor.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LocalDirectoryMonitor.swift; sourceTree = "<group>"; };
|
||||
ED7CCC4E2C1362E300E2A737 /* FileType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileType.swift; sourceTree = "<group>"; };
|
||||
ED808D0E2C38407800D52585 /* CircleImageButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CircleImageButton.swift; sourceTree = "<group>"; };
|
||||
ED810EC42D566E9B00ECDE2C /* SearchOnMapTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchOnMapTests.swift; sourceTree = "<group>"; };
|
||||
ED8270EF2C2071A3005966DA /* SettingsTableViewDetailedSwitchCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsTableViewDetailedSwitchCell.swift; sourceTree = "<group>"; };
|
||||
ED83880E2D54DEA4002A0536 /* UIImage+FilledWithColor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIImage+FilledWithColor.swift"; sourceTree = "<group>"; };
|
||||
ED914AB12D35063A00973C45 /* TextColorStyleSheet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextColorStyleSheet.swift; sourceTree = "<group>"; };
|
||||
|
@ -1488,7 +1515,6 @@
|
|||
EDC4E3482C5D1BEF009286A2 /* RecentlyDeletedCategoriesViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RecentlyDeletedCategoriesViewModel.swift; sourceTree = "<group>"; };
|
||||
EDC4E3492C5D1BEF009286A2 /* RecentlyDeletedTableViewCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RecentlyDeletedTableViewCell.swift; sourceTree = "<group>"; };
|
||||
EDCA7CDE2D317DF9003366CE /* StyleSheet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StyleSheet.swift; sourceTree = "<group>"; };
|
||||
EDDE060D2D6CAEAF000C328A /* SearchHistoryViewController.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = SearchHistoryViewController.xib; sourceTree = "<group>"; };
|
||||
EDE243D52B6CF3980057369B /* AboutController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AboutController.swift; sourceTree = "<group>"; };
|
||||
EDE243E02B6D3EA00057369B /* InfoView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InfoView.swift; sourceTree = "<group>"; };
|
||||
EDE243E42B6D3F400057369B /* OSMView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OSMView.swift; sourceTree = "<group>"; };
|
||||
|
@ -3180,6 +3206,7 @@
|
|||
ED1ADA312BC6B19E0029209F /* Tests */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
ED810EC02D566E6F00ECDE2C /* UI */,
|
||||
EDC4E3442C5D1BD3009286A2 /* Bookmarks */,
|
||||
4B4153B82BF970B800EE4B02 /* Classes */,
|
||||
4B4153B62BF9709100EE4B02 /* Core */,
|
||||
|
@ -3231,6 +3258,34 @@
|
|||
path = Products;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
ED70D5812D539A2500738C1E /* Presentation */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
ED70D57B2D539A2500738C1E /* MapPassthroughView.swift */,
|
||||
ED70D57C2D539A2500738C1E /* ModalScreenPresentationStep.swift */,
|
||||
ED70D57D2D539A2500738C1E /* SearchOnMapModalPresentationController.swift */,
|
||||
ED70D57E2D539A2500738C1E /* SearchOnMapModalTransitionManager.swift */,
|
||||
ED70D57F2D539A2500738C1E /* SideMenuDismissalAnimator.swift */,
|
||||
ED70D5802D539A2500738C1E /* SideMenuPresentationAnimator.swift */,
|
||||
);
|
||||
path = Presentation;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
ED70D5882D539A2500738C1E /* SearchOnMap */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
ED70D5812D539A2500738C1E /* Presentation */,
|
||||
ED70D5822D539A2500738C1E /* PlaceholderView.swift */,
|
||||
ED70D5832D539A2500738C1E /* SearchOnMapInteractor.swift */,
|
||||
ED70D5842D539A2500738C1E /* SearchOnMapManager.swift */,
|
||||
ED70D5852D539A2500738C1E /* SearchOnMapModels.swift */,
|
||||
ED70D5862D539A2500738C1E /* SearchOnMapPresenter.swift */,
|
||||
ED70D5872D539A2500738C1E /* SearchOnMapViewController.swift */,
|
||||
ED5BAF4A2D688F5A0088D7B1 /* SearchOnMapHeaderView.swift */,
|
||||
);
|
||||
path = SearchOnMap;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
ED79A5A92BD7AA7500952D1F /* LoadingOverlay */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
|
@ -3253,6 +3308,22 @@
|
|||
path = iCloud;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
ED810EC02D566E6F00ECDE2C /* UI */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
ED810EC32D566E7600ECDE2C /* SearchOnMapTests */,
|
||||
);
|
||||
path = UI;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
ED810EC32D566E7600ECDE2C /* SearchOnMapTests */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
ED810EC42D566E9B00ECDE2C /* SearchOnMapTests.swift */,
|
||||
);
|
||||
path = SearchOnMapTests;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
ED9857022C4ECFFC00694F6C /* MailComposer */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
|
@ -3822,6 +3893,7 @@
|
|||
F6E2FCE11E097B9F0083EBEC /* Search */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
ED70D5882D539A2500738C1E /* SearchOnMap */,
|
||||
F6E2FCF41E097B9F0083EBEC /* MWMSearchContentView.h */,
|
||||
F6E2FCF51E097B9F0083EBEC /* MWMSearchContentView.m */,
|
||||
F6E2FCF81E097B9F0083EBEC /* MWMSearchManager+Layout.h */,
|
||||
|
@ -3868,7 +3940,6 @@
|
|||
F6E2FD0F1E097B9F0083EBEC /* HistoryTab */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
EDDE060D2D6CAEAF000C328A /* SearchHistoryViewController.xib */,
|
||||
337F98B721D3D67E00C8AC27 /* SearchHistoryCell.swift */,
|
||||
337F98B321D3C9F200C8AC27 /* SearchHistoryViewController.swift */,
|
||||
);
|
||||
|
@ -4266,7 +4337,6 @@
|
|||
6741A9991BF340DE002C974C /* MWMAlertViewController.xib in Resources */,
|
||||
4501B1942077C35A001B9173 /* resources-xxxhdpi_light in Resources */,
|
||||
3467CEB7202C6FA900D3C670 /* BMCNotificationsCell.xib in Resources */,
|
||||
EDDE060E2D6CAEAF000C328A /* SearchHistoryViewController.xib in Resources */,
|
||||
4761BE2B252D3DB900EE2DE4 /* SubgroupCell.xib in Resources */,
|
||||
99F9A0E72462CA1700AE21E0 /* DownloadAllView.xib in Resources */,
|
||||
349D1AD51E2E325B004A2006 /* BottomMenuItemCell.xib in Resources */,
|
||||
|
@ -4556,6 +4626,7 @@
|
|||
ED43B8BD2C12063500D07BAA /* DocumentPicker.swift in Sources */,
|
||||
470E1674252AD7F2002D201A /* BookmarksListInfoViewController.swift in Sources */,
|
||||
47B9065521C7FA400079C85E /* NSString+MD5.m in Sources */,
|
||||
ED5BAF4B2D688F5B0088D7B1 /* SearchOnMapHeaderView.swift in Sources */,
|
||||
CDB4D5022231412900104869 /* SettingsTemplateBuilder.swift in Sources */,
|
||||
993DF10A23F6BDB100AC231A /* UISwitchRenderer.swift in Sources */,
|
||||
99C9642C2428C0F700E41723 /* PlacePageHeaderBuilder.swift in Sources */,
|
||||
|
@ -4735,6 +4806,18 @@
|
|||
CD9AD96C2281B56900EC174A /* CPViewPortState.swift in Sources */,
|
||||
EDE243DD2B6D2E640057369B /* AboutController.swift in Sources */,
|
||||
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 */,
|
||||
|
@ -4881,6 +4964,7 @@
|
|||
EDF838BE2C00B9D0007E4E67 /* LocalDirectoryMonitorDelegateMock.swift in Sources */,
|
||||
EDC4E3692C5E6F5B009286A2 /* MockRecentlyDeletedCategoriesManager.swift in Sources */,
|
||||
EDF838BF2C00B9D0007E4E67 /* SynchronizationStateManagerTests.swift in Sources */,
|
||||
ED810EC52D566E9B00ECDE2C /* SearchOnMapTests.swift in Sources */,
|
||||
4B83AE4B2C2E642100B0C3BC /* TTSTesterTest.m in Sources */,
|
||||
EDF838C22C00B9D6007E4E67 /* MetadataItemStubs.swift in Sources */,
|
||||
EDC4E3612C5E2576009286A2 /* RecentlyDeletedCategoriesViewModelTests.swift in Sources */,
|
||||
|
|
262
iphone/Maps/Tests/UI/SearchOnMapTests/SearchOnMapTests.swift
Normal file
262
iphone/Maps/Tests/UI/SearchOnMapTests/SearchOnMapTests.swift
Normal file
|
@ -0,0 +1,262 @@
|
|||
import XCTest
|
||||
@testable import Organic_Maps__Debug_
|
||||
|
||||
final class SearchOnMapTests: XCTestCase {
|
||||
|
||||
private var presenter: SearchOnMapPresenter!
|
||||
private var interactor: SearchOnMapInteractor!
|
||||
private var view: SearchOnMapViewMock!
|
||||
private var searchManager: SearchManagerMock.Type!
|
||||
private var currentState: SearchOnMapState = .searching
|
||||
|
||||
override func setUp() {
|
||||
super.setUp()
|
||||
searchManager = SearchManagerMock.self
|
||||
presenter = SearchOnMapPresenter(transitionManager: SearchOnMapModalTransitionManager(),
|
||||
isRouting: false,
|
||||
didChangeState: { [weak self] in self?.currentState = $0 })
|
||||
interactor = SearchOnMapInteractor(presenter: presenter, searchManager: searchManager)
|
||||
view = SearchOnMapViewMock()
|
||||
presenter.view = view
|
||||
}
|
||||
|
||||
override func tearDown() {
|
||||
presenter = nil
|
||||
interactor = nil
|
||||
view = nil
|
||||
searchManager.results = .empty
|
||||
searchManager = nil
|
||||
super.tearDown()
|
||||
}
|
||||
|
||||
func test_GivenViewIsLoading_WhenViewLoads_ThenShowsHistoryAndCategory() {
|
||||
interactor.handle(.openSearch)
|
||||
|
||||
XCTAssertEqual(currentState, .searching)
|
||||
XCTAssertEqual(view.viewModel.presentationStep, .fullScreen)
|
||||
XCTAssertEqual(view.viewModel.contentState, .historyAndCategory)
|
||||
XCTAssertEqual(view.viewModel.searchingText, nil)
|
||||
XCTAssertEqual(view.viewModel.isTyping, true)
|
||||
}
|
||||
|
||||
func test_GivenInitialState_WhenSelectCategory_ThenUpdateSearchResultsAndShowMap() {
|
||||
interactor.handle(.openSearch)
|
||||
|
||||
let searchText = SearchOnMap.SearchText("category")
|
||||
interactor.handle(.didSelectText(searchText, isCategory: true))
|
||||
|
||||
XCTAssertEqual(view.viewModel.presentationStep, .halfScreen)
|
||||
XCTAssertEqual(view.viewModel.contentState, .searching)
|
||||
XCTAssertEqual(view.viewModel.searchingText, searchText.text)
|
||||
XCTAssertEqual(view.viewModel.isTyping, false)
|
||||
|
||||
let results = SearchResult.stubResults()
|
||||
searchManager.results = results
|
||||
|
||||
XCTAssertEqual(currentState, .searching)
|
||||
XCTAssertEqual(view.viewModel.presentationStep, .halfScreen)
|
||||
XCTAssertEqual(view.viewModel.contentState, .results(results))
|
||||
XCTAssertEqual(view.viewModel.searchingText, nil)
|
||||
XCTAssertEqual(view.viewModel.isTyping, false)
|
||||
}
|
||||
|
||||
func test_GivenInitialState_WhenTypeText_ThenUpdateSearchResults() {
|
||||
interactor.handle(.openSearch)
|
||||
|
||||
let searchText = SearchOnMap.SearchText("text")
|
||||
interactor.handle(.didType(searchText))
|
||||
|
||||
XCTAssertEqual(view.viewModel.presentationStep, .fullScreen)
|
||||
XCTAssertEqual(view.viewModel.contentState, .searching)
|
||||
XCTAssertEqual(view.viewModel.searchingText, nil)
|
||||
XCTAssertEqual(view.viewModel.isTyping, true)
|
||||
|
||||
let results = SearchResult.stubResults()
|
||||
searchManager.results = results
|
||||
|
||||
XCTAssertEqual(currentState, .searching)
|
||||
XCTAssertEqual(view.viewModel.presentationStep, .fullScreen)
|
||||
XCTAssertEqual(view.viewModel.contentState, .results(results))
|
||||
XCTAssertEqual(view.viewModel.searchingText, nil)
|
||||
XCTAssertEqual(view.viewModel.isTyping, true)
|
||||
}
|
||||
|
||||
func test_GivenInitialState_WhenTapSearch_ThenUpdateSearchResultsAndShowMap() {
|
||||
interactor.handle(.openSearch)
|
||||
|
||||
let searchText = SearchOnMap.SearchText("text")
|
||||
interactor.handle(.didType(searchText))
|
||||
|
||||
let results = SearchResult.stubResults()
|
||||
searchManager.results = results
|
||||
|
||||
XCTAssertEqual(view.viewModel.presentationStep, .fullScreen)
|
||||
XCTAssertEqual(view.viewModel.contentState, .results(results))
|
||||
XCTAssertEqual(view.viewModel.searchingText, nil)
|
||||
XCTAssertEqual(view.viewModel.isTyping, true)
|
||||
|
||||
interactor.handle(.searchButtonDidTap(searchText))
|
||||
|
||||
XCTAssertEqual(currentState, .searching)
|
||||
XCTAssertEqual(view.viewModel.presentationStep, .halfScreen)
|
||||
XCTAssertEqual(view.viewModel.contentState, .results(results))
|
||||
XCTAssertEqual(view.viewModel.searchingText, nil)
|
||||
XCTAssertEqual(view.viewModel.isTyping, false)
|
||||
}
|
||||
|
||||
func test_GivenSearchIsOpened_WhenMapIsDragged_ThenCollapseSearchScreen() {
|
||||
interactor.handle(.openSearch)
|
||||
XCTAssertEqual(view.viewModel.presentationStep, .fullScreen)
|
||||
|
||||
interactor.handle(.didStartDraggingMap)
|
||||
XCTAssertEqual(view.viewModel.presentationStep, .compact)
|
||||
}
|
||||
|
||||
func test_GivenSearchIsOpened_WhenModalPresentationScreenIsDragged_ThenDisableTyping() {
|
||||
interactor.handle(.openSearch)
|
||||
XCTAssertEqual(view.viewModel.isTyping, true)
|
||||
|
||||
interactor.handle(.didStartDraggingSearch)
|
||||
XCTAssertEqual(view.viewModel.isTyping, false)
|
||||
}
|
||||
|
||||
func test_GivenResultsOnScreen_WhenSelectResult_ThenHideSearch() {
|
||||
interactor.handle(.openSearch)
|
||||
XCTAssertEqual(view.viewModel.isTyping, true)
|
||||
|
||||
let searchText = SearchOnMap.SearchText("text")
|
||||
interactor.handle(.didType(searchText))
|
||||
|
||||
let results = SearchResult.stubResults()
|
||||
searchManager.results = results
|
||||
|
||||
interactor.handle(.didSelectResult(results[0], atIndex: 0, withSearchText: searchText))
|
||||
XCTAssertEqual(currentState, .hidden)
|
||||
XCTAssertEqual(view.viewModel.presentationStep, .hidden)
|
||||
}
|
||||
|
||||
func test_GivenSearchIsActive_WhenSelectPlaceOnMap_ThenHideSearch() {
|
||||
interactor.handle(.openSearch)
|
||||
XCTAssertEqual(view.viewModel.presentationStep, .fullScreen)
|
||||
|
||||
interactor.handle(.didSelectPlaceOnMap)
|
||||
|
||||
if isIPad {
|
||||
XCTAssertNotEqual(view.viewModel.presentationStep, .hidden)
|
||||
} else {
|
||||
XCTAssertEqual(view.viewModel.presentationStep, .hidden)
|
||||
}
|
||||
}
|
||||
|
||||
func test_GivenSearchIsHidden_WhenPPDeselected_ThenShowSearch() {
|
||||
interactor.handle(.openSearch)
|
||||
XCTAssertEqual(view.viewModel.isTyping, true)
|
||||
|
||||
let searchText = SearchOnMap.SearchText("text")
|
||||
interactor.handle(.didType(searchText))
|
||||
|
||||
let results = SearchResult.stubResults()
|
||||
searchManager.results = results
|
||||
|
||||
interactor.handle(.didSelectResult(results[0], atIndex: 0, withSearchText: searchText))
|
||||
XCTAssertEqual(currentState, .hidden)
|
||||
XCTAssertEqual(view.viewModel.presentationStep, .hidden)
|
||||
|
||||
interactor.handle(.didDeselectPlaceOnMap)
|
||||
XCTAssertEqual(currentState, .searching)
|
||||
XCTAssertEqual(view.viewModel.presentationStep, .halfScreen)
|
||||
}
|
||||
|
||||
func test_GivenSearchIsOpen_WhenCloseSearch_ThenHideSearch() {
|
||||
interactor.handle(.openSearch)
|
||||
XCTAssertEqual(view.viewModel.presentationStep, .fullScreen)
|
||||
|
||||
interactor.handle(.closeSearch)
|
||||
XCTAssertEqual(currentState, .closed)
|
||||
}
|
||||
|
||||
func test_GivenSearchHasText_WhenClearSearch_ThenShowHistoryAndCategory() {
|
||||
interactor.handle(.openSearch)
|
||||
|
||||
let searchText = SearchOnMap.SearchText("text")
|
||||
interactor.handle(.didType(searchText))
|
||||
|
||||
interactor.handle(.clearButtonDidTap)
|
||||
XCTAssertEqual(view.viewModel.presentationStep, .fullScreen)
|
||||
XCTAssertEqual(view.viewModel.contentState, .historyAndCategory)
|
||||
XCTAssertEqual(view.viewModel.searchingText, "")
|
||||
XCTAssertEqual(view.viewModel.isTyping, true)
|
||||
}
|
||||
|
||||
func test_GivenSearchExecuted_WhenNoResults_ThenShowNoResults() {
|
||||
interactor.handle(.openSearch)
|
||||
|
||||
let searchText = SearchOnMap.SearchText("text")
|
||||
interactor.handle(.didType(searchText))
|
||||
|
||||
searchManager.results = SearchOnMap.SearchResults([])
|
||||
interactor.onSearchCompleted()
|
||||
|
||||
XCTAssertEqual(view.viewModel.contentState, .noResults)
|
||||
}
|
||||
|
||||
func test_GivenSearchIsActive_WhenSelectSuggestion_ThenSearchAgain() {
|
||||
interactor.handle(.openSearch)
|
||||
|
||||
let searchText = SearchOnMap.SearchText("old search")
|
||||
interactor.handle(.didType(searchText))
|
||||
|
||||
let suggestion = SearchResult(titleText: "", type: .suggestion, suggestion: "suggestion")
|
||||
interactor.handle(.didSelectResult(suggestion, atIndex: 0, withSearchText: searchText))
|
||||
|
||||
XCTAssertEqual(view.viewModel.searchingText, "suggestion")
|
||||
XCTAssertEqual(view.viewModel.contentState, .searching)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Mocks
|
||||
|
||||
private class SearchOnMapViewMock: SearchOnMapView {
|
||||
var viewModel: SearchOnMap.ViewModel = .initial
|
||||
var scrollViewDelegate: (any SearchOnMapScrollViewDelegate)?
|
||||
func render(_ viewModel: SearchOnMap.ViewModel) {
|
||||
self.viewModel = viewModel
|
||||
}
|
||||
}
|
||||
|
||||
private class SearchManagerMock: SearchManager {
|
||||
static var observers = ListenerContainer<MWMSearchObserver>()
|
||||
static var results = SearchOnMap.SearchResults.empty {
|
||||
didSet {
|
||||
observers.forEach { observer in
|
||||
observer.onSearchCompleted?()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static func add(_ observer: any MWMSearchObserver) {
|
||||
self.observers.addListener(observer)
|
||||
}
|
||||
|
||||
static func remove(_ observer: any MWMSearchObserver) {
|
||||
self.observers.removeListener(observer)
|
||||
}
|
||||
|
||||
static func saveQuery(_ query: String, forInputLocale inputLocale: String) {}
|
||||
static func searchQuery(_ query: String, forInputLocale inputLocale: String, withCategory isCategory: Bool) {}
|
||||
static func showResult(at index: UInt) {}
|
||||
static func showEverywhereSearchResultsOnMap() {}
|
||||
static func showViewportSearchResultsOnMap() {}
|
||||
static func clear() {}
|
||||
static func getResults() -> [SearchResult] { results.results }
|
||||
}
|
||||
|
||||
private extension SearchResult {
|
||||
static func stubResults() -> SearchOnMap.SearchResults {
|
||||
SearchOnMap.SearchResults([
|
||||
SearchResult(),
|
||||
SearchResult(),
|
||||
SearchResult()
|
||||
])
|
||||
}
|
||||
}
|
122
iphone/Maps/UI/Search/SearchOnMap/PlaceholderView.swift
Normal file
122
iphone/Maps/UI/Search/SearchOnMap/PlaceholderView.swift
Normal file
|
@ -0,0 +1,122 @@
|
|||
final class PlaceholderView: UIView {
|
||||
|
||||
private let activityIndicator: UIActivityIndicatorView?
|
||||
private let titleLabel = UILabel()
|
||||
private let subtitleLabel = UILabel()
|
||||
private let stackView = UIStackView()
|
||||
private var keyboardHeight: CGFloat = 0
|
||||
private var centerYConstraint: NSLayoutConstraint!
|
||||
private var containerModalYTranslation: CGFloat = 0
|
||||
private let minOffsetFromTheKeyboardTop: CGFloat = 20
|
||||
private let maxOffsetFromTheTop: CGFloat = 100
|
||||
|
||||
init(title: String? = nil, subtitle: String? = nil, hasActivityIndicator: Bool = false) {
|
||||
self.activityIndicator = hasActivityIndicator ? UIActivityIndicatorView() : nil
|
||||
super.init(frame: .zero)
|
||||
setupView(title: title, subtitle: subtitle)
|
||||
layoutView()
|
||||
setupKeyboardObservers()
|
||||
}
|
||||
|
||||
@available(*, unavailable)
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
deinit {
|
||||
NotificationCenter.default.removeObserver(self)
|
||||
}
|
||||
|
||||
private func setupKeyboardObservers() {
|
||||
NotificationCenter.default.addObserver(self,
|
||||
selector: #selector(keyboardWillShow(_:)),
|
||||
name: UIResponder.keyboardWillShowNotification,
|
||||
object: nil)
|
||||
NotificationCenter.default.addObserver(self,
|
||||
selector: #selector(keyboardWillHide(_:)),
|
||||
name: UIResponder.keyboardWillHideNotification,
|
||||
object: nil)
|
||||
}
|
||||
|
||||
@objc private func keyboardWillShow(_ notification: Notification) {
|
||||
if let keyboardFrame = notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? CGRect {
|
||||
keyboardHeight = keyboardFrame.height
|
||||
reloadConstraints()
|
||||
}
|
||||
}
|
||||
|
||||
@objc private func keyboardWillHide(_ notification: Notification) {
|
||||
keyboardHeight = 0
|
||||
reloadConstraints()
|
||||
}
|
||||
|
||||
override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
|
||||
super.traitCollectionDidChange(previousTraitCollection)
|
||||
if traitCollection.verticalSizeClass != previousTraitCollection?.verticalSizeClass {
|
||||
reloadConstraints()
|
||||
}
|
||||
}
|
||||
|
||||
private func setupView(title: String?, subtitle: String?) {
|
||||
if let activityIndicator = activityIndicator {
|
||||
activityIndicator.hidesWhenStopped = true
|
||||
activityIndicator.startAnimating()
|
||||
if #available(iOS 13.0, *) {
|
||||
activityIndicator.style = .medium
|
||||
} else {
|
||||
activityIndicator.style = .gray
|
||||
}
|
||||
}
|
||||
|
||||
titleLabel.text = title
|
||||
titleLabel.setFontStyle(.medium16, color: .blackPrimary)
|
||||
titleLabel.textAlignment = .center
|
||||
|
||||
subtitleLabel.text = subtitle
|
||||
subtitleLabel.setFontStyle(.regular14, color: .blackSecondary)
|
||||
subtitleLabel.textAlignment = .center
|
||||
subtitleLabel.isHidden = subtitle == nil
|
||||
subtitleLabel.numberOfLines = 2
|
||||
|
||||
stackView.axis = .vertical
|
||||
stackView.alignment = .center
|
||||
stackView.spacing = 8
|
||||
}
|
||||
|
||||
private func layoutView() {
|
||||
if let activityIndicator = activityIndicator {
|
||||
stackView.addArrangedSubview(activityIndicator)
|
||||
}
|
||||
if let title = titleLabel.text, !title.isEmpty {
|
||||
stackView.addArrangedSubview(titleLabel)
|
||||
}
|
||||
if let subtitle = subtitleLabel.text, !subtitle.isEmpty {
|
||||
stackView.addArrangedSubview(subtitleLabel)
|
||||
}
|
||||
|
||||
addSubview(stackView)
|
||||
stackView.translatesAutoresizingMaskIntoConstraints = false
|
||||
|
||||
centerYConstraint = stackView.centerYAnchor.constraint(equalTo: centerYAnchor)
|
||||
NSLayoutConstraint.activate([
|
||||
stackView.centerXAnchor.constraint(equalTo: centerXAnchor),
|
||||
stackView.widthAnchor.constraint(lessThanOrEqualTo: widthAnchor, multiplier: 0.8),
|
||||
centerYConstraint
|
||||
])
|
||||
}
|
||||
|
||||
private func reloadConstraints() {
|
||||
let offset = keyboardHeight > 0 ? max(bounds.height / 2 - keyboardHeight, minOffsetFromTheKeyboardTop + stackView.frame.height) : containerModalYTranslation / 2
|
||||
let maxOffset = bounds.height / 2 - maxOffsetFromTheTop
|
||||
centerYConstraint.constant = -min(offset, maxOffset)
|
||||
layoutIfNeeded()
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - ModallyPresentedViewController
|
||||
extension PlaceholderView: ModallyPresentedViewController {
|
||||
func translationYDidUpdate(_ translationY: CGFloat) {
|
||||
self.containerModalYTranslation = translationY
|
||||
reloadConstraints()
|
||||
}
|
||||
}
|
|
@ -0,0 +1,30 @@
|
|||
/// 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
|
||||
}
|
||||
}
|
|
@ -0,0 +1,94 @@
|
|||
enum ModalScreenPresentationStep {
|
||||
case fullScreen
|
||||
case halfScreen
|
||||
case compact
|
||||
case hidden
|
||||
}
|
||||
|
||||
extension ModalScreenPresentationStep {
|
||||
private enum Constants {
|
||||
static let iPadWidth: CGFloat = 350
|
||||
static let compactHeightOffset: CGFloat = 120
|
||||
static let fullScreenHeightFactorPortrait: CGFloat = 0.1
|
||||
static let halfScreenHeightFactorPortrait: CGFloat = 0.55
|
||||
static let landscapeTopInset: CGFloat = 10
|
||||
}
|
||||
|
||||
var upper: ModalScreenPresentationStep {
|
||||
switch self {
|
||||
case .fullScreen:
|
||||
return .fullScreen
|
||||
case .halfScreen:
|
||||
return .fullScreen
|
||||
case .compact:
|
||||
return .halfScreen
|
||||
case .hidden:
|
||||
return .compact
|
||||
}
|
||||
}
|
||||
|
||||
var lower: ModalScreenPresentationStep {
|
||||
switch self {
|
||||
case .fullScreen:
|
||||
return .halfScreen
|
||||
case .halfScreen:
|
||||
return .compact
|
||||
case .compact:
|
||||
return .compact
|
||||
case .hidden:
|
||||
return .hidden
|
||||
}
|
||||
}
|
||||
|
||||
var first: ModalScreenPresentationStep {
|
||||
.fullScreen
|
||||
}
|
||||
|
||||
var last: ModalScreenPresentationStep {
|
||||
.compact
|
||||
}
|
||||
|
||||
func frame(for viewController: UIViewController, in containerView: UIView) -> CGRect {
|
||||
let isIPad = UIDevice.current.userInterfaceIdiom == .pad
|
||||
let containerSize = containerView.bounds.size
|
||||
let safeAreaInsets = containerView.safeAreaInsets
|
||||
var frame = CGRect(origin: .zero, size: containerSize)
|
||||
|
||||
if isIPad {
|
||||
frame.size.width = Constants.iPadWidth
|
||||
switch self {
|
||||
case .hidden:
|
||||
frame.origin.x = -Constants.iPadWidth
|
||||
default:
|
||||
frame.origin.x = .zero
|
||||
}
|
||||
return frame
|
||||
}
|
||||
|
||||
let isPortraitOrientation = viewController.traitCollection.verticalSizeClass == .regular
|
||||
if isPortraitOrientation {
|
||||
switch self {
|
||||
case .fullScreen:
|
||||
frame.origin.y = containerSize.height * Constants.fullScreenHeightFactorPortrait
|
||||
case .halfScreen:
|
||||
frame.origin.y = containerSize.height * Constants.halfScreenHeightFactorPortrait
|
||||
case .compact:
|
||||
frame.origin.y = containerSize.height - Constants.compactHeightOffset
|
||||
case .hidden:
|
||||
frame.origin.y = containerSize.height
|
||||
}
|
||||
} else {
|
||||
frame.size.width = Constants.iPadWidth
|
||||
frame.origin.x = safeAreaInsets.left
|
||||
switch self {
|
||||
case .fullScreen:
|
||||
frame.origin.y = Constants.landscapeTopInset
|
||||
case .halfScreen, .compact:
|
||||
frame.origin.y = containerSize.height - Constants.compactHeightOffset
|
||||
case .hidden:
|
||||
frame.origin.y = containerSize.height
|
||||
}
|
||||
}
|
||||
return frame
|
||||
}
|
||||
}
|
|
@ -0,0 +1,226 @@
|
|||
protocol ModallyPresentedViewController {
|
||||
func translationYDidUpdate(_ translationY: CGFloat)
|
||||
}
|
||||
|
||||
protocol SearchOnMapModalPresentationView: AnyObject {
|
||||
func setPresentationStep(_ step: ModalScreenPresentationStep)
|
||||
func close()
|
||||
}
|
||||
|
||||
final class SearchOnMapModalPresentationController: UIPresentationController {
|
||||
|
||||
private enum StepChangeAnimation {
|
||||
case slide
|
||||
case slideAndBounce
|
||||
}
|
||||
|
||||
private enum Constants {
|
||||
static let animationDuration: TimeInterval = kDefaultAnimationDuration
|
||||
static let springDamping: CGFloat = 0.85
|
||||
static let springVelocity: CGFloat = 0.2
|
||||
static let iPhoneCornerRadius: CGFloat = 10
|
||||
static let slowSwipeVelocity: CGFloat = 500
|
||||
static let fastSwipeDownVelocity: CGFloat = 4000
|
||||
static let fastSwipeUpVelocity: CGFloat = 3000
|
||||
static let translationThreshold: CGFloat = 50
|
||||
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 presentationStep: ModalScreenPresentationStep = .fullScreen
|
||||
private var internalScrollViewContentOffset: CGFloat = 0
|
||||
private var maxAvailableFrameOfPresentedView: CGRect = .zero
|
||||
|
||||
// MARK: - Init
|
||||
override init(presentedViewController: UIViewController, presenting presentingViewController: UIViewController?) {
|
||||
super.init(presentedViewController: presentedViewController, presenting: presentingViewController)
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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()
|
||||
}
|
||||
|
||||
// MARK: - Layout
|
||||
override var frameOfPresentedViewInContainerView: CGRect {
|
||||
guard let containerView else { return .zero }
|
||||
let frame = presentationStep.frame(for: presentedViewController, in: containerView)
|
||||
updateMaxAvailableFrameOfPresentedView()
|
||||
return frame
|
||||
}
|
||||
|
||||
private func updateMaxAvailableFrameOfPresentedView() {
|
||||
guard let containerView else { return }
|
||||
maxAvailableFrameOfPresentedView = ModalScreenPresentationStep.fullScreen.frame(for: presentedViewController, in: containerView)
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Pan gesture handling
|
||||
@objc private func handlePan(_ gesture: UIPanGestureRecognizer) {
|
||||
guard let presentedView, maxAvailableFrameOfPresentedView != .zero else { return }
|
||||
interactor?.handle(.didStartDraggingSearch)
|
||||
|
||||
let translation = gesture.translation(in: presentedView)
|
||||
let velocity = gesture.velocity(in: presentedView)
|
||||
|
||||
switch gesture.state {
|
||||
case .began:
|
||||
initialTranslationY = presentedView.frame.origin.y
|
||||
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
|
||||
if velocity.y > Constants.fastSwipeDownVelocity {
|
||||
interactor?.handle(.closeSearch)
|
||||
return
|
||||
} else if velocity.y < -Constants.fastSwipeUpVelocity {
|
||||
nextStep = .fullScreen // fast swipe up
|
||||
} else if velocity.y > Constants.slowSwipeVelocity || translation.y > Constants.translationThreshold {
|
||||
if presentationStep == .compact {
|
||||
interactor?.handle(.closeSearch)
|
||||
return
|
||||
}
|
||||
nextStep = presentationStep.lower // regular swipe down
|
||||
} else if velocity.y < -Constants.slowSwipeVelocity || translation.y < -Constants.translationThreshold {
|
||||
nextStep = presentationStep.upper // regular swipe up
|
||||
} else {
|
||||
// TODO: swipe to closest step on the big translation
|
||||
nextStep = presentationStep
|
||||
}
|
||||
let animation: StepChangeAnimation = abs(velocity.y) > Constants.slowSwipeVelocity ? .slideAndBounce : .slide
|
||||
animateTo(nextStep, animation: animation)
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
private func animateTo(_ presentationStep: ModalScreenPresentationStep, animation: StepChangeAnimation = .slide) {
|
||||
guard let presentedView, let containerView else { return }
|
||||
self.presentationStep = presentationStep
|
||||
interactor?.handle(.didUpdatePresentationStep(presentationStep))
|
||||
|
||||
let updatedFrame = presentationStep.frame(for: presentedViewController, in: containerView)
|
||||
let targetYTranslation = updatedFrame.origin.y
|
||||
|
||||
switch animation {
|
||||
case .slide:
|
||||
UIView.animate(withDuration: Constants.animationDuration,
|
||||
delay: 0,
|
||||
options: .curveEaseOut,
|
||||
animations: { [weak self] in
|
||||
presentedView.frame = updatedFrame
|
||||
self?.translationYDidUpdate(targetYTranslation)
|
||||
self?.updateSideButtonsAvailableArea(targetYTranslation)
|
||||
})
|
||||
case .slideAndBounce:
|
||||
UIView.animate(withDuration: Constants.animationDuration,
|
||||
delay: 0,
|
||||
usingSpringWithDamping: Constants.springDamping,
|
||||
initialSpringVelocity: Constants.springVelocity,
|
||||
options: .curveLinear,
|
||||
animations: { [weak self] in
|
||||
presentedView.frame = updatedFrame
|
||||
self?.translationYDidUpdate(targetYTranslation)
|
||||
self?.updateSideButtonsAvailableArea(targetYTranslation)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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 {
|
||||
func translationYDidUpdate(_ translationY: CGFloat) {
|
||||
iPhoneSpecific {
|
||||
(presentedViewController as? SearchOnMapViewController)?.translationYDidUpdate(translationY)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - UIGestureRecognizerDelegate
|
||||
extension SearchOnMapModalPresentationController: UIGestureRecognizerDelegate {
|
||||
func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
|
||||
true
|
||||
}
|
||||
|
||||
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
|
||||
// threshold is used to soften transition from the internal scroll zero content offset
|
||||
internalScrollViewContentOffset < Constants.panGestureThreshold
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - SearchOnMapScrollViewDelegate
|
||||
extension SearchOnMapModalPresentationController: SearchOnMapScrollViewDelegate {
|
||||
func scrollViewDidScroll(_ scrollView: UIScrollView) {
|
||||
guard let presentedView else { return }
|
||||
let hasReachedTheTop = Int(presentedView.frame.origin.y) > Int(maxAvailableFrameOfPresentedView.origin.y)
|
||||
let hasZeroContentOffset = internalScrollViewContentOffset == 0
|
||||
if hasReachedTheTop && hasZeroContentOffset {
|
||||
// prevent the internal scroll view scrolling
|
||||
scrollView.contentOffset.y = internalScrollViewContentOffset
|
||||
return
|
||||
}
|
||||
internalScrollViewContentOffset = scrollView.contentOffset.y
|
||||
}
|
||||
}
|
|
@ -0,0 +1,21 @@
|
|||
@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
|
||||
}
|
||||
}
|
|
@ -0,0 +1,22 @@
|
|||
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)
|
||||
})
|
||||
}
|
||||
}
|
|
@ -0,0 +1,25 @@
|
|||
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)
|
||||
})
|
||||
}
|
||||
}
|
113
iphone/Maps/UI/Search/SearchOnMap/SearchOnMapHeaderView.swift
Normal file
113
iphone/Maps/UI/Search/SearchOnMap/SearchOnMapHeaderView.swift
Normal file
|
@ -0,0 +1,113 @@
|
|||
protocol SearchOnMapHeaderViewDelegate: UISearchBarDelegate {
|
||||
func cancelButtonDidTap()
|
||||
}
|
||||
|
||||
final class SearchOnMapHeaderView: UIView {
|
||||
weak var delegate: SearchOnMapHeaderViewDelegate? {
|
||||
didSet {
|
||||
searchBar.delegate = delegate
|
||||
}
|
||||
}
|
||||
|
||||
private enum Constants {
|
||||
static let grabberHeight: CGFloat = 5
|
||||
static let grabberWidth: CGFloat = 36
|
||||
static let grabberTopMargin: CGFloat = 5
|
||||
static let cancelButtonInsets: UIEdgeInsets = UIEdgeInsets(top: 0, left: 6, bottom: 0, right: 8)
|
||||
}
|
||||
|
||||
private let grabberView = UIView()
|
||||
private let searchBar = UISearchBar()
|
||||
private let cancelButton = UIButton()
|
||||
|
||||
override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
setupView()
|
||||
layoutView()
|
||||
}
|
||||
|
||||
@available(*, unavailable)
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
private func setupView() {
|
||||
setStyle(.searchHeader)
|
||||
layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner]
|
||||
|
||||
setupGrabberView()
|
||||
setupSearchBar()
|
||||
setupCancelButton()
|
||||
}
|
||||
|
||||
private func setupGrabberView() {
|
||||
grabberView.setStyle(.background)
|
||||
grabberView.layer.setCorner(radius: Constants.grabberHeight / 2)
|
||||
iPadSpecific { [weak self] in
|
||||
self?.grabberView.isHidden = true
|
||||
}
|
||||
}
|
||||
|
||||
private func setupSearchBar() {
|
||||
searchBar.placeholder = L("search")
|
||||
searchBar.showsCancelButton = false
|
||||
if #available(iOS 13.0, *) {
|
||||
searchBar.searchTextField.clearButtonMode = .always
|
||||
searchBar.returnKeyType = .search
|
||||
searchBar.searchTextField.enablesReturnKeyAutomatically = true
|
||||
}
|
||||
}
|
||||
|
||||
private func setupCancelButton() {
|
||||
cancelButton.tintColor = .whitePrimaryText()
|
||||
cancelButton.setStyle(.clearBackground)
|
||||
cancelButton.setTitle(L("cancel"), for: .normal)
|
||||
cancelButton.addTarget(self, action: #selector(cancelButtonTapped), for: .touchUpInside)
|
||||
}
|
||||
|
||||
private func layoutView() {
|
||||
addSubview(grabberView)
|
||||
addSubview(searchBar)
|
||||
addSubview(cancelButton)
|
||||
|
||||
grabberView.translatesAutoresizingMaskIntoConstraints = false
|
||||
searchBar.translatesAutoresizingMaskIntoConstraints = false
|
||||
cancelButton.translatesAutoresizingMaskIntoConstraints = false
|
||||
|
||||
NSLayoutConstraint.activate([
|
||||
grabberView.topAnchor.constraint(equalTo: topAnchor, constant: Constants.grabberTopMargin),
|
||||
grabberView.centerXAnchor.constraint(equalTo: centerXAnchor),
|
||||
grabberView.widthAnchor.constraint(equalToConstant: Constants.grabberWidth),
|
||||
grabberView.heightAnchor.constraint(equalToConstant: Constants.grabberHeight),
|
||||
|
||||
searchBar.topAnchor.constraint(equalTo: grabberView.bottomAnchor),
|
||||
searchBar.leadingAnchor.constraint(equalTo: leadingAnchor),
|
||||
searchBar.trailingAnchor.constraint(equalTo: cancelButton.leadingAnchor, constant: -Constants.cancelButtonInsets.left),
|
||||
|
||||
cancelButton.centerYAnchor.constraint(equalTo: searchBar.centerYAnchor),
|
||||
cancelButton.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -Constants.cancelButtonInsets.right),
|
||||
|
||||
bottomAnchor.constraint(equalTo: searchBar.bottomAnchor)
|
||||
])
|
||||
}
|
||||
|
||||
@objc private func cancelButtonTapped() {
|
||||
delegate?.cancelButtonDidTap()
|
||||
}
|
||||
|
||||
func setSearchText(_ text: String) {
|
||||
searchBar.text = text
|
||||
}
|
||||
|
||||
func setIsSearching(_ isSearching: Bool) {
|
||||
if isSearching {
|
||||
searchBar.becomeFirstResponder()
|
||||
} else if searchBar.isFirstResponder {
|
||||
searchBar.resignFirstResponder()
|
||||
}
|
||||
}
|
||||
|
||||
var searchText: SearchOnMap.SearchText {
|
||||
SearchOnMap.SearchText(searchBar.text ?? "", locale: searchBar.textInputMode?.primaryLanguage)
|
||||
}
|
||||
}
|
165
iphone/Maps/UI/Search/SearchOnMap/SearchOnMapInteractor.swift
Normal file
165
iphone/Maps/UI/Search/SearchOnMap/SearchOnMapInteractor.swift
Normal file
|
@ -0,0 +1,165 @@
|
|||
final class SearchOnMapInteractor: NSObject {
|
||||
|
||||
private let presenter: SearchOnMapPresenter
|
||||
private let searchManager: SearchManager.Type
|
||||
private let routeManager: MWMRouter.Type
|
||||
private var isUpdatesDisabled = false
|
||||
private var showResultsOnMap: Bool = false
|
||||
|
||||
var routingTooltipSearch: SearchOnMapRoutingTooltipSearch = .none
|
||||
|
||||
init(presenter: SearchOnMapPresenter,
|
||||
searchManager: SearchManager.Type = Search.self,
|
||||
routeManager: MWMRouter.Type = MWMRouter.self) {
|
||||
self.presenter = presenter
|
||||
self.searchManager = searchManager
|
||||
self.routeManager = routeManager
|
||||
super.init()
|
||||
searchManager.add(self)
|
||||
}
|
||||
|
||||
deinit {
|
||||
searchManager.remove(self)
|
||||
}
|
||||
|
||||
func handle(_ event: SearchOnMap.Request) {
|
||||
let response = resolve(event)
|
||||
presenter.process(response)
|
||||
}
|
||||
|
||||
private func resolve(_ event: SearchOnMap.Request) -> SearchOnMap.Response {
|
||||
switch event {
|
||||
case .openSearch:
|
||||
return .showHistoryAndCategory
|
||||
case .hideSearch:
|
||||
return .setSearchScreenHidden(true)
|
||||
case .didStartDraggingSearch:
|
||||
return .setIsTyping(false)
|
||||
case .didStartTyping:
|
||||
return .setIsTyping(true)
|
||||
case .didType(let searchText):
|
||||
return processTypedText(searchText)
|
||||
case .clearButtonDidTap:
|
||||
return processClearButtonDidTap()
|
||||
case .didSelectText(let searchText, let isCategory):
|
||||
return processSelectedText(searchText, isCategory: isCategory)
|
||||
case .searchButtonDidTap(let searchText):
|
||||
return processSearchButtonDidTap(searchText)
|
||||
case .didSelectResult(let result, let index, let searchText):
|
||||
return processSelectedResult(result, index: index, searchText: searchText)
|
||||
case .didSelectPlaceOnMap:
|
||||
return isIPad ? .none : .setSearchScreenHidden(true)
|
||||
case .didDeselectPlaceOnMap:
|
||||
return deselectPlaceOnMap()
|
||||
case .didStartDraggingMap:
|
||||
return .setSearchScreenCompact
|
||||
case .didUpdatePresentationStep(let step):
|
||||
return .updatePresentationStep(step)
|
||||
case .closeSearch:
|
||||
return closeSearch()
|
||||
}
|
||||
}
|
||||
|
||||
private func processClearButtonDidTap() -> SearchOnMap.Response {
|
||||
isUpdatesDisabled = true
|
||||
searchManager.clear()
|
||||
return .clearSearch
|
||||
}
|
||||
|
||||
private func processSearchButtonDidTap(_ searchText: SearchOnMap.SearchText) -> SearchOnMap.Response {
|
||||
searchManager.saveQuery(searchText.text,
|
||||
forInputLocale: searchText.locale)
|
||||
showResultsOnMap = true
|
||||
return .showOnTheMap
|
||||
}
|
||||
|
||||
private func processTypedText(_ searchText: SearchOnMap.SearchText) -> SearchOnMap.Response {
|
||||
isUpdatesDisabled = false
|
||||
showResultsOnMap = true
|
||||
searchManager.searchQuery(searchText.text,
|
||||
forInputLocale: searchText.locale,
|
||||
withCategory: false)
|
||||
return .startSearching
|
||||
}
|
||||
|
||||
private func processSelectedText(_ searchText: SearchOnMap.SearchText, isCategory: Bool) -> SearchOnMap.Response {
|
||||
isUpdatesDisabled = false
|
||||
searchManager.saveQuery(searchText.text,
|
||||
forInputLocale: searchText.locale)
|
||||
searchManager.searchQuery(searchText.text,
|
||||
forInputLocale: searchText.locale,
|
||||
withCategory: isCategory)
|
||||
showResultsOnMap = true
|
||||
return .selectText(searchText.text)
|
||||
}
|
||||
|
||||
private func processSelectedResult(_ result: SearchResult, index: Int, searchText: SearchOnMap.SearchText) -> SearchOnMap.Response {
|
||||
switch result.itemType {
|
||||
case .regular:
|
||||
searchManager.saveQuery(searchText.text,
|
||||
forInputLocale:searchText.locale)
|
||||
switch routingTooltipSearch {
|
||||
case .none:
|
||||
searchManager.showResult(at: UInt(index))
|
||||
case .start:
|
||||
let point = MWMRoutePoint(cgPoint: result.point,
|
||||
title: result.titleText,
|
||||
subtitle: result.addressText,
|
||||
type: .start,
|
||||
intermediateIndex: 0)
|
||||
routeManager.build(from: point, bestRouter: false)
|
||||
case .finish:
|
||||
let point = MWMRoutePoint(cgPoint: result.point,
|
||||
title: result.titleText,
|
||||
subtitle: result.addressText,
|
||||
type: .finish,
|
||||
intermediateIndex: 0)
|
||||
routeManager.build(to: point, bestRouter: false)
|
||||
@unknown default:
|
||||
fatalError("Unsupported routingTooltipSearch")
|
||||
}
|
||||
return isIPad ? .none : .setSearchScreenHidden(true)
|
||||
case .suggestion:
|
||||
searchManager.searchQuery(result.suggestion,
|
||||
forInputLocale: searchText.locale,
|
||||
withCategory: result.isPureSuggest)
|
||||
return .selectText(result.suggestion)
|
||||
@unknown default:
|
||||
fatalError("Unsupported result type")
|
||||
}
|
||||
}
|
||||
|
||||
private func deselectPlaceOnMap() -> SearchOnMap.Response {
|
||||
routingTooltipSearch = .none
|
||||
searchManager.showViewportSearchResultsOnMap()
|
||||
return .setSearchScreenHidden(false)
|
||||
}
|
||||
|
||||
private func closeSearch() -> SearchOnMap.Response {
|
||||
routingTooltipSearch = .none
|
||||
isUpdatesDisabled = true
|
||||
showResultsOnMap = false
|
||||
searchManager.clear()
|
||||
return .close
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - MWMSearchObserver
|
||||
extension SearchOnMapInteractor: MWMSearchObserver {
|
||||
func onSearchCompleted() {
|
||||
guard !isUpdatesDisabled else { return }
|
||||
let results = searchManager.getResults()
|
||||
if showResultsOnMap && !results.isEmpty {
|
||||
searchManager.showEverywhereSearchResultsOnMap()
|
||||
showResultsOnMap = false
|
||||
}
|
||||
presenter.process(.showResults(SearchOnMap.SearchResults(results), isSearchCompleted: true))
|
||||
}
|
||||
|
||||
func onSearchResultsUpdated() {
|
||||
guard !isUpdatesDisabled else { return }
|
||||
let results = searchManager.getResults()
|
||||
guard !results.isEmpty else { return }
|
||||
presenter.process(.showResults(SearchOnMap.SearchResults(results), isSearchCompleted: false))
|
||||
}
|
||||
}
|
96
iphone/Maps/UI/Search/SearchOnMap/SearchOnMapManager.swift
Normal file
96
iphone/Maps/UI/Search/SearchOnMap/SearchOnMapManager.swift
Normal file
|
@ -0,0 +1,96 @@
|
|||
@objc
|
||||
enum SearchOnMapState: Int {
|
||||
case searching
|
||||
case hidden
|
||||
case closed
|
||||
}
|
||||
|
||||
@objc
|
||||
enum SearchOnMapRoutingTooltipSearch: Int {
|
||||
case none
|
||||
case start
|
||||
case finish
|
||||
}
|
||||
|
||||
@objc
|
||||
protocol SearchOnMapManagerObserver: AnyObject {
|
||||
func searchManager(didChangeState state: SearchOnMapState)
|
||||
}
|
||||
|
||||
@objcMembers
|
||||
final class SearchOnMapManager: NSObject {
|
||||
private let navigationController: UINavigationController
|
||||
private weak var interactor: SearchOnMapInteractor?
|
||||
private let observers = ListenerContainer<SearchOnMapManagerObserver>()
|
||||
|
||||
// MARK: - Public properties
|
||||
weak var viewController: UIViewController?
|
||||
var isSearching: Bool { viewController != nil }
|
||||
|
||||
init(navigationController: UINavigationController = MapViewController.shared()!.navigationController!) {
|
||||
self.navigationController = navigationController
|
||||
}
|
||||
|
||||
// MARK: - Public methods
|
||||
func startSearching(isRouting: Bool) {
|
||||
if viewController != nil {
|
||||
interactor?.handle(.openSearch)
|
||||
return
|
||||
}
|
||||
FrameworkHelper.deactivateMapSelection()
|
||||
let viewController = buildViewController(isRouting: isRouting)
|
||||
self.viewController = viewController
|
||||
self.interactor = viewController.interactor
|
||||
navigationController.present(viewController, animated: true)
|
||||
}
|
||||
|
||||
func hide() {
|
||||
interactor?.handle(.hideSearch)
|
||||
}
|
||||
|
||||
func close() {
|
||||
interactor?.handle(.closeSearch)
|
||||
}
|
||||
|
||||
func setRoutingTooltip(_ tooltip: SearchOnMapRoutingTooltipSearch) {
|
||||
interactor?.routingTooltipSearch = tooltip
|
||||
}
|
||||
|
||||
func setPlaceOnMapSelected(_ isSelected: Bool) {
|
||||
interactor?.handle(isSelected ? .didSelectPlaceOnMap : .didDeselectPlaceOnMap)
|
||||
}
|
||||
|
||||
func setMapIsDragging() {
|
||||
interactor?.handle(.didStartDraggingMap)
|
||||
}
|
||||
|
||||
func searchText(_ text: String, locale: String, isCategory: Bool) {
|
||||
let searchText = SearchOnMap.SearchText(text, locale: locale)
|
||||
interactor?.handle(.didSelectText(searchText, isCategory: isCategory))
|
||||
}
|
||||
|
||||
func addObserver(_ observer: SearchOnMapManagerObserver) {
|
||||
observers.addListener(observer)
|
||||
}
|
||||
|
||||
func removeObserver(_ observer: SearchOnMapManagerObserver) {
|
||||
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) }
|
||||
})
|
||||
let interactor = SearchOnMapInteractor(presenter: presenter)
|
||||
let viewController = SearchOnMapViewController(interactor: interactor)
|
||||
presenter.view = viewController
|
||||
viewController.modalPresentationStyle = .custom
|
||||
viewController.transitioningDelegate = transitioningManager
|
||||
return viewController
|
||||
}
|
||||
}
|
94
iphone/Maps/UI/Search/SearchOnMap/SearchOnMapModels.swift
Normal file
94
iphone/Maps/UI/Search/SearchOnMap/SearchOnMapModels.swift
Normal file
|
@ -0,0 +1,94 @@
|
|||
enum SearchOnMap {
|
||||
struct ViewModel: Equatable {
|
||||
enum ContentState: Equatable {
|
||||
case historyAndCategory
|
||||
case results(SearchResults)
|
||||
case noResults
|
||||
case searching
|
||||
}
|
||||
|
||||
var isTyping: Bool
|
||||
var skipSuggestions: Bool
|
||||
var searchingText: String?
|
||||
var contentState: ContentState
|
||||
var presentationStep: ModalScreenPresentationStep
|
||||
}
|
||||
|
||||
struct SearchResults: Equatable {
|
||||
let results: [SearchResult]
|
||||
let hasPartialMatch: Bool
|
||||
let isEmpty: Bool
|
||||
let count: Int
|
||||
let suggestionsCount: Int
|
||||
|
||||
init(_ results: [SearchResult]) {
|
||||
self.results = results
|
||||
self.hasPartialMatch = !results.allSatisfy { $0.highlightRanges.isEmpty }
|
||||
self.isEmpty = results.isEmpty
|
||||
self.count = results.count
|
||||
self.suggestionsCount = results.filter { $0.itemType == .suggestion }.count
|
||||
}
|
||||
}
|
||||
|
||||
struct SearchText {
|
||||
let text: String
|
||||
let locale: String
|
||||
|
||||
init(_ text: String, locale: String? = nil) {
|
||||
self.text = text
|
||||
self.locale = locale ?? AppInfo.shared().languageId
|
||||
}
|
||||
}
|
||||
|
||||
enum Request {
|
||||
case openSearch
|
||||
case hideSearch
|
||||
case closeSearch
|
||||
case didStartDraggingSearch
|
||||
case didStartDraggingMap
|
||||
case didStartTyping
|
||||
case didType(SearchText)
|
||||
case didSelectText(SearchText, isCategory: Bool)
|
||||
case didSelectResult(SearchResult, atIndex: Int, withSearchText: SearchText)
|
||||
case searchButtonDidTap(SearchText)
|
||||
case clearButtonDidTap
|
||||
case didSelectPlaceOnMap
|
||||
case didDeselectPlaceOnMap
|
||||
case didUpdatePresentationStep(ModalScreenPresentationStep)
|
||||
}
|
||||
|
||||
enum Response: Equatable {
|
||||
case startSearching
|
||||
case showOnTheMap
|
||||
case setIsTyping(Bool)
|
||||
case showHistoryAndCategory
|
||||
case showResults(SearchResults, isSearchCompleted: Bool = false)
|
||||
case selectText(String?)
|
||||
case clearSearch
|
||||
case setSearchScreenHidden(Bool)
|
||||
case setSearchScreenCompact
|
||||
case updatePresentationStep(ModalScreenPresentationStep)
|
||||
case close
|
||||
case none
|
||||
}
|
||||
}
|
||||
|
||||
extension SearchOnMap.SearchResults {
|
||||
static let empty = SearchOnMap.SearchResults([])
|
||||
|
||||
subscript(index: Int) -> SearchResult {
|
||||
results[index]
|
||||
}
|
||||
|
||||
mutating func skipSuggestions() {
|
||||
self = SearchOnMap.SearchResults(results.filter { $0.itemType != .suggestion })
|
||||
}
|
||||
}
|
||||
|
||||
extension SearchOnMap.ViewModel {
|
||||
static let initial = SearchOnMap.ViewModel(isTyping: false,
|
||||
skipSuggestions: false,
|
||||
searchingText: nil,
|
||||
contentState: .historyAndCategory,
|
||||
presentationStep: .fullScreen)
|
||||
}
|
117
iphone/Maps/UI/Search/SearchOnMap/SearchOnMapPresenter.swift
Normal file
117
iphone/Maps/UI/Search/SearchOnMap/SearchOnMapPresenter.swift
Normal file
|
@ -0,0 +1,117 @@
|
|||
final class SearchOnMapPresenter {
|
||||
typealias Response = SearchOnMap.Response
|
||||
typealias ViewModel = SearchOnMap.ViewModel
|
||||
|
||||
weak var view: SearchOnMapView?
|
||||
weak var presentationView: SearchOnMapModalPresentationView? { transitionManager.presentationController }
|
||||
|
||||
private var searchState: SearchOnMapState = .searching {
|
||||
didSet {
|
||||
guard searchState != oldValue else { return }
|
||||
didChangeState?(searchState)
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
self.isRouting = isRouting
|
||||
self.didChangeState = didChangeState
|
||||
didChangeState?(searchState)
|
||||
}
|
||||
|
||||
func process(_ response: SearchOnMap.Response) {
|
||||
guard response != .none else { return }
|
||||
|
||||
if response == .close {
|
||||
searchState = .closed
|
||||
presentationView?.close()
|
||||
return
|
||||
}
|
||||
|
||||
let showSearch = response == .setSearchScreenHidden(false) || response == .showHistoryAndCategory
|
||||
guard viewModel.presentationStep != .hidden || showSearch else {
|
||||
return
|
||||
}
|
||||
|
||||
let newViewModel = resolve(action: response, with: viewModel)
|
||||
if viewModel != newViewModel {
|
||||
viewModel = newViewModel
|
||||
view?.render(newViewModel)
|
||||
searchState = newViewModel.presentationStep.searchState
|
||||
presentationView?.setPresentationStep(newViewModel.presentationStep)
|
||||
}
|
||||
}
|
||||
|
||||
private func resolve(action: Response, with previousViewModel: ViewModel) -> ViewModel {
|
||||
var viewModel = previousViewModel
|
||||
viewModel.searchingText = nil // should not be nil only when the text is passed to the search field
|
||||
|
||||
switch action {
|
||||
case .startSearching:
|
||||
viewModel.isTyping = true
|
||||
viewModel.skipSuggestions = false
|
||||
viewModel.contentState = .searching
|
||||
case .showOnTheMap:
|
||||
viewModel.isTyping = false
|
||||
viewModel.skipSuggestions = true
|
||||
viewModel.presentationStep = isRouting ? .hidden : .halfScreen
|
||||
if case .results(var results) = viewModel.contentState, !results.isEmpty {
|
||||
results.skipSuggestions()
|
||||
viewModel.contentState = .results(results)
|
||||
}
|
||||
case .setIsTyping(let isSearching):
|
||||
viewModel.isTyping = isSearching
|
||||
if isSearching {
|
||||
viewModel.presentationStep = .fullScreen
|
||||
}
|
||||
case .showHistoryAndCategory:
|
||||
viewModel.isTyping = true
|
||||
viewModel.contentState = .historyAndCategory
|
||||
viewModel.presentationStep = .fullScreen
|
||||
case .showResults(var searchResults, let isSearchCompleted):
|
||||
if (viewModel.skipSuggestions) {
|
||||
searchResults.skipSuggestions()
|
||||
}
|
||||
viewModel.contentState = searchResults.isEmpty && isSearchCompleted ? .noResults : .results(searchResults)
|
||||
case .selectText(let text):
|
||||
viewModel.isTyping = false
|
||||
viewModel.skipSuggestions = false
|
||||
viewModel.searchingText = text
|
||||
viewModel.contentState = .searching
|
||||
viewModel.presentationStep = isRouting ? .hidden : .halfScreen
|
||||
case .clearSearch:
|
||||
viewModel.searchingText = ""
|
||||
viewModel.isTyping = true
|
||||
viewModel.skipSuggestions = false
|
||||
viewModel.contentState = .historyAndCategory
|
||||
viewModel.presentationStep = .fullScreen
|
||||
case .setSearchScreenHidden(let isHidden):
|
||||
viewModel.isTyping = false
|
||||
viewModel.presentationStep = isHidden ? .hidden : (isRouting ? .fullScreen : .halfScreen)
|
||||
case .setSearchScreenCompact:
|
||||
viewModel.isTyping = false
|
||||
viewModel.presentationStep = .compact
|
||||
case .updatePresentationStep(let step):
|
||||
viewModel.presentationStep = step
|
||||
case .close, .none:
|
||||
break
|
||||
}
|
||||
return viewModel
|
||||
}
|
||||
}
|
||||
|
||||
private extension ModalScreenPresentationStep {
|
||||
var searchState: SearchOnMapState {
|
||||
switch self {
|
||||
case .fullScreen, .halfScreen, .compact:
|
||||
return .searching
|
||||
case .hidden:
|
||||
return .hidden
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,362 @@
|
|||
protocol SearchOnMapView: AnyObject {
|
||||
var scrollViewDelegate: SearchOnMapScrollViewDelegate? { get set }
|
||||
|
||||
func render(_ viewModel: SearchOnMap.ViewModel)
|
||||
}
|
||||
|
||||
@objc
|
||||
protocol SearchOnMapScrollViewDelegate: AnyObject {
|
||||
func scrollViewDidScroll(_ scrollView: UIScrollView)
|
||||
}
|
||||
|
||||
final class SearchOnMapViewController: UIViewController {
|
||||
typealias ViewModel = SearchOnMap.ViewModel
|
||||
typealias ContentState = SearchOnMap.ViewModel.ContentState
|
||||
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
|
||||
weak var scrollViewDelegate: SearchOnMapScrollViewDelegate?
|
||||
|
||||
private var searchResults = SearchOnMap.SearchResults([])
|
||||
|
||||
// MARK: - UI Elements
|
||||
private let headerView = SearchOnMapHeaderView()
|
||||
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
|
||||
super.init(nibName: nil, bundle: nil)
|
||||
}
|
||||
|
||||
@available(*, unavailable)
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
deinit {
|
||||
NotificationCenter.default.removeObserver(self)
|
||||
}
|
||||
|
||||
// MARK: - Lifecycle
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
setupViews()
|
||||
layoutViews()
|
||||
interactor.handle(.openSearch)
|
||||
}
|
||||
|
||||
override func viewWillDisappear(_ animated: Bool) {
|
||||
super.viewWillDisappear(animated)
|
||||
headerView.setIsSearching(false)
|
||||
}
|
||||
|
||||
// MARK: - Private methods
|
||||
private func setupViews() {
|
||||
view.setStyle(.background)
|
||||
setupTapGestureRecognizer()
|
||||
setupHeaderView()
|
||||
setupContainerView()
|
||||
setupResultsTableView()
|
||||
setupHistoryAndCategoryTabView()
|
||||
setupResultsTableView()
|
||||
setupFiltersCollectionView()
|
||||
}
|
||||
|
||||
private func setupTapGestureRecognizer() {
|
||||
let tapGesture = UITapGestureRecognizer(target: self, action: #selector(handleTapOutside))
|
||||
tapGesture.cancelsTouchesInView = false
|
||||
view.addGestureRecognizer(tapGesture)
|
||||
}
|
||||
|
||||
private func setupHeaderView() {
|
||||
headerView.delegate = self
|
||||
}
|
||||
|
||||
private func setupContainerView() {
|
||||
containerView.setStyle(.background)
|
||||
}
|
||||
|
||||
private func setupResultsTableView() {
|
||||
resultsTableView.setStyle(.background)
|
||||
resultsTableView.estimatedRowHeight = Constants.estimatedRowHeight
|
||||
resultsTableView.rowHeight = UITableView.automaticDimension
|
||||
resultsTableView.registerNib(cellClass: SearchSuggestionCell.self)
|
||||
resultsTableView.registerNib(cellClass: SearchCommonCell.self)
|
||||
resultsTableView.dataSource = self
|
||||
resultsTableView.delegate = self
|
||||
resultsTableView.keyboardDismissMode = .onDrag
|
||||
}
|
||||
|
||||
private func setupHistoryAndCategoryTabView() {
|
||||
historyAndCategoryTabViewController.delegate = self
|
||||
}
|
||||
|
||||
// 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)
|
||||
headerView.translatesAutoresizingMaskIntoConstraints = false
|
||||
containerView.translatesAutoresizingMaskIntoConstraints = false
|
||||
|
||||
NSLayoutConstraint.activate([
|
||||
headerView.topAnchor.constraint(equalTo: view.topAnchor),
|
||||
headerView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
|
||||
headerView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
|
||||
|
||||
containerView.topAnchor.constraint(equalTo: headerView.bottomAnchor),
|
||||
containerView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
|
||||
containerView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
|
||||
containerView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
|
||||
])
|
||||
|
||||
layoutResultsView()
|
||||
layoutHistoryAndCategoryTabView()
|
||||
layoutSearchNoResultsView()
|
||||
layoutSearchingView()
|
||||
}
|
||||
|
||||
private func layoutResultsView() {
|
||||
containerView.addSubview(resultsTableView)
|
||||
resultsTableView.translatesAutoresizingMaskIntoConstraints = false
|
||||
|
||||
NSLayoutConstraint.activate([
|
||||
resultsTableView.topAnchor.constraint(equalTo: containerView.topAnchor),
|
||||
resultsTableView.leadingAnchor.constraint(equalTo: containerView.leadingAnchor),
|
||||
resultsTableView.trailingAnchor.constraint(equalTo: containerView.trailingAnchor),
|
||||
resultsTableView.bottomAnchor.constraint(equalTo: containerView.bottomAnchor)
|
||||
])
|
||||
}
|
||||
|
||||
private func layoutHistoryAndCategoryTabView() {
|
||||
containerView.addSubview(historyAndCategoryTabViewController.view)
|
||||
historyAndCategoryTabViewController.view.translatesAutoresizingMaskIntoConstraints = false
|
||||
|
||||
NSLayoutConstraint.activate([
|
||||
historyAndCategoryTabViewController.view.topAnchor.constraint(equalTo: containerView.topAnchor),
|
||||
historyAndCategoryTabViewController.view.leadingAnchor.constraint(equalTo: containerView.leadingAnchor),
|
||||
historyAndCategoryTabViewController.view.trailingAnchor.constraint(equalTo: containerView.trailingAnchor),
|
||||
historyAndCategoryTabViewController.view.bottomAnchor.constraint(equalTo: containerView.bottomAnchor)
|
||||
])
|
||||
}
|
||||
|
||||
private func layoutSearchNoResultsView() {
|
||||
searchNoResultsView.translatesAutoresizingMaskIntoConstraints = false
|
||||
containerView.addSubview(searchNoResultsView)
|
||||
NSLayoutConstraint.activate([
|
||||
searchNoResultsView.topAnchor.constraint(equalTo: containerView.topAnchor),
|
||||
searchNoResultsView.leadingAnchor.constraint(equalTo: containerView.leadingAnchor),
|
||||
searchNoResultsView.trailingAnchor.constraint(equalTo: containerView.trailingAnchor),
|
||||
searchNoResultsView.bottomAnchor.constraint(equalTo: containerView.bottomAnchor)
|
||||
])
|
||||
}
|
||||
|
||||
private func layoutSearchingView() {
|
||||
containerView.insertSubview(searchingActivityView, at: 0)
|
||||
searchingActivityView.translatesAutoresizingMaskIntoConstraints = false
|
||||
NSLayoutConstraint.activate([
|
||||
searchingActivityView.leadingAnchor.constraint(equalTo: containerView.leadingAnchor),
|
||||
searchingActivityView.trailingAnchor.constraint(equalTo: containerView.trailingAnchor),
|
||||
searchingActivityView.topAnchor.constraint(equalTo: containerView.topAnchor),
|
||||
searchingActivityView.bottomAnchor.constraint(equalTo: containerView.bottomAnchor)
|
||||
])
|
||||
}
|
||||
|
||||
// MARK: - Handle Button Actions
|
||||
@objc private func handleTapOutside(_ gesture: UITapGestureRecognizer) {
|
||||
let location = gesture.location(in: view)
|
||||
if resultsTableView.frame.contains(location) && searchResults.isEmpty {
|
||||
headerView.setIsSearching(false)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Handle State Updates
|
||||
private func setContent(_ content: ContentState) {
|
||||
switch content {
|
||||
case .historyAndCategory:
|
||||
historyAndCategoryTabViewController.reloadSearchHistory()
|
||||
case let .results(results):
|
||||
if searchResults != results {
|
||||
searchResults = results
|
||||
resultsTableView.reloadData()
|
||||
}
|
||||
case .noResults:
|
||||
searchResults = .empty
|
||||
resultsTableView.reloadData()
|
||||
case .searching:
|
||||
break
|
||||
}
|
||||
showView(viewToShow(for: content))
|
||||
}
|
||||
|
||||
private func viewToShow(for content: ContentState) -> UIView {
|
||||
switch content {
|
||||
case .historyAndCategory:
|
||||
return historyAndCategoryTabViewController.view
|
||||
case .results:
|
||||
return resultsTableView
|
||||
case .noResults:
|
||||
return searchNoResultsView
|
||||
case .searching:
|
||||
return searchingActivityView
|
||||
}
|
||||
}
|
||||
|
||||
private func showView(_ view: UIView) {
|
||||
let viewsToHide: [UIView] = [resultsTableView,
|
||||
historyAndCategoryTabViewController.view,
|
||||
searchNoResultsView,
|
||||
searchingActivityView].filter { $0 != view }
|
||||
UIView.transition(with: containerView,
|
||||
duration: kDefaultAnimationDuration / 2,
|
||||
options: [.transitionCrossDissolve, .curveEaseInOut], animations: {
|
||||
viewsToHide.forEach { viewToHide in
|
||||
view.isHidden = false
|
||||
view.alpha = 1
|
||||
viewToHide.isHidden = true
|
||||
viewToHide.alpha = 0
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private func setIsSearching(_ isSearching: Bool) {
|
||||
headerView.setIsSearching(isSearching)
|
||||
}
|
||||
|
||||
private func replaceSearchText(with text: String) {
|
||||
headerView.setSearchText(text)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Public methods
|
||||
extension SearchOnMapViewController: SearchOnMapView {
|
||||
func render(_ viewModel: ViewModel) {
|
||||
setContent(viewModel.contentState)
|
||||
setIsSearching(viewModel.isTyping)
|
||||
if let searchingText = viewModel.searchingText {
|
||||
replaceSearchText(with: searchingText)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - ModallyPresentedViewController
|
||||
extension SearchOnMapViewController: ModallyPresentedViewController {
|
||||
func translationYDidUpdate(_ translationY: CGFloat) {
|
||||
self.containerModalYTranslation = translationY
|
||||
resultsTableView.contentInset.bottom = translationY
|
||||
historyAndCategoryTabViewController.translationYDidUpdate(translationY)
|
||||
searchNoResultsView.translationYDidUpdate(translationY)
|
||||
searchingActivityView.translationYDidUpdate(translationY)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - UITableViewDataSource
|
||||
extension SearchOnMapViewController: UITableViewDataSource {
|
||||
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
|
||||
searchResults.count
|
||||
}
|
||||
|
||||
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
|
||||
let result = searchResults[indexPath.row]
|
||||
switch result.itemType {
|
||||
case .regular:
|
||||
let cell = tableView.dequeueReusableCell(cell: SearchCommonCell.self, indexPath: indexPath)
|
||||
cell.configure(with: result, isPartialMatching: searchResults.hasPartialMatch)
|
||||
return cell
|
||||
case .suggestion:
|
||||
let cell = tableView.dequeueReusableCell(cell: SearchSuggestionCell.self, indexPath: indexPath)
|
||||
cell.configure(with: result, isPartialMatching: true)
|
||||
cell.isLastCell = indexPath.row == searchResults.suggestionsCount - 1
|
||||
return cell
|
||||
@unknown default:
|
||||
fatalError("Unknown item type")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - UITableViewDelegate
|
||||
extension SearchOnMapViewController: UITableViewDelegate {
|
||||
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
|
||||
let result = searchResults[indexPath.row]
|
||||
interactor.handle(.didSelectResult(result, atIndex: indexPath.row, withSearchText: headerView.searchText))
|
||||
tableView.deselectRow(at: indexPath, animated: true)
|
||||
}
|
||||
|
||||
func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
|
||||
interactor.handle(.didStartDraggingSearch)
|
||||
}
|
||||
|
||||
func scrollViewDidScroll(_ scrollView: UIScrollView) {
|
||||
scrollViewDelegate?.scrollViewDidScroll(scrollView)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - UICollectionViewDataSource
|
||||
extension SearchOnMapViewController: UICollectionViewDataSource {
|
||||
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
|
||||
// TODO: remove search from here
|
||||
Int(Search.resultsCount())
|
||||
}
|
||||
|
||||
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
|
||||
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "FilterCell", for: indexPath)
|
||||
return cell
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - SearchOnMapHeaderViewDelegate
|
||||
extension SearchOnMapViewController: SearchOnMapHeaderViewDelegate {
|
||||
func searchBarTextDidBeginEditing(_ searchBar: UISearchBar) {
|
||||
interactor.handle(.didStartTyping)
|
||||
}
|
||||
|
||||
func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) {
|
||||
guard !searchText.isEmpty else {
|
||||
interactor.handle(.clearButtonDidTap)
|
||||
return
|
||||
}
|
||||
interactor.handle(.didType(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)))
|
||||
}
|
||||
|
||||
func cancelButtonDidTap() {
|
||||
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))
|
||||
}
|
||||
}
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
protocol SearchCategoriesViewControllerDelegate: AnyObject {
|
||||
protocol SearchCategoriesViewControllerDelegate: SearchOnMapScrollViewDelegate {
|
||||
func categoriesViewController(_ viewController: SearchCategoriesViewController,
|
||||
didSelect category: String)
|
||||
}
|
||||
|
@ -41,8 +41,19 @@ final class SearchCategoriesViewController: MWMTableViewController {
|
|||
tableView.deselectRow(at: indexPath, animated: true)
|
||||
}
|
||||
|
||||
override func scrollViewDidScroll(_ scrollView: UIScrollView) {
|
||||
delegate?.scrollViewDidScroll(scrollView)
|
||||
}
|
||||
|
||||
func category(at indexPath: IndexPath) -> String {
|
||||
let index = indexPath.row
|
||||
return categories[index]
|
||||
}
|
||||
}
|
||||
|
||||
extension SearchCategoriesViewController: ModallyPresentedViewController {
|
||||
func translationYDidUpdate(_ translationY: CGFloat) {
|
||||
guard isViewLoaded else { return }
|
||||
tableView.contentInset.bottom = translationY + view.safeAreaInsets.bottom
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,59 +1,97 @@
|
|||
protocol SearchHistoryViewControllerDelegate: AnyObject {
|
||||
protocol SearchHistoryViewControllerDelegate: SearchOnMapScrollViewDelegate {
|
||||
func searchHistoryViewController(_ viewController: SearchHistoryViewController,
|
||||
didSelect query: String)
|
||||
}
|
||||
|
||||
final class SearchHistoryViewController: MWMViewController {
|
||||
private weak var delegate: SearchHistoryViewControllerDelegate?
|
||||
private var lastQueries: [String]
|
||||
private var lastQueries: [String] = []
|
||||
private let frameworkHelper: MWMSearchFrameworkHelper
|
||||
private static let clearCellIdentifier = "SearchHistoryViewController_clearCellIdentifier"
|
||||
|
||||
@IBOutlet private var tableView: UITableView!
|
||||
@IBOutlet private weak var noResultsViewContainer: UIView!
|
||||
|
||||
private let emptyHistoryView = PlaceholderView(title: L("search_history_title"),
|
||||
subtitle: L("search_history_text"))
|
||||
|
||||
private let tableView = UITableView()
|
||||
|
||||
// MARK: - Init
|
||||
init(frameworkHelper: MWMSearchFrameworkHelper, delegate: SearchHistoryViewControllerDelegate?) {
|
||||
self.delegate = delegate
|
||||
self.lastQueries = frameworkHelper.lastSearchQueries()
|
||||
self.frameworkHelper = frameworkHelper
|
||||
super.init(nibName: nil, bundle: nil)
|
||||
}
|
||||
|
||||
|
||||
@available(*, unavailable)
|
||||
required init?(coder aDecoder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Lifecycle
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
|
||||
if frameworkHelper.isSearchHistoryEmpty() {
|
||||
showNoResultsView()
|
||||
} else {
|
||||
tableView.register(cell: SearchHistoryCell.self)
|
||||
}
|
||||
setupTableView()
|
||||
setupNoResultsView()
|
||||
}
|
||||
|
||||
override func viewDidAppear(_ animated: Bool) {
|
||||
super.viewDidAppear(animated)
|
||||
reload()
|
||||
}
|
||||
|
||||
// MARK: - Private methods
|
||||
private func setupTableView() {
|
||||
tableView.setStyle(.background)
|
||||
tableView.register(cell: SearchHistoryCell.self)
|
||||
tableView.keyboardDismissMode = .onDrag
|
||||
tableView.delegate = self
|
||||
tableView.dataSource = self
|
||||
tableView.tableFooterView = UIView(frame: CGRect(x: 0, y: 0, width: 400, height: 1))
|
||||
|
||||
view.addSubview(tableView)
|
||||
tableView.translatesAutoresizingMaskIntoConstraints = false
|
||||
NSLayoutConstraint.activate([
|
||||
tableView.topAnchor.constraint(equalTo: view.topAnchor),
|
||||
tableView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
|
||||
tableView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
|
||||
tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor)
|
||||
])
|
||||
}
|
||||
|
||||
func showNoResultsView() {
|
||||
guard let noResultsView = MWMSearchNoResults.view(with: nil,
|
||||
title: L("search_history_title"),
|
||||
text: L("search_history_text")) else {
|
||||
assertionFailure()
|
||||
return
|
||||
|
||||
private func setupNoResultsView() {
|
||||
view.addSubview(emptyHistoryView)
|
||||
emptyHistoryView.translatesAutoresizingMaskIntoConstraints = false
|
||||
NSLayoutConstraint.activate([
|
||||
emptyHistoryView.topAnchor.constraint(equalTo: view.topAnchor),
|
||||
emptyHistoryView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
|
||||
emptyHistoryView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
|
||||
emptyHistoryView.bottomAnchor.constraint(equalTo: view.bottomAnchor)
|
||||
])
|
||||
}
|
||||
|
||||
private func showEmptyHistoryView(_ isVisible: Bool = true, animated: Bool = true) {
|
||||
UIView.transition(with: emptyHistoryView,
|
||||
duration: animated ? kDefaultAnimationDuration : 0,
|
||||
options: [.transitionCrossDissolve, .curveEaseInOut]) {
|
||||
self.emptyHistoryView.alpha = isVisible ? 1.0 : 0.0
|
||||
self.emptyHistoryView.isHidden = !isVisible
|
||||
}
|
||||
noResultsViewContainer.addSubview(noResultsView)
|
||||
tableView.isHidden = true
|
||||
}
|
||||
|
||||
func clearSearchHistory() {
|
||||
private func clearSearchHistory() {
|
||||
frameworkHelper.clearSearchHistory()
|
||||
lastQueries = []
|
||||
reload()
|
||||
}
|
||||
|
||||
// MARK: - Public methods
|
||||
func reload() {
|
||||
guard isViewLoaded else { return }
|
||||
lastQueries = frameworkHelper.lastSearchQueries()
|
||||
showEmptyHistoryView(lastQueries.isEmpty ? true : false)
|
||||
tableView.reloadData()
|
||||
}
|
||||
}
|
||||
|
||||
extension SearchHistoryViewController: UITableViewDataSource {
|
||||
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
|
||||
return frameworkHelper.isSearchHistoryEmpty() ? 0 : lastQueries.count + 1
|
||||
return lastQueries.isEmpty ? 0 : lastQueries.count + 1
|
||||
}
|
||||
|
||||
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
|
||||
|
@ -71,16 +109,22 @@ extension SearchHistoryViewController: UITableViewDelegate {
|
|||
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
|
||||
if indexPath.row == lastQueries.count {
|
||||
clearSearchHistory()
|
||||
UIView.animate(withDuration: kDefaultAnimationDuration,
|
||||
animations: {
|
||||
tableView.alpha = 0.0
|
||||
}) { _ in
|
||||
self.showNoResultsView()
|
||||
}
|
||||
} else {
|
||||
let query = lastQueries[indexPath.row]
|
||||
delegate?.searchHistoryViewController(self, didSelect: query)
|
||||
}
|
||||
tableView.deselectRow(at: indexPath, animated: true)
|
||||
}
|
||||
|
||||
func scrollViewDidScroll(_ scrollView: UIScrollView) {
|
||||
delegate?.scrollViewDidScroll(scrollView)
|
||||
}
|
||||
}
|
||||
|
||||
extension SearchHistoryViewController: ModallyPresentedViewController {
|
||||
func translationYDidUpdate(_ translationY: CGFloat) {
|
||||
guard isViewLoaded else { return }
|
||||
tableView.contentInset.bottom = translationY
|
||||
emptyHistoryView.translationYDidUpdate(translationY)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,56 +0,0 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="23504" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
|
||||
<device id="retina4_7" orientation="portrait" appearance="light"/>
|
||||
<dependencies>
|
||||
<deployment identifier="iOS"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="23506"/>
|
||||
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
|
||||
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||
</dependencies>
|
||||
<objects>
|
||||
<placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner" customClass="SearchHistoryViewController" customModule="Organic_Maps" customModuleProvider="target">
|
||||
<connections>
|
||||
<outlet property="noResultsViewContainer" destination="bcr-zs-NMw" id="zpc-sP-fbF"/>
|
||||
<outlet property="tableView" destination="cDq-G7-5cR" id="Qo8-a6-Q6V"/>
|
||||
<outlet property="view" destination="iN0-l3-epB" id="Ybt-gX-7O4"/>
|
||||
</connections>
|
||||
</placeholder>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="-2" customClass="UIResponder"/>
|
||||
<view contentMode="scaleToFill" id="iN0-l3-epB">
|
||||
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<subviews>
|
||||
<tableView clipsSubviews="YES" contentMode="scaleToFill" alwaysBounceVertical="YES" style="plain" separatorStyle="default" rowHeight="-1" estimatedRowHeight="-1" sectionHeaderHeight="28" sectionFooterHeight="28" translatesAutoresizingMaskIntoConstraints="NO" id="cDq-G7-5cR">
|
||||
<rect key="frame" x="0.0" y="20" width="375" height="647"/>
|
||||
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
<connections>
|
||||
<outlet property="dataSource" destination="-1" id="XhM-2x-4kQ"/>
|
||||
<outlet property="delegate" destination="-1" id="sDX-YJ-iGy"/>
|
||||
</connections>
|
||||
</tableView>
|
||||
<view userInteractionEnabled="NO" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="bcr-zs-NMw">
|
||||
<rect key="frame" x="27.5" y="183.5" width="320" height="320"/>
|
||||
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="height" constant="320" id="07E-Xc-KMw"/>
|
||||
<constraint firstAttribute="width" constant="320" id="sCQ-Q9-Pdw"/>
|
||||
</constraints>
|
||||
</view>
|
||||
</subviews>
|
||||
<viewLayoutGuide key="safeArea" id="vUN-kp-3ea"/>
|
||||
<color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
<constraints>
|
||||
<constraint firstItem="vUN-kp-3ea" firstAttribute="bottom" secondItem="cDq-G7-5cR" secondAttribute="bottom" id="9QA-Kd-FQb"/>
|
||||
<constraint firstItem="vUN-kp-3ea" firstAttribute="trailing" secondItem="cDq-G7-5cR" secondAttribute="trailing" id="T3b-23-qG6"/>
|
||||
<constraint firstItem="bcr-zs-NMw" firstAttribute="centerY" secondItem="vUN-kp-3ea" secondAttribute="centerY" id="Y7z-3L-jl5"/>
|
||||
<constraint firstItem="cDq-G7-5cR" firstAttribute="top" secondItem="vUN-kp-3ea" secondAttribute="top" id="ezl-g4-mlD"/>
|
||||
<constraint firstItem="cDq-G7-5cR" firstAttribute="leading" secondItem="vUN-kp-3ea" secondAttribute="leading" id="ot8-FU-S6n"/>
|
||||
<constraint firstItem="bcr-zs-NMw" firstAttribute="centerX" secondItem="vUN-kp-3ea" secondAttribute="centerX" id="uMb-A3-NrS"/>
|
||||
</constraints>
|
||||
<userDefinedRuntimeAttributes>
|
||||
<userDefinedRuntimeAttribute type="string" keyPath="styleName" value="Background"/>
|
||||
</userDefinedRuntimeAttributes>
|
||||
<point key="canvasLocation" x="53.600000000000001" y="48.125937031484263"/>
|
||||
</view>
|
||||
</objects>
|
||||
</document>
|
|
@ -1,5 +1,5 @@
|
|||
@objc(MWMSearchTabViewControllerDelegate)
|
||||
protocol SearchTabViewControllerDelegate: AnyObject {
|
||||
protocol SearchTabViewControllerDelegate: SearchOnMapScrollViewDelegate {
|
||||
func searchTabController(_ viewController: SearchTabViewController, didSearch: String, withCategory: Bool)
|
||||
}
|
||||
|
||||
|
@ -47,6 +47,22 @@ final class SearchTabViewController: TabViewController {
|
|||
super.viewDidDisappear(animated)
|
||||
activeTab = SearchActiveTab.init(rawValue: tabView.selectedIndex ?? 0) ?? .categories
|
||||
}
|
||||
|
||||
func reloadSearchHistory() {
|
||||
(viewControllers[SearchActiveTab.history.rawValue] as? SearchHistoryViewController)?.reload()
|
||||
}
|
||||
}
|
||||
|
||||
extension SearchTabViewController: ModallyPresentedViewController {
|
||||
func translationYDidUpdate(_ translationY: CGFloat) {
|
||||
viewControllers.forEach { ($0 as? ModallyPresentedViewController)?.translationYDidUpdate(translationY) }
|
||||
}
|
||||
}
|
||||
|
||||
extension SearchTabViewController: SearchOnMapScrollViewDelegate {
|
||||
func scrollViewDidScroll(_ scrollView: UIScrollView) {
|
||||
delegate?.scrollViewDidScroll(scrollView)
|
||||
}
|
||||
}
|
||||
|
||||
extension SearchTabViewController: SearchCategoriesViewControllerDelegate {
|
||||
|
|
Loading…
Add table
Reference in a new issue