diff --git a/iphone/Maps/Categories/UIView+Coordinates.swift b/iphone/Maps/Categories/UIView+Coordinates.swift new file mode 100644 index 0000000000..41aec39329 --- /dev/null +++ b/iphone/Maps/Categories/UIView+Coordinates.swift @@ -0,0 +1,14 @@ +import UIKit + +extension UIView { + func center(inContainerView containerView: UIView) -> CGPoint { + guard let sv = superview else { return CGPoint.zero } + var centerPoint = center + + if let scrollView = sv as? UIScrollView , scrollView.zoomScale != 1.0 { + centerPoint.x += (scrollView.bounds.width - scrollView.contentSize.width) / 2.0 + scrollView.contentOffset.x + centerPoint.y += (scrollView.bounds.height - scrollView.contentSize.height) / 2.0 + scrollView.contentOffset.y + } + return sv.convert(centerPoint, to: containerView) + } +} diff --git a/iphone/Maps/Categories/UIView+Snapshot.swift b/iphone/Maps/Categories/UIView+Snapshot.swift new file mode 100644 index 0000000000..7c06ac8e2a --- /dev/null +++ b/iphone/Maps/Categories/UIView+Snapshot.swift @@ -0,0 +1,23 @@ +import UIKit + +extension UIView { + var snapshot: UIView { + guard let contents = layer.contents else { + return snapshotView(afterScreenUpdates: true)! + } + let snapshot: UIView + if let view = self as? UIImageView { + snapshot = UIImageView(image: view.image) + snapshot.bounds = view.bounds + } else { + snapshot = UIView(frame: frame) + snapshot.layer.contents = contents + snapshot.layer.bounds = layer.bounds + } + snapshot.layer.cornerRadius = layer.cornerRadius + snapshot.layer.masksToBounds = layer.masksToBounds + snapshot.contentMode = contentMode + snapshot.transform = transform + return snapshot + } +} diff --git a/iphone/Maps/Common/Common.swift b/iphone/Maps/Common/Common.swift index 6270c63f78..72e6e53021 100644 --- a/iphone/Maps/Common/Common.swift +++ b/iphone/Maps/Common/Common.swift @@ -21,3 +21,8 @@ func iPhoneSpecific( _ f: () -> Void) { func toString(_ cls: AnyClass) -> String { return String(describing: cls) } + +func statusBarHeight() -> CGFloat { + let statusBarSize = UIApplication.shared.statusBarFrame.size + return min(statusBarSize.height, statusBarSize.width) +} diff --git a/iphone/Maps/Maps.xcodeproj/project.pbxproj b/iphone/Maps/Maps.xcodeproj/project.pbxproj index 52220630fa..5604e18eb9 100644 --- a/iphone/Maps/Maps.xcodeproj/project.pbxproj +++ b/iphone/Maps/Maps.xcodeproj/project.pbxproj @@ -9,6 +9,30 @@ /* Begin PBXBuildFile section */ 1D3623260D0F684500981E51 /* MapsAppDelegate.mm in Sources */ = {isa = PBXBuildFile; fileRef = 1D3623250D0F684500981E51 /* MapsAppDelegate.mm */; }; 1D60589B0D05DD56006BFB54 /* main.mm in Sources */ = {isa = PBXBuildFile; fileRef = 29B97316FDCFA39411CA2CEA /* main.mm */; }; + 3404163B1E7BDFE000E2B6D6 /* PhotosViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3404163A1E7BDFE000E2B6D6 /* PhotosViewController.swift */; }; + 3404163C1E7BDFE000E2B6D6 /* PhotosViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3404163A1E7BDFE000E2B6D6 /* PhotosViewController.swift */; }; + 3404163D1E7BDFE000E2B6D6 /* PhotosViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3404163A1E7BDFE000E2B6D6 /* PhotosViewController.swift */; }; + 340416431E7BED3900E2B6D6 /* PhotosTransitionAnimator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 340416421E7BED3900E2B6D6 /* PhotosTransitionAnimator.swift */; }; + 340416441E7BED3900E2B6D6 /* PhotosTransitionAnimator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 340416421E7BED3900E2B6D6 /* PhotosTransitionAnimator.swift */; }; + 340416451E7BED3900E2B6D6 /* PhotosTransitionAnimator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 340416421E7BED3900E2B6D6 /* PhotosTransitionAnimator.swift */; }; + 340416471E7BF28E00E2B6D6 /* UIView+Snapshot.swift in Sources */ = {isa = PBXBuildFile; fileRef = 340416461E7BF28E00E2B6D6 /* UIView+Snapshot.swift */; }; + 340416481E7BF28E00E2B6D6 /* UIView+Snapshot.swift in Sources */ = {isa = PBXBuildFile; fileRef = 340416461E7BF28E00E2B6D6 /* UIView+Snapshot.swift */; }; + 340416491E7BF28E00E2B6D6 /* UIView+Snapshot.swift in Sources */ = {isa = PBXBuildFile; fileRef = 340416461E7BF28E00E2B6D6 /* UIView+Snapshot.swift */; }; + 3404164B1E7BF42E00E2B6D6 /* UIView+Coordinates.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3404164A1E7BF42D00E2B6D6 /* UIView+Coordinates.swift */; }; + 3404164C1E7BF42E00E2B6D6 /* UIView+Coordinates.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3404164A1E7BF42D00E2B6D6 /* UIView+Coordinates.swift */; }; + 3404164D1E7BF42E00E2B6D6 /* UIView+Coordinates.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3404164A1E7BF42D00E2B6D6 /* UIView+Coordinates.swift */; }; + 3404164F1E7C085F00E2B6D6 /* PhotoViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3404164E1E7C085F00E2B6D6 /* PhotoViewController.swift */; }; + 340416501E7C086000E2B6D6 /* PhotoViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3404164E1E7C085F00E2B6D6 /* PhotoViewController.swift */; }; + 340416511E7C086000E2B6D6 /* PhotoViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3404164E1E7C085F00E2B6D6 /* PhotoViewController.swift */; }; + 340416531E7C09C200E2B6D6 /* PhotoScalingImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 340416521E7C09C200E2B6D6 /* PhotoScalingImageView.swift */; }; + 340416541E7C09C200E2B6D6 /* PhotoScalingImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 340416521E7C09C200E2B6D6 /* PhotoScalingImageView.swift */; }; + 340416551E7C09C200E2B6D6 /* PhotoScalingImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 340416521E7C09C200E2B6D6 /* PhotoScalingImageView.swift */; }; + 340416571E7C0D4100E2B6D6 /* PhotosOverlayView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 340416561E7C0D4100E2B6D6 /* PhotosOverlayView.swift */; }; + 340416581E7C0D4100E2B6D6 /* PhotosOverlayView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 340416561E7C0D4100E2B6D6 /* PhotosOverlayView.swift */; }; + 340416591E7C0D4100E2B6D6 /* PhotosOverlayView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 340416561E7C0D4100E2B6D6 /* PhotosOverlayView.swift */; }; + 3404165B1E7C29AE00E2B6D6 /* PhotosInteractionAnimator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3404165A1E7C29AE00E2B6D6 /* PhotosInteractionAnimator.swift */; }; + 3404165C1E7C29AE00E2B6D6 /* PhotosInteractionAnimator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3404165A1E7C29AE00E2B6D6 /* PhotosInteractionAnimator.swift */; }; + 3404165D1E7C29AE00E2B6D6 /* PhotosInteractionAnimator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3404165A1E7C29AE00E2B6D6 /* PhotosInteractionAnimator.swift */; }; 340474F01E08199D00C92850 /* Crashlytics.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 340474DC1E08199D00C92850 /* Crashlytics.framework */; }; 340474F11E08199D00C92850 /* Crashlytics.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 340474DC1E08199D00C92850 /* Crashlytics.framework */; }; 340474F21E08199D00C92850 /* Crashlytics.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 340474DC1E08199D00C92850 /* Crashlytics.framework */; }; @@ -1450,6 +1474,14 @@ 1D3623250D0F684500981E51 /* MapsAppDelegate.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; lineEnding = 0; path = MapsAppDelegate.mm; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.objcpp; }; 28A0AB4B0D9B1048005BE974 /* Maps_Prefix.pch */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; lineEnding = 0; path = Maps_Prefix.pch; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.objc; }; 29B97316FDCFA39411CA2CEA /* main.mm */ = {isa = PBXFileReference; explicitFileType = sourcecode.cpp.objcpp; fileEncoding = 4; path = main.mm; sourceTree = ""; }; + 3404163A1E7BDFE000E2B6D6 /* PhotosViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PhotosViewController.swift; sourceTree = ""; }; + 340416421E7BED3900E2B6D6 /* PhotosTransitionAnimator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PhotosTransitionAnimator.swift; sourceTree = ""; }; + 340416461E7BF28E00E2B6D6 /* UIView+Snapshot.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UIView+Snapshot.swift"; sourceTree = ""; }; + 3404164A1E7BF42D00E2B6D6 /* UIView+Coordinates.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UIView+Coordinates.swift"; sourceTree = ""; }; + 3404164E1E7C085F00E2B6D6 /* PhotoViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PhotoViewController.swift; sourceTree = ""; }; + 340416521E7C09C200E2B6D6 /* PhotoScalingImageView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PhotoScalingImageView.swift; sourceTree = ""; }; + 340416561E7C0D4100E2B6D6 /* PhotosOverlayView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PhotosOverlayView.swift; sourceTree = ""; }; + 3404165A1E7C29AE00E2B6D6 /* PhotosInteractionAnimator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PhotosInteractionAnimator.swift; sourceTree = ""; }; 340474DC1E08199D00C92850 /* Crashlytics.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; path = Crashlytics.framework; sourceTree = ""; }; 340474DD1E08199D00C92850 /* Fabric.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; path = Fabric.framework; sourceTree = ""; }; 340474DE1E08199D00C92850 /* FBSDKCoreKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; path = FBSDKCoreKit.framework; sourceTree = ""; }; @@ -2504,6 +2536,19 @@ name = Frameworks; sourceTree = ""; }; + 340416391E7BDFB800E2B6D6 /* Photos */ = { + isa = PBXGroup; + children = ( + 340416521E7C09C200E2B6D6 /* PhotoScalingImageView.swift */, + 3404165A1E7C29AE00E2B6D6 /* PhotosInteractionAnimator.swift */, + 340416561E7C0D4100E2B6D6 /* PhotosOverlayView.swift */, + 340416421E7BED3900E2B6D6 /* PhotosTransitionAnimator.swift */, + 3404163A1E7BDFE000E2B6D6 /* PhotosViewController.swift */, + 3404164E1E7C085F00E2B6D6 /* PhotoViewController.swift */, + ); + path = Photos; + sourceTree = ""; + }; 340474DB1E08199D00C92850 /* 3party */ = { isa = PBXGroup; children = ( @@ -2761,6 +2806,8 @@ 349D1CE21E3F836900A878FD /* UIViewController+Hierarchy.swift */, 34F7422F1E0834F400AC1FD6 /* UIViewController+Navigation.h */, 34F742301E0834F400AC1FD6 /* UIViewController+Navigation.mm */, + 340416461E7BF28E00E2B6D6 /* UIView+Snapshot.swift */, + 3404164A1E7BF42D00E2B6D6 /* UIView+Coordinates.swift */, ); path = Categories; sourceTree = ""; @@ -2845,6 +2892,7 @@ 346DB81C1E5C4F6700E3123E /* Gallery */ = { isa = PBXGroup; children = ( + 340416391E7BDFB800E2B6D6 /* Photos */, 346DB81D1E5C4F6700E3123E /* Cells */, 346DB8201E5C4F6700E3123E /* GalleryItemViewController.swift */, 346DB8211E5C4F6700E3123E /* GalleryItemViewController.xib */, @@ -4869,6 +4917,7 @@ F6E2FF081E097BA00083EBEC /* MWMSearchHistoryManager.mm in Sources */, F6E2FD8E1E097BA00083EBEC /* MWMNoMapsViewController.mm in Sources */, 34D3B0411E389D05004100F9 /* MWMEditorTextTableViewCell.mm in Sources */, + 3404163B1E7BDFE000E2B6D6 /* PhotosViewController.swift in Sources */, F6E2FE601E097BA00083EBEC /* MWMBookmarkCell.mm in Sources */, F6E2FEA21E097BA00083EBEC /* MWMPPView.mm in Sources */, F6791B131C43DEA7007A8A6E /* MWMStartButton.mm in Sources */, @@ -4933,6 +4982,7 @@ F607C18E1C047FDC00B53A87 /* MWMSegue.mm in Sources */, F6E2FE301E097BA00083EBEC /* MWMStreetEditorViewController.mm in Sources */, F6E2FE3F1E097BA00083EBEC /* MWMPlacePageEntity.mm in Sources */, + 340416471E7BF28E00E2B6D6 /* UIView+Snapshot.swift in Sources */, F6E2FE271E097BA00083EBEC /* MWMOpeningHoursSection.mm in Sources */, F6E2FE241E097BA00083EBEC /* MWMOpeningHoursModel.mm in Sources */, F6BD33811B62403B00F2CE18 /* MWMRoutePreview.mm in Sources */, @@ -5000,6 +5050,7 @@ F6E2FEC91E097BA00083EBEC /* MWMSearchFilterTransitioning.mm in Sources */, FA054612155C465E001F4E37 /* SelectSetVC.mm in Sources */, F6E2FE811E097BA00083EBEC /* MWMPlacePageOpeningHoursDayView.mm in Sources */, + 340416571E7C0D4100E2B6D6 /* PhotosOverlayView.swift in Sources */, F6E2FD6A1E097BA00083EBEC /* MWMMapDownloaderSubplaceTableViewCell.mm in Sources */, FAA614B8155F16950031C345 /* AddSetVC.mm in Sources */, F6E2FF681E097BA00083EBEC /* MWMUnitsController.mm in Sources */, @@ -5037,12 +5088,14 @@ F6E2FE9F1E097BA00083EBEC /* MWMPlacePageLayout.mm in Sources */, F6E2FEC31E097BA00083EBEC /* MWMSearchFilterPresentationController.mm in Sources */, 340475731E081A4600C92850 /* MWMStorage.mm in Sources */, + 340416531E7C09C200E2B6D6 /* PhotoScalingImageView.swift in Sources */, F6E2FD851E097BA00083EBEC /* MWMBaseMapDownloaderViewController.mm in Sources */, 34ABA6241C2D551900FE1BEC /* MWMInputValidatorFactory.mm in Sources */, 34943BBA1E2626B200B14F84 /* WelcomePageController.swift in Sources */, F6E2FEE11E097BA00083EBEC /* MWMSearchManager.mm in Sources */, 34D3AFE11E376F7E004100F9 /* UITableView+Updates.swift in Sources */, F6E2FE211E097BA00083EBEC /* MWMOpeningHoursEditorViewController.mm in Sources */, + 3404164B1E7BF42E00E2B6D6 /* UIView+Coordinates.swift in Sources */, 349D1ADA1E2E325C004A2006 /* MWMBottomMenuView.mm in Sources */, F6E2FD911E097BA00083EBEC /* MWMBookmarkColorViewController.mm in Sources */, F6F7787A1DABC6D800B603E7 /* MWMTaxiCollectionLayout.mm in Sources */, @@ -5051,6 +5104,7 @@ 34D3B0291E389D05004100F9 /* MWMEditorAdditionalNameTableViewCell.mm in Sources */, 34D4FA621E26572D003F53EF /* FirstLaunchController.swift in Sources */, 34C9BD041C6DB693000DC38D /* MWMViewController.mm in Sources */, + 3404165B1E7C29AE00E2B6D6 /* PhotosInteractionAnimator.swift in Sources */, 340475701E081A4600C92850 /* MWMSettings.mm in Sources */, 3404756D1E081A4600C92850 /* MWMSearch.mm in Sources */, 3486B5071E27A4B50069C126 /* LocalNotificationManager.mm in Sources */, @@ -5066,6 +5120,7 @@ F6E2FF021E097BA00083EBEC /* MWMSearchHistoryClearCell.mm in Sources */, F63774EA1B59376F00BCF54D /* MWMRoutingDisclaimerAlert.mm in Sources */, 340475081E08199E00C92850 /* MWMMyTarget.mm in Sources */, + 3404164F1E7C085F00E2B6D6 /* PhotoViewController.swift in Sources */, F64F19A31AB81A00006EAF7E /* MWMDownloadTransitMapAlert.mm in Sources */, 3404754C1E081A4600C92850 /* MWMKeyboard.mm in Sources */, F6BD33841B6240F200F2CE18 /* MWMNavigationView.mm in Sources */, @@ -5083,6 +5138,7 @@ ED48BBB517C267F5003E7E92 /* ColorPickerView.mm in Sources */, 34F5E0D71E3F334700B1C415 /* Types.swift in Sources */, F6E2FF561E097BA00083EBEC /* MWMMobileInternetViewController.mm in Sources */, + 340416431E7BED3900E2B6D6 /* PhotosTransitionAnimator.swift in Sources */, ED48BBBA17C2B1E2003E7E92 /* CircleView.mm in Sources */, F6E2FEEA1E097BA00083EBEC /* MWMSearchTextField.mm in Sources */, F6664C121E645A4100E703C2 /* MWMPPReviewCell.mm in Sources */, @@ -5141,6 +5197,7 @@ F6E2FF091E097BA00083EBEC /* MWMSearchHistoryManager.mm in Sources */, F6E2FD8F1E097BA00083EBEC /* MWMNoMapsViewController.mm in Sources */, 34D3B0421E389D05004100F9 /* MWMEditorTextTableViewCell.mm in Sources */, + 3404163C1E7BDFE000E2B6D6 /* PhotosViewController.swift in Sources */, F6E2FE611E097BA00083EBEC /* MWMBookmarkCell.mm in Sources */, F6E2FEA31E097BA00083EBEC /* MWMPPView.mm in Sources */, 3454D7D11E07F045004AF2AD /* UIImage+RGBAData.mm in Sources */, @@ -5205,6 +5262,7 @@ F6E2FE401E097BA00083EBEC /* MWMPlacePageEntity.mm in Sources */, F6E2FE281E097BA00083EBEC /* MWMOpeningHoursSection.mm in Sources */, 3406FA161C6E0C3300E9FAD2 /* MWMMapDownloadDialog.mm in Sources */, + 340416481E7BF28E00E2B6D6 /* UIView+Snapshot.swift in Sources */, F6E2FE251E097BA00083EBEC /* MWMOpeningHoursModel.mm in Sources */, 34C9BD031C6DB693000DC38D /* MWMTableViewController.mm in Sources */, F6E2FD8C1E097BA00083EBEC /* MWMNoMapsView.mm in Sources */, @@ -5272,6 +5330,7 @@ F682249B1E5B104600BC1C18 /* PPHotelDescriptionCell.swift in Sources */, F6E2FECA1E097BA00083EBEC /* MWMSearchFilterTransitioning.mm in Sources */, 3454D7DD1E07F045004AF2AD /* UISwitch+RuntimeAttributes.m in Sources */, + 340416581E7C0D4100E2B6D6 /* PhotosOverlayView.swift in Sources */, F6E2FE821E097BA00083EBEC /* MWMPlacePageOpeningHoursDayView.mm in Sources */, F6E2FD6B1E097BA00083EBEC /* MWMMapDownloaderSubplaceTableViewCell.mm in Sources */, 6741AA021BF340DE002C974C /* BookmarksRootVC.mm in Sources */, @@ -5309,12 +5368,14 @@ F6E2FF301E097BA00083EBEC /* MWMSearchCommonCell.mm in Sources */, F6E2FEA01E097BA00083EBEC /* MWMPlacePageLayout.mm in Sources */, F6E2FEC41E097BA00083EBEC /* MWMSearchFilterPresentationController.mm in Sources */, + 340416541E7C09C200E2B6D6 /* PhotoScalingImageView.swift in Sources */, 34ABA6251C2D551900FE1BEC /* MWMInputValidatorFactory.mm in Sources */, F6E2FD861E097BA00083EBEC /* MWMBaseMapDownloaderViewController.mm in Sources */, F6E2FEE21E097BA00083EBEC /* MWMSearchManager.mm in Sources */, F6E2FE221E097BA00083EBEC /* MWMOpeningHoursEditorViewController.mm in Sources */, 34943BBB1E2626B200B14F84 /* WelcomePageController.swift in Sources */, 34D3AFE21E376F7E004100F9 /* UITableView+Updates.swift in Sources */, + 3404164C1E7BF42E00E2B6D6 /* UIView+Coordinates.swift in Sources */, 6741AA141BF340DE002C974C /* MWMMultilineLabel.mm in Sources */, 349D1ADB1E2E325C004A2006 /* MWMBottomMenuView.mm in Sources */, F6E2FD921E097BA00083EBEC /* MWMBookmarkColorViewController.mm in Sources */, @@ -5323,6 +5384,7 @@ 3454D7CB1E07F045004AF2AD /* UIColor+MapsMeColor.mm in Sources */, 34D3B02A1E389D05004100F9 /* MWMEditorAdditionalNameTableViewCell.mm in Sources */, 340475711E081A4600C92850 /* MWMSettings.mm in Sources */, + 3404165C1E7C29AE00E2B6D6 /* PhotosInteractionAnimator.swift in Sources */, 34D4FA631E26572D003F53EF /* FirstLaunchController.swift in Sources */, 3404756E1E081A4600C92850 /* MWMSearch.mm in Sources */, 6741AA191BF340DE002C974C /* MWMDownloaderDialogCell.mm in Sources */, @@ -5338,6 +5400,7 @@ 6741AA221BF340DE002C974C /* MWMNavigationView.mm in Sources */, F6E2FF031E097BA00083EBEC /* MWMSearchHistoryClearCell.mm in Sources */, 340475091E08199E00C92850 /* MWMMyTarget.mm in Sources */, + 340416501E7C086000E2B6D6 /* PhotoViewController.swift in Sources */, 674A7E301C0DB10B003D48E1 /* MWMMapWidgets.mm in Sources */, 3404754D1E081A4600C92850 /* MWMKeyboard.mm in Sources */, 34EF94291C05A6F30050B714 /* MWMSegue.mm in Sources */, @@ -5355,6 +5418,7 @@ 6741AA281BF340DE002C974C /* MWMAlert.mm in Sources */, F6E2FF571E097BA00083EBEC /* MWMMobileInternetViewController.mm in Sources */, 34F5E0D81E3F334700B1C415 /* Types.swift in Sources */, + 340416441E7BED3900E2B6D6 /* PhotosTransitionAnimator.swift in Sources */, 6741AA291BF340DE002C974C /* ColorPickerView.mm in Sources */, 6741AA2B1BF340DE002C974C /* CircleView.mm in Sources */, F6E2FEEB1E097BA00083EBEC /* MWMSearchTextField.mm in Sources */, @@ -5413,6 +5477,7 @@ F6E2FF0A1E097BA00083EBEC /* MWMSearchHistoryManager.mm in Sources */, F6E2FD901E097BA00083EBEC /* MWMNoMapsViewController.mm in Sources */, 34D3B0431E389D05004100F9 /* MWMEditorTextTableViewCell.mm in Sources */, + 3404163D1E7BDFE000E2B6D6 /* PhotosViewController.swift in Sources */, F6E2FE621E097BA00083EBEC /* MWMBookmarkCell.mm in Sources */, F6E2FEA41E097BA00083EBEC /* MWMPPView.mm in Sources */, 849CF69F1DE842290024A8A5 /* MWMStartButton.mm in Sources */, @@ -5477,6 +5542,7 @@ 3404756C1E081A4600C92850 /* MWMSearch+CoreSpotlight.mm in Sources */, F6E2FE321E097BA00083EBEC /* MWMStreetEditorViewController.mm in Sources */, F6E2FE411E097BA00083EBEC /* MWMPlacePageEntity.mm in Sources */, + 340416491E7BF28E00E2B6D6 /* UIView+Snapshot.swift in Sources */, F6E2FE291E097BA00083EBEC /* MWMOpeningHoursSection.mm in Sources */, 849CF6D31DE842290024A8A5 /* MWMMapDownloadDialog.mm in Sources */, F6E2FE261E097BA00083EBEC /* MWMOpeningHoursModel.mm in Sources */, @@ -5544,6 +5610,7 @@ F6E2FECB1E097BA00083EBEC /* MWMSearchFilterTransitioning.mm in Sources */, 849CF70D1DE842290024A8A5 /* MWMAddPlaceNavigationBar.mm in Sources */, F6E2FE831E097BA00083EBEC /* MWMPlacePageOpeningHoursDayView.mm in Sources */, + 340416591E7C0D4100E2B6D6 /* PhotosOverlayView.swift in Sources */, F6E2FD6C1E097BA00083EBEC /* MWMMapDownloaderSubplaceTableViewCell.mm in Sources */, 849CF70F1DE842290024A8A5 /* MWMNavigationInfoView.mm in Sources */, F6E2FF6A1E097BA00083EBEC /* MWMUnitsController.mm in Sources */, @@ -5581,12 +5648,14 @@ F6E2FEA11E097BA00083EBEC /* MWMPlacePageLayout.mm in Sources */, F6E2FEC51E097BA00083EBEC /* MWMSearchFilterPresentationController.mm in Sources */, F6E2FD871E097BA00083EBEC /* MWMBaseMapDownloaderViewController.mm in Sources */, + 340416551E7C09C200E2B6D6 /* PhotoScalingImageView.swift in Sources */, F6E2FEE31E097BA00083EBEC /* MWMSearchManager.mm in Sources */, 34943BBC1E2626B200B14F84 /* WelcomePageController.swift in Sources */, F6E2FE231E097BA00083EBEC /* MWMOpeningHoursEditorViewController.mm in Sources */, 34D3AFE31E376F7E004100F9 /* UITableView+Updates.swift in Sources */, 849CF72F1DE842290024A8A5 /* MWMInputValidator.mm in Sources */, 349D1ADC1E2E325C004A2006 /* MWMBottomMenuView.mm in Sources */, + 3404164D1E7BF42E00E2B6D6 /* UIView+Coordinates.swift in Sources */, F6E2FD931E097BA00083EBEC /* MWMBookmarkColorViewController.mm in Sources */, 849CF7331DE842290024A8A5 /* MWMInputValidatorFactory.mm in Sources */, F6E2FDA51E097BA00083EBEC /* MWMCuisineEditorViewController.mm in Sources */, @@ -5595,6 +5664,7 @@ 34D3B02B1E389D05004100F9 /* MWMEditorAdditionalNameTableViewCell.mm in Sources */, 849CF7371DE842290024A8A5 /* MWMTaxiCollectionLayout.mm in Sources */, 3454D7C61E07F045004AF2AD /* UIButton+Orientation.mm in Sources */, + 3404165D1E7C29AE00E2B6D6 /* PhotosInteractionAnimator.swift in Sources */, 340475571E081A4600C92850 /* Statistics.mm in Sources */, 3486B5091E27A4B50069C126 /* LocalNotificationManager.mm in Sources */, F6E2FEFE1E097BA00083EBEC /* MWMSearchCategoryCell.mm in Sources */, @@ -5610,6 +5680,7 @@ 849CF74C1DE842290024A8A5 /* MWMLocationNotFoundAlert.mm in Sources */, F6E2FF041E097BA00083EBEC /* MWMSearchHistoryClearCell.mm in Sources */, 340475541E081A4600C92850 /* MWMCustomFacebookEvents.mm in Sources */, + 340416511E7C086000E2B6D6 /* PhotoViewController.swift in Sources */, 3404750A1E08199E00C92850 /* MWMMyTarget.mm in Sources */, 849CF7561DE842290024A8A5 /* MWMRoutingDisclaimerAlert.mm in Sources */, 849CF7581DE842290024A8A5 /* MWMDownloadTransitMapAlert.mm in Sources */, @@ -5627,6 +5698,7 @@ 849CF7631DE842290024A8A5 /* MWMAlert.mm in Sources */, 34F5E0D91E3F334700B1C415 /* Types.swift in Sources */, F6E2FF581E097BA00083EBEC /* MWMMobileInternetViewController.mm in Sources */, + 340416451E7BED3900E2B6D6 /* PhotosTransitionAnimator.swift in Sources */, 849CF7651DE842290024A8A5 /* ColorPickerView.mm in Sources */, F6E2FEEC1E097BA00083EBEC /* MWMSearchTextField.mm in Sources */, F6664C141E645A4100E703C2 /* MWMPPReviewCell.mm in Sources */, diff --git a/iphone/Maps/UI/PlacePage/MWMPlacePageButtonsProtocol.h b/iphone/Maps/UI/PlacePage/MWMPlacePageButtonsProtocol.h index ca18c73721..dd63225ecc 100644 --- a/iphone/Maps/UI/PlacePage/MWMPlacePageButtonsProtocol.h +++ b/iphone/Maps/UI/PlacePage/MWMPlacePageButtonsProtocol.h @@ -8,7 +8,9 @@ - (void)taxiTo; - (void)showAllReviews; - (void)showAllFacilities; -- (void)showPhotoAtIndex:(NSUInteger)index; +- (void)showPhotoAtIndex:(NSInteger)index + referenceView:(UIView *)referenceView + referenceViewWhenDismissingHandler:(UIView * (^)(NSInteger))referenceViewWhenDismissingHandler; - (void)showGalery; @end diff --git a/iphone/Maps/UI/PlacePage/MWMPlacePageManager.mm b/iphone/Maps/UI/PlacePage/MWMPlacePageManager.mm index 967fb4500b..caaaca0f99 100644 --- a/iphone/Maps/UI/PlacePage/MWMPlacePageManager.mm +++ b/iphone/Maps/UI/PlacePage/MWMPlacePageManager.mm @@ -311,22 +311,35 @@ void logSponsoredEvent(MWMPlacePageData * data, NSString * eventName) [[MapViewController controller] openUrl:self.data.URLToAllReviews]; } -- (void)showPhotoAtIndex:(NSUInteger)index +- (void)showPhotoAtIndex:(NSInteger)index + referenceView:(UIView *)referenceView + referenceViewWhenDismissingHandler:(UIView * (^)(NSInteger))referenceViewWhenDismissingHandler { logSponsoredEvent(self.data, kPlacePageHotelGallery); - auto model = self.data.photos[index]; - auto galleryVc = [MWMGalleryItemViewController instanceWithModel:model]; - [[MapViewController controller].navigationController pushViewController:galleryVc animated:YES]; + auto galleryModel = self.galleryModel; + auto initialPhoto = galleryModel.items[index]; + auto photoVC = [[MWMPhotosViewController alloc] initWithPhotos:galleryModel + initialPhoto:initialPhoto + referenceView:referenceView]; + photoVC.referenceViewForPhotoWhenDismissingHandler = ^UIView *(MWMGalleryItemModel * photo) { + return referenceViewWhenDismissingHandler([galleryModel.items indexOfObject:photo]); + }; + + [[MapViewController controller] presentViewController:photoVC animated:YES completion:nil]; } - (void)showGalery { logSponsoredEvent(self.data, kPlacePageHotelGallery); - auto galleryVc = [MWMGalleryViewController instanceWithModel:[[MWMGalleryModel alloc] - initWithTitle:self.hotelName items:self.data.photos]]; + auto galleryVc = [MWMGalleryViewController instanceWithModel:self.galleryModel]; [[MapViewController controller].navigationController pushViewController:galleryVc animated:YES]; } +- (MWMGalleryModel *)galleryModel +{ + return [[MWMGalleryModel alloc] initWithTitle:self.hotelName items:self.data.photos]; +} + - (void)showAllFacilities { logSponsoredEvent(self.data, kPlacePageHotelFacilities); diff --git a/iphone/Maps/UI/PlacePage/PlacePageLayout/Content/BookingCells/PPHotelCarouselCell.swift b/iphone/Maps/UI/PlacePage/PlacePageLayout/Content/BookingCells/PPHotelCarouselCell.swift index f6cb946683..fe0d69e05c 100644 --- a/iphone/Maps/UI/PlacePage/PlacePageLayout/Content/BookingCells/PPHotelCarouselCell.swift +++ b/iphone/Maps/UI/PlacePage/PlacePageLayout/Content/BookingCells/PPHotelCarouselCell.swift @@ -54,7 +54,12 @@ extension PPHotelCarouselCell: UICollectionViewDelegate, UICollectionViewDataSou if isLastCell(indexPath) { d.showGalery() } else { - d.showPhoto(at: UInt(indexPath.row)) + let section = indexPath.section + d.showPhoto(at: indexPath.item, + referenceView: collectionView.cellForItem(at: indexPath), referenceViewWhenDismissingHandler: { index -> UIView? in + let indexPath = IndexPath(item: index, section: section) + return collectionView.cellForItem(at: indexPath) + }) } } } diff --git a/iphone/Maps/UI/PlacePage/PlacePageLayout/Content/Gallery/GalleryViewController.swift b/iphone/Maps/UI/PlacePage/PlacePageLayout/Content/Gallery/GalleryViewController.swift index 0b814ffa51..0e162dac50 100644 --- a/iphone/Maps/UI/PlacePage/PlacePageLayout/Content/Gallery/GalleryViewController.swift +++ b/iphone/Maps/UI/PlacePage/PlacePageLayout/Content/Gallery/GalleryViewController.swift @@ -63,6 +63,17 @@ final class GalleryViewController: MWMCollectionViewController { // MARK: UICollectionViewDelegate override func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { - show(GalleryItemViewController.instance(model: model.items[indexPath.item]), sender: nil) + let currentPhoto = model.items[indexPath.item] + let cell = collectionView.cellForItem(at: indexPath) + let photoVC = PhotosViewController(photos: model, initialPhoto: currentPhoto, referenceView: cell) + + photoVC.referenceViewForPhotoWhenDismissingHandler = { [weak self] photo in + if let index = self?.model.items.index(where: {$0 === photo}) { + let indexPath = IndexPath(item: index, section: 0) + return collectionView.cellForItem(at: indexPath) + } + return nil + } + present(photoVC, animated: true, completion: nil) } } diff --git a/iphone/Maps/UI/PlacePage/PlacePageLayout/Content/Gallery/Photos/PhotoScalingImageView.swift b/iphone/Maps/UI/PlacePage/PlacePageLayout/Content/Gallery/Photos/PhotoScalingImageView.swift new file mode 100644 index 0000000000..01c5fc2f50 --- /dev/null +++ b/iphone/Maps/UI/PlacePage/PlacePageLayout/Content/Gallery/Photos/PhotoScalingImageView.swift @@ -0,0 +1,81 @@ +import AlamofireImage +import UIKit + +final class PhotoScalingImageView: UIScrollView { + lazy var imageView: UIImageView = { + let imageView = UIImageView(frame: self.bounds) + self.addSubview(imageView) + return imageView + }() + + var photo: GalleryItemModel? { + didSet { + updateImage(photo) + } + } + + override var frame: CGRect { + get { return super.frame } + set { + super.frame = newValue + updateZoomScale() + centerScrollViewContents() + } + } + + override init(frame: CGRect) { + super.init(frame: frame) + setupImageScrollView() + updateZoomScale() + } + + required init?(coder aDecoder: NSCoder) { + super.init(coder: aDecoder) + setupImageScrollView() + updateZoomScale() + } + + override func didAddSubview(_ subview: UIView) { + super.didAddSubview(subview) + centerScrollViewContents() + } + + private func setupImageScrollView() { + showsVerticalScrollIndicator = false + showsHorizontalScrollIndicator = false + bouncesZoom = true + decelerationRate = UIScrollViewDecelerationRateFast + } + + private func centerScrollViewContents() { + let horizontalInset: CGFloat = contentSize.width < bounds.width ? (bounds.width - contentSize.width) * 0.5 : 0 + let verticalInset: CGFloat = contentSize.height < bounds.height ? (bounds.height - contentSize.height) * 0.5 : 0 + contentInset = UIEdgeInsetsMake(verticalInset, horizontalInset, verticalInset, horizontalInset) + } + + private func updateImage(_ photo: GalleryItemModel?) { + guard let photo = photo else { return } + imageView.transform = CGAffineTransform.identity + imageView.af_setImage(withURL: photo.imageURL, + imageTransition: .crossDissolve(kDefaultAnimationDuration), + completion: { [weak self] response in + guard let s = self else { return } + s.contentSize = response.value?.size ?? CGSize.zero + s.imageView.frame = CGRect(origin: CGPoint.zero, size: s.contentSize) + s.updateZoomScale() + s.centerScrollViewContents() + }) + } + + private func updateZoomScale() { + guard let image = imageView.image else { return } + let selfSize = bounds.size + let imageSize = image.size + let wScale = selfSize.width / imageSize.width + let hScale = selfSize.height / imageSize.height + minimumZoomScale = min(wScale, hScale) + maximumZoomScale = max(max(wScale, hScale), maximumZoomScale) + zoomScale = minimumZoomScale + panGestureRecognizer.isEnabled = false + } +} diff --git a/iphone/Maps/UI/PlacePage/PlacePageLayout/Content/Gallery/Photos/PhotoViewController.swift b/iphone/Maps/UI/PlacePage/PlacePageLayout/Content/Gallery/Photos/PhotoViewController.swift new file mode 100644 index 0000000000..703bb542ff --- /dev/null +++ b/iphone/Maps/UI/PlacePage/PlacePageLayout/Content/Gallery/Photos/PhotoViewController.swift @@ -0,0 +1,76 @@ +import UIKit + +final class PhotoViewController: UIViewController { + let scalingImageView = PhotoScalingImageView() + + let photo: GalleryItemModel + + private(set) lazy var doubleTapGestureRecognizer: UITapGestureRecognizer = { + let gesture = UITapGestureRecognizer(target: self, action: #selector(handleDoubleTapWithGestureRecognizer(_:))) + gesture.numberOfTapsRequired = 2 + return gesture + }() + + init(photo: GalleryItemModel) { + self.photo = photo + super.init(nibName: nil, bundle: nil) + } + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func viewDidLoad() { + super.viewDidLoad() + + scalingImageView.delegate = self + scalingImageView.frame = view.bounds + scalingImageView.autoresizingMask = [.flexibleWidth, .flexibleHeight] + view.addSubview(scalingImageView) + + view.addGestureRecognizer(doubleTapGestureRecognizer) + + scalingImageView.photo = photo + } + + override func viewWillLayoutSubviews() { + super.viewWillLayoutSubviews() + scalingImageView.frame = view.bounds + } + + @objc + private func handleDoubleTapWithGestureRecognizer(_ recognizer: UITapGestureRecognizer) { + let pointInView = recognizer.location(in: scalingImageView.imageView) + var newZoomScale = scalingImageView.maximumZoomScale + + if scalingImageView.zoomScale >= scalingImageView.maximumZoomScale || + abs(scalingImageView.zoomScale - scalingImageView.maximumZoomScale) <= 0.01 { + newZoomScale = scalingImageView.minimumZoomScale + } + + let scrollViewSize = scalingImageView.bounds.size + let width = scrollViewSize.width / newZoomScale + let height = scrollViewSize.height / newZoomScale + let originX = pointInView.x - (width / 2.0) + let originY = pointInView.y - (height / 2.0) + + let rectToZoom = CGRect(x: originX, y: originY, width: width, height: height) + scalingImageView.zoom(to: rectToZoom, animated: true) + } +} + +extension PhotoViewController: UIScrollViewDelegate { + func viewForZooming(in scrollView: UIScrollView) -> UIView? { + return scalingImageView.imageView + } + + func scrollViewWillBeginZooming(_ scrollView: UIScrollView, with view: UIView?) { + scrollView.panGestureRecognizer.isEnabled = true + } + + func scrollViewDidEndZooming(_ scrollView: UIScrollView, with view: UIView?, atScale scale: CGFloat) { + if (scrollView.zoomScale == scrollView.minimumZoomScale) { + scrollView.panGestureRecognizer.isEnabled = false + } + } +} diff --git a/iphone/Maps/UI/PlacePage/PlacePageLayout/Content/Gallery/Photos/PhotosInteractionAnimator.swift b/iphone/Maps/UI/PlacePage/PlacePageLayout/Content/Gallery/Photos/PhotosInteractionAnimator.swift new file mode 100644 index 0000000000..13183712e1 --- /dev/null +++ b/iphone/Maps/UI/PlacePage/PlacePageLayout/Content/Gallery/Photos/PhotosInteractionAnimator.swift @@ -0,0 +1,133 @@ +import UIKit + +final class PhotosInteractionAnimator: NSObject { + struct Const { + static let returnToCenterVelocityAnimationRatio: CGFloat = 0.00007 + static let panDismissDistanceRatio: CGFloat = 0.075 + static let panDismissMaximumDuration: TimeInterval = 0.45 + } + + var animator: UIViewControllerAnimatedTransitioning? + var viewToHideWhenBeginningTransition: UIView? + var shouldAnimateUsingAnimator = false + + fileprivate var transitionContext: UIViewControllerContextTransitioning? + + func handlePanWithPanGestureRecognizer(_ gestureRecognizer: UIPanGestureRecognizer, viewToPan: UIView, anchorPoint: CGPoint) { + guard let fromView = transitionContext?.view(forKey: UITransitionContextViewKey.from) else { + return + } + let translatedPanGesturePoint = gestureRecognizer.translation(in: fromView) + let newCenterPoint = CGPoint(x: anchorPoint.x, y: anchorPoint.y + translatedPanGesturePoint.y) + + viewToPan.center = newCenterPoint + + let verticalDelta = newCenterPoint.y - anchorPoint.y + let backgroundAlpha = backgroundAlphaForPanningWithVerticalDelta(verticalDelta) + fromView.backgroundColor = fromView.backgroundColor?.withAlphaComponent(backgroundAlpha) + + if gestureRecognizer.state == .ended { + finishPanWithPanGestureRecognizer(gestureRecognizer, verticalDelta: verticalDelta,viewToPan: viewToPan, anchorPoint: anchorPoint) + } + } + + private func finishPanWithPanGestureRecognizer(_ gestureRecognizer: UIPanGestureRecognizer, verticalDelta: CGFloat, viewToPan: UIView, anchorPoint: CGPoint) { + guard let transitionContext = transitionContext, let fromView = transitionContext.view(forKey: UITransitionContextViewKey.from) else { + return + } + let dismissDistance = Const.panDismissDistanceRatio * fromView.bounds.height + let isDismissing = abs(verticalDelta) > dismissDistance + + if isDismissing, shouldAnimateUsingAnimator, let animator = animator { + animator.animateTransition(using: transitionContext) + self.transitionContext = nil + return + } + + let velocityY = gestureRecognizer.velocity(in: gestureRecognizer.view).y + + let finalPageViewCenterPoint: CGPoint + let animationDuration: TimeInterval + let finalBackgroundAlpha: CGFloat + + if isDismissing { + let modifier: CGFloat = verticalDelta.sign == .plus ? 1 : -1 + let finalCenterY = fromView.bounds.midY + modifier * fromView.bounds.height + finalPageViewCenterPoint = CGPoint(x: fromView.center.x, y: finalCenterY) + + let duration = TimeInterval(abs(finalPageViewCenterPoint.y - viewToPan.center.y) / abs(velocityY)) + animationDuration = min(duration, Const.panDismissMaximumDuration) + finalBackgroundAlpha = 0.0 + } else { + finalPageViewCenterPoint = anchorPoint + animationDuration = TimeInterval(abs(velocityY) * Const.returnToCenterVelocityAnimationRatio) + kDefaultAnimationDuration + finalBackgroundAlpha = 1.0 + } + let finalBackgroundColor = fromView.backgroundColor?.withAlphaComponent(finalBackgroundAlpha) + finishPanWithoutAnimator(duration: animationDuration, + viewToPan: viewToPan, fromView: fromView, + finalPageViewCenterPoint: finalPageViewCenterPoint, finalBackgroundColor: finalBackgroundColor, + isDismissing: isDismissing) + } + + private func finishPanWithoutAnimator(duration: TimeInterval, viewToPan: UIView, fromView: UIView, finalPageViewCenterPoint: CGPoint, finalBackgroundColor: UIColor?, isDismissing: Bool) { + UIView.animate(withDuration: duration, + delay: 0, + options: .curveEaseOut, + animations: { + viewToPan.center = finalPageViewCenterPoint + fromView.backgroundColor = finalBackgroundColor + }, + completion: { [weak self] _ in + guard let s = self else { return } + if isDismissing { + s.transitionContext?.finishInteractiveTransition() + } else { + s.transitionContext?.cancelInteractiveTransition() + if !s.isRadar20070670Fixed() { + s.fixCancellationStatusBarAppearanceBug() + } + } + s.viewToHideWhenBeginningTransition?.alpha = 1.0 + s.transitionContext?.completeTransition(isDismissing && !(s.transitionContext?.transitionWasCancelled ?? false)) + s.transitionContext = nil + }) + } + + private func fixCancellationStatusBarAppearanceBug() { + guard let toViewController = self.transitionContext?.viewController(forKey: UITransitionContextViewControllerKey.to), + let fromViewController = self.transitionContext?.viewController(forKey: UITransitionContextViewControllerKey.from) else { + return + } + + let statusBarViewControllerSelector = Selector("_setPresentedSta" + "tusBarViewController:") + if toViewController.responds(to: statusBarViewControllerSelector) && fromViewController.modalPresentationCapturesStatusBarAppearance { + toViewController.perform(statusBarViewControllerSelector, with: fromViewController) + } + } + + private func isRadar20070670Fixed() -> Bool { + return ProcessInfo.processInfo.isOperatingSystemAtLeast(OperatingSystemVersion.init(majorVersion: 8, minorVersion: 3, patchVersion: 0)) + } + + private func backgroundAlphaForPanningWithVerticalDelta(_ delta: CGFloat) -> CGFloat { + guard let fromView = transitionContext?.view(forKey: UITransitionContextViewKey.from) else { + return 0.0 + } + + let startingAlpha: CGFloat = 1.0 + let finalAlpha: CGFloat = 0.1 + let totalAvailableAlpha = startingAlpha - finalAlpha + + let maximumDelta = CGFloat(fromView.bounds.height / 2.0) + let deltaAsPercentageOfMaximum = min(abs(delta) / maximumDelta, 1.0) + return startingAlpha - (deltaAsPercentageOfMaximum * totalAvailableAlpha) + } +} + +extension PhotosInteractionAnimator: UIViewControllerInteractiveTransitioning { + func startInteractiveTransition(_ transitionContext: UIViewControllerContextTransitioning) { + viewToHideWhenBeginningTransition?.alpha = 0.0 + self.transitionContext = transitionContext + } +} diff --git a/iphone/Maps/UI/PlacePage/PlacePageLayout/Content/Gallery/Photos/PhotosOverlayView.swift b/iphone/Maps/UI/PlacePage/PlacePageLayout/Content/Gallery/Photos/PhotosOverlayView.swift new file mode 100644 index 0000000000..df221078c3 --- /dev/null +++ b/iphone/Maps/UI/PlacePage/PlacePageLayout/Content/Gallery/Photos/PhotosOverlayView.swift @@ -0,0 +1,86 @@ +import UIKit + +final class PhotosOverlayView: UIView { + private var navigationBar: UINavigationBar! + private var navigationItem: UINavigationItem! + + weak var photosViewController: PhotosViewController? + + var photo: GalleryItemModel? { + didSet { + guard let photo = photo else { + navigationItem.title = nil + return + } + guard let photosViewController = photosViewController else { return } + if let index = photosViewController.photos.items.index(where: { $0 === photo }) { + navigationItem.title = "\(index + 1) / \(photosViewController.photos.items.count)" + } + } + } + + override init(frame: CGRect) { + super.init(frame: frame) + setupNavigationBar() + } + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + @objc + private func closeButtonTapped(_ sender: UIBarButtonItem) { + photosViewController?.dismiss(animated: true, completion: nil) + } + + override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + if let hitView = super.hitTest(point, with: event) , hitView != self { + return hitView + } + return nil + } + + private func setupNavigationBar() { + navigationBar = UINavigationBar() + navigationBar.translatesAutoresizingMaskIntoConstraints = false + navigationBar.backgroundColor = UIColor.clear + navigationBar.barTintColor = nil + navigationBar.isTranslucent = true + navigationBar.shadowImage = UIImage() + navigationBar.setBackgroundImage(UIImage(), for: .default) + + navigationItem = UINavigationItem(title: "") + navigationBar.items = [navigationItem] + addSubview(navigationBar) + + let topConstraint = NSLayoutConstraint(item: navigationBar, attribute: .top, relatedBy: .equal, toItem: self, attribute: .top, multiplier: 1.0, constant: statusBarHeight()) + let widthConstraint = NSLayoutConstraint(item: navigationBar, attribute: .width, relatedBy: .equal, toItem: self, attribute: .width, multiplier: 1.0, constant: 0.0) + let horizontalPositionConstraint = NSLayoutConstraint(item: navigationBar, attribute: .centerX, relatedBy: .equal, toItem: self, attribute: .centerX, multiplier: 1.0, constant: 0.0) + addConstraints([topConstraint,widthConstraint,horizontalPositionConstraint]) + + navigationItem.leftBarButtonItem = UIBarButtonItem(image: #imageLiteral(resourceName: "ic_nav_bar_back"), style: .plain, target: self, action: #selector(closeButtonTapped(_:))) + } + + func setHidden(_ hidden: Bool, animated: Bool, animation: @escaping (() -> Void)) { + guard isHidden != hidden else { return } + guard animated else { + isHidden = hidden + animation() + return + } + isHidden = false + alpha = hidden ? 1.0 : 0.0 + + UIView.animate(withDuration: kDefaultAnimationDuration, + delay: 0.0, + options: [.allowAnimatedContent, .allowUserInteraction], + animations: { [weak self] in + self?.alpha = hidden ? 0.0 : 1.0 + animation() + }, + completion: { [weak self] _ in + self?.alpha = 1.0 + self?.isHidden = hidden + }) + } +} diff --git a/iphone/Maps/UI/PlacePage/PlacePageLayout/Content/Gallery/Photos/PhotosTransitionAnimator.swift b/iphone/Maps/UI/PlacePage/PlacePageLayout/Content/Gallery/Photos/PhotosTransitionAnimator.swift new file mode 100644 index 0000000000..b419c20b12 --- /dev/null +++ b/iphone/Maps/UI/PlacePage/PlacePageLayout/Content/Gallery/Photos/PhotosTransitionAnimator.swift @@ -0,0 +1,147 @@ +import UIKit + +final class PhotosTransitionAnimator: NSObject, UIViewControllerAnimatedTransitioning { + private struct Const { + static let animationDurationWithZooming = 2 * kDefaultAnimationDuration + static let animationDurationWithoutZooming = kDefaultAnimationDuration + static let animationDurationEndingViewFadeInRatio = 0.1 + static let animationDurationStartingViewFadeOutRatio = 0.05 + static let zoomingAnimationSpringDamping: CGFloat = 0.9 + static let animationDurationFadeRatio = 4.0 / 9.0 + } + + var dismissing = false + + var startingView: UIView? + var endingView: UIView? + + private var shouldPerformZoomingAnimation: Bool { + return startingView != nil && endingView != nil + } + + func animateTransition(using transitionContext: UIViewControllerContextTransitioning) { + setupTransitionContainerHierarchyWithTransitionContext(transitionContext) + if shouldPerformZoomingAnimation { + performZoomingAnimationWithTransitionContext(transitionContext) + } + performFadeAnimationWithTransitionContext(transitionContext) + } + + func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval { + return shouldPerformZoomingAnimation ? Const.animationDurationWithZooming : Const.animationDurationWithoutZooming + } + + private func fadeDurationForTransitionContext(_ transitionContext: UIViewControllerContextTransitioning) -> TimeInterval { + let transDuration = transitionDuration(using: transitionContext) + return shouldPerformZoomingAnimation ? transDuration * Const.animationDurationFadeRatio : transDuration + } + + private func setupTransitionContainerHierarchyWithTransitionContext(_ transitionContext: UIViewControllerContextTransitioning) { + if let toView = transitionContext.view(forKey: UITransitionContextViewKey.to), + let toViewController = transitionContext.viewController(forKey: UITransitionContextViewControllerKey.to) { + toView.frame = transitionContext.finalFrame(for: toViewController) + let containerView = transitionContext.containerView + + if !toView.isDescendant(of: containerView) { + containerView.addSubview(toView) + } + } + + if dismissing { + if let fromView = transitionContext.view(forKey: UITransitionContextViewKey.from) { + transitionContext.containerView.bringSubview(toFront: fromView) + } + } + } + + private func performZoomingAnimationWithTransitionContext(_ transitionContext: UIViewControllerContextTransitioning) { + let containerView = transitionContext.containerView + guard let startingView = startingView, let endingView = endingView else { return } + + let startingViewForAnimation = startingView.snapshot + let endingViewForAnimation = endingView.snapshot + + let finalEndingViewTransform = endingView.transform + let endingViewInitialTransform = startingViewForAnimation.frame.height / endingViewForAnimation.frame.height + let startingViewFinalTransform = 1.0 / endingViewInitialTransform + + let translatedStartingViewCenter = startingView.center(inContainerView: containerView) + let translatedEndingViewFinalCenter = endingView.center(inContainerView: containerView) + + startingViewForAnimation.center = translatedStartingViewCenter + + endingViewForAnimation.transform = endingViewForAnimation.transform.scaledBy(x: endingViewInitialTransform, y: endingViewInitialTransform) + endingViewForAnimation.center = translatedStartingViewCenter + endingViewForAnimation.alpha = 0.0 + + containerView.addSubview(startingViewForAnimation) + containerView.addSubview(endingViewForAnimation) + + endingView.alpha = 0.0 + startingView.alpha = 0.0 + + let transDuration = transitionDuration(using: transitionContext) + let fadeInDuration = transDuration * Const.animationDurationEndingViewFadeInRatio + let fadeOutDuration = transDuration * Const.animationDurationStartingViewFadeOutRatio + + UIView.animate(withDuration: fadeInDuration, + delay: 0.0, + options: [.allowAnimatedContent,.beginFromCurrentState], + animations: { endingViewForAnimation.alpha = 1.0 }, + completion: { _ in + UIView.animate(withDuration: fadeOutDuration, + delay: 0.0, + options: [.allowAnimatedContent,.beginFromCurrentState], + animations: { startingViewForAnimation.alpha = 0.0 }, + completion: { _ in + startingViewForAnimation.removeFromSuperview() + }) + + }) + + UIView.animate(withDuration: transDuration, + delay: 0.0, + usingSpringWithDamping:Const.zoomingAnimationSpringDamping, + initialSpringVelocity:0, + options: [.allowAnimatedContent,.beginFromCurrentState], + animations: { () -> Void in + endingViewForAnimation.transform = finalEndingViewTransform + endingViewForAnimation.center = translatedEndingViewFinalCenter + startingViewForAnimation.transform = startingViewForAnimation.transform.scaledBy(x: startingViewFinalTransform, y: startingViewFinalTransform) + startingViewForAnimation.center = translatedEndingViewFinalCenter + }, + completion: { [weak self] _ in + endingViewForAnimation.removeFromSuperview() + endingView.alpha = 1.0 + startingView.alpha = 1.0 + self?.completeTransitionWithTransitionContext(transitionContext) + }) + } + + private func performFadeAnimationWithTransitionContext(_ transitionContext: UIViewControllerContextTransitioning) { + let fadeView = dismissing ? transitionContext.view(forKey: UITransitionContextViewKey.from) : transitionContext.view(forKey: UITransitionContextViewKey.to) + let beginningAlpha: CGFloat = dismissing ? 1.0 : 0.0 + let endingAlpha: CGFloat = dismissing ? 0.0 : 1.0 + + fadeView?.alpha = beginningAlpha + + UIView.animate(withDuration: fadeDurationForTransitionContext(transitionContext), animations: { + fadeView?.alpha = endingAlpha + }) { finished in + if !self.shouldPerformZoomingAnimation { + self.completeTransitionWithTransitionContext(transitionContext) + } + } + } + + private func completeTransitionWithTransitionContext(_ transitionContext: UIViewControllerContextTransitioning) { + if transitionContext.isInteractive { + if transitionContext.transitionWasCancelled { + transitionContext.cancelInteractiveTransition() + } else { + transitionContext.finishInteractiveTransition() + } + } + transitionContext.completeTransition(!transitionContext.transitionWasCancelled) + } +} diff --git a/iphone/Maps/UI/PlacePage/PlacePageLayout/Content/Gallery/Photos/PhotosViewController.swift b/iphone/Maps/UI/PlacePage/PlacePageLayout/Content/Gallery/Photos/PhotosViewController.swift new file mode 100644 index 0000000000..9fb28f07b7 --- /dev/null +++ b/iphone/Maps/UI/PlacePage/PlacePageLayout/Content/Gallery/Photos/PhotosViewController.swift @@ -0,0 +1,221 @@ +import UIKit + +@objc(MWMPhotosViewController) +final class PhotosViewController: MWMViewController { + var referenceViewForPhotoWhenDismissingHandler: ((GalleryItemModel) -> UIView?)? + + private let pageViewController = UIPageViewController(transitionStyle: .scroll, navigationOrientation: .horizontal, options: [UIPageViewControllerOptionInterPageSpacingKey: 16.0]) + private(set) var photos: GalleryModel + + fileprivate let transitionAnimator = PhotosTransitionAnimator() + fileprivate let interactiveAnimator = PhotosInteractionAnimator() + fileprivate let overlayView = PhotosOverlayView(frame: CGRect.zero) + private var overlayViewHidden = false + + private lazy var singleTapGestureRecognizer: UITapGestureRecognizer = { + return UITapGestureRecognizer(target: self, action: #selector(handleSingleTapGestureRecognizer(_:))) + }() + private lazy var panGestureRecognizer: UIPanGestureRecognizer = { + return UIPanGestureRecognizer(target: self, action: #selector(handlePanGestureRecognizer(_:))) + }() + + fileprivate var interactiveDismissal = false + + private var currentPhotoViewController: PhotoViewController? { + return pageViewController.viewControllers?.first as? PhotoViewController + } + + fileprivate var currentPhoto: GalleryItemModel? { + return currentPhotoViewController?.photo + } + + init(photos: GalleryModel, initialPhoto: GalleryItemModel? = nil, referenceView: UIView? = nil) { + self.photos = photos + super.init(nibName: nil, bundle: nil) + initialSetupWithInitialPhoto(initialPhoto) + transitionAnimator.startingView = referenceView + transitionAnimator.endingView = currentPhotoViewController?.scalingImageView.imageView + } + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + fileprivate func initialSetupWithInitialPhoto(_ initialPhoto: GalleryItemModel? = nil) { + overlayView.photosViewController = self + setupPageViewController(initialPhoto: initialPhoto) + + modalPresentationStyle = .custom + transitioningDelegate = self + modalPresentationCapturesStatusBarAppearance = true + + overlayView.photosViewController = self + } + + private func setupPageViewController(initialPhoto: GalleryItemModel? = nil) { + pageViewController.view.backgroundColor = UIColor.clear + pageViewController.delegate = self + pageViewController.dataSource = self + + if let photo = initialPhoto { + let photoViewController = initializePhotoViewController(photo: photo) + pageViewController.setViewControllers([photoViewController], + direction: .forward, + animated: false, + completion: nil) + } + overlayView.photo = initialPhoto + } + + fileprivate func initializePhotoViewController(photo: GalleryItemModel) -> PhotoViewController { + let photoViewController = PhotoViewController(photo: photo) + singleTapGestureRecognizer.require(toFail: photoViewController.doubleTapGestureRecognizer) + return photoViewController + } + + // MARK: - View Life Cycle + override func viewDidLoad() { + super.viewDidLoad() + view.tintColor = UIColor.white + view.backgroundColor = UIColor.black + pageViewController.view.backgroundColor = UIColor.clear + + pageViewController.view.addGestureRecognizer(singleTapGestureRecognizer) + pageViewController.view.addGestureRecognizer(panGestureRecognizer) + + addChildViewController(pageViewController) + view.addSubview(pageViewController.view) + pageViewController.view.autoresizingMask = [.flexibleWidth, .flexibleHeight] + pageViewController.didMove(toParentViewController: self) + + setupOverlayView() + } + + private func setupOverlayView() { + overlayView.photo = currentPhoto + overlayView.autoresizingMask = [.flexibleWidth, .flexibleHeight] + overlayView.frame = view.bounds + view.addSubview(overlayView) + setOverlayHidden(false, animated: false) + } + + private func setOverlayHidden(_ hidden: Bool, animated: Bool) { + overlayViewHidden = hidden + overlayView.setHidden(hidden, animated: animated) { [weak self] in + self?.setNeedsStatusBarAppearanceUpdate() + } + } + + override func dismiss(animated flag: Bool, completion: (() -> Void)? = nil) { + guard presentedViewController == nil else { + super.dismiss(animated: flag, completion: completion) + return + } + transitionAnimator.startingView = currentPhotoViewController?.scalingImageView.imageView + if let currentPhoto = currentPhoto { + transitionAnimator.endingView = referenceViewForPhotoWhenDismissingHandler?(currentPhoto) + } else { + transitionAnimator.endingView = nil + } + let overlayWasHiddenBeforeTransition = overlayView.isHidden + setOverlayHidden(true, animated: true) + + super.dismiss(animated: flag) { [weak self] in + guard let s = self else { return } + let isStillOnscreen = s.view.window != nil + if isStillOnscreen && !overlayWasHiddenBeforeTransition { + s.setOverlayHidden(false, animated: true) + } + completion?() + } + } + + // MARK: - Gesture Recognizers + @objc + private func handlePanGestureRecognizer(_ gestureRecognizer: UIPanGestureRecognizer) { + if gestureRecognizer.state == .began { + interactiveDismissal = true + dismiss(animated: true, completion: nil) + } else { + interactiveDismissal = false + interactiveAnimator.handlePanWithPanGestureRecognizer(gestureRecognizer, viewToPan: pageViewController.view, anchorPoint: CGPoint(x: view.bounds.midX, y: view.bounds.midY)) + } + } + + @objc + private func handleSingleTapGestureRecognizer(_ gestureRecognizer: UITapGestureRecognizer) { + setOverlayHidden(!overlayView.isHidden, animated: true) + } + + //MARK: - Orientations + override var supportedInterfaceOrientations: UIInterfaceOrientationMask { + return .all + } + + // MARK: - Status Bar + override var prefersStatusBarHidden: Bool { + return overlayViewHidden + } + + override var preferredStatusBarStyle: UIStatusBarStyle { + return .lightContent + } + + override var preferredStatusBarUpdateAnimation: UIStatusBarAnimation { + return .fade + } +} + +extension PhotosViewController: UIViewControllerTransitioningDelegate { + func animationController(forPresented presented: UIViewController, presenting: UIViewController, source: UIViewController) -> UIViewControllerAnimatedTransitioning? { + transitionAnimator.dismissing = false + return transitionAnimator + } + + func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? { + transitionAnimator.dismissing = true + return transitionAnimator + } + + func interactionControllerForDismissal(using animator: UIViewControllerAnimatedTransitioning) -> UIViewControllerInteractiveTransitioning? { + if interactiveDismissal { + interactiveAnimator.animator = transitionAnimator + interactiveAnimator.shouldAnimateUsingAnimator = transitionAnimator.endingView != nil + interactiveAnimator.viewToHideWhenBeginningTransition = overlayView + return interactiveAnimator + } + return nil + } +} + +extension PhotosViewController: UIPageViewControllerDataSource { + func pageViewController(_ pageViewController: UIPageViewController, viewControllerBefore viewController: UIViewController) -> UIViewController? { + guard let photoViewController = viewController as? PhotoViewController, + let photoIndex = photos.items.index(where: { $0 === photoViewController.photo }), + photoIndex - 1 >= 0 else { + return nil + } + let newPhoto = photos.items[photoIndex - 1] + return initializePhotoViewController(photo: newPhoto) + } + + func pageViewController(_ pageViewController: UIPageViewController, viewControllerAfter viewController: UIViewController) -> UIViewController? { + guard let photoViewController = viewController as? PhotoViewController, + let photoIndex = photos.items.index(where: { $0 === photoViewController.photo }), + photoIndex + 1 < photos.items.count else { + return nil + } + let newPhoto = photos.items[photoIndex + 1] + return initializePhotoViewController(photo: newPhoto) + } +} + +extension PhotosViewController: UIPageViewControllerDelegate { + func pageViewController(_ pageViewController: UIPageViewController, didFinishAnimating finished: Bool, previousViewControllers: [UIViewController], transitionCompleted completed: Bool) { + if completed { + if let currentPhoto = currentPhoto { + overlayView.photo = currentPhoto + } + } + } +}