From 299cc12972192f15fe52bb9bb82af916bf8dd342 Mon Sep 17 00:00:00 2001 From: Martin Rechsteiner Date: Fri, 27 Mar 2020 22:15:30 +0100 Subject: [PATCH 01/12] Add custom page view controller implementation The implementation is split into two classes, PageViewController and PageViewManager. PageViewManager is responsible for all the logic related to adding/removing view controllers and calling the correct appearance transitions, while PageViewController is responsible for updating the UIScrollView. --- Parchment.xcodeproj/project.pbxproj | 48 +- Parchment/Classes/PageViewController.swift | 210 ++++ Parchment/Classes/PageViewManager.swift | 546 ++++++++++ Parchment/Classes/PagingController.swift | 67 +- Parchment/Classes/PagingViewController.swift | 90 +- Parchment/Enums/PageViewDirection.swift | 28 + Parchment/Enums/PageViewState.swift | 22 + Parchment/Enums/PagingDirection.swift | 2 +- .../PageViewControllerDataSource.swift | 10 + .../PageViewControllerDelegate.swift | 18 + .../Protocols/PageViewManagerDataSource.swift | 6 + .../Protocols/PageViewManagerDelegate.swift | 22 + .../Mocks/MockPageViewManagerDataSource.swift | 16 + .../Mocks/MockPageViewManagerDelegate.swift | 60 ++ ParchmentTests/PageViewManagerTests.swift | 937 ++++++++++++++++++ ParchmentTests/PagingControllerTests.swift | 18 +- .../PagingViewControllerTests.swift | 3 - 17 files changed, 2003 insertions(+), 100 deletions(-) create mode 100644 Parchment/Classes/PageViewController.swift create mode 100644 Parchment/Classes/PageViewManager.swift create mode 100644 Parchment/Enums/PageViewDirection.swift create mode 100644 Parchment/Enums/PageViewState.swift create mode 100644 Parchment/Protocols/PageViewControllerDataSource.swift create mode 100644 Parchment/Protocols/PageViewControllerDelegate.swift create mode 100644 Parchment/Protocols/PageViewManagerDataSource.swift create mode 100644 Parchment/Protocols/PageViewManagerDelegate.swift create mode 100644 ParchmentTests/Mocks/MockPageViewManagerDataSource.swift create mode 100644 ParchmentTests/Mocks/MockPageViewManagerDelegate.swift create mode 100644 ParchmentTests/PageViewManagerTests.swift diff --git a/Parchment.xcodeproj/project.pbxproj b/Parchment.xcodeproj/project.pbxproj index 2208d3cd..84ca850d 100644 --- a/Parchment.xcodeproj/project.pbxproj +++ b/Parchment.xcodeproj/project.pbxproj @@ -55,6 +55,7 @@ 955444C01FC9CCFF001EC26B /* PagingMenuHorizontalAlignment.swift in Sources */ = {isa = PBXBuildFile; fileRef = 955444BF1FC9CCFF001EC26B /* PagingMenuHorizontalAlignment.swift */; }; 955444C21FC9CD19001EC26B /* PagingMenuTransition.swift in Sources */ = {isa = PBXBuildFile; fileRef = 955444C11FC9CD19001EC26B /* PagingMenuTransition.swift */; }; 955444C41FC9CD2C001EC26B /* PagingMenuInteraction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 955444C31FC9CD2C001EC26B /* PagingMenuInteraction.swift */; }; + 955453C42413C80F00923BC8 /* PageViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 955453C32413C80F00923BC8 /* PageViewController.swift */; }; 955453C62413D76C00923BC8 /* ExamplesViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 955453C52413D76C00923BC8 /* ExamplesViewController.swift */; }; 955453C82413DB7900923BC8 /* UIView+constraints.swift in Sources */ = {isa = PBXBuildFile; fileRef = 955453C72413DB7900923BC8 /* UIView+constraints.swift */; }; 955453CF2413DC6900923BC8 /* UIColor+interpolation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 955453CE2413DC6900923BC8 /* UIColor+interpolation.swift */; }; @@ -89,6 +90,10 @@ 95A84B0520E586FA0031520F /* PagingStaticDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 95A84B0420E586FA0031520F /* PagingStaticDataSource.swift */; }; 95A84B0D20ED46920031520F /* AnyPagingItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 95A84B0C20ED46920031520F /* AnyPagingItem.swift */; }; 95B301171E59FCD500B95D02 /* UIEdgeInsets.swift in Sources */ = {isa = PBXBuildFile; fileRef = 95B301161E59FCD500B95D02 /* UIEdgeInsets.swift */; }; + 95D2AE36242BB22F00AC3D46 /* PageViewDirection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 95D2AE35242BB22F00AC3D46 /* PageViewDirection.swift */; }; + 95D2AE38242BB24800AC3D46 /* PageViewState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 95D2AE37242BB24800AC3D46 /* PageViewState.swift */; }; + 95D2AE3A242BB25F00AC3D46 /* PageViewManagerDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 95D2AE39242BB25F00AC3D46 /* PageViewManagerDataSource.swift */; }; + 95D2AE3C242BB28C00AC3D46 /* PageViewManagerDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 95D2AE3B242BB28C00AC3D46 /* PageViewManagerDelegate.swift */; }; 95D2AE44242BC1FA00AC3D46 /* ParchmentUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 95D2AE43242BC1FA00AC3D46 /* ParchmentUITests.swift */; }; 95D2AE52242BCC9500AC3D46 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 95D2AE51242BCC9500AC3D46 /* AppDelegate.swift */; }; 95D2AE54242BCC9500AC3D46 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 95D2AE53242BCC9500AC3D46 /* SceneDelegate.swift */; }; @@ -99,6 +104,8 @@ 95D2AE64242BED9B00AC3D46 /* Parchment.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3EA04A4B1C53BFE40054E5E0 /* Parchment.framework */; }; 95D2AE65242BED9B00AC3D46 /* Parchment.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 3EA04A4B1C53BFE40054E5E0 /* Parchment.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 95D2AE6A242BF58500AC3D46 /* PageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 95D2AE69242BF58500AC3D46 /* PageView.swift */; }; + 95D2AE6F242EA3EF00AC3D46 /* PageViewControllerDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 95D2AE6E242EA3EF00AC3D46 /* PageViewControllerDelegate.swift */; }; + 95D2AE71242EA40A00AC3D46 /* PageViewControllerDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 95D2AE70242EA40A00AC3D46 /* PageViewControllerDataSource.swift */; }; 95D78FDB2287138F00E6EE7C /* MockCollectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 95D78FDA2287138F00E6EE7C /* MockCollectionView.swift */; }; 95D78FDD228713B800E6EE7C /* MockCollectionViewLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = 95D78FDC228713B800E6EE7C /* MockCollectionViewLayout.swift */; }; 95D78FDF228715C800E6EE7C /* MockPagingControllerDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 95D78FDE228715C800E6EE7C /* MockPagingControllerDelegate.swift */; }; @@ -113,6 +120,10 @@ 95E4BA721FF15EFE008871A3 /* PagingFiniteDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 95E4BA711FF15EFE008871A3 /* PagingFiniteDataSource.swift */; }; 95F5660F2128707900F2A75E /* PagingMenuItemSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9D1BE1D211E409400E86B72 /* PagingMenuItemSource.swift */; }; 95FE3AF91FFEDBCE00E6F2AD /* PagingDistance.swift in Sources */ = {isa = PBXBuildFile; fileRef = 95FE3AF81FFEDBCE00E6F2AD /* PagingDistance.swift */; }; + 95FEEA4524215FCA009B5B64 /* PageViewManagerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 95FEEA4424215FCA009B5B64 /* PageViewManagerTests.swift */; }; + 95FEEA4D2423C44A009B5B64 /* MockPageViewManagerDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 95FEEA4C2423C44A009B5B64 /* MockPageViewManagerDelegate.swift */; }; + 95FEEA4F2423F213009B5B64 /* PageViewManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 95FEEA4E2423F213009B5B64 /* PageViewManager.swift */; }; + 95FEEA512423F752009B5B64 /* MockPageViewManagerDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 95FEEA502423F752009B5B64 /* MockPageViewManagerDataSource.swift */; }; E8CB720A23D70E1400A9B089 /* PagingMenuPosition.swift in Sources */ = {isa = PBXBuildFile; fileRef = E8CB720923D70E1400A9B089 /* PagingMenuPosition.swift */; }; /* End PBXBuildFile section */ @@ -239,6 +250,7 @@ 955444BF1FC9CCFF001EC26B /* PagingMenuHorizontalAlignment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PagingMenuHorizontalAlignment.swift; sourceTree = ""; }; 955444C11FC9CD19001EC26B /* PagingMenuTransition.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PagingMenuTransition.swift; sourceTree = ""; }; 955444C31FC9CD2C001EC26B /* PagingMenuInteraction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PagingMenuInteraction.swift; sourceTree = ""; }; + 955453C32413C80F00923BC8 /* PageViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PageViewController.swift; sourceTree = ""; }; 955453C52413D76C00923BC8 /* ExamplesViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExamplesViewController.swift; sourceTree = ""; }; 955453C72413DB7900923BC8 /* UIView+constraints.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIView+constraints.swift"; sourceTree = ""; }; 955453CE2413DC6900923BC8 /* UIColor+interpolation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIColor+interpolation.swift"; sourceTree = ""; }; @@ -265,6 +277,10 @@ 95A84B0420E586FA0031520F /* PagingStaticDataSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PagingStaticDataSource.swift; sourceTree = ""; }; 95A84B0C20ED46920031520F /* AnyPagingItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnyPagingItem.swift; sourceTree = ""; }; 95B301161E59FCD500B95D02 /* UIEdgeInsets.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UIEdgeInsets.swift; sourceTree = ""; }; + 95D2AE35242BB22F00AC3D46 /* PageViewDirection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PageViewDirection.swift; sourceTree = ""; }; + 95D2AE37242BB24800AC3D46 /* PageViewState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PageViewState.swift; sourceTree = ""; }; + 95D2AE39242BB25F00AC3D46 /* PageViewManagerDataSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PageViewManagerDataSource.swift; sourceTree = ""; }; + 95D2AE3B242BB28C00AC3D46 /* PageViewManagerDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PageViewManagerDelegate.swift; sourceTree = ""; }; 95D2AE41242BC1F900AC3D46 /* ParchmentUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = ParchmentUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 95D2AE43242BC1FA00AC3D46 /* ParchmentUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ParchmentUITests.swift; sourceTree = ""; }; 95D2AE45242BC1FA00AC3D46 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; @@ -277,6 +293,8 @@ 95D2AE5D242BCC9900AC3D46 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 95D2AE5F242BCC9900AC3D46 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 95D2AE69242BF58500AC3D46 /* PageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PageView.swift; sourceTree = ""; }; + 95D2AE6E242EA3EF00AC3D46 /* PageViewControllerDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PageViewControllerDelegate.swift; sourceTree = ""; }; + 95D2AE70242EA40A00AC3D46 /* PageViewControllerDataSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PageViewControllerDataSource.swift; sourceTree = ""; }; 95D78FDA2287138F00E6EE7C /* MockCollectionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockCollectionView.swift; sourceTree = ""; }; 95D78FDC228713B800E6EE7C /* MockCollectionViewLayout.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockCollectionViewLayout.swift; sourceTree = ""; }; 95D78FDE228715C800E6EE7C /* MockPagingControllerDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockPagingControllerDelegate.swift; sourceTree = ""; }; @@ -290,6 +308,10 @@ 95E4BA6F1FF15E84008871A3 /* PagingViewControllerDataSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PagingViewControllerDataSource.swift; sourceTree = ""; }; 95E4BA711FF15EFE008871A3 /* PagingFiniteDataSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PagingFiniteDataSource.swift; sourceTree = ""; }; 95FE3AF81FFEDBCE00E6F2AD /* PagingDistance.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PagingDistance.swift; sourceTree = ""; }; + 95FEEA4424215FCA009B5B64 /* PageViewManagerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PageViewManagerTests.swift; sourceTree = ""; }; + 95FEEA4C2423C44A009B5B64 /* MockPageViewManagerDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockPageViewManagerDelegate.swift; sourceTree = ""; }; + 95FEEA4E2423F213009B5B64 /* PageViewManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PageViewManager.swift; sourceTree = ""; }; + 95FEEA502423F752009B5B64 /* MockPageViewManagerDataSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockPageViewManagerDataSource.swift; sourceTree = ""; }; E8CB720923D70E1400A9B089 /* PagingMenuPosition.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PagingMenuPosition.swift; sourceTree = ""; }; E9D1BE1D211E409400E86B72 /* PagingMenuItemSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PagingMenuItemSource.swift; sourceTree = ""; }; /* End PBXFileReference section */ @@ -340,18 +362,20 @@ isa = PBXGroup; children = ( 95868C31200412D8004B392B /* InvalidationState.swift */, + 95D2AE35242BB22F00AC3D46 /* PageViewDirection.swift */, + 95D2AE37242BB24800AC3D46 /* PageViewState.swift */, 955444BB1FC9CCD9001EC26B /* PagingBorderOptions.swift */, 95868C2B2003DA87004B392B /* PagingContentInteraction.swift */, 3E4090961C88BCC900800E22 /* PagingDirection.swift */, 955444B91FC9CCBF001EC26B /* PagingIndicatorOptions.swift */, - E8CB720923D70E1400A9B089 /* PagingMenuPosition.swift */, 955444BF1FC9CCFF001EC26B /* PagingMenuHorizontalAlignment.swift */, 955444C31FC9CD2C001EC26B /* PagingMenuInteraction.swift */, 955444B71FC9CCA6001EC26B /* PagingMenuItemSize.swift */, + E9D1BE1D211E409400E86B72 /* PagingMenuItemSource.swift */, + E8CB720923D70E1400A9B089 /* PagingMenuPosition.swift */, 955444C11FC9CD19001EC26B /* PagingMenuTransition.swift */, 955444BD1FC9CCEC001EC26B /* PagingSelectedScrollPosition.swift */, 3E4090991C88BCC900800E22 /* PagingState.swift */, - E9D1BE1D211E409400E86B72 /* PagingMenuItemSource.swift */, ); path = Enums; sourceTree = ""; @@ -393,6 +417,8 @@ 3E562ACD1CE7CD8C007623B3 /* PagingTitleCell.swift */, 3E49C7231C8F5C13006269DD /* PagingView.swift */, 3E49C7241C8F5C13006269DD /* PagingViewController.swift */, + 955453C32413C80F00923BC8 /* PageViewController.swift */, + 95FEEA4E2423F213009B5B64 /* PageViewManager.swift */, ); path = Classes; sourceTree = ""; @@ -402,6 +428,7 @@ children = ( 3E504EC81C7465B000AE1CE3 /* Info.plist */, 954E7DEB1F48AE1300342ECF /* Item.swift */, + 95FEEA4424215FCA009B5B64 /* PageViewManagerTests.swift */, 95591F20222C3A0400677B4B /* PagingControllerTests.swift */, 3E5E93E81CE7E093000762A1 /* PagingDataStructureTests.swift */, 954842621F4252070072038C /* PagingDiffTests.swift */, @@ -477,6 +504,10 @@ isa = PBXGroup; children = ( 9568922A222C525C00AFF250 /* CollectionView.swift */, + 95D2AE70242EA40A00AC3D46 /* PageViewControllerDataSource.swift */, + 95D2AE6E242EA3EF00AC3D46 /* PageViewControllerDelegate.swift */, + 95D2AE39242BB25F00AC3D46 /* PageViewManagerDataSource.swift */, + 95D2AE3B242BB28C00AC3D46 /* PageViewManagerDelegate.swift */, 3E2AAD2B1CA869B50044AAA5 /* PagingItem.swift */, 95A52A141FF81F70002A2ED4 /* PagingLayout.swift */, 95D790152299D56300E6EE7C /* PagingMenuDataSource.swift */, @@ -674,6 +705,8 @@ 95D78FE0228715E100E6EE7C /* MockPagingControllerDataSource.swift */, 95D78FDE228715C800E6EE7C /* MockPagingControllerDelegate.swift */, 95D78FEB2288343F00E6EE7C /* MockPagingControllerSizeDelegate.swift */, + 95FEEA4C2423C44A009B5B64 /* MockPageViewManagerDelegate.swift */, + 95FEEA502423F752009B5B64 /* MockPageViewManagerDataSource.swift */, ); path = Mocks; sourceTree = ""; @@ -894,13 +927,16 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 95FEEA512423F752009B5B64 /* MockPageViewManagerDataSource.swift in Sources */, 95D78FE1228715E100E6EE7C /* MockPagingControllerDataSource.swift in Sources */, 3E5E93E91CE7E093000762A1 /* PagingDataStructureTests.swift in Sources */, 95D78FEC2288343F00E6EE7C /* MockPagingControllerSizeDelegate.swift in Sources */, + 95FEEA4D2423C44A009B5B64 /* MockPageViewManagerDelegate.swift in Sources */, 95591F21222C3A0400677B4B /* PagingControllerTests.swift in Sources */, 95D78FDF228715C800E6EE7C /* MockPagingControllerDelegate.swift in Sources */, 3E5E93E31CE7D899000762A1 /* PagingStateTests.swift in Sources */, 951E163720A21D3A0055E9D4 /* PagingViewControllerTests.swift in Sources */, + 95FEEA4524215FCA009B5B64 /* PageViewManagerTests.swift in Sources */, 95D78FDB2287138F00E6EE7C /* MockCollectionView.swift in Sources */, 954E7DEE1F48AE6E00342ECF /* Item.swift in Sources */, 954842631F4252070072038C /* PagingDiffTests.swift in Sources */, @@ -917,7 +953,9 @@ 95D2AE6A242BF58500AC3D46 /* PageView.swift in Sources */, 95868C34200412DE004B392B /* Tween.swift in Sources */, 95A52A151FF81F70002A2ED4 /* PagingLayout.swift in Sources */, + 95D2AE3A242BB25F00AC3D46 /* PageViewManagerDataSource.swift in Sources */, 955444C21FC9CD19001EC26B /* PagingMenuTransition.swift in Sources */, + 95FEEA4F2423F213009B5B64 /* PageViewManager.swift in Sources */, 3E2AAD2E1CA86A320044AAA5 /* PagingViewControllerDelegate.swift in Sources */, 3EADD99F1CC65C4D003171CF /* EMPageViewController.swift in Sources */, 9548425D1F42486B0072038C /* PagingDiff.swift in Sources */, @@ -932,6 +970,7 @@ 957F14091E35583500E562F8 /* PagingCellLayoutAttributes.swift in Sources */, 9568922B222C525C00AFF250 /* CollectionView.swift in Sources */, 3E2AAD281CA831AB0044AAA5 /* UIView+constraints.swift in Sources */, + 95D2AE6F242EA3EF00AC3D46 /* PageViewControllerDelegate.swift in Sources */, 95A84B0520E586FA0031520F /* PagingStaticDataSource.swift in Sources */, 3E49C7251C8F5C13006269DD /* PagingBorderView.swift in Sources */, E8CB720A23D70E1400A9B089 /* PagingMenuPosition.swift in Sources */, @@ -940,12 +979,15 @@ 3E49C7291C8F5C13006269DD /* PagingView.swift in Sources */, 3E4090AF1C88BDD100800E22 /* PagingCollectionViewLayout.swift in Sources */, 95591F23222C522800677B4B /* PagingController.swift in Sources */, + 955453C42413C80F00923BC8 /* PageViewController.swift in Sources */, 3E4090A21C88BD0A00800E22 /* PagingIndicatorMetric.swift in Sources */, + 95D2AE71242EA40A00AC3D46 /* PageViewControllerDataSource.swift in Sources */, 955444BE1FC9CCEC001EC26B /* PagingSelectedScrollPosition.swift in Sources */, 3E4189211C9573FA001E0284 /* PagingViewControllerInfiniteDataSource.swift in Sources */, 95868C32200412D8004B392B /* InvalidationState.swift in Sources */, 3E2AAD2C1CA869B50044AAA5 /* PagingItem.swift in Sources */, 3E40909C1C88BCC900800E22 /* PagingOptions.swift in Sources */, + 95D2AE3C242BB28C00AC3D46 /* PageViewManagerDelegate.swift in Sources */, 3E4090B11C88BDD100800E22 /* PagingIndicatorLayoutAttributes.swift in Sources */, 955444C41FC9CD2C001EC26B /* PagingMenuInteraction.swift in Sources */, 95D790142299D28A00E6EE7C /* PagingMenuView.swift in Sources */, @@ -956,6 +998,7 @@ 955444B11FC99E2C001EC26B /* PagingSizeCache.swift in Sources */, 3E4283FC1C99CF9000032D95 /* PagingItems.swift in Sources */, 3E49C7281C8F5C13006269DD /* PagingIndicatorView.swift in Sources */, + 95D2AE36242BB22F00AC3D46 /* PageViewDirection.swift in Sources */, 95B301171E59FCD500B95D02 /* UIEdgeInsets.swift in Sources */, 955444C01FC9CCFF001EC26B /* PagingMenuHorizontalAlignment.swift in Sources */, 3E49C7261C8F5C13006269DD /* PagingCell.swift in Sources */, @@ -967,6 +1010,7 @@ 955444B81FC9CCA6001EC26B /* PagingMenuItemSize.swift in Sources */, 3E562ACE1CE7CD8C007623B3 /* PagingTitleCell.swift in Sources */, 95FE3AF91FFEDBCE00E6F2AD /* PagingDistance.swift in Sources */, + 95D2AE38242BB24800AC3D46 /* PageViewState.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/Parchment/Classes/PageViewController.swift b/Parchment/Classes/PageViewController.swift new file mode 100644 index 00000000..b68a3d4d --- /dev/null +++ b/Parchment/Classes/PageViewController.swift @@ -0,0 +1,210 @@ +import UIKit + +public final class PageViewController: UIViewController { + public weak var dataSource: PageViewControllerDataSource? + public weak var delegate: PageViewControllerDelegate? + + public override var shouldAutomaticallyForwardAppearanceMethods: Bool { + return false + } + + public var selectedViewController: UIViewController? { + return manager.selectedViewController + } + + public private(set) lazy var scrollView: UIScrollView = { + let scrollView = UIScrollView() + scrollView.isPagingEnabled = true + scrollView.scrollsToTop = false + scrollView.bounces = true + scrollView.alwaysBounceHorizontal = true + scrollView.alwaysBounceVertical = false + scrollView.translatesAutoresizingMaskIntoConstraints = true + scrollView.showsHorizontalScrollIndicator = false + scrollView.showsVerticalScrollIndicator = false + return scrollView + }() + + private let manager = PageViewManager() + private let options: PagingOptions + + init(options: PagingOptions = PagingOptions()) { + self.options = options + super.init(nibName: nil, bundle: nil) + } + + required init?(coder: NSCoder) { + self.options = PagingOptions() + super.init(coder: coder) + } + + override public func viewDidLoad() { + super.viewDidLoad() + manager.delegate = self + manager.dataSource = self + view.addSubview(scrollView) + view.constrainToEdges(scrollView) + scrollView.delegate = self + + if #available(iOS 11.0, *) { + scrollView.contentInsetAdjustmentBehavior = .never + } + } + + public override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + view.layoutIfNeeded() + manager.viewWillAppear(animated: animated) + } + + // MARK: - Public Methods + + public func selectViewController(_ viewController: UIViewController, direction: PageViewDirection, animated: Bool = true) { + manager.select(viewController: viewController, direction: direction, animated: animated) + } + + public func selectNext(animated: Bool) { + manager.selectNext(animated: animated) + } + + public func selectPrevious(animated: Bool) { + manager.selectPrevious(animated: animated) + } + + public func removeAll() { + manager.removeAll() + } +} + +extension PageViewController: UIScrollViewDelegate { + public func scrollViewWillBeginDragging(_ scrollView: UIScrollView) { + manager.willBeginDragging() + } + + public func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer) { + manager.willEndDragging() + } + + public func scrollViewDidScroll(_ scrollView: UIScrollView) { + let distance = view.frame.size.width + var progress: CGFloat + + switch manager.state { + case .first, .empty, .single: + progress = scrollView.contentOffset.x / distance + case .center, .last: + progress = (scrollView.contentOffset.x - distance) / distance + } + + manager.didScroll(progress: progress) + } +} + +extension PageViewController: PageViewManagerDataSource { + func viewControllerAfter(_ viewController: UIViewController) -> UIViewController? { + return dataSource?.pageViewController(self, viewControllerAfterViewController: viewController) + } + + func viewControllerBefore(_ viewController: UIViewController) -> UIViewController? { + return dataSource?.pageViewController(self, viewControllerBeforeViewController: viewController) + } +} + +extension PageViewController: PageViewManagerDelegate { + func scrollForward() { + switch manager.state { + case .first: + let contentOffset = CGPoint(x: view.bounds.width, y: 0) + scrollView.setContentOffset(contentOffset, animated: true) + case .center: + let contentOffset = CGPoint(x: view.bounds.width * 2, y: 0) + scrollView.setContentOffset(contentOffset, animated: true) + case .single, .empty, .last: + break + } + } + + func scrollReverse() { + switch manager.state { + case .last, .center: + manager.willBeginDragging() + scrollView.setContentOffset(.zero, animated: true) + case .single, .empty, .first: + break + } + } + + func layoutViews(for viewControllers: [UIViewController]) { + for (index, viewController) in viewControllers.enumerated() { + viewController.view.frame = CGRect( + x: CGFloat(index) * scrollView.bounds.width, + y: 0, + width: scrollView.bounds.width, + height: scrollView.bounds.height) + } + + scrollView.contentSize = CGSize( + width: CGFloat(manager.state.count) * view.bounds.width, + height: view.bounds.height) + + switch manager.state { + case .first, .single, .empty: + scrollView.contentOffset = CGPoint(x: 0, y: 0) + case .last, .center: + scrollView.contentOffset = CGPoint(x: view.bounds.width, y: 0) + } + } + + func addViewController(_ viewController: UIViewController) { + viewController.willMove(toParent: self) + addChild(viewController) + scrollView.addSubview(viewController.view) + viewController.didMove(toParent: self) + } + + func removeViewController(_ viewController: UIViewController) { + viewController.willMove(toParent: nil) + viewController.removeFromParent() + viewController.view.removeFromSuperview() + viewController.didMove(toParent: nil) + } + + func beginAppearanceTransition(isAppearing: Bool, viewController: UIViewController) { + viewController.beginAppearanceTransition(isAppearing, animated: false) + } + + func endAppearanceTransition(viewController: UIViewController) { + viewController.endAppearanceTransition() + } + + func willScroll( + from selectedViewController: UIViewController, + to destinationViewController: UIViewController) { + delegate?.pageViewController( + self, + willStartScrollingFrom: selectedViewController, + destinationViewController: destinationViewController) + } + + func didFinishScrolling( + from selectedViewController: UIViewController, + to destinationViewController: UIViewController, + transitionSuccessful: Bool) { + delegate?.pageViewController( + self, + didFinishScrollingFrom: selectedViewController, + destinationViewController: destinationViewController, + transitionSuccessful: transitionSuccessful) + } + + func isScrolling( + from selectedViewController: UIViewController, + to destinationViewController: UIViewController?, + progress: CGFloat) { + delegate?.pageViewController( + self, + isScrollingFrom: selectedViewController, + destinationViewController: destinationViewController, + progress: progress) + } +} diff --git a/Parchment/Classes/PageViewManager.swift b/Parchment/Classes/PageViewManager.swift new file mode 100644 index 00000000..ecad60ff --- /dev/null +++ b/Parchment/Classes/PageViewManager.swift @@ -0,0 +1,546 @@ +import UIKit + +final class PageViewManager { + weak var dataSource: PageViewManagerDataSource? + weak var delegate: PageViewManagerDelegate? + + private weak var previousViewController: UIViewController? + private(set) weak var selectedViewController: UIViewController? + private weak var nextViewController: UIViewController? + + var state: PageViewState { + if previousViewController == nil && nextViewController == nil && selectedViewController == nil { + return .empty + } else if previousViewController == nil && nextViewController == nil { + return .single + } else if nextViewController == nil { + return .last + } else if previousViewController == nil { + return .first + } else { + return .center + } + } + + // MARK: - Private Properties + + private var didReload: Bool = false + private var didSelect: Bool = false + private var initialDirection: PageViewDirection = .none + + // MARK: - Public Methods + + func select( + viewController: UIViewController, + direction: PageViewDirection = .none, + animated: Bool = false) { + if state == .empty || animated == false { + selectViewController(viewController) + return + } else { + resetState() + didSelect = true + + switch direction { + case .forward, .none: + if let nextViewController = nextViewController { + delegate?.removeViewController(nextViewController) + } + delegate?.addViewController(viewController) + nextViewController = viewController + layoutsViews() + delegate?.scrollForward() + case .reverse: + if let previousViewController = previousViewController { + delegate?.removeViewController(previousViewController) + } + delegate?.addViewController(viewController) + previousViewController = viewController + layoutsViews() + delegate?.scrollReverse() + } + } + } + + func selectNext(animated: Bool) { + if animated { + resetState() + delegate?.scrollForward() + } else if let nextViewController = nextViewController, + let selectedViewController = selectedViewController { + + delegate?.beginAppearanceTransition(isAppearing: false, viewController: selectedViewController) + delegate?.beginAppearanceTransition(isAppearing: true, viewController: nextViewController) + + let newNextViewController = dataSource?.viewControllerAfter(nextViewController) + + if let previousViewController = previousViewController { + delegate?.removeViewController(previousViewController) + } + + if let newNextViewController = newNextViewController { + delegate?.addViewController(newNextViewController) + } + + self.previousViewController = selectedViewController + self.selectedViewController = nextViewController + self.nextViewController = newNextViewController + + layoutsViews() + + delegate?.endAppearanceTransition(viewController: selectedViewController) + delegate?.endAppearanceTransition(viewController: nextViewController) + } + } + + func selectPrevious(animated: Bool) { + if animated { + resetState() + delegate?.scrollReverse() + } else if let previousViewController = previousViewController, + let selectedViewController = selectedViewController { + + delegate?.beginAppearanceTransition(isAppearing: false, viewController: selectedViewController) + delegate?.beginAppearanceTransition(isAppearing: true, viewController: previousViewController) + + let newPreviousViewController = dataSource?.viewControllerBefore(previousViewController) + + if let nextViewController = nextViewController { + delegate?.removeViewController(nextViewController) + } + + if let newPreviousViewController = newPreviousViewController { + delegate?.addViewController(newPreviousViewController) + } + + self.previousViewController = newPreviousViewController + self.selectedViewController = previousViewController + self.nextViewController = selectedViewController + + layoutsViews() + + delegate?.endAppearanceTransition(viewController: selectedViewController) + delegate?.endAppearanceTransition(viewController: previousViewController) + } + } + + func removeAll() { + let oldSelectedViewController = selectedViewController + + if let selectedViewController = oldSelectedViewController { + delegate?.beginAppearanceTransition(isAppearing: false, viewController: selectedViewController) + delegate?.removeViewController(selectedViewController) + } + if let previousViewController = previousViewController { + delegate?.removeViewController(previousViewController) + } + if let nextViewController = nextViewController { + delegate?.removeViewController(nextViewController) + } + previousViewController = nil + selectedViewController = nil + nextViewController = nil + layoutsViews() + + if let oldSelectedViewController = oldSelectedViewController { + delegate?.endAppearanceTransition(viewController: oldSelectedViewController) + } + } + + func viewWillAppear(animated: Bool) { + layoutsViews() + } + + func willBeginDragging() { + resetState() + } + + func willEndDragging() { + resetState() + } + + func didScroll(progress: CGFloat) { + let currentDirection = PageViewDirection(progress: progress) + + // MARK: Begin scrolling + + if initialDirection == .none { + switch currentDirection { + case .forward: + initialDirection = .forward + onScroll(progress: progress) + willScrollForward() + case .reverse: + initialDirection = .reverse + onScroll(progress: progress) + willScrollReverse() + case .none: + onScroll(progress: progress) + } + } else { + // Check if the transition changed direction in the middle of + // the transactions. + if didReload == false { + switch (currentDirection, initialDirection) { + case (.reverse, .forward): + initialDirection = .reverse + cancelScrollForward() + onScroll(progress: progress) + willScrollReverse() + case (.forward, .reverse): + initialDirection = .forward + cancelScrollReverse() + onScroll(progress: progress) + willScrollForward() + default: + onScroll(progress: progress) + } + } else { + onScroll(progress: progress) + } + } + + // MARK: Finished scrolling + + if didReload == false { + if progress >= 1 { + didReload = true + didScrollForward() + } else if progress <= -1 { + didReload = true + didScrollReverse() + } else if progress == 0 { + switch initialDirection { + case .forward: + didReload = true + cancelScrollForward() + case .reverse: + didReload = true + cancelScrollReverse() + case .none: + break + } + } + } + } + + // MARK: - Private Methods + + private func selectViewController(_ viewController: UIViewController) { + let oldSelectedViewController = selectedViewController + let newPreviousViewController = dataSource?.viewControllerBefore(viewController) + let newNextViewController = dataSource?.viewControllerAfter(viewController) + + if let oldSelectedViewController = oldSelectedViewController { + delegate?.beginAppearanceTransition(isAppearing: false, viewController: oldSelectedViewController) + } + + if viewController !== selectedViewController { + delegate?.beginAppearanceTransition(isAppearing: true, viewController: viewController) + } + + if let oldPreviosViewController = previousViewController { + if oldPreviosViewController !== viewController && + oldPreviosViewController !== newPreviousViewController && + oldPreviosViewController !== newNextViewController { + delegate?.removeViewController(oldPreviosViewController) + } + } + + if let oldSelectedViewController = selectedViewController { + if oldSelectedViewController !== newPreviousViewController && + oldSelectedViewController !== newNextViewController { + delegate?.removeViewController(oldSelectedViewController) + } + } + + if let oldNextViewController = nextViewController { + if oldNextViewController !== viewController && + oldNextViewController !== newPreviousViewController && + oldNextViewController !== newNextViewController { + delegate?.removeViewController(oldNextViewController) + } + } + + if let newPreviousViewController = newPreviousViewController { + if newPreviousViewController !== selectedViewController && + newPreviousViewController !== previousViewController && + newPreviousViewController !== nextViewController { + delegate?.addViewController(newPreviousViewController) + } + } + + if viewController !== nextViewController && + viewController !== previousViewController { + delegate?.addViewController(viewController) + } + + if let newNextViewController = newNextViewController { + if newNextViewController !== selectedViewController && + newNextViewController !== previousViewController && + newNextViewController !== nextViewController { + delegate?.addViewController(newNextViewController) + } + } + + previousViewController = newPreviousViewController + selectedViewController = viewController + nextViewController = newNextViewController + + layoutsViews() + + if let oldSelectedViewController = oldSelectedViewController { + delegate?.endAppearanceTransition(viewController: oldSelectedViewController) + } + + if viewController !== oldSelectedViewController { + delegate?.endAppearanceTransition(viewController: viewController) + } + } + + private func resetState() { + if didReload { + initialDirection = .none + } + didReload = false + } + + private func onScroll(progress: CGFloat) { + // This means we are overshooting, so we need to continue + // reporting the old view controllers. + if didReload { + switch initialDirection { + case .forward: + if let previousViewController = previousViewController, + let selectedViewController = selectedViewController { + delegate?.isScrolling( + from: previousViewController, + to: selectedViewController, + progress: progress) + } + case .reverse: + if let nextViewController = nextViewController, + let selectedViewController = selectedViewController { + delegate?.isScrolling( + from: nextViewController, + to: selectedViewController, + progress: progress) + } + case .none: + break + } + } else { + // Report progress as normally + switch initialDirection { + case .forward: + if let selectedViewController = selectedViewController { + delegate?.isScrolling( + from: selectedViewController, + to: nextViewController, + progress: progress) + } + case .reverse: + if let selectedViewController = selectedViewController { + delegate?.isScrolling( + from: selectedViewController, + to: previousViewController, + progress: progress) + } + case .none: + break + } + } + } + + private func cancelScrollForward() { + guard let selectedViewController = selectedViewController else { return } + let oldNextViewController = nextViewController + + if let nextViewController = oldNextViewController { + delegate?.beginAppearanceTransition(isAppearing: true, viewController: selectedViewController) + delegate?.beginAppearanceTransition(isAppearing: false, viewController: nextViewController) + } + + if didSelect { + let newNextViewController = dataSource?.viewControllerAfter(selectedViewController) + if let oldNextViewController = oldNextViewController { + delegate?.removeViewController(oldNextViewController) + } + if let newNextViewController = newNextViewController { + delegate?.addViewController(newNextViewController) + } + nextViewController = newNextViewController + didSelect = false + layoutsViews() + } + + if let oldNextViewController = oldNextViewController { + delegate?.endAppearanceTransition(viewController: selectedViewController) + delegate?.endAppearanceTransition(viewController: oldNextViewController) + delegate?.didFinishScrolling( + from: selectedViewController, + to: oldNextViewController, + transitionSuccessful: false) + } + } + + private func cancelScrollReverse() { + guard let selectedViewController = selectedViewController else { return } + let oldPreviousViewController = previousViewController + + if let previousViewController = oldPreviousViewController { + delegate?.beginAppearanceTransition(isAppearing: true, viewController: selectedViewController) + delegate?.beginAppearanceTransition(isAppearing: false, viewController: previousViewController) + } + + if didSelect { + let newPreviousViewController = dataSource?.viewControllerBefore(selectedViewController) + if let oldPreviousViewController = oldPreviousViewController { + delegate?.removeViewController(oldPreviousViewController) + } + if let newPreviousViewController = newPreviousViewController { + delegate?.addViewController(newPreviousViewController) + } + previousViewController = newPreviousViewController + didSelect = false + layoutsViews() + } + + if let oldPreviousViewController = oldPreviousViewController { + delegate?.endAppearanceTransition(viewController: selectedViewController) + delegate?.endAppearanceTransition(viewController: oldPreviousViewController) + delegate?.didFinishScrolling( + from: selectedViewController, + to: oldPreviousViewController, + transitionSuccessful: false) + } + } + + private func willScrollForward() { + if let selectedViewController = selectedViewController, + let nextViewController = nextViewController { + delegate?.willScroll(from: selectedViewController, to: nextViewController) + delegate?.beginAppearanceTransition(isAppearing: true, viewController: nextViewController) + delegate?.beginAppearanceTransition(isAppearing: false, viewController: selectedViewController) + } + } + + private func willScrollReverse() { + if let selectedViewController = selectedViewController, + let previousViewController = previousViewController { + delegate?.willScroll(from: selectedViewController, to: previousViewController) + delegate?.beginAppearanceTransition(isAppearing: true, viewController: previousViewController) + delegate?.beginAppearanceTransition(isAppearing: false, viewController: selectedViewController) + } + } + + private func didScrollForward() { + guard + let oldSelectedViewController = selectedViewController, + let oldNextViewController = nextViewController else { return } + + delegate?.didFinishScrolling( + from: oldSelectedViewController, + to: oldNextViewController, + transitionSuccessful: true) + + let newNextViewController = dataSource?.viewControllerAfter(oldNextViewController) + + if let oldPreviousViewController = previousViewController { + if oldPreviousViewController !== newNextViewController { + delegate?.removeViewController(oldPreviousViewController) + } + } + + if let newNextViewController = newNextViewController { + if newNextViewController !== previousViewController { + delegate?.addViewController(newNextViewController) + } + } + + if didSelect { + let newPreviousViewController = dataSource?.viewControllerBefore(oldNextViewController) + if let oldSelectedViewController = selectedViewController { + delegate?.removeViewController(oldSelectedViewController) + } + if let newPreviousViewController = newPreviousViewController { + delegate?.addViewController(newPreviousViewController) + } + previousViewController = newPreviousViewController + didSelect = false + } else { + previousViewController = oldSelectedViewController + } + + selectedViewController = oldNextViewController + nextViewController = newNextViewController + + layoutsViews() + + delegate?.endAppearanceTransition(viewController: oldSelectedViewController) + delegate?.endAppearanceTransition(viewController: oldNextViewController) + } + + private func didScrollReverse() { + guard + let oldSelectedViewController = selectedViewController, + let oldPreviousViewController = previousViewController else { return } + + delegate?.didFinishScrolling( + from: oldSelectedViewController, + to: oldPreviousViewController, + transitionSuccessful: true) + + let newPreviousViewController = dataSource?.viewControllerBefore(oldPreviousViewController) + + if let oldNextViewController = nextViewController { + if oldNextViewController !== newPreviousViewController { + delegate?.removeViewController(oldNextViewController) + } + } + + if let newPreviousViewController = newPreviousViewController { + if newPreviousViewController !== nextViewController { + delegate?.addViewController(newPreviousViewController) + } + } + + if didSelect { + let newNextViewController = dataSource?.viewControllerAfter(oldPreviousViewController) + if let oldSelectedViewController = selectedViewController { + delegate?.removeViewController(oldSelectedViewController) + } + if let newNextViewController = newNextViewController { + delegate?.addViewController(newNextViewController) + } + nextViewController = newNextViewController + didSelect = false + } else { + nextViewController = oldSelectedViewController + } + + previousViewController = newPreviousViewController + selectedViewController = oldPreviousViewController + + layoutsViews() + + delegate?.endAppearanceTransition(viewController: oldSelectedViewController) + delegate?.endAppearanceTransition(viewController: oldPreviousViewController) + } + + private func layoutsViews() { + var viewControllers: [UIViewController] = [] + + if let previousViewController = previousViewController { + viewControllers.append(previousViewController) + } + if let selectedViewController = selectedViewController { + viewControllers.append(selectedViewController) + } + if let nextViewController = nextViewController { + viewControllers.append(nextViewController) + } + + delegate?.layoutViews(for: viewControllers) + } +} diff --git a/Parchment/Classes/PagingController.swift b/Parchment/Classes/PagingController.swift index 90652064..a753b518 100644 --- a/Parchment/Classes/PagingController.swift +++ b/Parchment/Classes/PagingController.swift @@ -90,31 +90,50 @@ final class PagingController: NSObject { case .selected: if let currentPagingItem = state.currentPagingItem { if pagingItem.isEqual(to: currentPagingItem) == false { - appendItemsIfNeeded(upcomingPagingItem: pagingItem) - let transition = calculateTransition( - from: currentPagingItem, - to: pagingItem - ) - - state = .scrolling( - pagingItem: currentPagingItem, - upcomingPagingItem: pagingItem, - progress: 0, - initialContentOffset: transition.contentOffset, - distance: transition.distance - ) - - let direction = visibleItems.direction( - from: currentPagingItem, - to: pagingItem - ) - - delegate?.selectContent( - pagingItem: pagingItem, - direction: direction, - animated: animated - ) + if animated { + appendItemsIfNeeded(upcomingPagingItem: pagingItem) + + let transition = calculateTransition( + from: currentPagingItem, + to: pagingItem + ) + + state = .scrolling( + pagingItem: currentPagingItem, + upcomingPagingItem: pagingItem, + progress: 0, + initialContentOffset: transition.contentOffset, + distance: transition.distance + ) + + let direction = visibleItems.direction( + from: currentPagingItem, + to: pagingItem + ) + + delegate?.selectContent( + pagingItem: pagingItem, + direction: direction, + animated: animated + ) + } else { + state = .selected(pagingItem: pagingItem) + + reloadItems(around: pagingItem) + + delegate?.selectContent( + pagingItem: pagingItem, + direction: .none, + animated: false + ) + + collectionView.selectItem( + at: visibleItems.indexPath(for: pagingItem), + animated: false, + scrollPosition: options.scrollPosition + ) + } } } diff --git a/Parchment/Classes/PagingViewController.swift b/Parchment/Classes/PagingViewController.swift index b2d2f680..2e0104b7 100644 --- a/Parchment/Classes/PagingViewController.swift +++ b/Parchment/Classes/PagingViewController.swift @@ -14,8 +14,8 @@ import UIKit open class PagingViewController: UIViewController, UICollectionViewDelegate, - EMPageViewControllerDataSource, - EMPageViewControllerDelegate { + PageViewControllerDataSource, + PageViewControllerDelegate { // MARK: Public Properties @@ -247,11 +247,8 @@ open class PagingViewController: /// is enabled in the collection view. public let collectionView: UICollectionView - /// Used to display the view controller that you are paging - /// between. Instead of using UIPageViewController we use a library - /// called EMPageViewController which fixes a lot of the common - /// issues with using UIPageViewController. - public let pageViewController: EMPageViewController + /// Used to display the view controllers that you are paging between. + public let pageViewController: PageViewController /// An instance that stores all the customization so that it's /// easier to share between other classes. @@ -301,7 +298,7 @@ open class PagingViewController: public init(options: PagingOptions = PagingOptions()) { self.options = options self.pagingController = PagingController(options: options) - self.pageViewController = EMPageViewController(navigationOrientation: .horizontal) + self.pageViewController = PageViewController(options: options) self.collectionViewLayout = createLayout(layout: options.menuLayoutClass.self) self.collectionView = UICollectionView(frame: .zero, collectionViewLayout: collectionViewLayout) super.init(nibName: nil, bundle: nil) @@ -335,7 +332,7 @@ open class PagingViewController: required public init?(coder: NSCoder) { self.options = PagingOptions() self.pagingController = PagingController(options: options) - self.pageViewController = EMPageViewController(navigationOrientation: .horizontal) + self.pageViewController = PageViewController(options: options) self.collectionViewLayout = createLayout(layout: options.menuLayoutClass.self) self.collectionView = UICollectionView(frame: .zero, collectionViewLayout: collectionViewLayout) super.init(coder: coder) @@ -475,12 +472,9 @@ open class PagingViewController: pageViewController.didMove(toParentViewController: self) #endif + pageViewController.delegate = self pageViewController.dataSource = self configureContentInteraction() - - if #available(iOS 11.0, *) { - pageViewController.scrollView.contentInsetAdjustmentBehavior = .never - } } open override func viewDidLayoutSubviews() { @@ -492,12 +486,6 @@ open class PagingViewController: if didLayoutSubviews == false { didLayoutSubviews = true pagingController.viewAppeared() - - // Selecting a view controller in the page view triggers the - // delegate methods even if the view has not appeared yet. This - // causes problems with the initial state when we select items, so - // we wait until the view has appeared before setting the delegate. - pageViewController.delegate = self } } @@ -640,9 +628,9 @@ open class PagingViewController: return } - // MARK: EMPageViewControllerDataSource + // MARK: PageViewControllerDataSource - open func em_pageViewController(_ pageViewController: EMPageViewController, viewControllerBeforeViewController viewController: UIViewController) -> UIViewController? { + public func pageViewController(_ pageViewController: PageViewController, viewControllerBeforeViewController viewController: UIViewController) -> UIViewController? { guard let dataSource = infiniteDataSource, let currentPagingItem = state.currentPagingItem, @@ -651,7 +639,7 @@ open class PagingViewController: return dataSource.pagingViewController(self, viewControllerFor: pagingItem) } - open func em_pageViewController(_ pageViewController: EMPageViewController, viewControllerAfterViewController viewController: UIViewController) -> UIViewController? { + public func pageViewController(_ pageViewController: PageViewController, viewControllerAfterViewController viewController: UIViewController) -> UIViewController? { guard let dataSource = infiniteDataSource, let currentPagingItem = state.currentPagingItem, @@ -660,39 +648,22 @@ open class PagingViewController: return dataSource.pagingViewController(self, viewControllerFor: pagingItem) } - // MARK: EMPageViewControllerDelegate + // MARK: PageViewControllerDelegate - open func em_pageViewController(_ pageViewController: EMPageViewController, isScrollingFrom startingViewController: UIViewController, destinationViewController: UIViewController?, progress: CGFloat) { + public func pageViewController(_ pageViewController: PageViewController, isScrollingFrom startingViewController: UIViewController, destinationViewController: UIViewController?, progress: CGFloat) { guard let currentPagingItem = state.currentPagingItem else { return } - let oldState = state - // EMPageViewController will trigger a scrolling event even if the - // view has not appeared, causing the wrong initial paging item. - if view.window != nil { - pagingController.contentScrolled(progress: progress) - - if case .selected = oldState { - if let upcomingPagingItem = state.upcomingPagingItem, - let destinationViewController = destinationViewController { - delegate?.pagingViewController( - self, - willScrollToItem: upcomingPagingItem, - startingViewController: startingViewController, - destinationViewController: destinationViewController) - } - } else { - delegate?.pagingViewController( - self, - isScrollingFromItem: currentPagingItem, - toItem: state.upcomingPagingItem, - startingViewController: startingViewController, - destinationViewController: destinationViewController, - progress: progress) - } - } + pagingController.contentScrolled(progress: progress) + delegate?.pagingViewController( + self, + isScrollingFromItem: currentPagingItem, + toItem: state.upcomingPagingItem, + startingViewController: startingViewController, + destinationViewController: destinationViewController, + progress: progress) } - open func em_pageViewController(_ pageViewController: EMPageViewController, willStartScrollingFrom startingViewController: UIViewController, destinationViewController: UIViewController) { + public func pageViewController(_ pageViewController: PageViewController, willStartScrollingFrom startingViewController: UIViewController, destinationViewController: UIViewController) { if let upcomingPagingItem = state.upcomingPagingItem { delegate?.pagingViewController( self, @@ -700,10 +671,9 @@ open class PagingViewController: startingViewController: startingViewController, destinationViewController: destinationViewController) } - return } - open func em_pageViewController(_ pageViewController: EMPageViewController, didFinishScrollingFrom startingViewController: UIViewController?, destinationViewController: UIViewController, transitionSuccessful: Bool) { + public func pageViewController(_ pageViewController: PageViewController, didFinishScrollingFrom startingViewController: UIViewController, destinationViewController: UIViewController, transitionSuccessful: Bool) { if transitionSuccessful { pagingController.contentFinishedScrolling() } @@ -746,25 +716,23 @@ extension PagingViewController: PagingMenuDelegate { switch direction { case .forward(true): - pageViewController.scrollForward(animated: animated, completion: nil) - pageViewController.view.layoutIfNeeded() + pageViewController.selectNext(animated: animated) case .reverse(true): - pageViewController.scrollReverse(animated: animated, completion: nil) - pageViewController.view.layoutIfNeeded() + pageViewController.selectPrevious(animated: animated) default: + let viewController = dataSource.pagingViewController(self, viewControllerFor: pagingItem) pageViewController.selectViewController( - dataSource.pagingViewController(self, viewControllerFor: pagingItem), - direction: direction.pageViewControllerNavigationDirection, - animated: animated, - completion: nil + viewController, + direction: PageViewDirection(from: direction), + animated: animated ) } } public func removeContent() { - pageViewController.removeAllViewControllers() + pageViewController.removeAll() } } diff --git a/Parchment/Enums/PageViewDirection.swift b/Parchment/Enums/PageViewDirection.swift new file mode 100644 index 00000000..fe9d7b92 --- /dev/null +++ b/Parchment/Enums/PageViewDirection.swift @@ -0,0 +1,28 @@ +import Foundation + +public enum PageViewDirection { + case forward + case reverse + case none + + init(from direction: PagingDirection) { + switch direction { + case .forward: + self = .forward + case .reverse: + self = .reverse + case .none: + self = .none + } + } + + init(progress: CGFloat) { + if progress > 0 { + self = .forward + } else if progress < 0 { + self = .reverse + } else { + self = .none + } + } +} diff --git a/Parchment/Enums/PageViewState.swift b/Parchment/Enums/PageViewState.swift new file mode 100644 index 00000000..1e5febf0 --- /dev/null +++ b/Parchment/Enums/PageViewState.swift @@ -0,0 +1,22 @@ +import Foundation + +enum PageViewState { + case empty + case single + case first + case center + case last + + var count: Int { + switch self { + case .empty: + return 0 + case .single: + return 1 + case .first, .last: + return 2 + case .center: + return 3 + } + } +} diff --git a/Parchment/Enums/PagingDirection.swift b/Parchment/Enums/PagingDirection.swift index 7fdb16f4..b10c52a4 100644 --- a/Parchment/Enums/PagingDirection.swift +++ b/Parchment/Enums/PagingDirection.swift @@ -8,7 +8,7 @@ public enum PagingDirection: Equatable { extension PagingDirection { - var pageViewControllerNavigationDirection: EMPageViewControllerNavigationDirection { + var pageViewControllerNavigationDirection: UIPageViewController.NavigationDirection { switch self { case .forward, .none: return .forward diff --git a/Parchment/Protocols/PageViewControllerDataSource.swift b/Parchment/Protocols/PageViewControllerDataSource.swift new file mode 100644 index 00000000..e3c89c3b --- /dev/null +++ b/Parchment/Protocols/PageViewControllerDataSource.swift @@ -0,0 +1,10 @@ +import UIKit + +public protocol PageViewControllerDataSource: class { + func pageViewController( + _ pageViewController: PageViewController, + viewControllerBeforeViewController viewController: UIViewController) -> UIViewController? + func pageViewController( + _ pageViewController: PageViewController, + viewControllerAfterViewController viewController: UIViewController) -> UIViewController? +} diff --git a/Parchment/Protocols/PageViewControllerDelegate.swift b/Parchment/Protocols/PageViewControllerDelegate.swift new file mode 100644 index 00000000..bd4805cc --- /dev/null +++ b/Parchment/Protocols/PageViewControllerDelegate.swift @@ -0,0 +1,18 @@ +import UIKit + +public protocol PageViewControllerDelegate: class { + func pageViewController( + _ pageViewController: PageViewController, + willStartScrollingFrom startingViewController: UIViewController, + destinationViewController: UIViewController) + func pageViewController( + _ pageViewController: PageViewController, + isScrollingFrom startingViewController: UIViewController, + destinationViewController: UIViewController?, + progress: CGFloat) + func pageViewController( + _ pageViewController: PageViewController, + didFinishScrollingFrom startingViewController: UIViewController, + destinationViewController: UIViewController, + transitionSuccessful: Bool) +} diff --git a/Parchment/Protocols/PageViewManagerDataSource.swift b/Parchment/Protocols/PageViewManagerDataSource.swift new file mode 100644 index 00000000..c01c5ef5 --- /dev/null +++ b/Parchment/Protocols/PageViewManagerDataSource.swift @@ -0,0 +1,6 @@ +import UIKit + +protocol PageViewManagerDataSource: class { + func viewControllerBefore(_ viewController: UIViewController) -> UIViewController? + func viewControllerAfter(_ viewController: UIViewController) -> UIViewController? +} diff --git a/Parchment/Protocols/PageViewManagerDelegate.swift b/Parchment/Protocols/PageViewManagerDelegate.swift new file mode 100644 index 00000000..3c56ee38 --- /dev/null +++ b/Parchment/Protocols/PageViewManagerDelegate.swift @@ -0,0 +1,22 @@ +import UIKit + +protocol PageViewManagerDelegate: class { + func scrollForward() + func scrollReverse() + func layoutViews(for viewControllers: [UIViewController]) + func addViewController(_ viewController: UIViewController) + func removeViewController(_ viewController: UIViewController) + func beginAppearanceTransition(isAppearing: Bool, viewController: UIViewController) + func endAppearanceTransition(viewController: UIViewController) + func willScroll( + from selectedViewController: UIViewController, + to destinationViewController: UIViewController) + func isScrolling( + from selectedViewController: UIViewController, + to destinationViewController: UIViewController?, + progress: CGFloat) + func didFinishScrolling( + from selectedViewController: UIViewController, + to destinationViewController: UIViewController, + transitionSuccessful: Bool) +} diff --git a/ParchmentTests/Mocks/MockPageViewManagerDataSource.swift b/ParchmentTests/Mocks/MockPageViewManagerDataSource.swift new file mode 100644 index 00000000..294d948e --- /dev/null +++ b/ParchmentTests/Mocks/MockPageViewManagerDataSource.swift @@ -0,0 +1,16 @@ +import Foundation +import XCTest +@testable import Parchment + +final class MockPageViewManagerDataSource: PageViewManagerDataSource { + var viewControllerBefore: ((UIViewController) -> UIViewController?)? + var viewControllerAfter: ((UIViewController) -> UIViewController?)? + + func viewControllerBefore(_ viewController: UIViewController) -> UIViewController? { + viewControllerBefore?(viewController) + } + + func viewControllerAfter(_ viewController: UIViewController) -> UIViewController? { + viewControllerAfter?(viewController) + } +} diff --git a/ParchmentTests/Mocks/MockPageViewManagerDelegate.swift b/ParchmentTests/Mocks/MockPageViewManagerDelegate.swift new file mode 100644 index 00000000..7a676e91 --- /dev/null +++ b/ParchmentTests/Mocks/MockPageViewManagerDelegate.swift @@ -0,0 +1,60 @@ +import Foundation +import XCTest +@testable import Parchment + +final class MockPageViewManagerDelegate: PageViewManagerDelegate { + enum Call: Equatable { + case scrollForward + case scrollReverse + case layoutViews([UIViewController]) + case addViewController(UIViewController) + case removeViewController(UIViewController) + case beginAppearanceTransition(Bool, UIViewController) + case endAppearanceTransition(UIViewController) + case willScroll(from: UIViewController, to: UIViewController) + case isScrolling(from: UIViewController, to: UIViewController?, progress: CGFloat) + case didFinishScrolling(from: UIViewController, to: UIViewController, success: Bool) + } + + var calls: [Call] = [] + + func scrollForward() { + calls.append(.scrollForward) + } + + func scrollReverse() { + calls.append(.scrollReverse) + } + + func layoutViews(for viewControllers: [UIViewController]) { + calls.append(.layoutViews(viewControllers)) + } + + func addViewController(_ viewController: UIViewController) { + calls.append(.addViewController(viewController)) + } + + func removeViewController(_ viewController: UIViewController) { + calls.append(.removeViewController(viewController)) + } + + func beginAppearanceTransition(isAppearing: Bool, viewController: UIViewController) { + calls.append(.beginAppearanceTransition(isAppearing, viewController)) + } + + func endAppearanceTransition(viewController: UIViewController) { + calls.append(.endAppearanceTransition(viewController)) + } + + func willScroll(from selectedViewController: UIViewController, to destinationViewController: UIViewController) { + calls.append(.willScroll(from: selectedViewController, to: destinationViewController)) + } + + func isScrolling(from selectedViewController: UIViewController, to destinationViewController: UIViewController?, progress: CGFloat) { + calls.append(.isScrolling(from: selectedViewController, to: destinationViewController, progress: progress)) + } + + func didFinishScrolling(from selectedViewController: UIViewController, to destinationViewController: UIViewController, transitionSuccessful: Bool) { + calls.append(.didFinishScrolling(from: selectedViewController, to: destinationViewController, success: transitionSuccessful)) + } +} diff --git a/ParchmentTests/PageViewManagerTests.swift b/ParchmentTests/PageViewManagerTests.swift new file mode 100644 index 00000000..8daa7216 --- /dev/null +++ b/ParchmentTests/PageViewManagerTests.swift @@ -0,0 +1,937 @@ +import Foundation +import XCTest +@testable import Parchment + +final class PageViewManagerTests: XCTestCase { + var dataSource: MockPageViewManagerDataSource! + var delegate: MockPageViewManagerDelegate! + var manager: PageViewManager! + + override func setUp() { + dataSource = MockPageViewManagerDataSource() + delegate = MockPageViewManagerDelegate() + manager = PageViewManager() + manager.dataSource = dataSource + manager.delegate = delegate + } + + // MARK: - Selection + + func testSelectWhenEmpty() { + let previousVc = UIViewController() + let selectedVc = UIViewController() + let nextVc = UIViewController() + + dataSource.viewControllerBefore = { _ in previousVc } + dataSource.viewControllerAfter = { _ in nextVc } + + manager.select(viewController: selectedVc, animated: true) + + XCTAssertEqual(delegate.calls, [ + .beginAppearanceTransition(true, selectedVc), + .addViewController(previousVc), + .addViewController(selectedVc), + .addViewController(nextVc), + .layoutViews([previousVc, selectedVc, nextVc]), + .endAppearanceTransition(selectedVc) + ]) + } + + func testSelectAllNewViewControllersForwardAnimated() { + let oldPreviousVc = UIViewController() + let oldSelectedVc = UIViewController() + let oldNextVc = UIViewController() + + let newPreviousVc = UIViewController() + let newSelectedVc = UIViewController() + let newNextVc = UIViewController() + + dataSource.viewControllerBefore = { _ in oldPreviousVc } + dataSource.viewControllerAfter = { _ in oldNextVc } + manager.select(viewController: oldSelectedVc) + + delegate.calls = [] + + dataSource.viewControllerBefore = { _ in newPreviousVc } + dataSource.viewControllerAfter = { _ in newNextVc } + manager.select(viewController: newSelectedVc, animated: true) + manager.didScroll(progress: 0.1) + manager.didScroll(progress: 1) + + XCTAssertEqual(delegate.calls, [ + // Add the new upcoming view controller + .removeViewController(oldNextVc), + .addViewController(newSelectedVc), + .layoutViews([oldPreviousVc, oldSelectedVc, newSelectedVc]), + + // Animate the scroll towards the new view + .scrollForward, + .isScrolling(from: oldSelectedVc, to: newSelectedVc, progress: 0.1), + .willScroll(from: oldSelectedVc, to: newSelectedVc), + .beginAppearanceTransition(true, newSelectedVc), + .beginAppearanceTransition(false, oldSelectedVc), + + // Replace the previously selected with the new previous view + // once the transition completes. Should be left with all the + // new view controllers. + .isScrolling(from: oldSelectedVc, to: newSelectedVc, progress: 1), + .didFinishScrolling(from: oldSelectedVc, to: newSelectedVc, success: true), + .removeViewController(oldPreviousVc), + .addViewController(newNextVc), + .removeViewController(oldSelectedVc), + .addViewController(newPreviousVc), + .layoutViews([newPreviousVc, newSelectedVc, newNextVc]), + + // End the appearance transitions after doing layout. + .endAppearanceTransition(oldSelectedVc), + .endAppearanceTransition(newSelectedVc) + ]) + } + + func testCancelSelectAllNewViewControllersForwardAnimated() { + let oldPreviousVc = UIViewController() + let oldSelectedVc = UIViewController() + let oldNextVc = UIViewController() + + let newPreviousVc = UIViewController() + let newSelectedVc = UIViewController() + let newNextVc = UIViewController() + + dataSource.viewControllerBefore = { _ in oldPreviousVc } + dataSource.viewControllerAfter = { _ in oldNextVc } + manager.select(viewController: oldSelectedVc) + + dataSource.viewControllerBefore = { _ in newPreviousVc } + dataSource.viewControllerAfter = { _ in newNextVc } + manager.select(viewController: newSelectedVc, animated: true) + manager.didScroll(progress: 0.1) + + delegate.calls = [] + + dataSource.viewControllerAfter = { _ in oldNextVc } + manager.didScroll(progress: 0.0) + + XCTAssertEqual(delegate.calls, [ + .isScrolling(from: oldSelectedVc, to: newSelectedVc, progress: 0.0), + .beginAppearanceTransition(true, oldSelectedVc), + .beginAppearanceTransition(false, newSelectedVc), + + // Expect that we remove the view controller that was selected + // and replace it with the "old next" view controller. + .removeViewController(newSelectedVc), + .addViewController(oldNextVc), + .layoutViews([oldPreviousVc, oldSelectedVc, oldNextVc]), + + .endAppearanceTransition(oldSelectedVc), + .endAppearanceTransition(newSelectedVc), + .didFinishScrolling(from: oldSelectedVc, to: newSelectedVc, success: false) + ]) + } + + func testSelectAllNewViewControllersReverseAnimated() { + let oldPreviousVc = UIViewController() + let oldSelectedVc = UIViewController() + let oldNextVc = UIViewController() + + let newPreviousVc = UIViewController() + let newSelectedVc = UIViewController() + let newNextVc = UIViewController() + + dataSource.viewControllerBefore = { _ in oldPreviousVc } + dataSource.viewControllerAfter = { _ in oldNextVc } + manager.select(viewController: oldSelectedVc) + + delegate.calls = [] + + dataSource.viewControllerBefore = { _ in newPreviousVc } + dataSource.viewControllerAfter = { _ in newNextVc } + manager.select(viewController: newSelectedVc, direction: .reverse, animated: true) + manager.didScroll(progress: -0.1) + manager.didScroll(progress: -1) + + XCTAssertEqual(delegate.calls, [ + // Add the new upcoming view controller + .removeViewController(oldPreviousVc), + .addViewController(newSelectedVc), + .layoutViews([newSelectedVc, oldSelectedVc, oldNextVc]), + + // Animate the scroll towards the new view + .scrollReverse, + .isScrolling(from: oldSelectedVc, to: newSelectedVc, progress: -0.1), + .willScroll(from: oldSelectedVc, to: newSelectedVc), + .beginAppearanceTransition(true, newSelectedVc), + .beginAppearanceTransition(false, oldSelectedVc), + + // Replace the previously selected with the new next view + // once the transition completes. Should be left with all the + // new view controllers. + .isScrolling(from: oldSelectedVc, to: newSelectedVc, progress: -1), + .didFinishScrolling(from: oldSelectedVc, to: newSelectedVc, success: true), + .removeViewController(oldNextVc), + .addViewController(newPreviousVc), + .removeViewController(oldSelectedVc), + .addViewController(newNextVc), + .layoutViews([newPreviousVc, newSelectedVc, newNextVc]), + + // End the appearance transitions after doing layout. + .endAppearanceTransition(oldSelectedVc), + .endAppearanceTransition(newSelectedVc) + ]) + } + + func testCancelSelectAllNewViewControllersReverseAnimated() { + let oldPreviousVc = UIViewController() + let oldSelectedVc = UIViewController() + let oldNextVc = UIViewController() + + let newPreviousVc = UIViewController() + let newSelectedVc = UIViewController() + let newNextVc = UIViewController() + + dataSource.viewControllerBefore = { _ in oldPreviousVc } + dataSource.viewControllerAfter = { _ in oldNextVc } + manager.select(viewController: oldSelectedVc) + + dataSource.viewControllerBefore = { _ in newPreviousVc } + dataSource.viewControllerAfter = { _ in newNextVc } + manager.select(viewController: newSelectedVc, direction: .reverse, animated: true) + manager.didScroll(progress: -0.1) + + delegate.calls = [] + + dataSource.viewControllerBefore = { _ in oldPreviousVc } + manager.didScroll(progress: 0.0) + + XCTAssertEqual(delegate.calls, [ + .isScrolling(from: oldSelectedVc, to: newSelectedVc, progress: 0.0), + .beginAppearanceTransition(true, oldSelectedVc), + .beginAppearanceTransition(false, newSelectedVc), + + // Expect that we remove the view controller that was selected + // and replace it with the "old previous" view controller. + .removeViewController(newSelectedVc), + .addViewController(oldPreviousVc), + .layoutViews([oldPreviousVc, oldSelectedVc, oldNextVc]), + + .endAppearanceTransition(oldSelectedVc), + .endAppearanceTransition(newSelectedVc), + .didFinishScrolling(from: oldSelectedVc, to: newSelectedVc, success: false) + ]) + } + + func testSelectAllNewViewControllersWithoutAnimation() { + let oldPreviousVc = UIViewController() + let oldSelectedVc = UIViewController() + let oldNextVc = UIViewController() + + let newPreviousVc = UIViewController() + let newSelectedVc = UIViewController() + let newNextVc = UIViewController() + + dataSource.viewControllerBefore = { _ in oldPreviousVc } + dataSource.viewControllerAfter = { _ in oldNextVc } + manager.select(viewController: oldSelectedVc) + + delegate.calls = [] + + dataSource.viewControllerBefore = { _ in newPreviousVc } + dataSource.viewControllerAfter = { _ in newNextVc } + manager.select(viewController: newSelectedVc) + + XCTAssertEqual(delegate.calls, [ + // Start the appearance transitions. + .beginAppearanceTransition(false, oldSelectedVc), + .beginAppearanceTransition(true, newSelectedVc), + + // Remove old view controllers and add new ones. + .removeViewController(oldPreviousVc), + .removeViewController(oldSelectedVc), + .removeViewController(oldNextVc), + .addViewController(newPreviousVc), + .addViewController(newSelectedVc), + .addViewController(newNextVc), + .layoutViews([newPreviousVc, newSelectedVc, newNextVc]), + + // End the appearance transitions after doing layout. + .endAppearanceTransition(oldSelectedVc), + .endAppearanceTransition(newSelectedVc) + ]) + } + + func testSelectShiftOneForwardWithoutAnimation() { + let viewController0 = UIViewController() + let viewController1 = UIViewController() + let viewController2 = UIViewController() + let viewController3 = UIViewController() + + dataSource.viewControllerBefore = { _ in viewController0 } + dataSource.viewControllerAfter = { _ in viewController2 } + manager.select(viewController: viewController1) + + delegate.calls = [] + + dataSource.viewControllerBefore = { _ in viewController1 } + dataSource.viewControllerAfter = { _ in viewController3 } + manager.select(viewController: viewController2) + + XCTAssertEqual(delegate.calls, [ + // Start the appearance transitions. + .beginAppearanceTransition(false, viewController1), + .beginAppearanceTransition(true, viewController2), + + // Remove the old view controller and add the new one. + .removeViewController(viewController0), + .addViewController(viewController3), + .layoutViews([viewController1, viewController2, viewController3]), + + // End the appearance transitions after doing layout. + .endAppearanceTransition(viewController1), + .endAppearanceTransition(viewController2) + ]) + } + + func testSelectShiftOneReverseWithoutAnimation() { + let viewController0 = UIViewController() + let viewController1 = UIViewController() + let viewController2 = UIViewController() + let viewController3 = UIViewController() + + dataSource.viewControllerBefore = { _ in viewController1 } + dataSource.viewControllerAfter = { _ in viewController3 } + manager.select(viewController: viewController2) + + delegate.calls = [] + + dataSource.viewControllerBefore = { _ in viewController0 } + dataSource.viewControllerAfter = { _ in viewController2 } + manager.select(viewController: viewController1) + + XCTAssertEqual(delegate.calls, [ + // Start the appearance transitions. + .beginAppearanceTransition(false, viewController2), + .beginAppearanceTransition(true, viewController1), + + // Remove the old view controller and add the new one. + .removeViewController(viewController3), + .addViewController(viewController0), + .layoutViews([viewController0, viewController1, viewController2]), + + // End the appearance transitions after doing layout. + .endAppearanceTransition(viewController2), + .endAppearanceTransition(viewController1) + ]) + } + + func testSelectNextAnimated() { + let viewController0 = UIViewController() + let viewController1 = UIViewController() + let viewController2 = UIViewController() + + dataSource.viewControllerBefore = { _ in viewController0 } + dataSource.viewControllerAfter = { _ in viewController2 } + manager.select(viewController: viewController1) + + delegate.calls = [] + + dataSource.viewControllerAfter = { _ in nil } + dataSource.viewControllerBefore = { _ in + XCTFail() + return nil + } + + manager.selectNext(animated: true) + manager.didScroll(progress: 0.1) + + // Assert that the willScroll event is triggered which means the + // initialDirection state was reset. + XCTAssertEqual(delegate.calls, [ + .scrollForward, + .isScrolling(from: viewController1, to: viewController2, progress: 0.1), + .willScroll(from: viewController1, to: viewController2), + .beginAppearanceTransition(true, viewController2), + .beginAppearanceTransition(false, viewController1) + ]) + } + + func testSelectNextWithoutAnimation() { + let viewController0 = UIViewController() + let viewController1 = UIViewController() + let viewController2 = UIViewController() + let viewController3 = UIViewController() + + dataSource.viewControllerBefore = { _ in viewController0 } + dataSource.viewControllerAfter = { _ in viewController2 } + manager.select(viewController: viewController1) + + delegate.calls = [] + + dataSource.viewControllerAfter = { _ in viewController3 } + dataSource.viewControllerBefore = { _ in + XCTFail() + return nil + } + + manager.selectNext(animated: false) + + // Expect that it moves the view controllers immediately instead + // of triggered the .scrollForward event. + XCTAssertEqual(delegate.calls, [ + .beginAppearanceTransition(false, viewController1), + .beginAppearanceTransition(true, viewController2), + .removeViewController(viewController0), + .addViewController(viewController3), + .layoutViews([viewController1, viewController2, viewController3]), + .endAppearanceTransition(viewController1), + .endAppearanceTransition(viewController2), + ]) + } + + func testSelectPreviousAnimated() { + let viewController0 = UIViewController() + let viewController1 = UIViewController() + let viewController2 = UIViewController() + + dataSource.viewControllerBefore = { _ in viewController0 } + dataSource.viewControllerAfter = { _ in viewController2 } + manager.select(viewController: viewController1) + + delegate.calls = [] + + dataSource.viewControllerBefore = { _ in nil } + dataSource.viewControllerAfter = { _ in + XCTFail() + return nil + } + + manager.selectPrevious(animated: true) + manager.didScroll(progress: -0.1) + + // Expect that the willScroll event is triggered which means the + // initialDirection state was reset. + XCTAssertEqual(delegate.calls, [ + .scrollReverse, + .isScrolling(from: viewController1, to: viewController0, progress: -0.1), + .willScroll(from: viewController1, to: viewController0), + .beginAppearanceTransition(true, viewController0), + .beginAppearanceTransition(false, viewController1) + ]) + } + + func testSelectPreviousAnimatedFalse() { + let viewController0 = UIViewController() + let viewController1 = UIViewController() + let viewController2 = UIViewController() + let viewController3 = UIViewController() + + dataSource.viewControllerBefore = { _ in viewController1 } + dataSource.viewControllerAfter = { _ in viewController3 } + manager.select(viewController: viewController2) + + delegate.calls = [] + + dataSource.viewControllerBefore = { _ in viewController0 } + dataSource.viewControllerAfter = { _ in + XCTFail() + return nil + } + + manager.selectPrevious(animated: false) + + // Expect that it moves the view controllers immediately instead + // of triggered the .scrollForward event. + XCTAssertEqual(delegate.calls, [ + .beginAppearanceTransition(false, viewController2), + .beginAppearanceTransition(true, viewController1), + .removeViewController(viewController3), + .addViewController(viewController0), + .layoutViews([viewController0, viewController1, viewController2]), + .endAppearanceTransition(viewController2), + .endAppearanceTransition(viewController1), + ]) + } + + // MARK: - Scrolling + + func testStartedScrollingForward() { + let selectedVc = UIViewController() + let nextVc = UIViewController() + + dataSource.viewControllerAfter = { _ in nextVc } + manager.select(viewController: selectedVc) + delegate.calls = [] + + manager.willBeginDragging() + manager.didScroll(progress: 0.1) + + XCTAssertEqual(delegate.calls, [ + .isScrolling(from: selectedVc, to: nextVc, progress: 0.1), + .willScroll(from: selectedVc, to: nextVc), + .beginAppearanceTransition(true, nextVc), + .beginAppearanceTransition(false, selectedVc) + ]) + } + + func testStartedScrollingForwardNextNil() { + let selectedVc = UIViewController() + + manager.select(viewController: selectedVc) + delegate.calls = [] + + manager.willBeginDragging() + manager.didScroll(progress: 0.1) + + XCTAssertEqual(delegate.calls, [ + .isScrolling(from: selectedVc, to: nil, progress: 0.1) + ]) + } + + func testStartedScrollingReverse() { + let selectedVc = UIViewController() + let previousVc = UIViewController() + + dataSource.viewControllerBefore = { _ in previousVc } + manager.select(viewController: selectedVc) + delegate.calls = [] + + manager.willBeginDragging() + manager.didScroll(progress: -0.1) + + XCTAssertEqual(delegate.calls, [ + .isScrolling(from: selectedVc, to: previousVc, progress: -0.1), + .willScroll(from: selectedVc, to: previousVc), + .beginAppearanceTransition(true, previousVc), + .beginAppearanceTransition(false, selectedVc) + ]) + } + + func testStartedScrollingReversePreviousNil() { + let selectedVc = UIViewController() + + manager.select(viewController: selectedVc) + delegate.calls = [] + + manager.willBeginDragging() + manager.didScroll(progress: -0.1) + + XCTAssertEqual(delegate.calls, [ + .isScrolling(from: selectedVc, to: nil, progress: -0.1) + ]) + } + + func testIsScrollingForward() { + let selectedVc = UIViewController() + let nextVc = UIViewController() + + dataSource.viewControllerAfter = { _ in nextVc } + manager.select(viewController: selectedVc) + + manager.willBeginDragging() + manager.didScroll(progress: 0.1) + delegate.calls = [] + manager.didScroll(progress: 0.2) + manager.didScroll(progress: 0.3) + + XCTAssertEqual(delegate.calls, [ + .isScrolling(from: selectedVc, to: nextVc, progress: 0.2), + .isScrolling(from: selectedVc, to: nextVc, progress: 0.3) + ]) + } + + func testIsScrollingReverse() { + let previousVc = UIViewController() + let selectedVc = UIViewController() + + dataSource.viewControllerBefore = { _ in previousVc } + manager.select(viewController: selectedVc) + + manager.willBeginDragging() + manager.didScroll(progress: -0.1) + delegate.calls = [] + manager.didScroll(progress: -0.2) + manager.didScroll(progress: -0.3) + + XCTAssertEqual(delegate.calls, [ + .isScrolling(from: selectedVc, to: previousVc, progress: -0.2), + .isScrolling(from: selectedVc, to: previousVc, progress: -0.3) + ]) + } + + func testFinishedScrollingForward() { + let viewController0 = UIViewController() + let viewController1 = UIViewController() + let viewController2 = UIViewController() + let viewController3 = UIViewController() + + dataSource.viewControllerBefore = { _ in viewController0 } + dataSource.viewControllerAfter = { _ in viewController2 } + manager.select(viewController: viewController1) + dataSource.viewControllerAfter = { _ in viewController3 } + + manager.willBeginDragging() + manager.didScroll(progress: 0.1) + delegate.calls = [] + manager.didScroll(progress: 1.0) + + XCTAssertEqual(delegate.calls, [ + .isScrolling(from: viewController1, to: viewController2, progress: 1.0), + .didFinishScrolling(from: viewController1, to: viewController2, success: true), + .removeViewController(viewController0), + .addViewController(viewController3), + .layoutViews([viewController1, viewController2, viewController3]), + .endAppearanceTransition(viewController1), + .endAppearanceTransition(viewController2) + ]) + } + + func testFinishedScrollingForwardNextNil() { + let viewController0 = UIViewController() + let viewController1 = UIViewController() + let viewController2 = UIViewController() + + dataSource.viewControllerBefore = { _ in viewController0 } + dataSource.viewControllerAfter = { _ in viewController2 } + manager.select(viewController: viewController1) + + dataSource.viewControllerAfter = { _ in nil } + + manager.willBeginDragging() + manager.didScroll(progress: 0.1) + delegate.calls = [] + manager.didScroll(progress: 1.0) + + XCTAssertEqual(delegate.calls, [ + .isScrolling(from: viewController1, to: viewController2, progress: 1.0), + .didFinishScrolling(from: viewController1, to: viewController2, success: true), + .removeViewController(viewController0), + .layoutViews([viewController1, viewController2]), + .endAppearanceTransition(viewController1), + .endAppearanceTransition(viewController2) + ]) + } + + func testFinishedScrollingReverse() { + let viewController0 = UIViewController() + let viewController1 = UIViewController() + let viewController2 = UIViewController() + let viewController3 = UIViewController() + + dataSource.viewControllerBefore = { _ in viewController1 } + dataSource.viewControllerAfter = { _ in viewController3 } + manager.select(viewController: viewController2) + + dataSource.viewControllerBefore = { _ in viewController0 } + + manager.willBeginDragging() + manager.didScroll(progress: -0.1) + delegate.calls = [] + manager.didScroll(progress: -1.0) + + XCTAssertEqual(delegate.calls, [ + .isScrolling(from: viewController2, to: viewController1, progress: -1.0), + .didFinishScrolling(from: viewController2, to: viewController1, success: true), + .removeViewController(viewController3), + .addViewController(viewController0), + .layoutViews([viewController0, viewController1, viewController2]), + .endAppearanceTransition(viewController2), + .endAppearanceTransition(viewController1) + ]) + } + + func testFinishedScrollingReversePreviousNil() { + let viewController1 = UIViewController() + let viewController2 = UIViewController() + let viewController3 = UIViewController() + + dataSource.viewControllerBefore = { _ in viewController1 } + dataSource.viewControllerAfter = { _ in viewController3 } + manager.select(viewController: viewController2) + + dataSource.viewControllerBefore = { _ in nil } + + manager.willBeginDragging() + manager.didScroll(progress: -0.1) + delegate.calls = [] + manager.didScroll(progress: -1.0) + + XCTAssertEqual(delegate.calls, [ + .isScrolling(from: viewController2, to: viewController1, progress: -1.0), + .didFinishScrolling(from: viewController2, to: viewController1, success: true), + .removeViewController(viewController3), + .layoutViews([viewController1, viewController2]), + .endAppearanceTransition(viewController2), + .endAppearanceTransition(viewController1), + ]) + } + + func testDidScrollAfterDraggingEnded() { + let viewController0 = UIViewController() + let viewController1 = UIViewController() + let viewController2 = UIViewController() + let viewController3 = UIViewController() + + dataSource.viewControllerBefore = { _ in viewController0 } + dataSource.viewControllerAfter = { _ in viewController2 } + manager.select(viewController: viewController1) + + dataSource.viewControllerAfter = { _ in viewController3 } + + manager.willBeginDragging() + manager.didScroll(progress: 0.1) + delegate.calls = [] + manager.willEndDragging() + manager.willBeginDragging() + manager.willEndDragging() + manager.didScroll(progress: 0.2) + manager.didScroll(progress: 0.3) + + // Expect that it continues to trigger .isScrolling events for the + // correct view controllers. + XCTAssertEqual(delegate.calls, [ + .isScrolling(from: viewController1, to: viewController2, progress: 0.2), + .isScrolling(from: viewController1, to: viewController2, progress: 0.3) + ]) + } + + func testFinishedScrollingOvershooting() { + let viewController0 = UIViewController() + let viewController1 = UIViewController() + let viewController2 = UIViewController() + let viewController3 = UIViewController() + + dataSource.viewControllerBefore = { _ in viewController0 } + dataSource.viewControllerAfter = { _ in viewController2 } + manager.select(viewController: viewController1) + + dataSource.viewControllerAfter = { _ in viewController3 } + + manager.willBeginDragging() + manager.didScroll(progress: 0.1) + manager.didScroll(progress: 1.0) + delegate.calls = [] + manager.didScroll(progress: 0.0) + manager.didScroll(progress: 0.01) + manager.didScroll(progress: -0.01) + + // Expect that it triggers .isScrolling events for scroll events + // when overshooting, but does not trigger appereance transitions + // for the next upcoming view (viewController3). + XCTAssertEqual(delegate.calls, [ + .isScrolling(from: viewController1, to: viewController2, progress: 0.0), + .isScrolling(from: viewController1, to: viewController2, progress: 0.01), + .isScrolling(from: viewController1, to: viewController2, progress: -0.01) + ]) + } + + func testCancelScrollForward() { + let viewController1 = UIViewController() + let viewController2 = UIViewController() + let viewController3 = UIViewController() + + dataSource.viewControllerBefore = { _ in viewController1 } + dataSource.viewControllerAfter = { _ in viewController3 } + manager.select(viewController: viewController2) + + manager.willBeginDragging() + manager.didScroll(progress: 0.1) + delegate.calls = [] + manager.didScroll(progress: 0) + + XCTAssertEqual(delegate.calls, [ + .isScrolling(from: viewController2, to: viewController3, progress: 0.0), + .beginAppearanceTransition(true, viewController2), + .beginAppearanceTransition(false, viewController3), + .endAppearanceTransition(viewController2), + .endAppearanceTransition(viewController3), + .didFinishScrolling(from: viewController2, to: viewController3, success: false) + ]) + } + + func testCancelScrollReverse() { + let viewController1 = UIViewController() + let viewController2 = UIViewController() + let viewController3 = UIViewController() + + dataSource.viewControllerBefore = { _ in viewController1 } + dataSource.viewControllerAfter = { _ in viewController3 } + manager.select(viewController: viewController2) + + manager.willBeginDragging() + manager.didScroll(progress: -0.1) + delegate.calls = [] + manager.didScroll(progress: 0) + + XCTAssertEqual(delegate.calls, [ + .isScrolling(from: viewController2, to: viewController1, progress: 0.0), + .beginAppearanceTransition(true, viewController2), + .beginAppearanceTransition(false, viewController1), + .endAppearanceTransition(viewController2), + .endAppearanceTransition(viewController1), + .didFinishScrolling(from: viewController2, to: viewController1, success: false) + ]) + } + + func testCancelScrollForwardThenSwipeForwardAgain() { + let viewController1 = UIViewController() + let viewController2 = UIViewController() + let viewController3 = UIViewController() + + dataSource.viewControllerBefore = { _ in viewController1 } + dataSource.viewControllerAfter = { _ in viewController3 } + manager.select(viewController: viewController2) + + manager.willBeginDragging() + manager.didScroll(progress: 0.1) + manager.didScroll(progress: 0) + delegate.calls = [] + manager.willEndDragging() + manager.willBeginDragging() + manager.willEndDragging() + manager.didScroll(progress: 0.1) + + XCTAssertEqual(delegate.calls, [ + .isScrolling(from: viewController2, to: viewController3, progress: 0.1), + .willScroll(from: viewController2, to: viewController3), + .beginAppearanceTransition(true, viewController3), + .beginAppearanceTransition(false, viewController2) + ]) + } + + func testCancelScrollReverseThenSwipeReverseAgain() { + let viewController1 = UIViewController() + let viewController2 = UIViewController() + let viewController3 = UIViewController() + + dataSource.viewControllerBefore = { _ in viewController1 } + dataSource.viewControllerAfter = { _ in viewController3 } + manager.select(viewController: viewController2) + + manager.willBeginDragging() + manager.didScroll(progress: -0.1) + manager.didScroll(progress: 0) + delegate.calls = [] + manager.willEndDragging() + manager.willBeginDragging() + manager.willEndDragging() + manager.didScroll(progress: -0.1) + + XCTAssertEqual(delegate.calls, [ + .isScrolling(from: viewController2, to: viewController1, progress: -0.1), + .willScroll(from: viewController2, to: viewController1), + .beginAppearanceTransition(true, viewController1), + .beginAppearanceTransition(false, viewController2) + ]) + } + + func testCancelScrollForwardThenSwipeReverse() { + let viewController1 = UIViewController() + let viewController2 = UIViewController() + let viewController3 = UIViewController() + + dataSource.viewControllerBefore = { _ in viewController1 } + dataSource.viewControllerAfter = { _ in viewController3 } + manager.select(viewController: viewController2) + + manager.willBeginDragging() + manager.didScroll(progress: 0.1) + delegate.calls = [] + manager.willEndDragging() + manager.willBeginDragging() + manager.willEndDragging() + manager.didScroll(progress: -0.1) + + XCTAssertEqual(delegate.calls, [ + .beginAppearanceTransition(true, viewController2), + .beginAppearanceTransition(false, viewController3), + .endAppearanceTransition(viewController2), + .endAppearanceTransition(viewController3), + .didFinishScrolling(from: viewController2, to: viewController3, success: false), + .isScrolling(from: viewController2, to: viewController1, progress: -0.1), + .willScroll(from: viewController2, to: viewController1), + .beginAppearanceTransition(true, viewController1), + .beginAppearanceTransition(false, viewController2), + ]) + } + + func testCancelScrollReverseThenSwipeForward() { + let viewController1 = UIViewController() + let viewController2 = UIViewController() + let viewController3 = UIViewController() + + dataSource.viewControllerBefore = { _ in viewController1 } + dataSource.viewControllerAfter = { _ in viewController3 } + manager.select(viewController: viewController2) + + manager.willBeginDragging() + manager.didScroll(progress: -0.1) + delegate.calls = [] + manager.willEndDragging() + manager.willBeginDragging() + manager.willEndDragging() + manager.didScroll(progress: 0.1) + + XCTAssertEqual(delegate.calls, [ + .beginAppearanceTransition(true, viewController2), + .beginAppearanceTransition(false, viewController1), + .endAppearanceTransition(viewController2), + .endAppearanceTransition(viewController1), + .didFinishScrolling(from: viewController2, to: viewController1, success: false), + .isScrolling(from: viewController2, to: viewController3, progress: 0.1), + .willScroll(from: viewController2, to: viewController3), + .beginAppearanceTransition(true, viewController3), + .beginAppearanceTransition(false, viewController2), + ]) + } + + func testStartedScrollingBeforeCurrentSwipeReloaded() { + let viewController1 = UIViewController() + let viewController2 = UIViewController() + let viewController3 = UIViewController() + let viewController4 = UIViewController() + + dataSource.viewControllerBefore = { _ in viewController1 } + dataSource.viewControllerAfter = { _ in viewController3 } + manager.select(viewController: viewController2) + dataSource.viewControllerAfter = { _ in viewController4 } + + manager.willBeginDragging() + manager.didScroll(progress: 0.1) + manager.willEndDragging() + manager.willBeginDragging() + manager.didScroll(progress: 1) + delegate.calls = [] + manager.willEndDragging() + manager.didScroll(progress: 0.1) + + XCTAssertEqual(delegate.calls, [ + .isScrolling(from: viewController3, to: viewController4, progress: 0.1), + .willScroll(from: viewController3, to: viewController4), + .beginAppearanceTransition(true, viewController4), + .beginAppearanceTransition(false, viewController3), + ]) + } + + // MARK: - Removing + + func testRemoveAll() { + let previousVc = UIViewController() + let selectedVc = UIViewController() + let nextVc = UIViewController() + + dataSource.viewControllerBefore = { _ in previousVc } + dataSource.viewControllerAfter = { _ in nextVc } + manager.select(viewController: selectedVc) + + delegate.calls = [] + + manager.removeAll() + + XCTAssertEqual(delegate.calls, [ + .beginAppearanceTransition(false, selectedVc), + .removeViewController(selectedVc), + .removeViewController(previousVc), + .removeViewController(nextVc), + .layoutViews([]), + .endAppearanceTransition(selectedVc) + ]) + } +} diff --git a/ParchmentTests/PagingControllerTests.swift b/ParchmentTests/PagingControllerTests.swift index f7a2bd2a..c9c0d9dd 100644 --- a/ParchmentTests/PagingControllerTests.swift +++ b/ParchmentTests/PagingControllerTests.swift @@ -491,7 +491,7 @@ final class PagingControllerTests: XCTestCase { delegate.calls = [] // Select a new item. - pagingController.select(pagingItem: Item(index: 0), animated: false) + pagingController.select(pagingItem: Item(index: 0), animated: true) // Expect it to enter the scrolling state. XCTAssertEqual(pagingController.state, PagingState.scrolling( @@ -516,7 +516,7 @@ final class PagingControllerTests: XCTestCase { delegate.calls = [] // Select the previous sibling. - pagingController.select(pagingItem: Item(index: 0), animated: false) + pagingController.select(pagingItem: Item(index: 0), animated: true) // Expect it to select the previous content view. XCTAssertEqual(collectionView.calls, []) @@ -525,7 +525,7 @@ final class PagingControllerTests: XCTestCase { .delegate(.selectContent( pagingItem: Item(index: 0), direction: .reverse(sibling: true), - animated: false + animated: true )) ]) } @@ -543,7 +543,7 @@ final class PagingControllerTests: XCTestCase { delegate.calls = [] // Select the previous sibling. - pagingController.select(pagingItem: Item(index: 2), animated: false) + pagingController.select(pagingItem: Item(index: 2), animated: true) // Expect it to select the previous content view. XCTAssertEqual(collectionView.calls, []) @@ -552,7 +552,7 @@ final class PagingControllerTests: XCTestCase { .delegate(.selectContent( pagingItem: Item(index: 2), direction: .forward(sibling: true), - animated: false + animated: true )) ]) } @@ -570,7 +570,7 @@ final class PagingControllerTests: XCTestCase { delegate.calls = [] // Select an item that is not the sibling of the selected item. - pagingController.select(pagingItem: Item(index: 4), animated: false) + pagingController.select(pagingItem: Item(index: 4), animated: true) // Expect it to select the content view. XCTAssertEqual(collectionView.calls, []) @@ -579,7 +579,7 @@ final class PagingControllerTests: XCTestCase { .delegate(.selectContent( pagingItem: Item(index: 4), direction: .forward(sibling: false), - animated: false + animated: true )) ]) } @@ -604,7 +604,7 @@ final class PagingControllerTests: XCTestCase { // Select the item next to the selected item, which is now // scrolled out of view. - pagingController.select(pagingItem: Item(index: 1), animated: false) + pagingController.select(pagingItem: Item(index: 1), animated: true) // The visible items should now contain the items that were // visible before scrolling (6..10), plus the items around @@ -639,7 +639,7 @@ final class PagingControllerTests: XCTestCase { .delegate(.selectContent( pagingItem: Item(index: 1), direction: .forward(sibling: true), - animated: false + animated: true )) ]) } diff --git a/ParchmentTests/PagingViewControllerTests.swift b/ParchmentTests/PagingViewControllerTests.swift index 581de638..2eedd266 100644 --- a/ParchmentTests/PagingViewControllerTests.swift +++ b/ParchmentTests/PagingViewControllerTests.swift @@ -45,7 +45,6 @@ final class PagingViewControllerTests: XCTestCase { // Should not updated the view controllers XCTAssertEqual(pagingViewController.pageViewController.selectedViewController, viewController0) - XCTAssertEqual(pagingViewController.pageViewController.afterViewController, viewController1) } func testReloadData() { @@ -86,7 +85,6 @@ final class PagingViewControllerTests: XCTestCase { XCTAssertEqual(cell3?.item, item3) XCTAssertEqual(pagingViewController.state, PagingState.selected(pagingItem: item2)) XCTAssertEqual(pagingViewController.pageViewController.selectedViewController, viewController2) - XCTAssertEqual(pagingViewController.pageViewController.afterViewController, viewController3) } func testReloadDataSameItemsUpdatesViewControllers() { @@ -118,7 +116,6 @@ final class PagingViewControllerTests: XCTestCase { // Assert XCTAssertEqual(pagingViewController.pageViewController.selectedViewController, viewController2) - XCTAssertEqual(pagingViewController.pageViewController.afterViewController, viewController3) } func testReloadDataSelectsPreviouslySelectedItem() { From a27458b0ebb0bc8061401db64f75711abd39bf6c Mon Sep 17 00:00:00 2001 From: Martin Rechsteiner Date: Fri, 27 Mar 2020 22:24:55 +0100 Subject: [PATCH 02/12] Adjust content offset so selected view is always centered --- Parchment/Classes/PageViewController.swift | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/Parchment/Classes/PageViewController.swift b/Parchment/Classes/PageViewController.swift index b68a3d4d..f95ec77c 100644 --- a/Parchment/Classes/PageViewController.swift +++ b/Parchment/Classes/PageViewController.swift @@ -143,15 +143,30 @@ extension PageViewController: PageViewManagerDelegate { height: scrollView.bounds.height) } + // When updating the content offset we need to account for the + // current content offset as well. This ensures that the selected + // page is fully centered when swiping so fast that you get the + // bounce effect in the scroll view. + var diff: CGFloat = 0 + if scrollView.contentOffset.x > view.bounds.width * 2 { + diff = scrollView.contentOffset.x - view.bounds.width * 2 + } else if scrollView.contentOffset.x > view.bounds.width && scrollView.contentOffset.x < view.bounds.width * 2 { + diff = scrollView.contentOffset.x - view.bounds.width + } else if scrollView.contentOffset.x < view.bounds.width && scrollView.contentOffset.x < 0 { + diff = scrollView.contentOffset.x + } + + // Need to set content size before updating content offset. If not + // the views will be misplaced when overshooting. scrollView.contentSize = CGSize( width: CGFloat(manager.state.count) * view.bounds.width, height: view.bounds.height) switch manager.state { case .first, .single, .empty: - scrollView.contentOffset = CGPoint(x: 0, y: 0) + scrollView.contentOffset = CGPoint(x: diff, y: 0) case .last, .center: - scrollView.contentOffset = CGPoint(x: view.bounds.width, y: 0) + scrollView.contentOffset = CGPoint(x: view.bounds.width + diff, y: 0) } } From e300ecbc67015c52f4c87f38388abe5d1f059df5 Mon Sep 17 00:00:00 2001 From: Martin Rechsteiner Date: Fri, 27 Mar 2020 22:27:23 +0100 Subject: [PATCH 03/12] Layout page view when transitioning size Adds another flag to the layoutViews method so we can opt-out of accounting for the current content offset when rotating. --- Parchment/Classes/PageViewController.swift | 24 +++++++++++++------ Parchment/Classes/PageViewManager.swift | 12 ++++++---- .../Protocols/PageViewManagerDelegate.swift | 2 +- .../Mocks/MockPageViewManagerDelegate.swift | 2 +- 4 files changed, 27 insertions(+), 13 deletions(-) diff --git a/Parchment/Classes/PageViewController.swift b/Parchment/Classes/PageViewController.swift index f95ec77c..258780e1 100644 --- a/Parchment/Classes/PageViewController.swift +++ b/Parchment/Classes/PageViewController.swift @@ -57,6 +57,14 @@ public final class PageViewController: UIViewController { manager.viewWillAppear(animated: animated) } + + public override func willTransition(to newCollection: UITraitCollection, with coordinator: UIViewControllerTransitionCoordinator) { + super.willTransition(to: newCollection, with: coordinator) + coordinator.animate(alongsideTransition: { _ in + self.manager.viewWillTransitionSize() + }) + } + // MARK: - Public Methods public func selectViewController(_ viewController: UIViewController, direction: PageViewDirection, animated: Bool = true) { @@ -134,7 +142,7 @@ extension PageViewController: PageViewManagerDelegate { } } - func layoutViews(for viewControllers: [UIViewController]) { + func layoutViews(for viewControllers: [UIViewController], keepContentOffset: Bool) { for (index, viewController) in viewControllers.enumerated() { viewController.view.frame = CGRect( x: CGFloat(index) * scrollView.bounds.width, @@ -148,12 +156,14 @@ extension PageViewController: PageViewManagerDelegate { // page is fully centered when swiping so fast that you get the // bounce effect in the scroll view. var diff: CGFloat = 0 - if scrollView.contentOffset.x > view.bounds.width * 2 { - diff = scrollView.contentOffset.x - view.bounds.width * 2 - } else if scrollView.contentOffset.x > view.bounds.width && scrollView.contentOffset.x < view.bounds.width * 2 { - diff = scrollView.contentOffset.x - view.bounds.width - } else if scrollView.contentOffset.x < view.bounds.width && scrollView.contentOffset.x < 0 { - diff = scrollView.contentOffset.x + if keepContentOffset { + if scrollView.contentOffset.x > view.bounds.width * 2 { + diff = scrollView.contentOffset.x - view.bounds.width * 2 + } else if scrollView.contentOffset.x > view.bounds.width && scrollView.contentOffset.x < view.bounds.width * 2 { + diff = scrollView.contentOffset.x - view.bounds.width + } else if scrollView.contentOffset.x < view.bounds.width && scrollView.contentOffset.x < 0 { + diff = scrollView.contentOffset.x + } } // Need to set content size before updating content offset. If not diff --git a/Parchment/Classes/PageViewManager.swift b/Parchment/Classes/PageViewManager.swift index ecad60ff..264d8f26 100644 --- a/Parchment/Classes/PageViewManager.swift +++ b/Parchment/Classes/PageViewManager.swift @@ -4,9 +4,9 @@ final class PageViewManager { weak var dataSource: PageViewManagerDataSource? weak var delegate: PageViewManagerDelegate? - private weak var previousViewController: UIViewController? + private(set) weak var previousViewController: UIViewController? private(set) weak var selectedViewController: UIViewController? - private weak var nextViewController: UIViewController? + private(set) weak var nextViewController: UIViewController? var state: PageViewState { if previousViewController == nil && nextViewController == nil && selectedViewController == nil { @@ -159,6 +159,10 @@ final class PageViewManager { resetState() } + func viewWillTransitionSize() { + layoutsViews(keepContentOffset: false) + } + func didScroll(progress: CGFloat) { let currentDirection = PageViewDirection(progress: progress) @@ -528,7 +532,7 @@ final class PageViewManager { delegate?.endAppearanceTransition(viewController: oldPreviousViewController) } - private func layoutsViews() { + private func layoutsViews(keepContentOffset: Bool = true) { var viewControllers: [UIViewController] = [] if let previousViewController = previousViewController { @@ -541,6 +545,6 @@ final class PageViewManager { viewControllers.append(nextViewController) } - delegate?.layoutViews(for: viewControllers) + delegate?.layoutViews(for: viewControllers, keepContentOffset: keepContentOffset) } } diff --git a/Parchment/Protocols/PageViewManagerDelegate.swift b/Parchment/Protocols/PageViewManagerDelegate.swift index 3c56ee38..1cdb4c47 100644 --- a/Parchment/Protocols/PageViewManagerDelegate.swift +++ b/Parchment/Protocols/PageViewManagerDelegate.swift @@ -3,7 +3,7 @@ import UIKit protocol PageViewManagerDelegate: class { func scrollForward() func scrollReverse() - func layoutViews(for viewControllers: [UIViewController]) + func layoutViews(for viewControllers: [UIViewController], keepContentOffset: Bool) func addViewController(_ viewController: UIViewController) func removeViewController(_ viewController: UIViewController) func beginAppearanceTransition(isAppearing: Bool, viewController: UIViewController) diff --git a/ParchmentTests/Mocks/MockPageViewManagerDelegate.swift b/ParchmentTests/Mocks/MockPageViewManagerDelegate.swift index 7a676e91..7b48aba3 100644 --- a/ParchmentTests/Mocks/MockPageViewManagerDelegate.swift +++ b/ParchmentTests/Mocks/MockPageViewManagerDelegate.swift @@ -26,7 +26,7 @@ final class MockPageViewManagerDelegate: PageViewManagerDelegate { calls.append(.scrollReverse) } - func layoutViews(for viewControllers: [UIViewController]) { + func layoutViews(for viewControllers: [UIViewController], keepContentOffset: Bool) { calls.append(.layoutViews(viewControllers)) } From 7375c11ab850b41dc00b45e68b4eb9556d677eed Mon Sep 17 00:00:00 2001 From: Martin Rechsteiner Date: Sun, 29 Mar 2020 14:04:26 +0200 Subject: [PATCH 04/12] Fix appearance transitions in page view controller Make sure viewWillAppear, viewDidAppear, viewWillDisappear and viewDidDisappear is forwarded to the selected view controller and include the correct animated flag. --- Parchment/Classes/PageViewController.swift | 20 +- Parchment/Classes/PageViewManager.swift | 147 +++++++--- .../Protocols/PageViewManagerDelegate.swift | 5 +- .../Mocks/MockPageViewManagerDelegate.swift | 6 +- ParchmentTests/PageViewManagerTests.swift | 250 ++++++++++++++---- 5 files changed, 337 insertions(+), 91 deletions(-) diff --git a/Parchment/Classes/PageViewController.swift b/Parchment/Classes/PageViewController.swift index 258780e1..7f4069a8 100644 --- a/Parchment/Classes/PageViewController.swift +++ b/Parchment/Classes/PageViewController.swift @@ -54,9 +54,23 @@ public final class PageViewController: UIViewController { public override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) view.layoutIfNeeded() - manager.viewWillAppear(animated: animated) + manager.viewWillAppear(animated) } + public override func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + manager.viewDidAppear(animated) + } + + public override func viewWillDisappear(_ animated: Bool) { + super.viewWillDisappear(animated) + manager.viewWillDisappear(animated) + } + + public override func viewDidDisappear(_ animated: Bool) { + super.viewDidDisappear(animated) + manager.viewDidDisappear(animated) + } public override func willTransition(to newCollection: UITraitCollection, with coordinator: UIViewControllerTransitionCoordinator) { super.willTransition(to: newCollection, with: coordinator) @@ -194,8 +208,8 @@ extension PageViewController: PageViewManagerDelegate { viewController.didMove(toParent: nil) } - func beginAppearanceTransition(isAppearing: Bool, viewController: UIViewController) { - viewController.beginAppearanceTransition(isAppearing, animated: false) + func beginAppearanceTransition(isAppearing: Bool, viewController: UIViewController, animated: Bool) { + viewController.beginAppearanceTransition(isAppearing, animated: animated) } func endAppearanceTransition(viewController: UIViewController) { diff --git a/Parchment/Classes/PageViewManager.swift b/Parchment/Classes/PageViewManager.swift index 264d8f26..0549719f 100644 --- a/Parchment/Classes/PageViewManager.swift +++ b/Parchment/Classes/PageViewManager.swift @@ -24,6 +24,14 @@ final class PageViewManager { // MARK: - Private Properties + private enum AppearanceState { + case appearing(animated: Bool) + case disappearing(animated: Bool) + case disappeared + case appeared + } + + private var appearanceState: AppearanceState = .disappeared private var didReload: Bool = false private var didSelect: Bool = false private var initialDirection: PageViewDirection = .none @@ -35,7 +43,7 @@ final class PageViewManager { direction: PageViewDirection = .none, animated: Bool = false) { if state == .empty || animated == false { - selectViewController(viewController) + selectViewController(viewController, animated: animated) return } else { resetState() @@ -69,8 +77,8 @@ final class PageViewManager { } else if let nextViewController = nextViewController, let selectedViewController = selectedViewController { - delegate?.beginAppearanceTransition(isAppearing: false, viewController: selectedViewController) - delegate?.beginAppearanceTransition(isAppearing: true, viewController: nextViewController) + beginAppearanceTransition(false, for: selectedViewController, animated: animated) + beginAppearanceTransition(true, for: nextViewController, animated: animated) let newNextViewController = dataSource?.viewControllerAfter(nextViewController) @@ -88,8 +96,8 @@ final class PageViewManager { layoutsViews() - delegate?.endAppearanceTransition(viewController: selectedViewController) - delegate?.endAppearanceTransition(viewController: nextViewController) + endAppearanceTransition(for: selectedViewController) + endAppearanceTransition(for: nextViewController) } } @@ -100,8 +108,8 @@ final class PageViewManager { } else if let previousViewController = previousViewController, let selectedViewController = selectedViewController { - delegate?.beginAppearanceTransition(isAppearing: false, viewController: selectedViewController) - delegate?.beginAppearanceTransition(isAppearing: true, viewController: previousViewController) + beginAppearanceTransition(false, for: selectedViewController, animated: animated) + beginAppearanceTransition(true, for: previousViewController, animated: animated) let newPreviousViewController = dataSource?.viewControllerBefore(previousViewController) @@ -119,8 +127,8 @@ final class PageViewManager { layoutsViews() - delegate?.endAppearanceTransition(viewController: selectedViewController) - delegate?.endAppearanceTransition(viewController: previousViewController) + endAppearanceTransition(for: selectedViewController) + endAppearanceTransition(for: previousViewController) } } @@ -128,7 +136,7 @@ final class PageViewManager { let oldSelectedViewController = selectedViewController if let selectedViewController = oldSelectedViewController { - delegate?.beginAppearanceTransition(isAppearing: false, viewController: selectedViewController) + beginAppearanceTransition(false, for: selectedViewController, animated: false) delegate?.removeViewController(selectedViewController) } if let previousViewController = previousViewController { @@ -143,12 +151,49 @@ final class PageViewManager { layoutsViews() if let oldSelectedViewController = oldSelectedViewController { - delegate?.endAppearanceTransition(viewController: oldSelectedViewController) + endAppearanceTransition(for: oldSelectedViewController) } } - func viewWillAppear(animated: Bool) { - layoutsViews() + func viewWillAppear(_ animated: Bool) { + appearanceState = .appearing(animated: animated) + if let selectedViewController = selectedViewController { + delegate?.beginAppearanceTransition( + isAppearing: true, + viewController: selectedViewController, + animated: animated) + } + + switch state { + case .center, .first, .last, .single: + layoutsViews() + case .empty: + break + } + } + + func viewDidAppear(_ animated: Bool) { + appearanceState = .appeared + if let selectedViewController = selectedViewController { + delegate?.endAppearanceTransition(viewController: selectedViewController) + } + } + + func viewWillDisappear(_ animated: Bool) { + appearanceState = .disappearing(animated: animated) + if let selectedViewController = selectedViewController { + delegate?.beginAppearanceTransition( + isAppearing: false, + viewController: selectedViewController, + animated: animated) + } + } + + func viewDidDisappear(_ animated: Bool) { + appearanceState = .disappeared + if let selectedViewController = selectedViewController { + delegate?.endAppearanceTransition(viewController: selectedViewController) + } } func willBeginDragging() { @@ -230,17 +275,17 @@ final class PageViewManager { // MARK: - Private Methods - private func selectViewController(_ viewController: UIViewController) { + private func selectViewController(_ viewController: UIViewController, animated: Bool) { let oldSelectedViewController = selectedViewController let newPreviousViewController = dataSource?.viewControllerBefore(viewController) let newNextViewController = dataSource?.viewControllerAfter(viewController) if let oldSelectedViewController = oldSelectedViewController { - delegate?.beginAppearanceTransition(isAppearing: false, viewController: oldSelectedViewController) + beginAppearanceTransition(false, for: oldSelectedViewController, animated: animated) } if viewController !== selectedViewController { - delegate?.beginAppearanceTransition(isAppearing: true, viewController: viewController) + beginAppearanceTransition(true, for: viewController, animated: animated) } if let oldPreviosViewController = previousViewController { @@ -294,11 +339,11 @@ final class PageViewManager { layoutsViews() if let oldSelectedViewController = oldSelectedViewController { - delegate?.endAppearanceTransition(viewController: oldSelectedViewController) + endAppearanceTransition(for: oldSelectedViewController) } if viewController !== oldSelectedViewController { - delegate?.endAppearanceTransition(viewController: viewController) + endAppearanceTransition(for: viewController) } } @@ -361,8 +406,8 @@ final class PageViewManager { let oldNextViewController = nextViewController if let nextViewController = oldNextViewController { - delegate?.beginAppearanceTransition(isAppearing: true, viewController: selectedViewController) - delegate?.beginAppearanceTransition(isAppearing: false, viewController: nextViewController) + beginAppearanceTransition(true, for: selectedViewController, animated: true) + beginAppearanceTransition(false, for: nextViewController, animated: true) } if didSelect { @@ -379,8 +424,8 @@ final class PageViewManager { } if let oldNextViewController = oldNextViewController { - delegate?.endAppearanceTransition(viewController: selectedViewController) - delegate?.endAppearanceTransition(viewController: oldNextViewController) + endAppearanceTransition(for: selectedViewController) + endAppearanceTransition(for: oldNextViewController) delegate?.didFinishScrolling( from: selectedViewController, to: oldNextViewController, @@ -393,8 +438,8 @@ final class PageViewManager { let oldPreviousViewController = previousViewController if let previousViewController = oldPreviousViewController { - delegate?.beginAppearanceTransition(isAppearing: true, viewController: selectedViewController) - delegate?.beginAppearanceTransition(isAppearing: false, viewController: previousViewController) + beginAppearanceTransition(true, for: selectedViewController, animated: true) + beginAppearanceTransition(false, for: previousViewController, animated: true) } if didSelect { @@ -411,8 +456,8 @@ final class PageViewManager { } if let oldPreviousViewController = oldPreviousViewController { - delegate?.endAppearanceTransition(viewController: selectedViewController) - delegate?.endAppearanceTransition(viewController: oldPreviousViewController) + endAppearanceTransition(for: selectedViewController) + endAppearanceTransition(for: oldPreviousViewController) delegate?.didFinishScrolling( from: selectedViewController, to: oldPreviousViewController, @@ -424,8 +469,8 @@ final class PageViewManager { if let selectedViewController = selectedViewController, let nextViewController = nextViewController { delegate?.willScroll(from: selectedViewController, to: nextViewController) - delegate?.beginAppearanceTransition(isAppearing: true, viewController: nextViewController) - delegate?.beginAppearanceTransition(isAppearing: false, viewController: selectedViewController) + beginAppearanceTransition(true, for: nextViewController, animated: true) + beginAppearanceTransition(false, for: selectedViewController, animated: true) } } @@ -433,8 +478,8 @@ final class PageViewManager { if let selectedViewController = selectedViewController, let previousViewController = previousViewController { delegate?.willScroll(from: selectedViewController, to: previousViewController) - delegate?.beginAppearanceTransition(isAppearing: true, viewController: previousViewController) - delegate?.beginAppearanceTransition(isAppearing: false, viewController: selectedViewController) + beginAppearanceTransition(true, for: previousViewController, animated: true) + beginAppearanceTransition(false, for: selectedViewController, animated: true) } } @@ -481,8 +526,8 @@ final class PageViewManager { layoutsViews() - delegate?.endAppearanceTransition(viewController: oldSelectedViewController) - delegate?.endAppearanceTransition(viewController: oldNextViewController) + endAppearanceTransition(for: oldSelectedViewController) + endAppearanceTransition(for: oldNextViewController) } private func didScrollReverse() { @@ -528,8 +573,8 @@ final class PageViewManager { layoutsViews() - delegate?.endAppearanceTransition(viewController: oldSelectedViewController) - delegate?.endAppearanceTransition(viewController: oldPreviousViewController) + endAppearanceTransition(for: oldSelectedViewController) + endAppearanceTransition(for: oldPreviousViewController) } private func layoutsViews(keepContentOffset: Bool = true) { @@ -547,4 +592,38 @@ final class PageViewManager { delegate?.layoutViews(for: viewControllers, keepContentOffset: keepContentOffset) } + + private func beginAppearanceTransition( + _ isAppearing: Bool, + for viewController: UIViewController, + animated: Bool) { + switch appearanceState { + case .appeared: + delegate?.beginAppearanceTransition( + isAppearing: isAppearing, + viewController: viewController, + animated: animated) + case let .appearing(animated): + // Override the given animated flag with the animated flag of + // the parent views appearance transition. + delegate?.beginAppearanceTransition( + isAppearing: isAppearing, + viewController: viewController, + animated: animated) + case let .disappearing(animated): + // When the parent view is about to disappear we always set + // isAppearing to false. + delegate?.beginAppearanceTransition( + isAppearing: false, + viewController: viewController, + animated: animated) + default: + break + } + } + + private func endAppearanceTransition(for viewController: UIViewController) { + guard case .appeared = appearanceState else { return } + delegate?.endAppearanceTransition(viewController: viewController) + } } diff --git a/Parchment/Protocols/PageViewManagerDelegate.swift b/Parchment/Protocols/PageViewManagerDelegate.swift index 1cdb4c47..8e70ecb9 100644 --- a/Parchment/Protocols/PageViewManagerDelegate.swift +++ b/Parchment/Protocols/PageViewManagerDelegate.swift @@ -6,7 +6,10 @@ protocol PageViewManagerDelegate: class { func layoutViews(for viewControllers: [UIViewController], keepContentOffset: Bool) func addViewController(_ viewController: UIViewController) func removeViewController(_ viewController: UIViewController) - func beginAppearanceTransition(isAppearing: Bool, viewController: UIViewController) + func beginAppearanceTransition( + isAppearing: Bool, + viewController: UIViewController, + animated: Bool) func endAppearanceTransition(viewController: UIViewController) func willScroll( from selectedViewController: UIViewController, diff --git a/ParchmentTests/Mocks/MockPageViewManagerDelegate.swift b/ParchmentTests/Mocks/MockPageViewManagerDelegate.swift index 7b48aba3..712b5f8b 100644 --- a/ParchmentTests/Mocks/MockPageViewManagerDelegate.swift +++ b/ParchmentTests/Mocks/MockPageViewManagerDelegate.swift @@ -9,7 +9,7 @@ final class MockPageViewManagerDelegate: PageViewManagerDelegate { case layoutViews([UIViewController]) case addViewController(UIViewController) case removeViewController(UIViewController) - case beginAppearanceTransition(Bool, UIViewController) + case beginAppearanceTransition(Bool, UIViewController, Bool) case endAppearanceTransition(UIViewController) case willScroll(from: UIViewController, to: UIViewController) case isScrolling(from: UIViewController, to: UIViewController?, progress: CGFloat) @@ -38,8 +38,8 @@ final class MockPageViewManagerDelegate: PageViewManagerDelegate { calls.append(.removeViewController(viewController)) } - func beginAppearanceTransition(isAppearing: Bool, viewController: UIViewController) { - calls.append(.beginAppearanceTransition(isAppearing, viewController)) + func beginAppearanceTransition(isAppearing: Bool, viewController: UIViewController, animated: Bool) { + calls.append(.beginAppearanceTransition(isAppearing, viewController, animated)) } func endAppearanceTransition(viewController: UIViewController) { diff --git a/ParchmentTests/PageViewManagerTests.swift b/ParchmentTests/PageViewManagerTests.swift index 8daa7216..458cb903 100644 --- a/ParchmentTests/PageViewManagerTests.swift +++ b/ParchmentTests/PageViewManagerTests.swift @@ -25,10 +25,11 @@ final class PageViewManagerTests: XCTestCase { dataSource.viewControllerBefore = { _ in previousVc } dataSource.viewControllerAfter = { _ in nextVc } + manager.viewDidAppear(false) manager.select(viewController: selectedVc, animated: true) XCTAssertEqual(delegate.calls, [ - .beginAppearanceTransition(true, selectedVc), + .beginAppearanceTransition(true, selectedVc, true), .addViewController(previousVc), .addViewController(selectedVc), .addViewController(nextVc), @@ -48,6 +49,7 @@ final class PageViewManagerTests: XCTestCase { dataSource.viewControllerBefore = { _ in oldPreviousVc } dataSource.viewControllerAfter = { _ in oldNextVc } + manager.viewDidAppear(false) manager.select(viewController: oldSelectedVc) delegate.calls = [] @@ -68,8 +70,8 @@ final class PageViewManagerTests: XCTestCase { .scrollForward, .isScrolling(from: oldSelectedVc, to: newSelectedVc, progress: 0.1), .willScroll(from: oldSelectedVc, to: newSelectedVc), - .beginAppearanceTransition(true, newSelectedVc), - .beginAppearanceTransition(false, oldSelectedVc), + .beginAppearanceTransition(true, newSelectedVc, true), + .beginAppearanceTransition(false, oldSelectedVc, true), // Replace the previously selected with the new previous view // once the transition completes. Should be left with all the @@ -99,6 +101,7 @@ final class PageViewManagerTests: XCTestCase { dataSource.viewControllerBefore = { _ in oldPreviousVc } dataSource.viewControllerAfter = { _ in oldNextVc } + manager.viewDidAppear(false) manager.select(viewController: oldSelectedVc) dataSource.viewControllerBefore = { _ in newPreviousVc } @@ -113,8 +116,8 @@ final class PageViewManagerTests: XCTestCase { XCTAssertEqual(delegate.calls, [ .isScrolling(from: oldSelectedVc, to: newSelectedVc, progress: 0.0), - .beginAppearanceTransition(true, oldSelectedVc), - .beginAppearanceTransition(false, newSelectedVc), + .beginAppearanceTransition(true, oldSelectedVc, true), + .beginAppearanceTransition(false, newSelectedVc, true), // Expect that we remove the view controller that was selected // and replace it with the "old next" view controller. @@ -139,6 +142,7 @@ final class PageViewManagerTests: XCTestCase { dataSource.viewControllerBefore = { _ in oldPreviousVc } dataSource.viewControllerAfter = { _ in oldNextVc } + manager.viewDidAppear(false) manager.select(viewController: oldSelectedVc) delegate.calls = [] @@ -159,8 +163,8 @@ final class PageViewManagerTests: XCTestCase { .scrollReverse, .isScrolling(from: oldSelectedVc, to: newSelectedVc, progress: -0.1), .willScroll(from: oldSelectedVc, to: newSelectedVc), - .beginAppearanceTransition(true, newSelectedVc), - .beginAppearanceTransition(false, oldSelectedVc), + .beginAppearanceTransition(true, newSelectedVc, true), + .beginAppearanceTransition(false, oldSelectedVc, true), // Replace the previously selected with the new next view // once the transition completes. Should be left with all the @@ -190,6 +194,7 @@ final class PageViewManagerTests: XCTestCase { dataSource.viewControllerBefore = { _ in oldPreviousVc } dataSource.viewControllerAfter = { _ in oldNextVc } + manager.viewDidAppear(false) manager.select(viewController: oldSelectedVc) dataSource.viewControllerBefore = { _ in newPreviousVc } @@ -204,8 +209,8 @@ final class PageViewManagerTests: XCTestCase { XCTAssertEqual(delegate.calls, [ .isScrolling(from: oldSelectedVc, to: newSelectedVc, progress: 0.0), - .beginAppearanceTransition(true, oldSelectedVc), - .beginAppearanceTransition(false, newSelectedVc), + .beginAppearanceTransition(true, oldSelectedVc, true), + .beginAppearanceTransition(false, newSelectedVc, true), // Expect that we remove the view controller that was selected // and replace it with the "old previous" view controller. @@ -230,18 +235,19 @@ final class PageViewManagerTests: XCTestCase { dataSource.viewControllerBefore = { _ in oldPreviousVc } dataSource.viewControllerAfter = { _ in oldNextVc } + manager.viewDidAppear(false) manager.select(viewController: oldSelectedVc) delegate.calls = [] dataSource.viewControllerBefore = { _ in newPreviousVc } dataSource.viewControllerAfter = { _ in newNextVc } - manager.select(viewController: newSelectedVc) + manager.select(viewController: newSelectedVc, animated: false) XCTAssertEqual(delegate.calls, [ // Start the appearance transitions. - .beginAppearanceTransition(false, oldSelectedVc), - .beginAppearanceTransition(true, newSelectedVc), + .beginAppearanceTransition(false, oldSelectedVc, false), + .beginAppearanceTransition(true, newSelectedVc, false), // Remove old view controllers and add new ones. .removeViewController(oldPreviousVc), @@ -266,18 +272,19 @@ final class PageViewManagerTests: XCTestCase { dataSource.viewControllerBefore = { _ in viewController0 } dataSource.viewControllerAfter = { _ in viewController2 } + manager.viewDidAppear(false) manager.select(viewController: viewController1) delegate.calls = [] dataSource.viewControllerBefore = { _ in viewController1 } dataSource.viewControllerAfter = { _ in viewController3 } - manager.select(viewController: viewController2) + manager.select(viewController: viewController2, animated: false) XCTAssertEqual(delegate.calls, [ // Start the appearance transitions. - .beginAppearanceTransition(false, viewController1), - .beginAppearanceTransition(true, viewController2), + .beginAppearanceTransition(false, viewController1, false), + .beginAppearanceTransition(true, viewController2, false), // Remove the old view controller and add the new one. .removeViewController(viewController0), @@ -298,18 +305,19 @@ final class PageViewManagerTests: XCTestCase { dataSource.viewControllerBefore = { _ in viewController1 } dataSource.viewControllerAfter = { _ in viewController3 } + manager.viewDidAppear(false) manager.select(viewController: viewController2) delegate.calls = [] dataSource.viewControllerBefore = { _ in viewController0 } dataSource.viewControllerAfter = { _ in viewController2 } - manager.select(viewController: viewController1) + manager.select(viewController: viewController1, animated: false) XCTAssertEqual(delegate.calls, [ // Start the appearance transitions. - .beginAppearanceTransition(false, viewController2), - .beginAppearanceTransition(true, viewController1), + .beginAppearanceTransition(false, viewController2, false), + .beginAppearanceTransition(true, viewController1, false), // Remove the old view controller and add the new one. .removeViewController(viewController3), @@ -329,6 +337,7 @@ final class PageViewManagerTests: XCTestCase { dataSource.viewControllerBefore = { _ in viewController0 } dataSource.viewControllerAfter = { _ in viewController2 } + manager.viewDidAppear(false) manager.select(viewController: viewController1) delegate.calls = [] @@ -348,8 +357,8 @@ final class PageViewManagerTests: XCTestCase { .scrollForward, .isScrolling(from: viewController1, to: viewController2, progress: 0.1), .willScroll(from: viewController1, to: viewController2), - .beginAppearanceTransition(true, viewController2), - .beginAppearanceTransition(false, viewController1) + .beginAppearanceTransition(true, viewController2, true), + .beginAppearanceTransition(false, viewController1, true) ]) } @@ -361,6 +370,7 @@ final class PageViewManagerTests: XCTestCase { dataSource.viewControllerBefore = { _ in viewController0 } dataSource.viewControllerAfter = { _ in viewController2 } + manager.viewDidAppear(false) manager.select(viewController: viewController1) delegate.calls = [] @@ -376,8 +386,8 @@ final class PageViewManagerTests: XCTestCase { // Expect that it moves the view controllers immediately instead // of triggered the .scrollForward event. XCTAssertEqual(delegate.calls, [ - .beginAppearanceTransition(false, viewController1), - .beginAppearanceTransition(true, viewController2), + .beginAppearanceTransition(false, viewController1, false), + .beginAppearanceTransition(true, viewController2, false), .removeViewController(viewController0), .addViewController(viewController3), .layoutViews([viewController1, viewController2, viewController3]), @@ -393,6 +403,7 @@ final class PageViewManagerTests: XCTestCase { dataSource.viewControllerBefore = { _ in viewController0 } dataSource.viewControllerAfter = { _ in viewController2 } + manager.viewDidAppear(false) manager.select(viewController: viewController1) delegate.calls = [] @@ -412,12 +423,12 @@ final class PageViewManagerTests: XCTestCase { .scrollReverse, .isScrolling(from: viewController1, to: viewController0, progress: -0.1), .willScroll(from: viewController1, to: viewController0), - .beginAppearanceTransition(true, viewController0), - .beginAppearanceTransition(false, viewController1) + .beginAppearanceTransition(true, viewController0, true), + .beginAppearanceTransition(false, viewController1, true) ]) } - func testSelectPreviousAnimatedFalse() { + func testSelectPreviousWithoutAnimation() { let viewController0 = UIViewController() let viewController1 = UIViewController() let viewController2 = UIViewController() @@ -425,6 +436,7 @@ final class PageViewManagerTests: XCTestCase { dataSource.viewControllerBefore = { _ in viewController1 } dataSource.viewControllerAfter = { _ in viewController3 } + manager.viewDidAppear(false) manager.select(viewController: viewController2) delegate.calls = [] @@ -440,8 +452,8 @@ final class PageViewManagerTests: XCTestCase { // Expect that it moves the view controllers immediately instead // of triggered the .scrollForward event. XCTAssertEqual(delegate.calls, [ - .beginAppearanceTransition(false, viewController2), - .beginAppearanceTransition(true, viewController1), + .beginAppearanceTransition(false, viewController2, false), + .beginAppearanceTransition(true, viewController1, false), .removeViewController(viewController3), .addViewController(viewController0), .layoutViews([viewController0, viewController1, viewController2]), @@ -457,6 +469,7 @@ final class PageViewManagerTests: XCTestCase { let nextVc = UIViewController() dataSource.viewControllerAfter = { _ in nextVc } + manager.viewDidAppear(false) manager.select(viewController: selectedVc) delegate.calls = [] @@ -466,14 +479,15 @@ final class PageViewManagerTests: XCTestCase { XCTAssertEqual(delegate.calls, [ .isScrolling(from: selectedVc, to: nextVc, progress: 0.1), .willScroll(from: selectedVc, to: nextVc), - .beginAppearanceTransition(true, nextVc), - .beginAppearanceTransition(false, selectedVc) + .beginAppearanceTransition(true, nextVc, true), + .beginAppearanceTransition(false, selectedVc, true) ]) } func testStartedScrollingForwardNextNil() { let selectedVc = UIViewController() + manager.viewDidAppear(false) manager.select(viewController: selectedVc) delegate.calls = [] @@ -490,6 +504,7 @@ final class PageViewManagerTests: XCTestCase { let previousVc = UIViewController() dataSource.viewControllerBefore = { _ in previousVc } + manager.viewDidAppear(false) manager.select(viewController: selectedVc) delegate.calls = [] @@ -499,14 +514,15 @@ final class PageViewManagerTests: XCTestCase { XCTAssertEqual(delegate.calls, [ .isScrolling(from: selectedVc, to: previousVc, progress: -0.1), .willScroll(from: selectedVc, to: previousVc), - .beginAppearanceTransition(true, previousVc), - .beginAppearanceTransition(false, selectedVc) + .beginAppearanceTransition(true, previousVc, true), + .beginAppearanceTransition(false, selectedVc, true) ]) } func testStartedScrollingReversePreviousNil() { let selectedVc = UIViewController() + manager.viewDidAppear(false) manager.select(viewController: selectedVc) delegate.calls = [] @@ -522,6 +538,7 @@ final class PageViewManagerTests: XCTestCase { let selectedVc = UIViewController() let nextVc = UIViewController() + manager.viewDidAppear(false) dataSource.viewControllerAfter = { _ in nextVc } manager.select(viewController: selectedVc) @@ -542,6 +559,7 @@ final class PageViewManagerTests: XCTestCase { let selectedVc = UIViewController() dataSource.viewControllerBefore = { _ in previousVc } + manager.viewDidAppear(false) manager.select(viewController: selectedVc) manager.willBeginDragging() @@ -564,6 +582,7 @@ final class PageViewManagerTests: XCTestCase { dataSource.viewControllerBefore = { _ in viewController0 } dataSource.viewControllerAfter = { _ in viewController2 } + manager.viewDidAppear(false) manager.select(viewController: viewController1) dataSource.viewControllerAfter = { _ in viewController3 } @@ -590,6 +609,7 @@ final class PageViewManagerTests: XCTestCase { dataSource.viewControllerBefore = { _ in viewController0 } dataSource.viewControllerAfter = { _ in viewController2 } + manager.viewDidAppear(false) manager.select(viewController: viewController1) dataSource.viewControllerAfter = { _ in nil } @@ -617,6 +637,7 @@ final class PageViewManagerTests: XCTestCase { dataSource.viewControllerBefore = { _ in viewController1 } dataSource.viewControllerAfter = { _ in viewController3 } + manager.viewDidAppear(false) manager.select(viewController: viewController2) dataSource.viewControllerBefore = { _ in viewController0 } @@ -644,6 +665,7 @@ final class PageViewManagerTests: XCTestCase { dataSource.viewControllerBefore = { _ in viewController1 } dataSource.viewControllerAfter = { _ in viewController3 } + manager.viewDidAppear(false) manager.select(viewController: viewController2) dataSource.viewControllerBefore = { _ in nil } @@ -671,6 +693,7 @@ final class PageViewManagerTests: XCTestCase { dataSource.viewControllerBefore = { _ in viewController0 } dataSource.viewControllerAfter = { _ in viewController2 } + manager.viewDidAppear(false) manager.select(viewController: viewController1) dataSource.viewControllerAfter = { _ in viewController3 } @@ -700,6 +723,7 @@ final class PageViewManagerTests: XCTestCase { dataSource.viewControllerBefore = { _ in viewController0 } dataSource.viewControllerAfter = { _ in viewController2 } + manager.viewDidAppear(false) manager.select(viewController: viewController1) dataSource.viewControllerAfter = { _ in viewController3 } @@ -729,6 +753,7 @@ final class PageViewManagerTests: XCTestCase { dataSource.viewControllerBefore = { _ in viewController1 } dataSource.viewControllerAfter = { _ in viewController3 } + manager.viewDidAppear(false) manager.select(viewController: viewController2) manager.willBeginDragging() @@ -738,8 +763,8 @@ final class PageViewManagerTests: XCTestCase { XCTAssertEqual(delegate.calls, [ .isScrolling(from: viewController2, to: viewController3, progress: 0.0), - .beginAppearanceTransition(true, viewController2), - .beginAppearanceTransition(false, viewController3), + .beginAppearanceTransition(true, viewController2, true), + .beginAppearanceTransition(false, viewController3, true), .endAppearanceTransition(viewController2), .endAppearanceTransition(viewController3), .didFinishScrolling(from: viewController2, to: viewController3, success: false) @@ -753,6 +778,7 @@ final class PageViewManagerTests: XCTestCase { dataSource.viewControllerBefore = { _ in viewController1 } dataSource.viewControllerAfter = { _ in viewController3 } + manager.viewDidAppear(false) manager.select(viewController: viewController2) manager.willBeginDragging() @@ -762,8 +788,8 @@ final class PageViewManagerTests: XCTestCase { XCTAssertEqual(delegate.calls, [ .isScrolling(from: viewController2, to: viewController1, progress: 0.0), - .beginAppearanceTransition(true, viewController2), - .beginAppearanceTransition(false, viewController1), + .beginAppearanceTransition(true, viewController2, true), + .beginAppearanceTransition(false, viewController1, true), .endAppearanceTransition(viewController2), .endAppearanceTransition(viewController1), .didFinishScrolling(from: viewController2, to: viewController1, success: false) @@ -777,6 +803,7 @@ final class PageViewManagerTests: XCTestCase { dataSource.viewControllerBefore = { _ in viewController1 } dataSource.viewControllerAfter = { _ in viewController3 } + manager.viewDidAppear(false) manager.select(viewController: viewController2) manager.willBeginDragging() @@ -791,8 +818,8 @@ final class PageViewManagerTests: XCTestCase { XCTAssertEqual(delegate.calls, [ .isScrolling(from: viewController2, to: viewController3, progress: 0.1), .willScroll(from: viewController2, to: viewController3), - .beginAppearanceTransition(true, viewController3), - .beginAppearanceTransition(false, viewController2) + .beginAppearanceTransition(true, viewController3, true), + .beginAppearanceTransition(false, viewController2, true) ]) } @@ -803,6 +830,7 @@ final class PageViewManagerTests: XCTestCase { dataSource.viewControllerBefore = { _ in viewController1 } dataSource.viewControllerAfter = { _ in viewController3 } + manager.viewDidAppear(false) manager.select(viewController: viewController2) manager.willBeginDragging() @@ -817,8 +845,8 @@ final class PageViewManagerTests: XCTestCase { XCTAssertEqual(delegate.calls, [ .isScrolling(from: viewController2, to: viewController1, progress: -0.1), .willScroll(from: viewController2, to: viewController1), - .beginAppearanceTransition(true, viewController1), - .beginAppearanceTransition(false, viewController2) + .beginAppearanceTransition(true, viewController1, true), + .beginAppearanceTransition(false, viewController2, true) ]) } @@ -829,6 +857,7 @@ final class PageViewManagerTests: XCTestCase { dataSource.viewControllerBefore = { _ in viewController1 } dataSource.viewControllerAfter = { _ in viewController3 } + manager.viewDidAppear(false) manager.select(viewController: viewController2) manager.willBeginDragging() @@ -840,15 +869,15 @@ final class PageViewManagerTests: XCTestCase { manager.didScroll(progress: -0.1) XCTAssertEqual(delegate.calls, [ - .beginAppearanceTransition(true, viewController2), - .beginAppearanceTransition(false, viewController3), + .beginAppearanceTransition(true, viewController2, true), + .beginAppearanceTransition(false, viewController3, true), .endAppearanceTransition(viewController2), .endAppearanceTransition(viewController3), .didFinishScrolling(from: viewController2, to: viewController3, success: false), .isScrolling(from: viewController2, to: viewController1, progress: -0.1), .willScroll(from: viewController2, to: viewController1), - .beginAppearanceTransition(true, viewController1), - .beginAppearanceTransition(false, viewController2), + .beginAppearanceTransition(true, viewController1, true), + .beginAppearanceTransition(false, viewController2, true), ]) } @@ -859,6 +888,7 @@ final class PageViewManagerTests: XCTestCase { dataSource.viewControllerBefore = { _ in viewController1 } dataSource.viewControllerAfter = { _ in viewController3 } + manager.viewDidAppear(false) manager.select(viewController: viewController2) manager.willBeginDragging() @@ -870,15 +900,15 @@ final class PageViewManagerTests: XCTestCase { manager.didScroll(progress: 0.1) XCTAssertEqual(delegate.calls, [ - .beginAppearanceTransition(true, viewController2), - .beginAppearanceTransition(false, viewController1), + .beginAppearanceTransition(true, viewController2, true), + .beginAppearanceTransition(false, viewController1, true), .endAppearanceTransition(viewController2), .endAppearanceTransition(viewController1), .didFinishScrolling(from: viewController2, to: viewController1, success: false), .isScrolling(from: viewController2, to: viewController3, progress: 0.1), .willScroll(from: viewController2, to: viewController3), - .beginAppearanceTransition(true, viewController3), - .beginAppearanceTransition(false, viewController2), + .beginAppearanceTransition(true, viewController3, true), + .beginAppearanceTransition(false, viewController2, true), ]) } @@ -890,6 +920,7 @@ final class PageViewManagerTests: XCTestCase { dataSource.viewControllerBefore = { _ in viewController1 } dataSource.viewControllerAfter = { _ in viewController3 } + manager.viewDidAppear(false) manager.select(viewController: viewController2) dataSource.viewControllerAfter = { _ in viewController4 } @@ -905,8 +936,8 @@ final class PageViewManagerTests: XCTestCase { XCTAssertEqual(delegate.calls, [ .isScrolling(from: viewController3, to: viewController4, progress: 0.1), .willScroll(from: viewController3, to: viewController4), - .beginAppearanceTransition(true, viewController4), - .beginAppearanceTransition(false, viewController3), + .beginAppearanceTransition(true, viewController4, true), + .beginAppearanceTransition(false, viewController3, true), ]) } @@ -919,14 +950,17 @@ final class PageViewManagerTests: XCTestCase { dataSource.viewControllerBefore = { _ in previousVc } dataSource.viewControllerAfter = { _ in nextVc } + manager.viewDidAppear(false) manager.select(viewController: selectedVc) delegate.calls = [] manager.removeAll() + // Expects that it removes all view controller and starts + // appearance transitions without animations. XCTAssertEqual(delegate.calls, [ - .beginAppearanceTransition(false, selectedVc), + .beginAppearanceTransition(false, selectedVc, false), .removeViewController(selectedVc), .removeViewController(previousVc), .removeViewController(nextVc), @@ -934,4 +968,120 @@ final class PageViewManagerTests: XCTestCase { .endAppearanceTransition(selectedVc) ]) } + + // MARK: - View Appearance + + func testViewAppeared() { + let viewController = UIViewController() + manager.select(viewController: viewController) + delegate.calls = [] + manager.viewWillAppear(false) + manager.viewDidAppear(false) + + XCTAssertEqual(delegate.calls, [ + .beginAppearanceTransition(true, viewController, false), + .layoutViews([viewController]), + .endAppearanceTransition(viewController) + ]) + } + + func testViewAppearedAnimated() { + let viewController = UIViewController() + manager.select(viewController: viewController) + delegate.calls = [] + manager.viewWillAppear(true) + manager.viewDidAppear(true) + + XCTAssertEqual(delegate.calls, [ + .beginAppearanceTransition(true, viewController, true), + .layoutViews([viewController]), + .endAppearanceTransition(viewController) + ]) + } + + func testViewDisappeared() { + let viewController = UIViewController() + manager.select(viewController: viewController) + delegate.calls = [] + manager.viewWillDisappear(false) + manager.viewDidDisappear(false) + + XCTAssertEqual(delegate.calls, [ + .beginAppearanceTransition(false, viewController, false), + .endAppearanceTransition(viewController) + ]) + } + + func testViewDidDisappearAnimated() { + let viewController = UIViewController() + manager.select(viewController: viewController) + delegate.calls = [] + manager.viewWillDisappear(true) + manager.viewDidDisappear(true) + + XCTAssertEqual(delegate.calls, [ + .beginAppearanceTransition(false, viewController, true), + .endAppearanceTransition(viewController) + ]) + } + + func testSelectBeforeViewAppeared() { + let viewController = UIViewController() + manager.select(viewController: viewController) + + // Expect that the appearance transitions methods are not called + // for the selected view controller. + XCTAssertEqual(delegate.calls, [ + .addViewController(viewController), + .layoutViews([viewController]) + ]) + } + + func testSelectWhenAppearing() { + let viewController = UIViewController() + manager.viewWillAppear(true) + manager.select(viewController: viewController, animated: false) + manager.viewDidAppear(true) + + // Expect that it begins appearance transitions with the same + // animated flag as viewWillAppear. + XCTAssertEqual(delegate.calls, [ + .beginAppearanceTransition(true, viewController, true), + .addViewController(viewController), + .layoutViews([viewController]), + .endAppearanceTransition(viewController) + ]) + } + + func testSelectWhenDisappearing() { + let viewController = UIViewController() + manager.viewWillAppear(true) + manager.viewDidAppear(true) + manager.viewWillDisappear(true) + manager.select(viewController: viewController, animated: false) + manager.viewDidDisappear(true) + + // Expect that it begins appearance transitions with the same + // animated flag as viewWillDisappear. + XCTAssertEqual(delegate.calls, [ + .beginAppearanceTransition(false, viewController, true), + .addViewController(viewController), + .layoutViews([viewController]), + .endAppearanceTransition(viewController) + ]) + } + + func testSelectWhenDisappeared() { + let viewController = UIViewController() + manager.viewWillAppear(true) + manager.viewDidAppear(true) + manager.viewWillDisappear(true) + manager.viewDidDisappear(true) + manager.select(viewController: viewController, animated: false) + + XCTAssertEqual(delegate.calls, [ + .addViewController(viewController), + .layoutViews([viewController]) + ]) + } } From 380d0030d4a2ea3fe793d76f9efd43b0a8d15ccf Mon Sep 17 00:00:00 2001 From: Martin Rechsteiner Date: Fri, 3 Apr 2020 20:46:54 +0200 Subject: [PATCH 05/12] Add support for right-to-left languages --- Parchment/Classes/PageViewController.swift | 104 +++++++++++++----- .../Classes/PagingCollectionViewLayout.swift | 4 + 2 files changed, 82 insertions(+), 26 deletions(-) diff --git a/Parchment/Classes/PageViewController.swift b/Parchment/Classes/PageViewController.swift index 7f4069a8..20fa0037 100644 --- a/Parchment/Classes/PageViewController.swift +++ b/Parchment/Classes/PageViewController.swift @@ -27,6 +27,17 @@ public final class PageViewController: UIViewController { private let manager = PageViewManager() private let options: PagingOptions + + private var isRightToLeft: Bool { + if #available(iOS 9.0, *), + UIView.userInterfaceLayoutDirection(for: view.semanticContentAttribute) == .rightToLeft { + return true + } else if UIApplication.shared.userInterfaceLayoutDirection == .rightToLeft { + return true + } else { + return false + } + } init(options: PagingOptions = PagingOptions()) { self.options = options @@ -110,12 +121,21 @@ extension PageViewController: UIScrollViewDelegate { public func scrollViewDidScroll(_ scrollView: UIScrollView) { let distance = view.frame.size.width var progress: CGFloat - - switch manager.state { - case .first, .empty, .single: - progress = scrollView.contentOffset.x / distance - case .center, .last: - progress = (scrollView.contentOffset.x - distance) / distance + + if isRightToLeft { + switch manager.state { + case .last, .empty, .single: + progress = -(scrollView.contentOffset.x / distance) + case .center, .first: + progress = -((scrollView.contentOffset.x - distance) / distance) + } + } else { + switch manager.state { + case .first, .empty, .single: + progress = scrollView.contentOffset.x / distance + case .center, .last: + progress = (scrollView.contentOffset.x - distance) / distance + } } manager.didScroll(progress: progress) @@ -134,29 +154,52 @@ extension PageViewController: PageViewManagerDataSource { extension PageViewController: PageViewManagerDelegate { func scrollForward() { - switch manager.state { - case .first: - let contentOffset = CGPoint(x: view.bounds.width, y: 0) - scrollView.setContentOffset(contentOffset, animated: true) - case .center: - let contentOffset = CGPoint(x: view.bounds.width * 2, y: 0) - scrollView.setContentOffset(contentOffset, animated: true) - case .single, .empty, .last: - break + if isRightToLeft { + switch manager.state { + case .first, .center: + scrollView.setContentOffset(.zero, animated: true) + case .single, .empty, .last: + break + } + } else { + switch manager.state { + case .first: + let contentOffset = CGPoint(x: view.bounds.width, y: 0) + scrollView.setContentOffset(contentOffset, animated: true) + case .center: + let contentOffset = CGPoint(x: view.bounds.width * 2, y: 0) + scrollView.setContentOffset(contentOffset, animated: true) + case .single, .empty, .last: + break + } } } func scrollReverse() { - switch manager.state { - case .last, .center: - manager.willBeginDragging() - scrollView.setContentOffset(.zero, animated: true) - case .single, .empty, .first: - break + if isRightToLeft { + switch manager.state { + case .last: + let contentOffset = CGPoint(x: view.bounds.width, y: 0) + scrollView.setContentOffset(contentOffset, animated: true) + case .center: + let contentOffset = CGPoint(x: view.bounds.width * 2, y: 0) + scrollView.setContentOffset(contentOffset, animated: true) + case .single, .empty, .first: + break + } + } else { + switch manager.state { + case .last, .center: + scrollView.setContentOffset(.zero, animated: true) + case .single, .empty, .first: + break + } } } func layoutViews(for viewControllers: [UIViewController], keepContentOffset: Bool) { + let viewControllers = isRightToLeft ? viewControllers.reversed() : viewControllers + for (index, viewController) in viewControllers.enumerated() { viewController.view.frame = CGRect( x: CGFloat(index) * scrollView.bounds.width, @@ -186,11 +229,20 @@ extension PageViewController: PageViewManagerDelegate { width: CGFloat(manager.state.count) * view.bounds.width, height: view.bounds.height) - switch manager.state { - case .first, .single, .empty: - scrollView.contentOffset = CGPoint(x: diff, y: 0) - case .last, .center: - scrollView.contentOffset = CGPoint(x: view.bounds.width + diff, y: 0) + if isRightToLeft { + switch manager.state { + case .first, .center: + scrollView.contentOffset = CGPoint(x: view.bounds.width + diff, y: 0) + case .single, .empty, .last: + scrollView.contentOffset = CGPoint(x: diff, y: 0) + } + } else { + switch manager.state { + case .first, .single, .empty: + scrollView.contentOffset = CGPoint(x: diff, y: 0) + case .last, .center: + scrollView.contentOffset = CGPoint(x: view.bounds.width + diff, y: 0) + } } } diff --git a/Parchment/Classes/PagingCollectionViewLayout.swift b/Parchment/Classes/PagingCollectionViewLayout.swift index 267620ac..792eb846 100644 --- a/Parchment/Classes/PagingCollectionViewLayout.swift +++ b/Parchment/Classes/PagingCollectionViewLayout.swift @@ -62,6 +62,10 @@ open class PagingCollectionViewLayout: UICollectionViewLayout, PagingLayout { return PagingCellLayoutAttributes.self } + open override var flipsHorizontallyInOppositeLayoutDirection: Bool { + return true + } + // MARK: Initializers public override required init() { From 7093a1e4f0e2f2e9c6bb7971c8c2220e9bcf421a Mon Sep 17 00:00:00 2001 From: Martin Rechsteiner Date: Fri, 3 Apr 2020 22:15:47 +0200 Subject: [PATCH 06/12] Add support for vertical scrolling in PageViewController --- Parchment.xcodeproj/project.pbxproj | 4 + Parchment/Classes/PageViewController.swift | 169 +++++++++++++----- Parchment/Classes/PagingOptions.swift | 5 + Parchment/Classes/PagingViewController.swift | 8 + .../Structs/PagingNavigationOrientation.swift | 6 + 5 files changed, 149 insertions(+), 43 deletions(-) create mode 100644 Parchment/Structs/PagingNavigationOrientation.swift diff --git a/Parchment.xcodeproj/project.pbxproj b/Parchment.xcodeproj/project.pbxproj index 84ca850d..542c6f51 100644 --- a/Parchment.xcodeproj/project.pbxproj +++ b/Parchment.xcodeproj/project.pbxproj @@ -40,6 +40,7 @@ 3EA04A671C53BFF40054E5E0 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 3EA04A651C53BFF40054E5E0 /* LaunchScreen.storyboard */; }; 3EADD99F1CC65C4D003171CF /* EMPageViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3EADD99E1CC65C4D003171CF /* EMPageViewController.swift */; }; 3EFEFBF71C80B8820023C949 /* PagingIndicatorLayoutAttributesTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3EFEFBF61C80B8820023C949 /* PagingIndicatorLayoutAttributesTests.swift */; }; + 950ABE422437BD4D00CAD458 /* PagingNavigationOrientation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 950ABE412437BD4D00CAD458 /* PagingNavigationOrientation.swift */; }; 951E163720A21D3A0055E9D4 /* PagingViewControllerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 954842601F4251F90072038C /* PagingViewControllerTests.swift */; }; 952D802F1E37CC09003DCB18 /* PagingTransition.swift in Sources */ = {isa = PBXBuildFile; fileRef = 952D802E1E37CC09003DCB18 /* PagingTransition.swift */; }; 953B8D352416C3DC0047BBA1 /* SelfSizingViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 953B8D342416C3DC0047BBA1 /* SelfSizingViewController.swift */; }; @@ -235,6 +236,7 @@ 3EADD99E1CC65C4D003171CF /* EMPageViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EMPageViewController.swift; sourceTree = ""; }; 3EC0184C1C95F993005421AD /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; name = Info.plist; path = Resources/Info.plist; sourceTree = ""; }; 3EFEFBF61C80B8820023C949 /* PagingIndicatorLayoutAttributesTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PagingIndicatorLayoutAttributesTests.swift; sourceTree = ""; }; + 950ABE412437BD4D00CAD458 /* PagingNavigationOrientation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PagingNavigationOrientation.swift; sourceTree = ""; }; 952D802E1E37CC09003DCB18 /* PagingTransition.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PagingTransition.swift; sourceTree = ""; }; 953B8D342416C3DC0047BBA1 /* SelfSizingViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelfSizingViewController.swift; sourceTree = ""; }; 954842581F42438E0072038C /* PagingInvalidationContext.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PagingInvalidationContext.swift; sourceTree = ""; }; @@ -392,6 +394,7 @@ 3E4090A11C88BD0A00800E22 /* PagingIndicatorMetric.swift */, 3E4283FB1C99CF9000032D95 /* PagingItems.swift */, 952D802E1E37CC09003DCB18 /* PagingTransition.swift */, + 950ABE412437BD4D00CAD458 /* PagingNavigationOrientation.swift */, ); path = Structs; sourceTree = ""; @@ -964,6 +967,7 @@ 9597F2951E3903F4003FD289 /* UIColor+interpolation.swift in Sources */, 95A0AF001FF707910043B90A /* PagingIndexItem.swift in Sources */, 3E49C72A1C8F5C13006269DD /* PagingViewController.swift in Sources */, + 950ABE422437BD4D00CAD458 /* PagingNavigationOrientation.swift in Sources */, 95A84B0D20ED46920031520F /* AnyPagingItem.swift in Sources */, 95D790102299CE6100E6EE7C /* PagingViewControllerSizeDelegate.swift in Sources */, 95E4BA701FF15E84008871A3 /* PagingViewControllerDataSource.swift in Sources */, diff --git a/Parchment/Classes/PageViewController.swift b/Parchment/Classes/PageViewController.swift index 20fa0037..886e59ca 100644 --- a/Parchment/Classes/PageViewController.swift +++ b/Parchment/Classes/PageViewController.swift @@ -1,6 +1,9 @@ import UIKit public final class PageViewController: UIViewController { + + // MARK: Public Properties + public weak var dataSource: PageViewControllerDataSource? public weak var delegate: PageViewControllerDelegate? @@ -17,25 +20,81 @@ public final class PageViewController: UIViewController { scrollView.isPagingEnabled = true scrollView.scrollsToTop = false scrollView.bounces = true - scrollView.alwaysBounceHorizontal = true - scrollView.alwaysBounceVertical = false - scrollView.translatesAutoresizingMaskIntoConstraints = true scrollView.showsHorizontalScrollIndicator = false scrollView.showsVerticalScrollIndicator = false return scrollView }() + public var options: PagingOptions { + didSet { + switch options.contentNavigationOrientation { + case .vertical: + scrollView.alwaysBounceHorizontal = false + scrollView.alwaysBounceVertical = true + case .horizontal: + scrollView.alwaysBounceHorizontal = true + scrollView.alwaysBounceVertical = false + } + } + } + + // MARK: Private Properties + private let manager = PageViewManager() - private let options: PagingOptions + + /// The size of a single page. + private var pageSize: CGFloat { + switch options.contentNavigationOrientation { + case .vertical: + return view.bounds.height + case .horizontal: + return view.bounds.width + } + } + + /// The size of all the pages in the scroll view. + private var contentSize: CGSize { + switch options.contentNavigationOrientation { + case .horizontal: + return CGSize( + width: CGFloat(manager.state.count) * view.bounds.width, + height: view.bounds.height) + case .vertical: + return CGSize( + width: view.bounds.width, + height: CGFloat(manager.state.count) * view.bounds.height) + } + } + + /// The content offset of the scroll view, adjusted for the current + /// navigation orientation. + private var contentOffset: CGFloat { + get { + switch options.contentNavigationOrientation { + case .horizontal: + return scrollView.contentOffset.x + case .vertical: + return scrollView.contentOffset.y + } + } + set { + scrollView.contentOffset = point(newValue) + } + } private var isRightToLeft: Bool { - if #available(iOS 9.0, *), - UIView.userInterfaceLayoutDirection(for: view.semanticContentAttribute) == .rightToLeft { - return true - } else if UIApplication.shared.userInterfaceLayoutDirection == .rightToLeft { - return true - } else { + switch options.contentNavigationOrientation { + case .vertical: return false + case .horizontal: + if #available(iOS 9.0, *), + UIView.userInterfaceLayoutDirection(for: view.semanticContentAttribute) == .rightToLeft { + return true + } else if UIApplication.shared.userInterfaceLayoutDirection == .rightToLeft { + return true + } else { + return false + } } } @@ -90,7 +149,7 @@ public final class PageViewController: UIViewController { }) } - // MARK: - Public Methods + // MARK: Public Methods public func selectViewController(_ viewController: UIViewController, direction: PageViewDirection, animated: Bool = true) { manager.select(viewController: viewController, direction: direction, animated: animated) @@ -107,8 +166,25 @@ public final class PageViewController: UIViewController { public func removeAll() { manager.removeAll() } + + // MARK: Private Methods + + private func setContentOffset(_ value: CGFloat, animated: Bool) { + scrollView.setContentOffset(point(value), animated: animated) + } + + private func point(_ value: CGFloat) -> CGPoint { + switch options.contentNavigationOrientation { + case .horizontal: + return CGPoint(x: value, y: 0) + case .vertical: + return CGPoint(x: 0, y: value) + } + } } +// MARK: - UIScrollViewDelegate + extension PageViewController: UIScrollViewDelegate { public func scrollViewWillBeginDragging(_ scrollView: UIScrollView) { manager.willBeginDragging() @@ -119,22 +195,22 @@ extension PageViewController: UIScrollViewDelegate { } public func scrollViewDidScroll(_ scrollView: UIScrollView) { - let distance = view.frame.size.width + let distance = pageSize var progress: CGFloat if isRightToLeft { switch manager.state { case .last, .empty, .single: - progress = -(scrollView.contentOffset.x / distance) + progress = -(contentOffset / distance) case .center, .first: - progress = -((scrollView.contentOffset.x - distance) / distance) + progress = -((contentOffset - distance) / distance) } } else { switch manager.state { case .first, .empty, .single: - progress = scrollView.contentOffset.x / distance + progress = contentOffset / distance case .center, .last: - progress = (scrollView.contentOffset.x - distance) / distance + progress = (contentOffset - distance) / distance } } @@ -142,6 +218,8 @@ extension PageViewController: UIScrollViewDelegate { } } +// MARK: - PageViewManagerDataSource + extension PageViewController: PageViewManagerDataSource { func viewControllerAfter(_ viewController: UIViewController) -> UIViewController? { return dataSource?.pageViewController(self, viewControllerAfterViewController: viewController) @@ -152,23 +230,23 @@ extension PageViewController: PageViewManagerDataSource { } } +// MARK: - PageViewManagerDelegate + extension PageViewController: PageViewManagerDelegate { func scrollForward() { if isRightToLeft { switch manager.state { case .first, .center: - scrollView.setContentOffset(.zero, animated: true) + setContentOffset(.zero, animated: true) case .single, .empty, .last: break } } else { switch manager.state { case .first: - let contentOffset = CGPoint(x: view.bounds.width, y: 0) - scrollView.setContentOffset(contentOffset, animated: true) + setContentOffset(pageSize, animated: true) case .center: - let contentOffset = CGPoint(x: view.bounds.width * 2, y: 0) - scrollView.setContentOffset(contentOffset, animated: true) + setContentOffset(pageSize * 2, animated: true) case .single, .empty, .last: break } @@ -179,11 +257,9 @@ extension PageViewController: PageViewManagerDelegate { if isRightToLeft { switch manager.state { case .last: - let contentOffset = CGPoint(x: view.bounds.width, y: 0) - scrollView.setContentOffset(contentOffset, animated: true) + setContentOffset(pageSize, animated: true) case .center: - let contentOffset = CGPoint(x: view.bounds.width * 2, y: 0) - scrollView.setContentOffset(contentOffset, animated: true) + setContentOffset(pageSize * 2, animated: true) case .single, .empty, .first: break } @@ -201,11 +277,20 @@ extension PageViewController: PageViewManagerDelegate { let viewControllers = isRightToLeft ? viewControllers.reversed() : viewControllers for (index, viewController) in viewControllers.enumerated() { - viewController.view.frame = CGRect( - x: CGFloat(index) * scrollView.bounds.width, - y: 0, - width: scrollView.bounds.width, - height: scrollView.bounds.height) + switch options.contentNavigationOrientation { + case .horizontal: + viewController.view.frame = CGRect( + x: CGFloat(index) * scrollView.bounds.width, + y: 0, + width: scrollView.bounds.width, + height: scrollView.bounds.height) + case .vertical: + viewController.view.frame = CGRect( + x: 0, + y: CGFloat(index) * scrollView.bounds.height, + width: scrollView.bounds.width, + height: scrollView.bounds.height) + } } // When updating the content offset we need to account for the @@ -214,34 +299,32 @@ extension PageViewController: PageViewManagerDelegate { // bounce effect in the scroll view. var diff: CGFloat = 0 if keepContentOffset { - if scrollView.contentOffset.x > view.bounds.width * 2 { - diff = scrollView.contentOffset.x - view.bounds.width * 2 - } else if scrollView.contentOffset.x > view.bounds.width && scrollView.contentOffset.x < view.bounds.width * 2 { - diff = scrollView.contentOffset.x - view.bounds.width - } else if scrollView.contentOffset.x < view.bounds.width && scrollView.contentOffset.x < 0 { - diff = scrollView.contentOffset.x + if contentOffset > pageSize * 2 { + diff = contentOffset - pageSize * 2 + } else if contentOffset > pageSize && contentOffset < pageSize * 2 { + diff = contentOffset - pageSize + } else if contentOffset < pageSize && contentOffset < 0 { + diff = contentOffset } } // Need to set content size before updating content offset. If not // the views will be misplaced when overshooting. - scrollView.contentSize = CGSize( - width: CGFloat(manager.state.count) * view.bounds.width, - height: view.bounds.height) + scrollView.contentSize = contentSize if isRightToLeft { switch manager.state { case .first, .center: - scrollView.contentOffset = CGPoint(x: view.bounds.width + diff, y: 0) + contentOffset = pageSize + diff case .single, .empty, .last: - scrollView.contentOffset = CGPoint(x: diff, y: 0) + contentOffset = diff } } else { switch manager.state { case .first, .single, .empty: - scrollView.contentOffset = CGPoint(x: diff, y: 0) + contentOffset = diff case .last, .center: - scrollView.contentOffset = CGPoint(x: view.bounds.width + diff, y: 0) + contentOffset = pageSize + diff } } } diff --git a/Parchment/Classes/PagingOptions.swift b/Parchment/Classes/PagingOptions.swift index 669fd04d..9a92efe9 100644 --- a/Parchment/Classes/PagingOptions.swift +++ b/Parchment/Classes/PagingOptions.swift @@ -92,6 +92,10 @@ public struct PagingOptions { /// The background color for the view behind the menu items. public var menuBackgroundColor: UIColor + /// The scroll navigation orientation of the content in the page + /// view controller. _Default: .horizontal_ + public var contentNavigationOrientation: PagingNavigationOrientation + #if swift(>=4.2) public var scrollPosition: UICollectionView.ScrollPosition { switch selectedScrollPosition { @@ -171,5 +175,6 @@ public struct PagingOptions { menuBackgroundColor = UIColor.white borderColor = UIColor(white: 0.9, alpha: 1) indicatorColor = UIColor(red: 3/255, green: 125/255, blue: 233/255, alpha: 1) + contentNavigationOrientation = .horizontal } } diff --git a/Parchment/Classes/PagingViewController.swift b/Parchment/Classes/PagingViewController.swift index 2e0104b7..c5d27f13 100644 --- a/Parchment/Classes/PagingViewController.swift +++ b/Parchment/Classes/PagingViewController.swift @@ -178,6 +178,13 @@ open class PagingViewController: set { options.menuBackgroundColor = newValue } } + /// The scroll navigation orientation of the content in the page + /// view controller. _Default: .horizontal_ + public var contentNavigationOrientation: PagingNavigationOrientation { + get { return options.contentNavigationOrientation } + set { options.contentNavigationOrientation = newValue } + } + /// Determine how users can interact with the page view controller. /// _Default: .scrolling_ public var contentInteraction: PagingContentInteraction = .scrolling { @@ -264,6 +271,7 @@ open class PagingViewController: collectionViewLayout.options = options } + pageViewController.options = options pagingController.options = options pagingView.options = options } diff --git a/Parchment/Structs/PagingNavigationOrientation.swift b/Parchment/Structs/PagingNavigationOrientation.swift new file mode 100644 index 00000000..e8c9c1be --- /dev/null +++ b/Parchment/Structs/PagingNavigationOrientation.swift @@ -0,0 +1,6 @@ +import Foundation + +public enum PagingNavigationOrientation { + case vertical + case horizontal +} From b29f061a63609559f4c7f193ea1537cc1bc55552 Mon Sep 17 00:00:00 2001 From: Martin Rechsteiner Date: Fri, 3 Apr 2020 23:29:49 +0200 Subject: [PATCH 07/12] Remove EMPageViewController --- Parchment.xcodeproj/project.pbxproj | 4 - Parchment/Classes/EMPageViewController.swift | 673 ------------------- README.md | 8 +- 3 files changed, 2 insertions(+), 683 deletions(-) delete mode 100644 Parchment/Classes/EMPageViewController.swift diff --git a/Parchment.xcodeproj/project.pbxproj b/Parchment.xcodeproj/project.pbxproj index 542c6f51..aaca6d43 100644 --- a/Parchment.xcodeproj/project.pbxproj +++ b/Parchment.xcodeproj/project.pbxproj @@ -38,7 +38,6 @@ 3EA04A621C53BFF40054E5E0 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 3EA04A601C53BFF40054E5E0 /* Main.storyboard */; }; 3EA04A641C53BFF40054E5E0 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 3EA04A631C53BFF40054E5E0 /* Assets.xcassets */; }; 3EA04A671C53BFF40054E5E0 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 3EA04A651C53BFF40054E5E0 /* LaunchScreen.storyboard */; }; - 3EADD99F1CC65C4D003171CF /* EMPageViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3EADD99E1CC65C4D003171CF /* EMPageViewController.swift */; }; 3EFEFBF71C80B8820023C949 /* PagingIndicatorLayoutAttributesTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3EFEFBF61C80B8820023C949 /* PagingIndicatorLayoutAttributesTests.swift */; }; 950ABE422437BD4D00CAD458 /* PagingNavigationOrientation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 950ABE412437BD4D00CAD458 /* PagingNavigationOrientation.swift */; }; 951E163720A21D3A0055E9D4 /* PagingViewControllerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 954842601F4251F90072038C /* PagingViewControllerTests.swift */; }; @@ -233,7 +232,6 @@ 3EA04A631C53BFF40054E5E0 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 3EA04A661C53BFF40054E5E0 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 3EA04A681C53BFF40054E5E0 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; - 3EADD99E1CC65C4D003171CF /* EMPageViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EMPageViewController.swift; sourceTree = ""; }; 3EC0184C1C95F993005421AD /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; name = Info.plist; path = Resources/Info.plist; sourceTree = ""; }; 3EFEFBF61C80B8820023C949 /* PagingIndicatorLayoutAttributesTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PagingIndicatorLayoutAttributesTests.swift; sourceTree = ""; }; 950ABE412437BD4D00CAD458 /* PagingNavigationOrientation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PagingNavigationOrientation.swift; sourceTree = ""; }; @@ -402,7 +400,6 @@ 3E4090A31C88BD1700800E22 /* Classes */ = { isa = PBXGroup; children = ( - 3EADD99E1CC65C4D003171CF /* EMPageViewController.swift */, 3E4090A91C88BDD100800E22 /* PagingBorderLayoutAttributes.swift */, 3E49C71F1C8F5C13006269DD /* PagingBorderView.swift */, 3E49C7201C8F5C13006269DD /* PagingCell.swift */, @@ -960,7 +957,6 @@ 955444C21FC9CD19001EC26B /* PagingMenuTransition.swift in Sources */, 95FEEA4F2423F213009B5B64 /* PageViewManager.swift in Sources */, 3E2AAD2E1CA86A320044AAA5 /* PagingViewControllerDelegate.swift in Sources */, - 3EADD99F1CC65C4D003171CF /* EMPageViewController.swift in Sources */, 9548425D1F42486B0072038C /* PagingDiff.swift in Sources */, 3E40909D1C88BCC900800E22 /* PagingState.swift in Sources */, 954842591F42438E0072038C /* PagingInvalidationContext.swift in Sources */, diff --git a/Parchment/Classes/EMPageViewController.swift b/Parchment/Classes/EMPageViewController.swift deleted file mode 100644 index 82b7dd83..00000000 --- a/Parchment/Classes/EMPageViewController.swift +++ /dev/null @@ -1,673 +0,0 @@ -/* - - EMPageViewController.swift - - Copyright (c) 2015-2016 Erik Malyak - - Permission is hereby granted, free of charge, to any person obtaining a copy - of this software and associated documentation files (the "Software"), to deal - in the Software without restriction, including without limitation the rights - to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - copies of the Software, and to permit persons to whom the Software is - furnished to do so, subject to the following conditions: - - The above copyright notice and this permission notice shall be included in all - copies or substantial portions of the Software. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - SOFTWARE. - - */ - -import UIKit - -/** - The `EMPageViewControllerDataSource` protocol is adopted to provide the view controllers that are displayed when the user scrolls through pages. Methods are called on an as-needed basis. - - Each method returns a `UIViewController` object or `nil` if there are no view controllers to be displayed. - - - note: If the data source is `nil`, gesture based scrolling will be disabled and all view controllers must be provided through `selectViewController:direction:animated:completion:`. - */ -@objc public protocol EMPageViewControllerDataSource { - - /** - Called to optionally return a view controller that is to the left of a given view controller in a horizontal orientation, or above a given view controller in a vertical orientation. - - - parameter pageViewController: The page view controller - - parameter viewController: The point of reference view controller - - - returns: The view controller that is to the left of the given `viewController` in a horizontal orientation, or above the given `viewController` in a vertical orientation, or `nil` if there is no view controller to be displayed. - */ - func em_pageViewController(_ pageViewController: EMPageViewController, viewControllerBeforeViewController viewController: UIViewController) -> UIViewController? - - /** - Called to optionally return a view controller that is to the right of a given view controller. - - - parameter pageViewController: The page view controller - - parameter viewController: The point of reference view controller - - - returns: The view controller that is to the right of the given `viewController` in a horizontal orientation, or below the given `viewController` in a vertical orientation, or `nil` if there is no view controller to be displayed. - */ - func em_pageViewController(_ pageViewController: EMPageViewController, viewControllerAfterViewController viewController: UIViewController) -> UIViewController? -} - -/** - The EMPageViewControllerDelegate protocol is adopted to receive messages for all important events of the page transition process. - */ -@objc public protocol EMPageViewControllerDelegate { - - /** - Called before scrolling to a new view controller. - - - note: This method will not be called if the starting view controller is `nil`. A common scenario where this will occur is when you initialize the page view controller and use `selectViewController:direction:animated:completion:` to load the first selected view controller. - - - important: If bouncing is enabled, it is possible this method will be called more than once for one page transition. It can be called before the initial scroll to the destination view controller (which is when it is usually called), and it can also be called when the scroll momentum carries over slightly to the view controller after the original destination view controller. - - - parameter pageViewController: The page view controller - - parameter startingViewController: The currently selected view controller the transition is starting from - - parameter destinationViewController: The view controller that will be scrolled to, where the transition should end - */ - @objc optional func em_pageViewController(_ pageViewController: EMPageViewController, willStartScrollingFrom startingViewController: UIViewController, destinationViewController:UIViewController) - - /** - Called whenever there has been a scroll position change in a page transition. This method is very useful if you need to know the exact progress of the page transition animation. - - - note: This method will not be called if the starting view controller is `nil`. A common scenario where this will occur is when you initialize the page view controller and use `selectViewController:direction:animated:completion:` to load the first selected view controller. - - - parameter pageViewController: The page view controller - - parameter startingViewController: The currently selected view controller the transition is starting from - - parameter destinationViewController: The view controller being scrolled to where the transition should end - - parameter progress: The progress of the transition, where 0 is a neutral scroll position, >= 1 is a complete transition to the right view controller in a horizontal orientation, or the below view controller in a vertical orientation, and <= -1 is a complete transition to the left view controller in a horizontal orientation, or the above view controller in a vertical orientation. Values may be greater than 1 or less than -1 if bouncing is enabled and the scroll velocity is quick enough. - */ - @objc optional func em_pageViewController(_ pageViewController: EMPageViewController, isScrollingFrom startingViewController: UIViewController, destinationViewController: UIViewController?, progress: CGFloat) - - /** - Called after a page transition attempt has completed. - - - important: If bouncing is enabled, it is possible this method will be called more than once for one page transition. It can be called after the scroll transition to the intended destination view controller (which is when it is usually called), and it can also be called when the scroll momentum carries over slightly to the view controller after the intended destination view controller. In the latter scenario, `transitionSuccessful` will return `false` the second time it's called because the scroll view will bounce back to the intended destination view controller. - - - parameter pageViewController: The page view controller - - parameter startingViewController: The currently selected view controller the transition is starting from - - parameter destinationViewController: The view controller that has been attempted to be selected - - parameter transitionSuccessful: A Boolean whether the transition to the destination view controller was successful or not. If `true`, the new selected view controller is `destinationViewController`. If `false`, the transition returned to the view controller it started from, so the selected view controller is still `startingViewController`. - */ - @objc optional func em_pageViewController(_ pageViewController: EMPageViewController, didFinishScrollingFrom startingViewController: UIViewController?, destinationViewController:UIViewController, transitionSuccessful: Bool) -} - -/** - The navigation scroll direction. - */ -@objc public enum EMPageViewControllerNavigationDirection : Int { - /// Forward direction. Can be right in a horizontal orientation or down in a vertical orientation. - case forward - /// Reverse direction. Can be left in a horizontal orientation or up in a vertical orientation. - case reverse -} - -/** - The navigation scroll orientation. - */ -@objc public enum EMPageViewControllerNavigationOrientation: Int { - /// Horiziontal orientation. Scrolls left and right. - case horizontal - /// Vertical orientation. Scrolls up and down. - case vertical -} - -/// Manages page navigation between view controllers. View controllers can be navigated via swiping gestures, or called programmatically. -open class EMPageViewController: UIViewController, UIScrollViewDelegate { - - /// The object that provides view controllers on an as-needed basis throughout the navigation of the page view controller. - /// - /// If the data source is `nil`, gesture based scrolling will be disabled and all view controllers must be provided through `selectViewController:direction:animated:completion:`. - /// - /// - important: If you are using a data source, make sure you set `dataSource` before calling `selectViewController:direction:animated:completion:`. - open weak var dataSource: EMPageViewControllerDataSource? - - /// The object that receives messages throughout the navigation process of the page view controller. - open weak var delegate: EMPageViewControllerDelegate? - - /// The direction scrolling navigation occurs - open private(set) var navigationOrientation: EMPageViewControllerNavigationOrientation = .horizontal - - private var isOrientationHorizontal: Bool { - return self.navigationOrientation == .horizontal - } - - /// The underlying `UIScrollView` responsible for scrolling page views. - /// - important: Properties should be set with caution to prevent unexpected behavior. - open private(set) lazy var scrollView: UIScrollView = { - let scrollView = UIScrollView() - scrollView.isPagingEnabled = true - scrollView.scrollsToTop = false - scrollView.autoresizingMask = [.flexibleTopMargin, .flexibleRightMargin, .flexibleBottomMargin, .flexibleLeftMargin] - scrollView.bounces = true - scrollView.alwaysBounceHorizontal = self.isOrientationHorizontal - scrollView.alwaysBounceVertical = !self.isOrientationHorizontal - scrollView.translatesAutoresizingMaskIntoConstraints = true - scrollView.showsHorizontalScrollIndicator = false - scrollView.showsVerticalScrollIndicator = false - return scrollView - }() - - /// The view controller before the selected view controller. - var beforeViewController: UIViewController? - - /// The currently selected view controller. Can be `nil` if no view controller is selected. - open private(set) var selectedViewController: UIViewController? - - /// The view controller after the selected view controller. - var afterViewController: UIViewController? - - /// Boolean that indicates whether the page controller is currently in the process of scrolling. - open private(set) var scrolling = false - - /// The direction the page controller is scrolling towards. - open private(set) var navigationDirection: EMPageViewControllerNavigationDirection? - - private var adjustingContentOffset = false // Flag used to prevent isScrolling delegate when shifting scrollView - private var loadNewAdjoiningViewControllersOnFinish = false - private var didFinishScrollingCompletionHandler: ((_ transitionSuccessful: Bool) -> Void)? - private var transitionAnimated = false // Used for accurate view appearance messages - - // MARK: - Public Methods - - /// Initializes a newly created page view controller with the specified navigation orientation. - /// - parameter navigationOrientation: The page view controller's navigation scroll direction. - /// - returns: The initialized page view controller. - public convenience init(navigationOrientation: EMPageViewControllerNavigationOrientation) { - self.init() - self.navigationOrientation = navigationOrientation - } - - /** - Sets the view controller that will be selected after the animation. This method is also used to provide the first view controller that will be selected in the page view controller. - - If a data source has been set, the view controllers before and after the selected view controller will also be loaded but not appear yet. - - - important: If you are using a data source, make sure you set `dataSource` before calling `selectViewController:direction:animated:completion:` - - - parameter viewController: The view controller to be selected. - - parameter direction: The direction of the navigation and animation, if applicable. - - parameter completion: A block that's called after the transition is finished. The block parameter `transitionSuccessful` is `true` if the transition to the selected view controller was completed successfully. - */ - open func selectViewController(_ viewController: UIViewController, direction: EMPageViewControllerNavigationDirection, animated: Bool, completion: ((_ transitionSuccessful: Bool) -> Void)?) { - - if viewController == self.selectedViewController { - return - } - - if (direction == .forward) { - self.afterViewController = viewController - self.layoutViews() - self.loadNewAdjoiningViewControllersOnFinish = true - self.scrollForward(animated: animated, completion: completion) - } else if (direction == .reverse) { - self.beforeViewController = viewController - self.layoutViews() - self.loadNewAdjoiningViewControllersOnFinish = true - self.scrollReverse(animated: animated, completion: completion) - } - - } - - open func removeAllViewControllers() { - self.removeChildIfNeeded(beforeViewController) - self.removeChildIfNeeded(selectedViewController) - self.removeChildIfNeeded(afterViewController) - - beforeViewController = nil - selectedViewController = nil - afterViewController = nil - } - - /** - Transitions to the view controller right of the currently selected view controller in a horizontal orientation, or below the currently selected view controller in a vertical orientation. Also described as going to the next page. - - - parameter animated: A Boolean whether or not to animate the transition - - parameter completion: A block that's called after the transition is finished. The block parameter `transitionSuccessful` is `true` if the transition to the selected view controller was completed successfully. If `false`, the transition returned to the view controller it started from. - */ - @objc(scrollForwardAnimated:completion:) - open func scrollForward(animated: Bool, completion: ((_ transitionSuccessful: Bool) -> Void)?) { - - if (self.afterViewController != nil) { - - // Cancel current animation and move - if self.scrolling { - if self.isOrientationHorizontal { - self.scrollView.setContentOffset(CGPoint(x: self.view.bounds.width * 2, y: 0), animated: false) - } else { - self.scrollView.setContentOffset(CGPoint(x: 0, y: self.view.bounds.height * 2), animated: false) - } - - } - - self.didFinishScrollingCompletionHandler = completion - self.transitionAnimated = animated - if self.isOrientationHorizontal { - self.scrollView.setContentOffset(CGPoint(x: self.view.bounds.width * 2, y: 0), animated: animated) - } else { - self.scrollView.setContentOffset(CGPoint(x: 0, y: self.view.bounds.height * 2), animated: animated) - } - - } - } - - /** - Transitions to the view controller left of the currently selected view controller in a horizontal orientation, or above the currently selected view controller in a vertical orientation. Also described as going to the previous page. - - - parameter animated: A Boolean whether or not to animate the transition - - parameter completion: A block that's called after the transition is finished. The block parameter `transitionSuccessful` is `true` if the transition to the selected view controller was completed successfully. If `false`, the transition returned to the view controller it started from. - */ - @objc(scrollReverseAnimated:completion:) - open func scrollReverse(animated: Bool, completion: ((_ transitionSuccessful: Bool) -> Void)?) { - if (self.beforeViewController != nil) { - - // Cancel current animation and move - if self.scrolling { - self.scrollView.setContentOffset(CGPoint(x: 0, y: 0), animated: false) - } - - self.didFinishScrollingCompletionHandler = completion - self.transitionAnimated = animated - self.scrollView.setContentOffset(CGPoint(x: 0, y: 0), animated: animated) - } - } - - - @nonobjc @available(*, unavailable, renamed: "scrollForward(animated:completion:)") - open func scrollForwardAnimated(_ animated: Bool, completion: ((_ transitionSuccessful: Bool) -> Void)?) { - self.scrollForward(animated: animated, completion: completion) - } - - @nonobjc @available(*, unavailable, renamed: "scrollReverse(animated:completion:)") - open func scrollReverseAnimated(_ animated: Bool, completion: ((_ transitionSuccessful: Bool) -> Void)?) { - self.scrollReverse(animated: animated, completion: completion) - } - - // MARK: - View Controller Overrides - - open override func viewWillAppear(_ animated: Bool) { - super.viewWillAppear(animated) - if let selectedViewController = selectedViewController { - selectedViewController.beginAppearanceTransition(true, animated: animated) - } - } - - private var didViewAppear: Bool = false - - open override func viewDidAppear(_ animated: Bool) { - super.viewDidAppear(animated) - didViewAppear = true - if let selectedViewController = selectedViewController { - selectedViewController.endAppearanceTransition() - } - } - - open override func viewWillDisappear(_ animated: Bool) { - super.viewWillDisappear(animated) - if let selectedViewController = selectedViewController { - selectedViewController.beginAppearanceTransition(false, animated: animated) - } - } - - open override func viewDidDisappear(_ animated: Bool) { - super.viewDidDisappear(animated) - didViewAppear = false - if let selectedViewController = selectedViewController { - selectedViewController.endAppearanceTransition() - } - } - - // Overriden to have control of accurate view appearance method calls - open override var shouldAutomaticallyForwardAppearanceMethods : Bool { - return false - } - - open override func viewDidLoad() { - super.viewDidLoad() - - self.scrollView.delegate = self - self.view.addSubview(self.scrollView) - } - - open override func viewWillLayoutSubviews() { - super.viewWillLayoutSubviews() - - adjustingContentOffset = true - - self.scrollView.frame = self.view.bounds - if self.isOrientationHorizontal { - self.scrollView.contentSize = CGSize(width: self.view.bounds.width * 3, height: self.view.bounds.height) - } else { - self.scrollView.contentSize = CGSize(width: self.view.bounds.width, height: self.view.bounds.height * 3) - } - - self.layoutViews() - } - - - // MARK: - View Controller Management - - private func loadViewControllers(_ selectedViewController: UIViewController) { - - // Scrolled forward - if (selectedViewController == self.afterViewController) { - - // Shift view controllers forward - self.beforeViewController = self.selectedViewController - self.selectedViewController = self.afterViewController - - self.removeChildIfNeeded(self.beforeViewController) - - if didViewAppear { - self.selectedViewController?.endAppearanceTransition() - self.beforeViewController?.endAppearanceTransition() - } - - self.delegate?.em_pageViewController?(self, didFinishScrollingFrom: self.beforeViewController, destinationViewController: self.selectedViewController!, transitionSuccessful: true) - - self.didFinishScrollingCompletionHandler?(true) - self.didFinishScrollingCompletionHandler = nil - - // Load new before view controller if required - if self.loadNewAdjoiningViewControllersOnFinish { - self.loadBeforeViewController(for: selectedViewController) - self.loadNewAdjoiningViewControllersOnFinish = false - } - - // Load new after view controller - self.loadAfterViewController(for: selectedViewController) - - - // Scrolled reverse - } else if (selectedViewController == self.beforeViewController) { - - // Shift view controllers reverse - self.afterViewController = self.selectedViewController - self.selectedViewController = self.beforeViewController - - self.removeChildIfNeeded(self.afterViewController) - - if didViewAppear { - self.selectedViewController?.endAppearanceTransition() - self.afterViewController?.endAppearanceTransition() - } - - self.delegate?.em_pageViewController?(self, didFinishScrollingFrom: self.afterViewController!, destinationViewController: self.selectedViewController!, transitionSuccessful: true) - - self.didFinishScrollingCompletionHandler?(true) - self.didFinishScrollingCompletionHandler = nil - - // Load new after view controller if required - if self.loadNewAdjoiningViewControllersOnFinish { - self.loadAfterViewController(for: selectedViewController) - self.loadNewAdjoiningViewControllersOnFinish = false - } - - // Load new before view controller - self.loadBeforeViewController(for: selectedViewController) - - // Scrolled but ended up where started - } else if (selectedViewController == self.selectedViewController) { - - self.selectedViewController!.beginAppearanceTransition(true, animated: self.transitionAnimated) - - if (self.navigationDirection == .forward) { - self.afterViewController!.beginAppearanceTransition(false, animated: self.transitionAnimated) - } else if (self.navigationDirection == .reverse) { - self.beforeViewController!.beginAppearanceTransition(false, animated: self.transitionAnimated) - } - - if didViewAppear { - self.selectedViewController?.endAppearanceTransition() - } - - // Remove hidden view controllers - self.removeChildIfNeeded(self.beforeViewController) - self.removeChildIfNeeded(self.afterViewController) - - if (self.navigationDirection == .forward) { - self.afterViewController!.endAppearanceTransition() - self.delegate?.em_pageViewController?(self, didFinishScrollingFrom: self.selectedViewController!, destinationViewController: self.afterViewController!, transitionSuccessful: false) - } else if (self.navigationDirection == .reverse) { - self.beforeViewController!.endAppearanceTransition() - self.delegate?.em_pageViewController?(self, didFinishScrollingFrom: self.selectedViewController!, destinationViewController: self.beforeViewController!, transitionSuccessful: false) - } else { - self.delegate?.em_pageViewController?(self, didFinishScrollingFrom: self.selectedViewController!, destinationViewController: self.selectedViewController!, transitionSuccessful: true) - } - - self.didFinishScrollingCompletionHandler?(false) - self.didFinishScrollingCompletionHandler = nil - - if self.loadNewAdjoiningViewControllersOnFinish { - if (self.navigationDirection == .forward) { - self.loadAfterViewController(for: selectedViewController) - } else if (self.navigationDirection == .reverse) { - self.loadBeforeViewController(for: selectedViewController) - } - } - - } - - self.navigationDirection = nil - self.scrolling = false - - } - - private func loadBeforeViewController(for selectedViewController:UIViewController) { - // Retreive the new before controller from the data source if available, otherwise set as nil - if let beforeViewController = self.dataSource?.em_pageViewController(self, viewControllerBeforeViewController: selectedViewController) { - self.beforeViewController = beforeViewController - } else { - self.beforeViewController = nil - } - } - - private func loadAfterViewController(for selectedViewController:UIViewController) { - // Retreive the new after controller from the data source if available, otherwise set as nil - if let afterViewController = self.dataSource?.em_pageViewController(self, viewControllerAfterViewController: selectedViewController) { - self.afterViewController = afterViewController - } else { - self.afterViewController = nil - } - } - - - // MARK: - View Management - - private func addChildIfNeeded(_ viewController: UIViewController) { - self.scrollView.addSubview(viewController.view) - - #if swift(>=4.2) - self.addChild(viewController) - viewController.didMove(toParent: self) - #else - self.addChildViewController(viewController) - viewController.didMove(toParentViewController: self) - #endif - } - - private func removeChildIfNeeded(_ viewController: UIViewController?) { - viewController?.view.removeFromSuperview() - - #if swift(>=4.2) - viewController?.didMove(toParent: nil) - viewController?.removeFromParent() - #else - viewController?.didMove(toParentViewController: nil) - viewController?.removeFromParentViewController() - #endif - } - - private func layoutViews() { - - let viewWidth = self.view.bounds.width - let viewHeight = self.view.bounds.height - - var beforeInset:CGFloat = 0 - var afterInset:CGFloat = 0 - - if (self.beforeViewController == nil) { - beforeInset = self.isOrientationHorizontal ? -viewWidth : -viewHeight - } - - if (self.afterViewController == nil) { - afterInset = self.isOrientationHorizontal ? -viewWidth : -viewHeight - } - - self.adjustingContentOffset = true - self.scrollView.contentOffset = CGPoint(x: self.isOrientationHorizontal ? viewWidth : 0, y: self.isOrientationHorizontal ? 0 : viewHeight) - if self.isOrientationHorizontal { - self.scrollView.contentInset = UIEdgeInsets(top: 0, left: beforeInset, bottom: 0, right: afterInset) - } else { - self.scrollView.contentInset = UIEdgeInsets(top: beforeInset, left: 0, bottom: afterInset, right: 0) - } - self.adjustingContentOffset = false - - if self.isOrientationHorizontal { - self.beforeViewController?.view.frame = CGRect(x: 0, y: 0, width: viewWidth, height: viewHeight) - self.selectedViewController?.view.frame = CGRect(x: viewWidth, y: 0, width: viewWidth, height: viewHeight) - self.afterViewController?.view.frame = CGRect(x: viewWidth * 2, y: 0, width: viewWidth, height: viewHeight) - } else { - self.beforeViewController?.view.frame = CGRect(x: 0, y: 0, width: viewWidth, height: viewHeight) - self.selectedViewController?.view.frame = CGRect(x: 0, y: viewHeight, width: viewWidth, height: viewHeight) - self.afterViewController?.view.frame = CGRect(x: 0, y: viewHeight * 2, width: viewWidth, height: viewHeight) - } - - } - - - // MARK: - Internal Callbacks - - private func willScroll(from startingViewController: UIViewController?, to destinationViewController: UIViewController) { - if (startingViewController != nil) { - self.delegate?.em_pageViewController?(self, willStartScrollingFrom: startingViewController!, destinationViewController: destinationViewController) - } - - destinationViewController.beginAppearanceTransition(true, animated: self.transitionAnimated) - startingViewController?.beginAppearanceTransition(false, animated: self.transitionAnimated) - self.addChildIfNeeded(destinationViewController) - } - - private func didFinishScrolling(to viewController: UIViewController) { - self.loadViewControllers(viewController) - self.layoutViews() - } - - - // MARK: - UIScrollView Delegate - - open func scrollViewDidScroll(_ scrollView: UIScrollView) { - if !adjustingContentOffset { - - let distance = self.isOrientationHorizontal ? self.view.bounds.width : self.view.bounds.height - let progress = ((self.isOrientationHorizontal ? scrollView.contentOffset.x : scrollView.contentOffset.y) - distance) / distance - - // Scrolling forward / after - if (progress > 0) { - if let afterViewController = afterViewController { - if !scrolling { // call willScroll once - self.willScroll(from: self.selectedViewController, to: afterViewController) - self.scrolling = true - } - - if let selectedViewController = selectedViewController, - self.navigationDirection == .reverse { // check if direction changed - self.didFinishScrolling(to: selectedViewController) - self.willScroll(from: selectedViewController, to: afterViewController) - } - - self.navigationDirection = .forward - - if let selectedViewController = selectedViewController { - self.delegate?.em_pageViewController?(self, isScrollingFrom: selectedViewController, destinationViewController: afterViewController, progress: progress) - } - } else { - if let selectedViewController = selectedViewController { - self.delegate?.em_pageViewController?(self, - isScrollingFrom: selectedViewController, - destinationViewController: nil, - progress: progress) - } - } - - // Scrolling reverse / before - } else if (progress < 0) { - if let beforeViewController = beforeViewController { - if !scrolling { // call willScroll once - self.willScroll(from: selectedViewController, to: beforeViewController) - self.scrolling = true - } - - if let selectedViewController = selectedViewController, - self.navigationDirection == .forward { // check if direction changed - self.didFinishScrolling(to: selectedViewController) - self.willScroll(from: selectedViewController, to: beforeViewController) - } - - self.navigationDirection = .reverse - - if let selectedViewController = selectedViewController { - self.delegate?.em_pageViewController?(self, isScrollingFrom: selectedViewController, destinationViewController: beforeViewController, progress: progress) - } - } else { - if let selectedViewController = selectedViewController { - self.delegate?.em_pageViewController?(self, - isScrollingFrom: selectedViewController, - destinationViewController: nil, - progress: progress) - } - } - - // At zero - } else { - if (self.navigationDirection == .forward) { - self.delegate?.em_pageViewController?(self, isScrollingFrom: self.selectedViewController!, destinationViewController: self.afterViewController!, progress: progress) - } else if (self.navigationDirection == .reverse) { - self.delegate?.em_pageViewController?(self, isScrollingFrom: self.selectedViewController!, destinationViewController: self.beforeViewController!, progress: progress) - } - } - - // Thresholds to update view layouts call delegates - if (progress >= 1 && self.afterViewController != nil) { - self.didFinishScrolling(to: self.afterViewController!) - } else if (progress <= -1 && self.beforeViewController != nil) { - self.didFinishScrolling(to: self.beforeViewController!) - } else if (progress == 0 && self.selectedViewController != nil) { - self.didFinishScrolling(to: self.selectedViewController!) - } - - } - - } - - open func scrollViewWillBeginDragging(_ scrollView: UIScrollView) { - self.transitionAnimated = true - } - - open func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) { - // setContentOffset is called to center the selected view after bounces - // This prevents yucky behavior at the beginning and end of the page collection by making sure setContentOffset is called only if... - - if self.isOrientationHorizontal { - if (self.beforeViewController != nil && self.afterViewController != nil) || // It isn't at the beginning or end of the page collection - (self.afterViewController != nil && self.beforeViewController == nil && scrollView.contentOffset.x > abs(scrollView.contentInset.left)) || // If it's at the beginning of the collection, the decelleration can't be triggered by scrolling away from, than torwards the inset - (self.beforeViewController != nil && self.afterViewController == nil && scrollView.contentOffset.x < abs(scrollView.contentInset.right)) { // Same as the last condition, but at the end of the collection - scrollView.setContentOffset(CGPoint(x: self.view.bounds.width, y: 0), animated: true) - } - } else { - if (self.beforeViewController != nil && self.afterViewController != nil) || // It isn't at the beginning or end of the page collection - (self.afterViewController != nil && self.beforeViewController == nil && scrollView.contentOffset.y > abs(scrollView.contentInset.top)) || // If it's at the beginning of the collection, the decelleration can't be triggered by scrolling away from, than torwards the inset - (self.beforeViewController != nil && self.afterViewController == nil && scrollView.contentOffset.y < abs(scrollView.contentInset.bottom)) { // Same as the last condition, but at the end of the collection - scrollView.setContentOffset(CGPoint(x: 0, y: self.view.bounds.height), animated: true) - } - } - - } -} diff --git a/README.md b/README.md index f237b1fc..af024e7b 100644 --- a/README.md +++ b/README.md @@ -216,7 +216,7 @@ protocol PagingViewControllerDelegate: class { startingViewController: UIViewController?, destinationViewController: UIViewController, transitionSuccessful: Bool) - + func pagingViewController( _ pagingViewController: PagingViewController, didSelectItem pagingItem: PagingItem) @@ -245,7 +245,7 @@ pagingViewController.sizeDelegate = self ## Customization -Parchment is built to be very flexible. The menu items are displayed using UICollectionView, so they can display pretty much whatever you want. If you need any further customization you can even subclass the collection view layout. All customization is handled by the properties listed below. +Parchment is built to be very flexible. The menu items are displayed using UICollectionView, so they can display pretty much whatever you want. If you need any further customization you can even subclass the collection view layout. All customization is handled by the properties listed below. ### Custom cells @@ -529,10 +529,6 @@ Parchment also supports [Carthage](https://github.com/Carthage/Carthage). To ins See [this guide](https://github.com/Carthage/Carthage#adding-frameworks-to-an-application) for more details on using Carthage. -## Acknowledgements - -* Parchment uses [`EMPageViewController`](https://github.com/emalyak/EMPageViewController) as a replacement for `UIPageViewController`. - ## Changelog This can be found in the [CHANGELOG](/CHANGELOG.md) file. From 4f69e5b6ddfbd11fa0a0537b26d9f381a07eb9ac Mon Sep 17 00:00:00 2001 From: Martin Rechsteiner Date: Sat, 4 Apr 2020 12:46:56 +0200 Subject: [PATCH 08/12] Expose initializer for PageViewController class --- Parchment/Classes/PageViewController.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Parchment/Classes/PageViewController.swift b/Parchment/Classes/PageViewController.swift index 886e59ca..187443d4 100644 --- a/Parchment/Classes/PageViewController.swift +++ b/Parchment/Classes/PageViewController.swift @@ -98,12 +98,12 @@ public final class PageViewController: UIViewController { } } - init(options: PagingOptions = PagingOptions()) { + public init(options: PagingOptions = PagingOptions()) { self.options = options super.init(nibName: nil, bundle: nil) } - required init?(coder: NSCoder) { + public required init?(coder: NSCoder) { self.options = PagingOptions() super.init(coder: coder) } From 886b0bd077b68eb11ac44d2ef93e1dac8e41e68c Mon Sep 17 00:00:00 2001 From: Martin Rechsteiner Date: Sat, 4 Apr 2020 12:47:17 +0200 Subject: [PATCH 09/12] Fix issue selecting in PageViewController before view loads Moves the PageViewManager configuration to the initializers to ensure that we handle the selectViewController method if it's called before viewDidLoad has been triggered. --- Parchment/Classes/PageViewController.swift | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/Parchment/Classes/PageViewController.swift b/Parchment/Classes/PageViewController.swift index 187443d4..b46da23d 100644 --- a/Parchment/Classes/PageViewController.swift +++ b/Parchment/Classes/PageViewController.swift @@ -101,17 +101,19 @@ public final class PageViewController: UIViewController { public init(options: PagingOptions = PagingOptions()) { self.options = options super.init(nibName: nil, bundle: nil) + manager.delegate = self + manager.dataSource = self } public required init?(coder: NSCoder) { self.options = PagingOptions() super.init(coder: coder) + manager.delegate = self + manager.dataSource = self } override public func viewDidLoad() { super.viewDidLoad() - manager.delegate = self - manager.dataSource = self view.addSubview(scrollView) view.constrainToEdges(scrollView) scrollView.delegate = self From c73510fc736f4ee13670af5596c85113b9b83a15 Mon Sep 17 00:00:00 2001 From: Martin Rechsteiner Date: Sat, 4 Apr 2020 12:48:59 +0200 Subject: [PATCH 10/12] Add example for PageViewController --- .../PageViewExampleViewController.swift | 70 +++++++++++++++++++ Example/ExamplesViewController.swift | 5 ++ Parchment.xcodeproj/project.pbxproj | 12 ++++ 3 files changed, 87 insertions(+) create mode 100644 Example/Examples/PageViewController/PageViewExampleViewController.swift diff --git a/Example/Examples/PageViewController/PageViewExampleViewController.swift b/Example/Examples/PageViewController/PageViewExampleViewController.swift new file mode 100644 index 00000000..7ed07605 --- /dev/null +++ b/Example/Examples/PageViewController/PageViewExampleViewController.swift @@ -0,0 +1,70 @@ +import UIKit +import Parchment + +/// Parchment provides a custom `UIPageViewController` alternative +/// if you need better delegate methods called `PageViewController`. +class PageViewExampleViewController: UIViewController { + let viewControllers: [UIViewController] = [ + ContentViewController(index: 0), + ContentViewController(index: 1), + ContentViewController(index: 2), + ContentViewController(index: 3), + ContentViewController(index: 4), + ] + + override func viewDidLoad() { + let pageViewController = PageViewController() + pageViewController.dataSource = self + pageViewController.delegate = self + pageViewController.selectViewController(viewControllers[0], direction: .none) + + addChild(pageViewController) + view.addSubview(pageViewController.view) + view.constrainToEdges(pageViewController.view) + pageViewController.didMove(toParent: self) + } +} + +extension PageViewExampleViewController: PageViewControllerDataSource { + func pageViewController( + _ pageViewController: PageViewController, + viewControllerBeforeViewController viewController: UIViewController) -> UIViewController? { + guard let index = viewControllers.firstIndex(of: viewController) else { return nil } + if index > 0 { + return viewControllers[index - 1] + } + return nil + } + + func pageViewController( + _ pageViewController: PageViewController, + viewControllerAfterViewController viewController: UIViewController) -> UIViewController? { + guard let index = viewControllers.firstIndex(of: viewController) else { return nil } + if index < viewControllers.count - 1 { + return viewControllers[index + 1] + } + return nil + } +} + +extension PageViewExampleViewController: PageViewControllerDelegate { + func pageViewController(_ pageViewController: PageViewController, willStartScrollingFrom startingViewController: UIViewController, destinationViewController: UIViewController) { + print("willStartScrollingFrom: ", + startingViewController.title ?? "", + destinationViewController.title ?? "") + } + + func pageViewController(_ pageViewController: PageViewController, isScrollingFrom startingViewController: UIViewController, destinationViewController: UIViewController?, progress: CGFloat) { + print("isScrollingFrom: ", + startingViewController.title ?? "", + destinationViewController?.title ?? "", + progress) + } + + func pageViewController(_ pageViewController: PageViewController, didFinishScrollingFrom startingViewController: UIViewController, destinationViewController: UIViewController, transitionSuccessful: Bool) { + print("didFinishScrollingFrom: ", + startingViewController.title ?? "", + destinationViewController.title ?? "", + transitionSuccessful) + } +} diff --git a/Example/ExamplesViewController.swift b/Example/ExamplesViewController.swift index 6042c26d..4257dae6 100644 --- a/Example/ExamplesViewController.swift +++ b/Example/ExamplesViewController.swift @@ -13,6 +13,7 @@ enum Example: CaseIterable { case scroll case header case multipleCells + case pageViewController var title: String { switch self { @@ -40,6 +41,8 @@ enum Example: CaseIterable { return "Header above menu" case .multipleCells: return "Multiple cells" + case .pageViewController: + return "PageViewController" } } } @@ -118,6 +121,8 @@ final class ExamplesViewController: UITableViewController { return HeaderViewController(nibName: nil, bundle: nil) case .multipleCells: return MultipleCellsViewController(nibName: nil, bundle: nil) + case .pageViewController: + return PageViewExampleViewController(nibName: nil, bundle: nil) } } diff --git a/Parchment.xcodeproj/project.pbxproj b/Parchment.xcodeproj/project.pbxproj index aaca6d43..9a8044b7 100644 --- a/Parchment.xcodeproj/project.pbxproj +++ b/Parchment.xcodeproj/project.pbxproj @@ -40,6 +40,7 @@ 3EA04A671C53BFF40054E5E0 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 3EA04A651C53BFF40054E5E0 /* LaunchScreen.storyboard */; }; 3EFEFBF71C80B8820023C949 /* PagingIndicatorLayoutAttributesTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3EFEFBF61C80B8820023C949 /* PagingIndicatorLayoutAttributesTests.swift */; }; 950ABE422437BD4D00CAD458 /* PagingNavigationOrientation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 950ABE412437BD4D00CAD458 /* PagingNavigationOrientation.swift */; }; + 950ABE452438975300CAD458 /* PageViewExampleViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 950ABE442438975300CAD458 /* PageViewExampleViewController.swift */; }; 951E163720A21D3A0055E9D4 /* PagingViewControllerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 954842601F4251F90072038C /* PagingViewControllerTests.swift */; }; 952D802F1E37CC09003DCB18 /* PagingTransition.swift in Sources */ = {isa = PBXBuildFile; fileRef = 952D802E1E37CC09003DCB18 /* PagingTransition.swift */; }; 953B8D352416C3DC0047BBA1 /* SelfSizingViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 953B8D342416C3DC0047BBA1 /* SelfSizingViewController.swift */; }; @@ -235,6 +236,7 @@ 3EC0184C1C95F993005421AD /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; name = Info.plist; path = Resources/Info.plist; sourceTree = ""; }; 3EFEFBF61C80B8820023C949 /* PagingIndicatorLayoutAttributesTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PagingIndicatorLayoutAttributesTests.swift; sourceTree = ""; }; 950ABE412437BD4D00CAD458 /* PagingNavigationOrientation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PagingNavigationOrientation.swift; sourceTree = ""; }; + 950ABE442438975300CAD458 /* PageViewExampleViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PageViewExampleViewController.swift; sourceTree = ""; }; 952D802E1E37CC09003DCB18 /* PagingTransition.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PagingTransition.swift; sourceTree = ""; }; 953B8D342416C3DC0047BBA1 /* SelfSizingViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelfSizingViewController.swift; sourceTree = ""; }; 954842581F42438E0072038C /* PagingInvalidationContext.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PagingInvalidationContext.swift; sourceTree = ""; }; @@ -521,6 +523,14 @@ path = Protocols; sourceTree = ""; }; + 950ABE432438972400CAD458 /* PageViewController */ = { + isa = PBXGroup; + children = ( + 950ABE442438975300CAD458 /* PageViewExampleViewController.swift */, + ); + path = PageViewController; + sourceTree = ""; + }; 953B8D332416C3C60047BBA1 /* SelfSizing */ = { isa = PBXGroup; children = ( @@ -559,6 +569,7 @@ 955453E82413F29400923BC8 /* Scroll */, 955453EB2415164600923BC8 /* Header */, 955453EE241516CE00923BC8 /* MultipleCells */, + 950ABE432438972400CAD458 /* PageViewController */, ); path = Examples; sourceTree = ""; @@ -1031,6 +1042,7 @@ 955453DF2413E1B200923BC8 /* IconViewController.swift in Sources */, 953B8D352416C3DC0047BBA1 /* SelfSizingViewController.swift in Sources */, 955453D62413DD8E00923BC8 /* SizeDelegateViewController.swift in Sources */, + 950ABE452438975300CAD458 /* PageViewExampleViewController.swift in Sources */, 955453D92413E11300923BC8 /* ImageCollectionViewCell.swift in Sources */, 955453EA241515D900923BC8 /* ScrollViewController.swift in Sources */, 955453E72413E39C00923BC8 /* LargeTitlesViewController.swift in Sources */, From 97cf83120871596330e395fbaf2d8db8c7de3552 Mon Sep 17 00:00:00 2001 From: Martin Rechsteiner Date: Wed, 22 Apr 2020 19:41:00 +0200 Subject: [PATCH 11/12] Add before/after view controller properties to PageViewController Ideally we would keep these private, but the current implementation exposes them so it better not to break existing apps. --- Parchment/Classes/PageViewController.swift | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/Parchment/Classes/PageViewController.swift b/Parchment/Classes/PageViewController.swift index b46da23d..f168c1a1 100644 --- a/Parchment/Classes/PageViewController.swift +++ b/Parchment/Classes/PageViewController.swift @@ -11,10 +11,25 @@ public final class PageViewController: UIViewController { return false } + /// The view controller before the selected view controller. + public var beforeViewController: UIViewController? { + return manager.previousViewController + } + + /// The currently selected view controller. Can be `nil` if no view + /// controller is selected. public var selectedViewController: UIViewController? { return manager.selectedViewController } + /// The view controller after the selected view controller. + private var afterViewController: UIViewController? { + return manager.nextViewController + } + + /// The underlying scroll view where the page view controllers are + /// added. Changing the properties on this scroll view might cause + /// undefined behaviour. public private(set) lazy var scrollView: UIScrollView = { let scrollView = UIScrollView() scrollView.isPagingEnabled = true From e455f2ff4cda1e5ef0e3edf830dec5750664f20b Mon Sep 17 00:00:00 2001 From: Martin Rechsteiner Date: Wed, 22 Apr 2020 19:47:06 +0200 Subject: [PATCH 12/12] Update documentation for `PageViewController` --- Parchment/Classes/PageViewController.swift | 3 ++ .../PageViewControllerDataSource.swift | 14 ++++++++ .../PageViewControllerDelegate.swift | 36 +++++++++++++++++++ 3 files changed, 53 insertions(+) diff --git a/Parchment/Classes/PageViewController.swift b/Parchment/Classes/PageViewController.swift index f168c1a1..6563262d 100644 --- a/Parchment/Classes/PageViewController.swift +++ b/Parchment/Classes/PageViewController.swift @@ -1,5 +1,8 @@ import UIKit +/// PageViewController is a replacement for `UIPageViewController` +/// using `UIScrollView`. It provides detailed delegate methods, which +/// is the main issue with `UIPageViewController`. public final class PageViewController: UIViewController { // MARK: Public Properties diff --git a/Parchment/Protocols/PageViewControllerDataSource.swift b/Parchment/Protocols/PageViewControllerDataSource.swift index e3c89c3b..b5af2951 100644 --- a/Parchment/Protocols/PageViewControllerDataSource.swift +++ b/Parchment/Protocols/PageViewControllerDataSource.swift @@ -1,9 +1,23 @@ import UIKit +/// The `PageViewControllerDataSource` protocol is used to provide +/// the view controller you want to display. public protocol PageViewControllerDataSource: class { + + /// Return the view controller before a given view controller. + /// + /// - Parameters: + /// - pageViewController: The `PageViewController` instance. + /// - viewController: The current view controller. func pageViewController( _ pageViewController: PageViewController, viewControllerBeforeViewController viewController: UIViewController) -> UIViewController? + + /// Return the view controller after a given view controller. + /// + /// - Parameters: + /// - pageViewController: The `PageViewController` instance. + /// - viewController: The current view controller. func pageViewController( _ pageViewController: PageViewController, viewControllerAfterViewController viewController: UIViewController) -> UIViewController? diff --git a/Parchment/Protocols/PageViewControllerDelegate.swift b/Parchment/Protocols/PageViewControllerDelegate.swift index bd4805cc..524949ac 100644 --- a/Parchment/Protocols/PageViewControllerDelegate.swift +++ b/Parchment/Protocols/PageViewControllerDelegate.swift @@ -1,15 +1,51 @@ import UIKit +/// The `PageViewControllerDelegate` protocol defines methods that +/// can used to determine when the user navigates between view +/// controllers. public protocol PageViewControllerDelegate: class { + + /// Called whenever the user is about to start scrolling to a view + /// controller. + /// + /// - Parameters: + /// - pageViewController: The `PageViewController` instance. + /// - startingViewController: The view controller the user is + /// scrolling from. + /// - destinationViewController: The view controller the user is + /// scrolling towards. func pageViewController( _ pageViewController: PageViewController, willStartScrollingFrom startingViewController: UIViewController, destinationViewController: UIViewController) + + /// Called whenever a scroll transition is in progress. + /// + /// - Parameters: + /// - pageViewController: The `PageViewController` instance. + /// - startingViewController: The view controller the user is + /// scrolling from. + /// - destinationViewController: The view controller the user is + /// scrolling towards. Will be nil if the user is scrolling + /// towards one of the edges. + /// - progress: The progress of the scroll transition. Between 0 + /// and 1. func pageViewController( _ pageViewController: PageViewController, isScrollingFrom startingViewController: UIViewController, destinationViewController: UIViewController?, progress: CGFloat) + + /// Called when the user finished scrolling to a new view. + /// + /// - Parameters: + /// - pageViewController: The `PageViewController` instance. + /// - startingViewController: The view controller the user is + /// scrolling from. + /// - destinationViewController: The view controller the user is + /// scrolling towards. + /// - transitionSuccessful: A boolean indicating whether the + /// transition completed, or was cancelled by the user. func pageViewController( _ pageViewController: PageViewController, didFinishScrollingFrom startingViewController: UIViewController,