├── .gitignore ├── Example ├── Example.xcodeproj │ ├── project.pbxproj │ └── project.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ ├── IDEWorkspaceChecks.plist │ │ └── WorkspaceSettings.xcsettings └── Example │ ├── AppDelegate.swift │ ├── Assets.xcassets │ ├── AppIcon.appiconset │ │ └── Contents.json │ └── Contents.json │ ├── Base.lproj │ └── LaunchScreen.storyboard │ ├── ContentView.swift │ ├── Example Views │ ├── CustomViews.swift │ ├── ElegantHListExample.swift │ ├── ElegantHPagesExample.swift │ ├── ElegantVListExample.swift │ ├── ElegantVPagesExample.swift │ └── ScrollToPageButton.swift │ ├── Info.plist │ ├── Preview Content │ └── Preview Assets.xcassets │ │ └── Contents.json │ └── SceneDelegate.swift ├── LICENSE ├── Package.swift ├── README.md └── Sources └── ElegantPages ├── Extensions ├── Comparable+Clamped.swift └── View+Erased.swift ├── Lists ├── Internal │ ├── ElegantListController.swift │ └── ElegantListView.swift └── Public │ ├── ElegantHList.swift │ └── ElegantVList.swift ├── Models ├── Internal │ ├── Constants.swift │ ├── PageState.swift │ └── ScrollDirection.swift └── Public │ ├── ElegantListManager.swift │ ├── ElegantPagesManager.swift │ ├── PageTurnType.swift │ └── PageViewBuilder.swift └── Pages ├── Internal └── ElegantPagesView.swift └── Public ├── ElegantHPages.swift └── ElegantVPages.swift /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | # 3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 4 | 5 | ## User settings 6 | xcuserdata/ 7 | 8 | ## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9) 9 | *.xcscmblueprint 10 | *.xccheckout 11 | 12 | ## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4) 13 | build/ 14 | DerivedData/ 15 | *.moved-aside 16 | *.pbxuser 17 | !default.pbxuser 18 | *.mode1v3 19 | !default.mode1v3 20 | *.mode2v3 21 | !default.mode2v3 22 | *.perspectivev3 23 | !default.perspectivev3 24 | 25 | ## Obj-C/Swift specific 26 | *.hmap 27 | 28 | ## App packaging 29 | *.ipa 30 | *.dSYM.zip 31 | *.dSYM 32 | 33 | ## Playgrounds 34 | timeline.xctimeline 35 | playground.xcworkspace 36 | 37 | # Swift Package Manager 38 | # 39 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 40 | # Packages/ 41 | # Package.pins 42 | # Package.resolved 43 | # *.xcodeproj 44 | # 45 | # Xcode automatically generates this directory with a .xcworkspacedata file and xcuserdata 46 | # hence it is not needed unless you have added a package configuration file to your project 47 | .swiftpm 48 | 49 | .build/ 50 | 51 | # CocoaPods 52 | # 53 | # We recommend against adding the Pods directory to your .gitignore. However 54 | # you should judge for yourself, the pros and cons are mentioned at: 55 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 56 | # 57 | # Pods/ 58 | # 59 | # Add this line if you want to avoid checking in source code from the Xcode workspace 60 | # *.xcworkspace 61 | 62 | # Carthage 63 | # 64 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 65 | # Carthage/Checkouts 66 | 67 | Carthage/Build/ 68 | 69 | # Accio dependency management 70 | Dependencies/ 71 | .accio/ 72 | 73 | # fastlane 74 | # 75 | # It is recommended to not store the screenshots in the git repo. 76 | # Instead, use fastlane to re-generate the screenshots whenever they are needed. 77 | # For more information about the recommended setup visit: 78 | # https://docs.fastlane.tools/best-practices/source-control/#source-control 79 | 80 | fastlane/report.xml 81 | fastlane/Preview.html 82 | fastlane/screenshots/**/*.png 83 | fastlane/test_output 84 | 85 | # Code Injection 86 | # 87 | # After new code Injection tools there's a generated folder /iOSInjectionProject 88 | # https://github.com/johnno1962/injectionforxcode 89 | 90 | iOSInjectionProject/ 91 | -------------------------------------------------------------------------------- /Example/Example.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 50; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 1E0315AF24C17CDF003C632D /* Constants.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E0315AE24C17CDF003C632D /* Constants.swift */; }; 11 | 1E15AC5E24AD8D1D00623E98 /* ElegantListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E15AC4624AD8D1D00623E98 /* ElegantListView.swift */; }; 12 | 1E15AC5F24AD8D1D00623E98 /* ElegantListController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E15AC4724AD8D1D00623E98 /* ElegantListController.swift */; }; 13 | 1E15AC6024AD8D1D00623E98 /* ElegantHList.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E15AC4924AD8D1D00623E98 /* ElegantHList.swift */; }; 14 | 1E15AC6124AD8D1D00623E98 /* ElegantVList.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E15AC4A24AD8D1D00623E98 /* ElegantVList.swift */; }; 15 | 1E15AC6224AD8D1D00623E98 /* PageState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E15AC4D24AD8D1D00623E98 /* PageState.swift */; }; 16 | 1E15AC6324AD8D1D00623E98 /* ScrollDirection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E15AC4E24AD8D1D00623E98 /* ScrollDirection.swift */; }; 17 | 1E15AC6424AD8D1D00623E98 /* PageViewBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E15AC5024AD8D1D00623E98 /* PageViewBuilder.swift */; }; 18 | 1E15AC6524AD8D1D00623E98 /* ElegantPagesManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E15AC5124AD8D1D00623E98 /* ElegantPagesManager.swift */; }; 19 | 1E15AC6724AD8D1D00623E98 /* PageTurnType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E15AC5324AD8D1D00623E98 /* PageTurnType.swift */; }; 20 | 1E15AC6824AD8D1D00623E98 /* ElegantListManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E15AC5424AD8D1D00623E98 /* ElegantListManager.swift */; }; 21 | 1E15AC6924AD8D1D00623E98 /* View+Erased.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E15AC5624AD8D1D00623E98 /* View+Erased.swift */; }; 22 | 1E15AC6A24AD8D1D00623E98 /* Comparable+Clamped.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E15AC5724AD8D1D00623E98 /* Comparable+Clamped.swift */; }; 23 | 1E15AC6B24AD8D1D00623E98 /* ElegantPagesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E15AC5A24AD8D1D00623E98 /* ElegantPagesView.swift */; }; 24 | 1E15AC6C24AD8D1D00623E98 /* ElegantVPages.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E15AC5C24AD8D1D00623E98 /* ElegantVPages.swift */; }; 25 | 1E15AC6D24AD8D1D00623E98 /* ElegantHPages.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E15AC5D24AD8D1D00623E98 /* ElegantHPages.swift */; }; 26 | 1E6DA72024A3245300001565 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E6DA71F24A3245300001565 /* AppDelegate.swift */; }; 27 | 1E6DA72224A3245300001565 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E6DA72124A3245300001565 /* SceneDelegate.swift */; }; 28 | 1E6DA72424A3245300001565 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E6DA72324A3245300001565 /* ContentView.swift */; }; 29 | 1E6DA72624A3245400001565 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 1E6DA72524A3245400001565 /* Assets.xcassets */; }; 30 | 1E6DA72924A3245400001565 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 1E6DA72824A3245400001565 /* Preview Assets.xcassets */; }; 31 | 1E6DA72C24A3245400001565 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 1E6DA72A24A3245400001565 /* LaunchScreen.storyboard */; }; 32 | 1E6DA75A24A3256700001565 /* ElegantHListExample.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E6DA75924A3256700001565 /* ElegantHListExample.swift */; }; 33 | 1E6DA75E24A32EFD00001565 /* ElegantVListExample.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E6DA75D24A32EFD00001565 /* ElegantVListExample.swift */; }; 34 | 1E6DA76024A32F1B00001565 /* ScrollToPageButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E6DA75F24A32F1B00001565 /* ScrollToPageButton.swift */; }; 35 | 1E6DA76224A32FCE00001565 /* ElegantHPagesExample.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E6DA76124A32FCE00001565 /* ElegantHPagesExample.swift */; }; 36 | 1E6DA76424A3307700001565 /* CustomViews.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E6DA76324A3307700001565 /* CustomViews.swift */; }; 37 | 1E6DA76624A3319900001565 /* ElegantVPagesExample.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E6DA76524A3319900001565 /* ElegantVPagesExample.swift */; }; 38 | /* End PBXBuildFile section */ 39 | 40 | /* Begin PBXFileReference section */ 41 | 1E0315AE24C17CDF003C632D /* Constants.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Constants.swift; sourceTree = ""; }; 42 | 1E15AC4624AD8D1D00623E98 /* ElegantListView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ElegantListView.swift; sourceTree = ""; }; 43 | 1E15AC4724AD8D1D00623E98 /* ElegantListController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ElegantListController.swift; sourceTree = ""; }; 44 | 1E15AC4924AD8D1D00623E98 /* ElegantHList.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ElegantHList.swift; sourceTree = ""; }; 45 | 1E15AC4A24AD8D1D00623E98 /* ElegantVList.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ElegantVList.swift; sourceTree = ""; }; 46 | 1E15AC4D24AD8D1D00623E98 /* PageState.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PageState.swift; sourceTree = ""; }; 47 | 1E15AC4E24AD8D1D00623E98 /* ScrollDirection.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ScrollDirection.swift; sourceTree = ""; }; 48 | 1E15AC5024AD8D1D00623E98 /* PageViewBuilder.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PageViewBuilder.swift; sourceTree = ""; }; 49 | 1E15AC5124AD8D1D00623E98 /* ElegantPagesManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ElegantPagesManager.swift; sourceTree = ""; }; 50 | 1E15AC5324AD8D1D00623E98 /* PageTurnType.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PageTurnType.swift; sourceTree = ""; }; 51 | 1E15AC5424AD8D1D00623E98 /* ElegantListManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ElegantListManager.swift; sourceTree = ""; }; 52 | 1E15AC5624AD8D1D00623E98 /* View+Erased.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "View+Erased.swift"; sourceTree = ""; }; 53 | 1E15AC5724AD8D1D00623E98 /* Comparable+Clamped.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Comparable+Clamped.swift"; sourceTree = ""; }; 54 | 1E15AC5A24AD8D1D00623E98 /* ElegantPagesView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ElegantPagesView.swift; sourceTree = ""; }; 55 | 1E15AC5C24AD8D1D00623E98 /* ElegantVPages.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ElegantVPages.swift; sourceTree = ""; }; 56 | 1E15AC5D24AD8D1D00623E98 /* ElegantHPages.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ElegantHPages.swift; sourceTree = ""; }; 57 | 1E6DA71C24A3245300001565 /* Example.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Example.app; sourceTree = BUILT_PRODUCTS_DIR; }; 58 | 1E6DA71F24A3245300001565 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 59 | 1E6DA72124A3245300001565 /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; }; 60 | 1E6DA72324A3245300001565 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; 61 | 1E6DA72524A3245400001565 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 62 | 1E6DA72824A3245400001565 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; 63 | 1E6DA72B24A3245400001565 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 64 | 1E6DA72D24A3245400001565 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 65 | 1E6DA75924A3256700001565 /* ElegantHListExample.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ElegantHListExample.swift; sourceTree = ""; }; 66 | 1E6DA75D24A32EFD00001565 /* ElegantVListExample.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ElegantVListExample.swift; sourceTree = ""; }; 67 | 1E6DA75F24A32F1B00001565 /* ScrollToPageButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScrollToPageButton.swift; sourceTree = ""; }; 68 | 1E6DA76124A32FCE00001565 /* ElegantHPagesExample.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ElegantHPagesExample.swift; sourceTree = ""; }; 69 | 1E6DA76324A3307700001565 /* CustomViews.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomViews.swift; sourceTree = ""; }; 70 | 1E6DA76524A3319900001565 /* ElegantVPagesExample.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ElegantVPagesExample.swift; sourceTree = ""; }; 71 | /* End PBXFileReference section */ 72 | 73 | /* Begin PBXFrameworksBuildPhase section */ 74 | 1E6DA71924A3245300001565 /* Frameworks */ = { 75 | isa = PBXFrameworksBuildPhase; 76 | buildActionMask = 2147483647; 77 | files = ( 78 | ); 79 | runOnlyForDeploymentPostprocessing = 0; 80 | }; 81 | /* End PBXFrameworksBuildPhase section */ 82 | 83 | /* Begin PBXGroup section */ 84 | 1E15AC4324AD8D1D00623E98 /* ElegantPages */ = { 85 | isa = PBXGroup; 86 | children = ( 87 | 1E15AC4424AD8D1D00623E98 /* Lists */, 88 | 1E15AC4B24AD8D1D00623E98 /* Models */, 89 | 1E15AC5524AD8D1D00623E98 /* Extensions */, 90 | 1E15AC5824AD8D1D00623E98 /* Pages */, 91 | ); 92 | name = ElegantPages; 93 | path = ../../Sources/ElegantPages; 94 | sourceTree = ""; 95 | }; 96 | 1E15AC4424AD8D1D00623E98 /* Lists */ = { 97 | isa = PBXGroup; 98 | children = ( 99 | 1E15AC4524AD8D1D00623E98 /* Internal */, 100 | 1E15AC4824AD8D1D00623E98 /* Public */, 101 | ); 102 | path = Lists; 103 | sourceTree = ""; 104 | }; 105 | 1E15AC4524AD8D1D00623E98 /* Internal */ = { 106 | isa = PBXGroup; 107 | children = ( 108 | 1E15AC4724AD8D1D00623E98 /* ElegantListController.swift */, 109 | 1E15AC4624AD8D1D00623E98 /* ElegantListView.swift */, 110 | ); 111 | path = Internal; 112 | sourceTree = ""; 113 | }; 114 | 1E15AC4824AD8D1D00623E98 /* Public */ = { 115 | isa = PBXGroup; 116 | children = ( 117 | 1E15AC4924AD8D1D00623E98 /* ElegantHList.swift */, 118 | 1E15AC4A24AD8D1D00623E98 /* ElegantVList.swift */, 119 | ); 120 | path = Public; 121 | sourceTree = ""; 122 | }; 123 | 1E15AC4B24AD8D1D00623E98 /* Models */ = { 124 | isa = PBXGroup; 125 | children = ( 126 | 1E15AC4C24AD8D1D00623E98 /* Internal */, 127 | 1E15AC4F24AD8D1D00623E98 /* Public */, 128 | ); 129 | path = Models; 130 | sourceTree = ""; 131 | }; 132 | 1E15AC4C24AD8D1D00623E98 /* Internal */ = { 133 | isa = PBXGroup; 134 | children = ( 135 | 1E0315AE24C17CDF003C632D /* Constants.swift */, 136 | 1E15AC4D24AD8D1D00623E98 /* PageState.swift */, 137 | 1E15AC4E24AD8D1D00623E98 /* ScrollDirection.swift */, 138 | ); 139 | path = Internal; 140 | sourceTree = ""; 141 | }; 142 | 1E15AC4F24AD8D1D00623E98 /* Public */ = { 143 | isa = PBXGroup; 144 | children = ( 145 | 1E15AC5424AD8D1D00623E98 /* ElegantListManager.swift */, 146 | 1E15AC5124AD8D1D00623E98 /* ElegantPagesManager.swift */, 147 | 1E15AC5324AD8D1D00623E98 /* PageTurnType.swift */, 148 | 1E15AC5024AD8D1D00623E98 /* PageViewBuilder.swift */, 149 | ); 150 | path = Public; 151 | sourceTree = ""; 152 | }; 153 | 1E15AC5524AD8D1D00623E98 /* Extensions */ = { 154 | isa = PBXGroup; 155 | children = ( 156 | 1E15AC5724AD8D1D00623E98 /* Comparable+Clamped.swift */, 157 | 1E15AC5624AD8D1D00623E98 /* View+Erased.swift */, 158 | ); 159 | path = Extensions; 160 | sourceTree = ""; 161 | }; 162 | 1E15AC5824AD8D1D00623E98 /* Pages */ = { 163 | isa = PBXGroup; 164 | children = ( 165 | 1E15AC5924AD8D1D00623E98 /* Internal */, 166 | 1E15AC5B24AD8D1D00623E98 /* Public */, 167 | ); 168 | path = Pages; 169 | sourceTree = ""; 170 | }; 171 | 1E15AC5924AD8D1D00623E98 /* Internal */ = { 172 | isa = PBXGroup; 173 | children = ( 174 | 1E15AC5A24AD8D1D00623E98 /* ElegantPagesView.swift */, 175 | ); 176 | path = Internal; 177 | sourceTree = ""; 178 | }; 179 | 1E15AC5B24AD8D1D00623E98 /* Public */ = { 180 | isa = PBXGroup; 181 | children = ( 182 | 1E15AC5D24AD8D1D00623E98 /* ElegantHPages.swift */, 183 | 1E15AC5C24AD8D1D00623E98 /* ElegantVPages.swift */, 184 | ); 185 | path = Public; 186 | sourceTree = ""; 187 | }; 188 | 1E6DA71324A3245300001565 = { 189 | isa = PBXGroup; 190 | children = ( 191 | 1E6DA71E24A3245300001565 /* Example */, 192 | 1E6DA71D24A3245300001565 /* Products */, 193 | ); 194 | sourceTree = ""; 195 | }; 196 | 1E6DA71D24A3245300001565 /* Products */ = { 197 | isa = PBXGroup; 198 | children = ( 199 | 1E6DA71C24A3245300001565 /* Example.app */, 200 | ); 201 | name = Products; 202 | sourceTree = ""; 203 | }; 204 | 1E6DA71E24A3245300001565 /* Example */ = { 205 | isa = PBXGroup; 206 | children = ( 207 | 1E6DA75824A3255300001565 /* Example Views */, 208 | 1E6DA71F24A3245300001565 /* AppDelegate.swift */, 209 | 1E6DA72124A3245300001565 /* SceneDelegate.swift */, 210 | 1E6DA72324A3245300001565 /* ContentView.swift */, 211 | 1E15AC4324AD8D1D00623E98 /* ElegantPages */, 212 | 1E6DA72524A3245400001565 /* Assets.xcassets */, 213 | 1E6DA72A24A3245400001565 /* LaunchScreen.storyboard */, 214 | 1E6DA72D24A3245400001565 /* Info.plist */, 215 | 1E6DA72724A3245400001565 /* Preview Content */, 216 | ); 217 | path = Example; 218 | sourceTree = ""; 219 | }; 220 | 1E6DA72724A3245400001565 /* Preview Content */ = { 221 | isa = PBXGroup; 222 | children = ( 223 | 1E6DA72824A3245400001565 /* Preview Assets.xcassets */, 224 | ); 225 | path = "Preview Content"; 226 | sourceTree = ""; 227 | }; 228 | 1E6DA75824A3255300001565 /* Example Views */ = { 229 | isa = PBXGroup; 230 | children = ( 231 | 1E6DA75924A3256700001565 /* ElegantHListExample.swift */, 232 | 1E6DA75D24A32EFD00001565 /* ElegantVListExample.swift */, 233 | 1E6DA76124A32FCE00001565 /* ElegantHPagesExample.swift */, 234 | 1E6DA76524A3319900001565 /* ElegantVPagesExample.swift */, 235 | 1E6DA75F24A32F1B00001565 /* ScrollToPageButton.swift */, 236 | 1E6DA76324A3307700001565 /* CustomViews.swift */, 237 | ); 238 | path = "Example Views"; 239 | sourceTree = ""; 240 | }; 241 | /* End PBXGroup section */ 242 | 243 | /* Begin PBXNativeTarget section */ 244 | 1E6DA71B24A3245300001565 /* Example */ = { 245 | isa = PBXNativeTarget; 246 | buildConfigurationList = 1E6DA73024A3245400001565 /* Build configuration list for PBXNativeTarget "Example" */; 247 | buildPhases = ( 248 | 1E6DA71824A3245300001565 /* Sources */, 249 | 1E6DA71924A3245300001565 /* Frameworks */, 250 | 1E6DA71A24A3245300001565 /* Resources */, 251 | ); 252 | buildRules = ( 253 | ); 254 | dependencies = ( 255 | ); 256 | name = Example; 257 | productName = Example; 258 | productReference = 1E6DA71C24A3245300001565 /* Example.app */; 259 | productType = "com.apple.product-type.application"; 260 | }; 261 | /* End PBXNativeTarget section */ 262 | 263 | /* Begin PBXProject section */ 264 | 1E6DA71424A3245300001565 /* Project object */ = { 265 | isa = PBXProject; 266 | attributes = { 267 | LastSwiftUpdateCheck = 1150; 268 | LastUpgradeCheck = 1150; 269 | ORGANIZATIONNAME = "Kevin Li"; 270 | TargetAttributes = { 271 | 1E6DA71B24A3245300001565 = { 272 | CreatedOnToolsVersion = 11.5; 273 | }; 274 | }; 275 | }; 276 | buildConfigurationList = 1E6DA71724A3245300001565 /* Build configuration list for PBXProject "Example" */; 277 | compatibilityVersion = "Xcode 9.3"; 278 | developmentRegion = en; 279 | hasScannedForEncodings = 0; 280 | knownRegions = ( 281 | en, 282 | Base, 283 | ); 284 | mainGroup = 1E6DA71324A3245300001565; 285 | productRefGroup = 1E6DA71D24A3245300001565 /* Products */; 286 | projectDirPath = ""; 287 | projectRoot = ""; 288 | targets = ( 289 | 1E6DA71B24A3245300001565 /* Example */, 290 | ); 291 | }; 292 | /* End PBXProject section */ 293 | 294 | /* Begin PBXResourcesBuildPhase section */ 295 | 1E6DA71A24A3245300001565 /* Resources */ = { 296 | isa = PBXResourcesBuildPhase; 297 | buildActionMask = 2147483647; 298 | files = ( 299 | 1E6DA72C24A3245400001565 /* LaunchScreen.storyboard in Resources */, 300 | 1E6DA72924A3245400001565 /* Preview Assets.xcassets in Resources */, 301 | 1E6DA72624A3245400001565 /* Assets.xcassets in Resources */, 302 | ); 303 | runOnlyForDeploymentPostprocessing = 0; 304 | }; 305 | /* End PBXResourcesBuildPhase section */ 306 | 307 | /* Begin PBXSourcesBuildPhase section */ 308 | 1E6DA71824A3245300001565 /* Sources */ = { 309 | isa = PBXSourcesBuildPhase; 310 | buildActionMask = 2147483647; 311 | files = ( 312 | 1E6DA72024A3245300001565 /* AppDelegate.swift in Sources */, 313 | 1E15AC5F24AD8D1D00623E98 /* ElegantListController.swift in Sources */, 314 | 1E15AC6B24AD8D1D00623E98 /* ElegantPagesView.swift in Sources */, 315 | 1E6DA76224A32FCE00001565 /* ElegantHPagesExample.swift in Sources */, 316 | 1E15AC6C24AD8D1D00623E98 /* ElegantVPages.swift in Sources */, 317 | 1E6DA75A24A3256700001565 /* ElegantHListExample.swift in Sources */, 318 | 1E6DA72224A3245300001565 /* SceneDelegate.swift in Sources */, 319 | 1E0315AF24C17CDF003C632D /* Constants.swift in Sources */, 320 | 1E6DA75E24A32EFD00001565 /* ElegantVListExample.swift in Sources */, 321 | 1E15AC6924AD8D1D00623E98 /* View+Erased.swift in Sources */, 322 | 1E15AC6124AD8D1D00623E98 /* ElegantVList.swift in Sources */, 323 | 1E6DA72424A3245300001565 /* ContentView.swift in Sources */, 324 | 1E15AC6D24AD8D1D00623E98 /* ElegantHPages.swift in Sources */, 325 | 1E6DA76624A3319900001565 /* ElegantVPagesExample.swift in Sources */, 326 | 1E6DA76024A32F1B00001565 /* ScrollToPageButton.swift in Sources */, 327 | 1E15AC6024AD8D1D00623E98 /* ElegantHList.swift in Sources */, 328 | 1E15AC6524AD8D1D00623E98 /* ElegantPagesManager.swift in Sources */, 329 | 1E6DA76424A3307700001565 /* CustomViews.swift in Sources */, 330 | 1E15AC6224AD8D1D00623E98 /* PageState.swift in Sources */, 331 | 1E15AC6824AD8D1D00623E98 /* ElegantListManager.swift in Sources */, 332 | 1E15AC5E24AD8D1D00623E98 /* ElegantListView.swift in Sources */, 333 | 1E15AC6324AD8D1D00623E98 /* ScrollDirection.swift in Sources */, 334 | 1E15AC6A24AD8D1D00623E98 /* Comparable+Clamped.swift in Sources */, 335 | 1E15AC6724AD8D1D00623E98 /* PageTurnType.swift in Sources */, 336 | 1E15AC6424AD8D1D00623E98 /* PageViewBuilder.swift in Sources */, 337 | ); 338 | runOnlyForDeploymentPostprocessing = 0; 339 | }; 340 | /* End PBXSourcesBuildPhase section */ 341 | 342 | /* Begin PBXVariantGroup section */ 343 | 1E6DA72A24A3245400001565 /* LaunchScreen.storyboard */ = { 344 | isa = PBXVariantGroup; 345 | children = ( 346 | 1E6DA72B24A3245400001565 /* Base */, 347 | ); 348 | name = LaunchScreen.storyboard; 349 | sourceTree = ""; 350 | }; 351 | /* End PBXVariantGroup section */ 352 | 353 | /* Begin XCBuildConfiguration section */ 354 | 1E6DA72E24A3245400001565 /* Debug */ = { 355 | isa = XCBuildConfiguration; 356 | buildSettings = { 357 | ALWAYS_SEARCH_USER_PATHS = NO; 358 | CLANG_ANALYZER_NONNULL = YES; 359 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 360 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 361 | CLANG_CXX_LIBRARY = "libc++"; 362 | CLANG_ENABLE_MODULES = YES; 363 | CLANG_ENABLE_OBJC_ARC = YES; 364 | CLANG_ENABLE_OBJC_WEAK = YES; 365 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 366 | CLANG_WARN_BOOL_CONVERSION = YES; 367 | CLANG_WARN_COMMA = YES; 368 | CLANG_WARN_CONSTANT_CONVERSION = YES; 369 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 370 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 371 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 372 | CLANG_WARN_EMPTY_BODY = YES; 373 | CLANG_WARN_ENUM_CONVERSION = YES; 374 | CLANG_WARN_INFINITE_RECURSION = YES; 375 | CLANG_WARN_INT_CONVERSION = YES; 376 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 377 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 378 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 379 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 380 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 381 | CLANG_WARN_STRICT_PROTOTYPES = YES; 382 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 383 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 384 | CLANG_WARN_UNREACHABLE_CODE = YES; 385 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 386 | COPY_PHASE_STRIP = NO; 387 | DEBUG_INFORMATION_FORMAT = dwarf; 388 | ENABLE_STRICT_OBJC_MSGSEND = YES; 389 | ENABLE_TESTABILITY = YES; 390 | GCC_C_LANGUAGE_STANDARD = gnu11; 391 | GCC_DYNAMIC_NO_PIC = NO; 392 | GCC_NO_COMMON_BLOCKS = YES; 393 | GCC_OPTIMIZATION_LEVEL = 0; 394 | GCC_PREPROCESSOR_DEFINITIONS = ( 395 | "DEBUG=1", 396 | "$(inherited)", 397 | ); 398 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 399 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 400 | GCC_WARN_UNDECLARED_SELECTOR = YES; 401 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 402 | GCC_WARN_UNUSED_FUNCTION = YES; 403 | GCC_WARN_UNUSED_VARIABLE = YES; 404 | IPHONEOS_DEPLOYMENT_TARGET = 13.5; 405 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; 406 | MTL_FAST_MATH = YES; 407 | ONLY_ACTIVE_ARCH = YES; 408 | SDKROOT = iphoneos; 409 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 410 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 411 | }; 412 | name = Debug; 413 | }; 414 | 1E6DA72F24A3245400001565 /* Release */ = { 415 | isa = XCBuildConfiguration; 416 | buildSettings = { 417 | ALWAYS_SEARCH_USER_PATHS = NO; 418 | CLANG_ANALYZER_NONNULL = YES; 419 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 420 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 421 | CLANG_CXX_LIBRARY = "libc++"; 422 | CLANG_ENABLE_MODULES = YES; 423 | CLANG_ENABLE_OBJC_ARC = YES; 424 | CLANG_ENABLE_OBJC_WEAK = YES; 425 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 426 | CLANG_WARN_BOOL_CONVERSION = YES; 427 | CLANG_WARN_COMMA = YES; 428 | CLANG_WARN_CONSTANT_CONVERSION = YES; 429 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 430 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 431 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 432 | CLANG_WARN_EMPTY_BODY = YES; 433 | CLANG_WARN_ENUM_CONVERSION = YES; 434 | CLANG_WARN_INFINITE_RECURSION = YES; 435 | CLANG_WARN_INT_CONVERSION = YES; 436 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 437 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 438 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 439 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 440 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 441 | CLANG_WARN_STRICT_PROTOTYPES = YES; 442 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 443 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 444 | CLANG_WARN_UNREACHABLE_CODE = YES; 445 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 446 | COPY_PHASE_STRIP = NO; 447 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 448 | ENABLE_NS_ASSERTIONS = NO; 449 | ENABLE_STRICT_OBJC_MSGSEND = YES; 450 | GCC_C_LANGUAGE_STANDARD = gnu11; 451 | GCC_NO_COMMON_BLOCKS = YES; 452 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 453 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 454 | GCC_WARN_UNDECLARED_SELECTOR = YES; 455 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 456 | GCC_WARN_UNUSED_FUNCTION = YES; 457 | GCC_WARN_UNUSED_VARIABLE = YES; 458 | IPHONEOS_DEPLOYMENT_TARGET = 13.5; 459 | MTL_ENABLE_DEBUG_INFO = NO; 460 | MTL_FAST_MATH = YES; 461 | SDKROOT = iphoneos; 462 | SWIFT_COMPILATION_MODE = wholemodule; 463 | SWIFT_OPTIMIZATION_LEVEL = "-O"; 464 | VALIDATE_PRODUCT = YES; 465 | }; 466 | name = Release; 467 | }; 468 | 1E6DA73124A3245400001565 /* Debug */ = { 469 | isa = XCBuildConfiguration; 470 | buildSettings = { 471 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 472 | CODE_SIGN_STYLE = Automatic; 473 | DEVELOPMENT_ASSET_PATHS = "\"Example/Preview Content\""; 474 | DEVELOPMENT_TEAM = 75H2NCGMKS; 475 | ENABLE_PREVIEWS = YES; 476 | INFOPLIST_FILE = Example/Info.plist; 477 | IPHONEOS_DEPLOYMENT_TARGET = 13.0; 478 | LD_RUNPATH_SEARCH_PATHS = ( 479 | "$(inherited)", 480 | "@executable_path/Frameworks", 481 | ); 482 | PRODUCT_BUNDLE_IDENTIFIER = com.kevinli.Example; 483 | PRODUCT_NAME = "$(TARGET_NAME)"; 484 | SWIFT_VERSION = 5.0; 485 | TARGETED_DEVICE_FAMILY = "1,2"; 486 | }; 487 | name = Debug; 488 | }; 489 | 1E6DA73224A3245400001565 /* Release */ = { 490 | isa = XCBuildConfiguration; 491 | buildSettings = { 492 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 493 | CODE_SIGN_STYLE = Automatic; 494 | DEVELOPMENT_ASSET_PATHS = "\"Example/Preview Content\""; 495 | DEVELOPMENT_TEAM = 75H2NCGMKS; 496 | ENABLE_PREVIEWS = YES; 497 | INFOPLIST_FILE = Example/Info.plist; 498 | IPHONEOS_DEPLOYMENT_TARGET = 13.0; 499 | LD_RUNPATH_SEARCH_PATHS = ( 500 | "$(inherited)", 501 | "@executable_path/Frameworks", 502 | ); 503 | PRODUCT_BUNDLE_IDENTIFIER = com.kevinli.Example; 504 | PRODUCT_NAME = "$(TARGET_NAME)"; 505 | SWIFT_VERSION = 5.0; 506 | TARGETED_DEVICE_FAMILY = "1,2"; 507 | }; 508 | name = Release; 509 | }; 510 | /* End XCBuildConfiguration section */ 511 | 512 | /* Begin XCConfigurationList section */ 513 | 1E6DA71724A3245300001565 /* Build configuration list for PBXProject "Example" */ = { 514 | isa = XCConfigurationList; 515 | buildConfigurations = ( 516 | 1E6DA72E24A3245400001565 /* Debug */, 517 | 1E6DA72F24A3245400001565 /* Release */, 518 | ); 519 | defaultConfigurationIsVisible = 0; 520 | defaultConfigurationName = Release; 521 | }; 522 | 1E6DA73024A3245400001565 /* Build configuration list for PBXNativeTarget "Example" */ = { 523 | isa = XCConfigurationList; 524 | buildConfigurations = ( 525 | 1E6DA73124A3245400001565 /* Debug */, 526 | 1E6DA73224A3245400001565 /* Release */, 527 | ); 528 | defaultConfigurationIsVisible = 0; 529 | defaultConfigurationName = Release; 530 | }; 531 | /* End XCConfigurationList section */ 532 | }; 533 | rootObject = 1E6DA71424A3245300001565 /* Project object */; 534 | } 535 | -------------------------------------------------------------------------------- /Example/Example.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Example/Example.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Example/Example.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PreviewsEnabled 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Example/Example/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // Kevin Li - 11:04 PM - 6/23/20 2 | 3 | import UIKit 4 | 5 | @UIApplicationMain 6 | class AppDelegate: UIResponder, UIApplicationDelegate { 7 | 8 | 9 | 10 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { 11 | // Override point for customization after application launch. 12 | return true 13 | } 14 | 15 | // MARK: UISceneSession Lifecycle 16 | 17 | func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration { 18 | // Called when a new scene session is being created. 19 | // Use this method to select a configuration to create the new scene with. 20 | return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role) 21 | } 22 | 23 | func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set) { 24 | // Called when the user discards a scene session. 25 | // If any sessions were discarded while the application was not running, this will be called shortly after application:didFinishLaunchingWithOptions. 26 | // Use this method to release any resources that were specific to the discarded scenes, as they will not return. 27 | } 28 | 29 | 30 | } 31 | 32 | -------------------------------------------------------------------------------- /Example/Example/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "scale" : "2x", 6 | "size" : "20x20" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "scale" : "3x", 11 | "size" : "20x20" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "scale" : "2x", 16 | "size" : "29x29" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "scale" : "3x", 21 | "size" : "29x29" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "scale" : "2x", 26 | "size" : "40x40" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "scale" : "3x", 31 | "size" : "40x40" 32 | }, 33 | { 34 | "idiom" : "iphone", 35 | "scale" : "2x", 36 | "size" : "60x60" 37 | }, 38 | { 39 | "idiom" : "iphone", 40 | "scale" : "3x", 41 | "size" : "60x60" 42 | }, 43 | { 44 | "idiom" : "ipad", 45 | "scale" : "1x", 46 | "size" : "20x20" 47 | }, 48 | { 49 | "idiom" : "ipad", 50 | "scale" : "2x", 51 | "size" : "20x20" 52 | }, 53 | { 54 | "idiom" : "ipad", 55 | "scale" : "1x", 56 | "size" : "29x29" 57 | }, 58 | { 59 | "idiom" : "ipad", 60 | "scale" : "2x", 61 | "size" : "29x29" 62 | }, 63 | { 64 | "idiom" : "ipad", 65 | "scale" : "1x", 66 | "size" : "40x40" 67 | }, 68 | { 69 | "idiom" : "ipad", 70 | "scale" : "2x", 71 | "size" : "40x40" 72 | }, 73 | { 74 | "idiom" : "ipad", 75 | "scale" : "1x", 76 | "size" : "76x76" 77 | }, 78 | { 79 | "idiom" : "ipad", 80 | "scale" : "2x", 81 | "size" : "76x76" 82 | }, 83 | { 84 | "idiom" : "ipad", 85 | "scale" : "2x", 86 | "size" : "83.5x83.5" 87 | }, 88 | { 89 | "idiom" : "ios-marketing", 90 | "scale" : "1x", 91 | "size" : "1024x1024" 92 | } 93 | ], 94 | "info" : { 95 | "author" : "xcode", 96 | "version" : 1 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /Example/Example/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Example/Example/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /Example/Example/ContentView.swift: -------------------------------------------------------------------------------- 1 | // Kevin Li - 11:04 PM - 6/23/20 2 | 3 | import SwiftUI 4 | 5 | struct ContentView: View { 6 | 7 | @State var tab: Int = 0 8 | 9 | var body: some View { 10 | TabView { 11 | ElegantHListExample() 12 | .tabItem({ 13 | Text("HList") 14 | }).tag(0) 15 | 16 | ElegantVListExample() 17 | .tabItem({ 18 | Text("VList") 19 | }).tag(1) 20 | 21 | ElegantHPagesExample() 22 | .tabItem({ 23 | Text("HPages") 24 | }).tag(2) 25 | 26 | ElegantVPagesExample() 27 | .tabItem({ 28 | Text("VPages") 29 | }).tag(3) 30 | } 31 | } 32 | 33 | } 34 | 35 | struct ContentView_Previews: PreviewProvider { 36 | static var previews: some View { 37 | ContentView() 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /Example/Example/Example Views/CustomViews.swift: -------------------------------------------------------------------------------- 1 | // Kevin Li - 11:55 PM - 6/23/20 2 | 3 | import SwiftUI 4 | 5 | struct CustomButtonView: View { 6 | 7 | var body: some View { 8 | VStack { 9 | Button(action: { 10 | print("Button 1 tapped") 11 | }, label: { 12 | Text("Button 1") 13 | }) 14 | Button(action: { 15 | print("Button 2 tapped") 16 | }, label: { 17 | Text("Button 2") 18 | }) 19 | } 20 | } 21 | 22 | } 23 | 24 | struct CustomView: View { 25 | 26 | var body: some View { 27 | VStack { 28 | Image(systemName: "globe").resizable() 29 | .scaledToFit() 30 | .frame(width: 50, height: 50) 31 | .foregroundColor(.orange) 32 | Text("Hello world") 33 | .font(.system(size: 24)) 34 | .fontWeight(.bold) 35 | } 36 | } 37 | 38 | } 39 | 40 | struct CustomListView: View { 41 | 42 | var body: some View { 43 | List(0..<10) { (i) in 44 | HStack { 45 | Text("Cell \(i)") 46 | Spacer() 47 | Text("Detail") 48 | } 49 | } 50 | .padding(.top, 150) 51 | .frame(height: screen.height) 52 | } 53 | 54 | } 55 | 56 | let listData = (1...40).map { _ in "Ideally, this should be more dynamic content to make the most use out of this list" } 57 | 58 | struct ExampleView: View { 59 | 60 | let page: Int 61 | 62 | var body: some View { 63 | VStack { 64 | Text("Page \(page)") 65 | .font(.largeTitle) 66 | Text(listData[page]) 67 | .font(.title) 68 | } 69 | .padding() 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /Example/Example/Example Views/ElegantHListExample.swift: -------------------------------------------------------------------------------- 1 | // Kevin Li - 11:08 PM - 6/23/20 2 | 3 | import SwiftUI 4 | 5 | struct ElegantHListExample: View { 6 | 7 | let manager = ElegantListManager(pageCount: listData.count) 8 | 9 | var body: some View { 10 | ZStack(alignment: .topTrailing) { 11 | ElegantHList(manager: manager, 12 | pageTurnType: .earlyCutOffDefault, 13 | viewForPage: exampleView) 14 | .onPageChanged { page in 15 | print("Page \(page) will display") 16 | } 17 | 18 | ScrollToPageButton(pageCount: listData.count, action: animatedScroll) 19 | .padding(.top, 90) 20 | .padding(.trailing, 30) 21 | } 22 | } 23 | 24 | private func exampleView(for page: Int) -> AnyView { 25 | ExampleView(page: page).erased 26 | } 27 | 28 | private func animatedScroll(to page: Int) { 29 | manager.scroll(to: page) 30 | } 31 | 32 | private func unanimatedScroll(to page: Int) { 33 | manager.scroll(to: page, animated: false) 34 | } 35 | 36 | } 37 | 38 | struct ElegantHListExample_Previews: PreviewProvider { 39 | static var previews: some View { 40 | ElegantHListExample() 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /Example/Example/Example Views/ElegantHPagesExample.swift: -------------------------------------------------------------------------------- 1 | // Kevin Li - 11:53 PM - 6/23/20 2 | 3 | import SwiftUI 4 | 5 | struct ElegantHPagesExample: View { 6 | 7 | let manager = ElegantPagesManager(startingPage: 1, pageTurnType: .earlyCutOffDefault) 8 | 9 | var body: some View { 10 | ZStack(alignment: .topTrailing) { 11 | ElegantHPages(manager: manager) { 12 | CustomButtonView() 13 | CustomView() 14 | CustomListView() 15 | } 16 | .onPageChanged { page in 17 | print("Page \(page) will display") 18 | } 19 | 20 | ScrollToPageButton(pageCount: 3, action: animatedScroll) 21 | .padding(.top, 90) 22 | .padding(.trailing, 30) 23 | } 24 | } 25 | 26 | private func animatedScroll(to page: Int) { 27 | manager.scroll(to: page) 28 | } 29 | 30 | private func unanimatedScroll(to page: Int) { 31 | manager.scroll(to: page, animated: false) 32 | } 33 | 34 | } 35 | 36 | struct ElegantHPagesExample_Previews: PreviewProvider { 37 | static var previews: some View { 38 | ElegantHPagesExample() 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /Example/Example/Example Views/ElegantVListExample.swift: -------------------------------------------------------------------------------- 1 | // Kevin Li - 11:49 PM - 6/23/20 2 | 3 | import SwiftUI 4 | 5 | struct ElegantVListExample: View { 6 | 7 | let manager = ElegantListManager(pageCount: listData.count) 8 | 9 | var body: some View { 10 | ZStack(alignment: .topTrailing) { 11 | ElegantVList(manager: manager, 12 | pageTurnType: .earlyCutOffDefault, 13 | viewForPage: exampleView) 14 | .onPageChanged { page in 15 | print("Page \(page) will display") 16 | } 17 | 18 | ScrollToPageButton(pageCount: listData.count, action: animatedScroll) 19 | .padding(.top, 90) 20 | .padding(.trailing, 30) 21 | } 22 | } 23 | 24 | private func exampleView(for page: Int) -> AnyView { 25 | ExampleView(page: page).erased 26 | } 27 | 28 | private func animatedScroll(to page: Int) { 29 | manager.scroll(to: page) 30 | } 31 | 32 | private func unanimatedScroll(to page: Int) { 33 | manager.scroll(to: page, animated: false) 34 | } 35 | 36 | } 37 | 38 | struct ElegantVListExample_Previews: PreviewProvider { 39 | static var previews: some View { 40 | ElegantVListExample() 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /Example/Example/Example Views/ElegantVPagesExample.swift: -------------------------------------------------------------------------------- 1 | // Kevin Li - 12:00 AM - 6/24/20 2 | 3 | import SwiftUI 4 | 5 | struct ElegantVPagesExample: View { 6 | 7 | let manager = ElegantPagesManager(startingPage: 1, pageTurnType: .earlyCutOffDefault) 8 | 9 | var body: some View { 10 | ZStack(alignment: .topTrailing) { 11 | ElegantVPages(manager: manager) { 12 | CustomButtonView() 13 | CustomView() 14 | CustomListView() 15 | } 16 | .onPageChanged { page in 17 | print("Page \(page) will display") 18 | } 19 | 20 | ScrollToPageButton(pageCount: 3, action: animatedScroll) 21 | .padding(.top, 90) 22 | .padding(.trailing, 30) 23 | } 24 | } 25 | 26 | private func animatedScroll(to page: Int) { 27 | manager.scroll(to: page) 28 | } 29 | 30 | private func unanimatedScroll(to page: Int) { 31 | manager.scroll(to: page, animated: false) 32 | } 33 | 34 | } 35 | 36 | struct ElegantVPagesExample_Previews: PreviewProvider { 37 | static var previews: some View { 38 | ElegantVPagesExample() 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /Example/Example/Example Views/ScrollToPageButton.swift: -------------------------------------------------------------------------------- 1 | // Kevin Li - 11:50 PM - 6/23/20 2 | 3 | import SwiftUI 4 | 5 | struct ScrollToPageButton: View { 6 | 7 | let pageCount: Int 8 | let action: (Int) -> Void 9 | 10 | var body: some View { 11 | Button(action: scrollToRandomPage) { 12 | Image(systemName: "shuffle") 13 | .resizable() 14 | .aspectRatio(contentMode: .fit) 15 | .frame(width: 40, height: 40) 16 | } 17 | } 18 | 19 | private func scrollToRandomPage() { 20 | action(Int.random(in: 0.. 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | LSRequiresIPhoneOS 22 | 23 | UIApplicationSceneManifest 24 | 25 | UIApplicationSupportsMultipleScenes 26 | 27 | UISceneConfigurations 28 | 29 | UIWindowSceneSessionRoleApplication 30 | 31 | 32 | UISceneConfigurationName 33 | Default Configuration 34 | UISceneDelegateClassName 35 | $(PRODUCT_MODULE_NAME).SceneDelegate 36 | 37 | 38 | 39 | 40 | UILaunchStoryboardName 41 | LaunchScreen 42 | UIRequiredDeviceCapabilities 43 | 44 | armv7 45 | 46 | UISupportedInterfaceOrientations 47 | 48 | UIInterfaceOrientationPortrait 49 | 50 | UISupportedInterfaceOrientations~ipad 51 | 52 | UIInterfaceOrientationPortrait 53 | UIInterfaceOrientationPortraitUpsideDown 54 | UIInterfaceOrientationLandscapeLeft 55 | UIInterfaceOrientationLandscapeRight 56 | 57 | 58 | 59 | -------------------------------------------------------------------------------- /Example/Example/Preview Content/Preview Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Example/Example/SceneDelegate.swift: -------------------------------------------------------------------------------- 1 | // Kevin Li - 11:04 PM - 6/23/20 2 | 3 | import UIKit 4 | import SwiftUI 5 | 6 | class SceneDelegate: UIResponder, UIWindowSceneDelegate { 7 | 8 | var window: UIWindow? 9 | 10 | 11 | func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { 12 | // Use this method to optionally configure and attach the UIWindow `window` to the provided UIWindowScene `scene`. 13 | // If using a storyboard, the `window` property will automatically be initialized and attached to the scene. 14 | // This delegate does not imply the connecting scene or session are new (see `application:configurationForConnectingSceneSession` instead). 15 | 16 | // Create the SwiftUI view that provides the window contents. 17 | let contentView = ContentView() 18 | 19 | // Use a UIHostingController as window root view controller. 20 | if let windowScene = scene as? UIWindowScene { 21 | let window = UIWindow(windowScene: windowScene) 22 | window.rootViewController = UIHostingController(rootView: contentView) 23 | self.window = window 24 | window.makeKeyAndVisible() 25 | } 26 | } 27 | 28 | func sceneDidDisconnect(_ scene: UIScene) { 29 | // Called as the scene is being released by the system. 30 | // This occurs shortly after the scene enters the background, or when its session is discarded. 31 | // Release any resources associated with this scene that can be re-created the next time the scene connects. 32 | // The scene may re-connect later, as its session was not neccessarily discarded (see `application:didDiscardSceneSessions` instead). 33 | } 34 | 35 | func sceneDidBecomeActive(_ scene: UIScene) { 36 | // Called when the scene has moved from an inactive state to an active state. 37 | // Use this method to restart any tasks that were paused (or not yet started) when the scene was inactive. 38 | } 39 | 40 | func sceneWillResignActive(_ scene: UIScene) { 41 | // Called when the scene will move from an active state to an inactive state. 42 | // This may occur due to temporary interruptions (ex. an incoming phone call). 43 | } 44 | 45 | func sceneWillEnterForeground(_ scene: UIScene) { 46 | // Called as the scene transitions from the background to the foreground. 47 | // Use this method to undo the changes made on entering the background. 48 | } 49 | 50 | func sceneDidEnterBackground(_ scene: UIScene) { 51 | // Called as the scene transitions from the foreground to the background. 52 | // Use this method to save data, release shared resources, and store enough scene-specific state information 53 | // to restore the scene back to its current state. 54 | } 55 | 56 | 57 | } 58 | 59 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Kevin Li 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.1 2 | 3 | import PackageDescription 4 | 5 | let package = Package( 6 | name: "ElegantPages", 7 | platforms: [ 8 | .iOS(.v13) 9 | ], 10 | products: [ 11 | .library( 12 | name: "ElegantPages", 13 | targets: ["ElegantPages"]), 14 | ], 15 | dependencies: [], 16 | targets: [ 17 | .target( 18 | name: "ElegantPages", 19 | dependencies: []) 20 | ] 21 | ) 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ElegantPages 2 | 3 |

4 | Platforms 5 | 6 | License: MIT 7 |

8 | 9 | ElegantPages is an efficient and customizable full screen page view written in SwiftUI. 10 | 11 |
12 | 13 | 14 | 15 | - [Introduction](#introduction) 16 | - [Basic Usage](#basic-usage) 17 | - [How It Works](#how-it-works) 18 | - [Customization](#customization) 19 | - [Demos](#demos) 20 | - [Requirements](#requirements) 21 | - [Contributing](#contributing) 22 | - [Installation](#installation) 23 | - [License](#license) 24 | 25 | ## Introduction 26 | 27 | `ElegantPages` comes with 2 types of components, [`ElegantPagesView`](https://github.com/ThasianX/ElegantPages/blob/master/Sources/ElegantPages/Pages/Internal/ElegantPagesView.swift) and [`ElegantListView`](https://github.com/ThasianX/ElegantPages/blob/master/Sources/ElegantPages/Lists/Internal/ElegantListView.swift). 28 | 29 | For simpler usage, `ElegantPagesView` is recommended as it loads all page views immediately. 30 | 31 | For more complex usage, `ElegantListView` is recommended as it loads page views on demand([learn more](#how-it-works)). 32 | 33 | The elegance of both these views is that they work as a paging component should be intended to work. One bug that is often seen in SwiftUI is that `ScrollView`, `List`, or any `Gesture` almost certainly interferes with other gestures in the view. However, `ElegantPages` fixes this issue and scrolling through a paging component even with embedded `Gestures` works elegantly. 34 | 35 | ## Basic usage 36 | 37 | The `ElegantPagesView` component is available through [`ElegantHPages`](https://github.com/ThasianX/ElegantPages/blob/master/Sources/ElegantPages/Pages/Public/ElegantHPages.swift) and [`ElegantVPages`](https://github.com/ThasianX/ElegantPages/blob/master/Sources/ElegantPages/Pages/Public/ElegantVPages.swift). 38 | 39 | ```swift 40 | 41 | import ElegantPages 42 | 43 | struct ElegantVPagesExample: View { 44 | 45 | let manager = ElegantPagesManager(startingPage: 1, pageTurnType: .earlyCutOffDefault) 46 | 47 | var body: some View { 48 | ElegantVPages(manager: manager) { 49 | CustomButtonView() 50 | CustomView() 51 | CustomListView() 52 | } 53 | } 54 | 55 | } 56 | ``` 57 | 58 | The `ElegantListView` component is available through [`ElegantHList`](https://github.com/ThasianX/ElegantPages/blob/master/Sources/ElegantPages/Lists/Public/ElegantHList.swift) and [`ElegantVList`](https://github.com/ThasianX/ElegantPages/blob/master/Sources/ElegantPages/Lists/Public/ElegantVList.swift). 59 | 60 | ```swift 61 | 62 | import ElegantPages 63 | 64 | let listData = (1...40).map { _ in "Ideally, this should be more dynamic content to make the most use out of this list" } 65 | 66 | struct ElegantVListExample: View { 67 | 68 | let manager = ElegantListManager(pageCount: vListData.count, pageTurnType: .earlyCutOffDefault) 69 | 70 | var body: some View { 71 | ElegantVList(manager: manager, 72 | pageTurnType: .earlyCutOffDefault) { page in 73 | ExampleView(page: page).erased 74 | } 75 | } 76 | 77 | } 78 | 79 | struct ExampleView: View { 80 | 81 | let page: Int 82 | 83 | var body: some View { 84 | VStack { 85 | Text("Page \(page)") 86 | .font(.largeTitle) 87 | Text(listData[page]) 88 | .font(.title) 89 | } 90 | .padding() 91 | } 92 | 93 | } 94 | 95 | ``` 96 | 97 | ## How it works 98 | 99 | `ElegantPagesView` is pretty simple. It uses a [function builder](https://github.com/apple/swift-evolution/blob/9992cf3c11c2d5e0ea20bee98657d93902d5b174/proposals/XXXX-function-builders.md) to gather the page views and puts them in either a `HStack` or `VStack` depending on the type of `ElegantPages` view chosen. As a result, all views are created immediately. 100 | 101 | `ElegantListView` is quite interesting. For more flexibility, it uses a `@ViewBuilder` to get the view for any given page(it's the closure at the end of the `ElegantVList` declaration. When it is first initialized, it calls this closure at most 3 times, to get the views for the starting pages. These views are used to initialize an array of at most 3 `UIHostingControllers`, whose `rootViews` are set to a specific origin in a `UIViewController`. Here's the catch, at any given moment, there are at most only 3 pages loaded. As the user scrolls to the next page, old pages are removed and new pages are inserted; the views themselves are juggled as their origins are changed per page turn. This keeps overall memory usage down and also makes scrolling blazingly fast. If you're curious, take a [peek](https://github.com/ThasianX/ElegantPages/blob/master/Sources/ElegantPages/Lists/Internal/ElegantListController.swift). 102 | 103 | ## Customization 104 | 105 | The following aspects of any `ElegantPages` component can be customized: 106 | 107 | #### `pageTurnType`: Whether to scroll to the next page early or until the user lets go of the drag 108 | 109 | ```swift 110 | 111 | public enum PageTurnType { 112 | 113 | case regular(pageTurnDelta: CGFloat) 114 | case earlyCutoff(config: EarlyCutOffConfiguration) 115 | 116 | } 117 | 118 | public struct EarlyCutOffConfiguration { 119 | 120 | public let scrollResistanceCutOff: CGFloat 121 | public let pageTurnCutOff: CGFloat 122 | public let pageTurnAnimation: Animation 123 | 124 | } 125 | 126 | ``` 127 | 128 | A regular page turn only turns the page after the user ends their drag. 129 | 130 | - The `pageTurnDelta` represents the percentage of how far across the screen the user has to drag in order for the page to turn when they let go. The default value for this is 0.3, as part of an extension of `PageTurnType`. 131 | - The default regular page turn can be accessed through `PageTurnType.regularDefault` 132 | 133 | An early cutoff page turn turns the page when the user drags a certain distance across the screen. 134 | 135 | - `scrollResistanceCutOff`: The distance that the view is offset as the user drags. 136 | - `pageTurnCutOff`: The distance across the screen the user has to drag before the page is turned(once this value is reached, the page automatically gets turned to and the user's ongoing gesture is invalidated). 137 | - `pageTurnAnimation`: The animation used when the page is turned 138 | - The default early cut off page turn can be accessed through `PageTurnType.earlyCutOffDefault` 139 | 140 | In case `scrollResistanceCutOff` isn't clear, here's an example. Say we have a horizontally draggable view. If you drag 80 pixels to the right, the offset that is visible to you is also 80 pixels. The amount you scroll is equal to the visible offset. However, if you have a scroll resistance of say 40 pixels, after dragging 80 pixels to the right, you only see that the view has moved 40 pixels to the right. That is why it is called resistance. 141 | 142 | ###$ `viewForPage`: datasource method called whenever a new page is displayed that asks for the view of the new page. Available only for `ElegantList` components 143 | 144 | ```swift 145 | 146 | // Use as a function 147 | ElegantVList(..., viewForPage: exampleView) 148 | 149 | func exampleView(for page: Int) -> AnyView { ExampleView(...) } 150 | 151 | // Use as a closure 152 | ElegantHList(...) { page in ExampleView(...) } 153 | 154 | ``` 155 | 156 | #### `onPageChanged`: called whenever a new page is shown. Available for all `ElegantPages` components. 157 | 158 | ```swift 159 | 160 | ElegantVList(...) 161 | .onPageChanged(...) 162 | 163 | ElegantHList(...) 164 | .onPageChanged(...) 165 | 166 | ElegantHPages(...) 167 | .onPageChanged(...) 168 | 169 | ElegantVPages(...) 170 | .onPageChanged(...) 171 | 172 | ``` 173 | 174 | #### `frame`: used to set a custom height or width for `ElegantList` components 175 | 176 | ```swift 177 | 178 | // You may want a smaller width for the VList. However, height for the VList will always be the screen height 179 | ElegantVList(...) 180 | .frame(width: ...) 181 | 182 | // You may want a smaller height for the HList. However, width for the HList will always be the screen width 183 | ElegantHList(...) 184 | .frame(height: ...) 185 | 186 | ``` 187 | 188 | ## Demos 189 | 190 | The demo shown in the GIF can be checked out on [ElegantCalendar](https://github.com/ThasianX/ElegantCalendar). 191 | 192 | For simpler demos, look at the [example repo](https://github.com/ThasianX/ElegantPages/tree/master/Example). 193 | 194 | ## Installation 195 | 196 | `ElegantPages` is available using the [Swift Package Manager](https://swift.org/package-manager/): 197 | 198 | Using Xcode 11, go to `File -> Swift Packages -> Add Package Dependency` and enter https://github.com/ThasianX/ElegantPages 199 | 200 | If you are using `Package.swift`, you can also add `ElegantPages` as a dependency easily. 201 | 202 | ```swift 203 | 204 | let package = Package( 205 | name: "TestProject", 206 | dependencies: [ 207 | .package(url: "https://github.com/ThasianX/ElegantPages", from: "1.4.0") 208 | ], 209 | targets: [ 210 | .target(name: "TestProject", dependencies: ["ElegantPages"]) 211 | ] 212 | ) 213 | 214 | ``` 215 | 216 | ## Requirements 217 | 218 | - iOS 13.0+ 219 | - Xcode 11.0+ 220 | 221 | ## Contributing 222 | 223 | If you find a bug, or would like to suggest a new feature or enhancement, it'd be nice if you could [search the issue tracker](https://github.com/ThasianX/ElegantPages/issues) first; while we don't mind duplicates, keeping issues unique helps us save time and considates effort. If you can't find your issue, feel free to [file a new one](https://github.com/ThasianX/ElegantPages/issues/new). 224 | 225 | ## License 226 | 227 | This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details 228 | -------------------------------------------------------------------------------- /Sources/ElegantPages/Extensions/Comparable+Clamped.swift: -------------------------------------------------------------------------------- 1 | // Kevin Li - 6:00 PM - 6/23/20 2 | 3 | import Foundation 4 | 5 | extension Comparable { 6 | 7 | func clamped(to limits: ClosedRange) -> Self { 8 | return min(max(self, limits.lowerBound), limits.upperBound) 9 | } 10 | 11 | } 12 | -------------------------------------------------------------------------------- /Sources/ElegantPages/Extensions/View+Erased.swift: -------------------------------------------------------------------------------- 1 | // Kevin Li - 11:26 PM - 6/23/20 2 | 3 | import SwiftUI 4 | 5 | public extension View { 6 | 7 | var erased: AnyView { 8 | AnyView(self) 9 | } 10 | 11 | } 12 | -------------------------------------------------------------------------------- /Sources/ElegantPages/Lists/Internal/ElegantListController.swift: -------------------------------------------------------------------------------- 1 | // Kevin Li - 6:14 PM - 6/23/20 2 | 3 | import SwiftUI 4 | 5 | private class UpdateUIViewControllerBugFixClass { } 6 | 7 | struct ElegantListController: UIViewControllerRepresentable, ElegantListManagerDirectAccess, PageTurnTypeDirectAccess { 8 | 9 | typealias UIViewControllerType = ElegantTriadPagesController 10 | 11 | // See https://stackoverflow.com/questions/58635048/in-a-uiviewcontrollerrepresentable-how-can-i-pass-an-observedobjects-value-to 12 | private let bugFix = UpdateUIViewControllerBugFixClass() 13 | 14 | @ObservedObject var manager: ElegantListManager 15 | 16 | let axis: Axis 17 | let length: CGFloat 18 | let pageTurnType: PageTurnType 19 | let viewForPage: (Int) -> AnyView 20 | 21 | func makeUIViewController(context: Context) -> ElegantTriadPagesController { 22 | ElegantTriadPagesController(manager: manager, 23 | axis: axis, 24 | length: length, 25 | viewForPage: viewForPage) 26 | } 27 | 28 | func updateUIViewController(_ controller: ElegantTriadPagesController, context: Context) { 29 | controller.viewForPage = viewForPage 30 | DispatchQueue.main.async { 31 | self.setProperPage(for: controller) 32 | } 33 | } 34 | 35 | private func setProperPage(for controller: ElegantTriadPagesController) { 36 | switch currentPage.state { 37 | case .rearrange: 38 | controller.rearrange(manager: manager) { 39 | self.setActiveIndex(1, animated: false, complete: true) // resets to center 40 | } 41 | case let .scroll(animated): 42 | let isFirstPage = currentPage.index == 0 43 | let isLastPage = currentPage.index == pageCount-1 44 | 45 | if isFirstPage || isLastPage { 46 | let pageToTurnTo = isFirstPage ? 0 : maxPageIndex 47 | setActiveIndex(pageToTurnTo, animated: animated, complete: true) 48 | controller.reset(manager: manager) 49 | } else { 50 | let pageToTurnTo = currentPage.index > controller.previousPage ? maxPageIndex : 0 51 | // This first call to `setActiveIndex` is responsible for animating the page 52 | // turn to whatever page we want to scroll to 53 | setActiveIndex(pageToTurnTo, animated: animated, complete: false) 54 | controller.reset(manager: manager) { 55 | self.setActiveIndex(1, animated: false, complete: true) 56 | } 57 | } 58 | case .completed: 59 | () 60 | } 61 | } 62 | 63 | private func setActiveIndex(_ index: Int, animated: Bool, complete: Bool) { 64 | withAnimation(animated ? pageTurnAnimation : nil) { 65 | self.manager.activeIndex = index 66 | } 67 | 68 | if complete { 69 | manager.currentPage.state = .completed 70 | } 71 | } 72 | 73 | } 74 | 75 | class ElegantTriadPagesController: UIViewController { 76 | 77 | private var controllers: [UIHostingController] 78 | private(set) var previousPage: Int 79 | 80 | let axis: Axis 81 | var viewForPage: (Int) -> AnyView 82 | 83 | init(manager: ElegantListManager, 84 | axis: Axis, 85 | length: CGFloat, 86 | viewForPage: @escaping (Int) -> AnyView) { 87 | self.axis = axis 88 | self.viewForPage = viewForPage 89 | previousPage = manager.currentPage.index 90 | 91 | controllers = manager.pageRange.map { page in 92 | UIHostingController(rootView: viewForPage(page)) 93 | } 94 | super.init(nibName: nil, bundle: nil) 95 | 96 | controllers.enumerated().forEach { i, controller in 97 | addChild(controller) 98 | 99 | if axis == .horizontal { 100 | controller.view.frame = CGRect(x: screen.width * CGFloat(i), 101 | y: 0, 102 | width: screen.width, 103 | height: length) 104 | } else { 105 | controller.view.frame = CGRect(x: 0, 106 | y: screen.height * CGFloat(i), 107 | width: length, 108 | height: screen.height) 109 | } 110 | 111 | view.addSubview(controller.view) 112 | controller.didMove(toParent: self) 113 | } 114 | } 115 | 116 | required init?(coder: NSCoder) { 117 | fatalError("init(coder:) has not been implemented") 118 | } 119 | 120 | func rearrange(manager: ElegantListManager, completion: @escaping () -> Void) { 121 | defer { 122 | previousPage = manager.currentPage.index 123 | } 124 | 125 | // rearrange if... 126 | guard manager.currentPage.index != previousPage && // not same page 127 | (previousPage != 0 && 128 | manager.currentPage.index != 0) && // not 1st or 2nd page 129 | (previousPage != manager.pageCount-1 && 130 | manager.currentPage.index != manager.pageCount-1) // not last page or 2nd to last page 131 | else { 132 | manager.currentPage.state = .completed 133 | return 134 | } 135 | 136 | rearrangeControllersAndUpdatePage(manager: manager) 137 | resetPagePositions() 138 | completion() 139 | } 140 | 141 | private func rearrangeControllersAndUpdatePage(manager: ElegantListManager) { 142 | if manager.currentPage.index > previousPage { // scrolled down 143 | controllers.append(controllers.removeFirst()) 144 | controllers.last!.rootView = viewForPage(manager.currentPage.index+1) 145 | } else { // scrolled up 146 | controllers.insert(controllers.removeLast(), at: 0) 147 | controllers.first!.rootView = viewForPage(manager.currentPage.index-1) 148 | } 149 | } 150 | 151 | private func resetPagePositions() { 152 | controllers.enumerated().forEach { i, controller in 153 | if axis == .horizontal { 154 | controller.view.frame.origin = CGPoint(x: screen.width * CGFloat(i), y: 0) 155 | } else { 156 | controller.view.frame.origin = CGPoint(x: 0, y: screen.height * CGFloat(i)) 157 | } 158 | } 159 | } 160 | 161 | func reset(manager: ElegantListManager, completion: (() -> Void)? = nil) { 162 | defer { 163 | previousPage = manager.currentPage.index 164 | } 165 | 166 | zip(controllers, manager.pageRange).forEach { controller, page in 167 | controller.rootView = viewForPage(page) 168 | } 169 | 170 | completion?() 171 | } 172 | 173 | } 174 | 175 | private extension ElegantListManager { 176 | 177 | var pageRange: ClosedRange { 178 | let startingPage: Int 179 | 180 | if currentPage.index == pageCount-1 { 181 | startingPage = (pageCount-3).clamped(to: 0...pageCount-1) 182 | } else { 183 | startingPage = (currentPage.index-1).clamped(to: 0...pageCount-1) 184 | } 185 | 186 | let trailingPage = (startingPage+2).clamped(to: 0...pageCount-1) 187 | 188 | return startingPage...trailingPage 189 | } 190 | 191 | } 192 | -------------------------------------------------------------------------------- /Sources/ElegantPages/Lists/Internal/ElegantListView.swift: -------------------------------------------------------------------------------- 1 | // Kevin Li - 8:48 PM - 6/23/20 2 | 3 | import SwiftUI 4 | 5 | struct ElegantListView: View, ElegantListManagerDirectAccess, PageTurnTypeDirectAccess where List: View { 6 | 7 | @State private var translation: CGFloat = .zero 8 | @State private var isTurningPage = false 9 | 10 | @ObservedObject var manager: ElegantListManager 11 | 12 | let listView: List 13 | let isHorizontal: Bool 14 | let pageTurnType: PageTurnType 15 | let bounces: Bool 16 | 17 | public var body: some View { 18 | listView 19 | .offset(offset) 20 | .contentShape(Rectangle()) 21 | .simultaneousGesture( 22 | DragGesture() 23 | .onChanged { value in 24 | let axisOffset = self.isHorizontal ? value.translation.width : value.translation.height 25 | let nonAxisOffset = self.isHorizontal ? value.translation.height : value.translation.width 26 | 27 | guard abs(nonAxisOffset) < abs(axisOffset) else { 28 | self.translation = .zero 29 | return 30 | } 31 | 32 | withAnimation(self.pageTurnAnimation) { 33 | self.setTranslationForOffset(axisOffset) 34 | self.turnPageIfNeededForChangingOffset(axisOffset) 35 | } 36 | } 37 | .onEnded { value in 38 | let axisOffset = self.isHorizontal ? value.translation.width : value.translation.height 39 | withAnimation(self.pageTurnAnimation) { 40 | self.turnPageIfNeededForEndOffset(axisOffset) 41 | } 42 | } 43 | ) 44 | } 45 | 46 | } 47 | 48 | private extension ElegantListView { 49 | 50 | var offset: CGSize { 51 | if isHorizontal { 52 | return CGSize(width: horizontalScrollOffset, height: 0.0) 53 | } else { 54 | return CGSize(width: 0.0, height: verticalScrollOffset) 55 | } 56 | } 57 | 58 | var horizontalScrollOffset: CGFloat { 59 | horizontalOffset + properTranslation 60 | } 61 | 62 | var horizontalOffset: CGFloat { 63 | -CGFloat(activeIndex) * screen.width 64 | } 65 | 66 | var verticalOffset: CGFloat { 67 | -CGFloat(activeIndex) * screen.height 68 | } 69 | 70 | var verticalScrollOffset: CGFloat { 71 | verticalOffset + properTranslation 72 | } 73 | 74 | var properTranslation: CGFloat { 75 | guard !bounces else { return translation } 76 | 77 | if (activeIndex == 0 && translation > 0) || 78 | (activeIndex == maxPageIndex && translation < 0) { 79 | return 0 80 | } 81 | return translation 82 | } 83 | 84 | } 85 | 86 | private extension ElegantListView { 87 | 88 | func setTranslationForOffset(_ offset: CGFloat) { 89 | switch pageTurnType { 90 | case .regular: 91 | translation = offset 92 | case let .earlyCutoff(config): 93 | translation = isTurningPage ? .zero : (offset / config.pageTurnCutOff) * config.scrollResistanceCutOff 94 | } 95 | } 96 | 97 | func turnPageIfNeededForChangingOffset(_ offset: CGFloat) { 98 | switch pageTurnType { 99 | case .regular: 100 | return 101 | case let .earlyCutoff(config): 102 | guard !isTurningPage else { return } 103 | 104 | if offset > 0 && offset > config.pageTurnCutOff { 105 | guard activeIndex != 0 else { return } 106 | 107 | scroll(direction: .backward) 108 | } else if offset < 0 && offset < -config.pageTurnCutOff { 109 | guard activeIndex != maxPageIndex else { return } 110 | 111 | scroll(direction: .forward) 112 | } 113 | } 114 | } 115 | 116 | func scroll(direction: ScrollDirection) { 117 | isTurningPage = true // Prevents user drag from continuing 118 | translation = .zero 119 | 120 | manager.activeIndex = activeIndex + direction.additiveFactor 121 | manager.setCurrentPageToBeRearranged() 122 | } 123 | 124 | func turnPageIfNeededForEndOffset(_ offset: CGFloat) { 125 | translation = .zero 126 | 127 | switch pageTurnType { 128 | case let .regular(delta): 129 | let axisLength = isHorizontal ? screen.width : screen.height 130 | let dragDelta = offset / axisLength 131 | 132 | if abs(dragDelta) > delta { 133 | let properNewIndex = (dragDelta > 0 ? activeIndex-1 : activeIndex+1).clamped(to: 0...maxPageIndex) 134 | manager.activeIndex = properNewIndex 135 | manager.setCurrentPageToBeRearranged() 136 | } 137 | case .earlyCutoff: 138 | isTurningPage = false 139 | } 140 | } 141 | 142 | } 143 | -------------------------------------------------------------------------------- /Sources/ElegantPages/Lists/Public/ElegantHList.swift: -------------------------------------------------------------------------------- 1 | // Kevin Li - 6:13 PM - 6/23/20 2 | 3 | import SwiftUI 4 | 5 | public struct ElegantHList: View, ElegantListManagerDirectAccess { 6 | 7 | @ObservedObject public var manager: ElegantListManager 8 | public let pageTurnType: PageTurnType 9 | public let bounces: Bool 10 | public let viewForPage: (Int) -> AnyView 11 | 12 | private var pagerWidth: CGFloat { 13 | screen.width * CGFloat(maxPageIndex+1) 14 | } 15 | 16 | public init(manager: ElegantListManager, 17 | pageTurnType: PageTurnType, 18 | bounces: Bool = false, 19 | viewForPage: @escaping (Int) -> AnyView) { 20 | self.manager = manager 21 | self.pageTurnType = pageTurnType 22 | self.bounces = bounces 23 | self.viewForPage = viewForPage 24 | } 25 | 26 | public var body: some View { 27 | GeometryReader { geometry in 28 | ElegantListView(manager: self.manager, 29 | listView: self.listView(geometry: geometry), 30 | isHorizontal: true, 31 | pageTurnType: self.pageTurnType, 32 | bounces: self.bounces) 33 | } 34 | } 35 | 36 | private func listView(geometry: GeometryProxy) -> some View { 37 | HStack(alignment: .center, spacing: 0) { 38 | ElegantListController(manager: manager, 39 | axis: .horizontal, 40 | length: geometry.size.height, 41 | pageTurnType: pageTurnType, 42 | viewForPage: viewForPage) 43 | .frame(width: pagerWidth) 44 | } 45 | .frame(width: screen.width, height: geometry.size.height, alignment: .leading) 46 | } 47 | 48 | } 49 | 50 | extension ElegantHList { 51 | 52 | public func onPageChanged(_ callback: ((Int) -> Void)?) -> Self { 53 | manager.anyCancellable = manager.$currentPage 54 | .dropFirst() 55 | .filter { $0.state == .completed } 56 | .map { $0.index } 57 | .sink { page in 58 | callback?(page) 59 | } 60 | return self 61 | } 62 | 63 | } 64 | -------------------------------------------------------------------------------- /Sources/ElegantPages/Lists/Public/ElegantVList.swift: -------------------------------------------------------------------------------- 1 | // Kevin Li - 6:13 PM - 6/23/20 2 | 3 | import SwiftUI 4 | 5 | public struct ElegantVList: View, ElegantListManagerDirectAccess { 6 | 7 | @ObservedObject public var manager: ElegantListManager 8 | public let pageTurnType: PageTurnType 9 | public let bounces: Bool 10 | public let viewForPage: (Int) -> AnyView 11 | 12 | private var pagerHeight: CGFloat { 13 | screen.height * CGFloat(maxPageIndex+1) 14 | } 15 | 16 | public init(manager: ElegantListManager, 17 | pageTurnType: PageTurnType, 18 | bounces: Bool = false, 19 | viewForPage: @escaping (Int) -> AnyView) { 20 | self.manager = manager 21 | self.pageTurnType = pageTurnType 22 | self.bounces = bounces 23 | self.viewForPage = viewForPage 24 | } 25 | 26 | public var body: some View { 27 | GeometryReader { geometry in 28 | ElegantListView(manager: self.manager, 29 | listView: self.listView(geometry: geometry), 30 | isHorizontal: false, 31 | pageTurnType: self.pageTurnType, 32 | bounces: self.bounces) 33 | } 34 | } 35 | 36 | private func listView(geometry: GeometryProxy) -> some View { 37 | VStack(alignment: .center, spacing: 0) { 38 | ElegantListController(manager: manager, 39 | axis: .vertical, 40 | length: geometry.size.width, 41 | pageTurnType: pageTurnType, 42 | viewForPage: viewForPage) 43 | .frame(height: pagerHeight) 44 | } 45 | .frame(width: geometry.size.width, height: screen.height, alignment: .top) 46 | } 47 | 48 | } 49 | 50 | extension ElegantVList { 51 | 52 | public func onPageChanged(_ callback: ((Int) -> Void)?) -> Self { 53 | manager.anyCancellable = manager.$currentPage 54 | .dropFirst() 55 | .filter { $0.state == .completed } 56 | .map { $0.index } 57 | .sink { page in 58 | callback?(page) 59 | } 60 | return self 61 | } 62 | 63 | } 64 | -------------------------------------------------------------------------------- /Sources/ElegantPages/Models/Internal/Constants.swift: -------------------------------------------------------------------------------- 1 | // Kevin Li - 11:29 PM - 7/16/20 2 | 3 | import UIKit 4 | 5 | let screen = UIScreen.main.bounds 6 | -------------------------------------------------------------------------------- /Sources/ElegantPages/Models/Internal/PageState.swift: -------------------------------------------------------------------------------- 1 | // Kevin Li - 6:07 PM - 6/23/20 2 | 3 | import Foundation 4 | 5 | enum PageState: Equatable { 6 | 7 | case rearrange 8 | case scroll(animated: Bool) 9 | case completed 10 | 11 | } 12 | -------------------------------------------------------------------------------- /Sources/ElegantPages/Models/Internal/ScrollDirection.swift: -------------------------------------------------------------------------------- 1 | // Kevin Li - 6:15 PM - 6/23/20 2 | 3 | import Foundation 4 | 5 | enum ScrollDirection { 6 | 7 | case backward 8 | case forward 9 | 10 | var additiveFactor: Int { 11 | self == .backward ? -1 : 1 12 | } 13 | 14 | } 15 | -------------------------------------------------------------------------------- /Sources/ElegantPages/Models/Public/ElegantListManager.swift: -------------------------------------------------------------------------------- 1 | // Kevin Li - 6:35 PM - 6/23/20 2 | 3 | import Combine 4 | import SwiftUI 5 | 6 | public class ElegantListManager: ObservableObject { 7 | 8 | @Published var currentPage: (index: Int, state: PageState) 9 | @Published var activeIndex: Int 10 | 11 | public let pageCount: Int 12 | 13 | let maxPageIndex: Int 14 | 15 | public var currentPageIndex: Int { 16 | currentPage.index 17 | } 18 | 19 | var anyCancellable: AnyCancellable? 20 | 21 | public init(startingPage: Int = 0, pageCount: Int) { 22 | guard pageCount > 0 else { fatalError("Error: pages must exist") } 23 | 24 | currentPage = (startingPage, .completed) 25 | self.pageCount = pageCount 26 | maxPageIndex = (pageCount-1).clamped(to: 0...2) 27 | 28 | if startingPage == 0 { 29 | activeIndex = 0 30 | } else if startingPage == pageCount-1 { 31 | activeIndex = maxPageIndex 32 | } else { 33 | activeIndex = 1 34 | } 35 | } 36 | 37 | public func scroll(to page: Int, animated: Bool = true) { 38 | currentPage = (page, .scroll(animated: animated)) 39 | } 40 | 41 | public func reloadPages() { 42 | currentPage = (currentPage.index, .scroll(animated: false)) 43 | } 44 | 45 | // Only ever called for a page view with more than 3 pages 46 | func setCurrentPageToBeRearranged() { 47 | var currentIndex = currentPage.index 48 | 49 | // only ever called when turning from the first or last page 50 | if activeIndex == 1 { 51 | if currentIndex == 0 { 52 | // just scrolled from first page to second page 53 | currentIndex += 1 54 | } else if currentIndex == pageCount-1 { 55 | // just scrolled from last page to second to last page 56 | currentIndex -= 1 57 | } else { 58 | return 59 | } 60 | } else { 61 | if activeIndex == 0 { 62 | // case where you're on the first page and you drag and stay on the first page 63 | guard currentIndex != 0 else { return } 64 | currentIndex -= 1 65 | } else if activeIndex == 2 { 66 | // case where you're on the last page and you drag and stay on the last page 67 | guard currentIndex != pageCount-1 else { return } 68 | currentIndex += 1 69 | } 70 | } 71 | 72 | currentPage = (currentIndex, .rearrange) 73 | } 74 | 75 | } 76 | 77 | protocol ElegantListManagerDirectAccess { 78 | 79 | var manager: ElegantListManager { get } 80 | 81 | } 82 | 83 | extension ElegantListManagerDirectAccess { 84 | 85 | var currentPage: (index: Int, state: PageState) { 86 | manager.currentPage 87 | } 88 | 89 | var pageCount: Int { 90 | manager.pageCount 91 | } 92 | 93 | var activeIndex: Int { 94 | manager.activeIndex 95 | } 96 | 97 | var maxPageIndex: Int { 98 | manager.maxPageIndex 99 | } 100 | 101 | } 102 | -------------------------------------------------------------------------------- /Sources/ElegantPages/Models/Public/ElegantPagesManager.swift: -------------------------------------------------------------------------------- 1 | // Kevin Li - 5:57 PM - 6/23/20 2 | 3 | import Combine 4 | import SwiftUI 5 | 6 | public class ElegantPagesManager: ObservableObject, PageTurnTypeDirectAccess { 7 | 8 | /** 9 | The index of current page 0...N-1. 10 | 11 | To set a new current page with animation, use `scroll(to:)`. 12 | If you want a `Binding` variable, you can create a custom `Binding` through: 13 | ``` 14 | let binding = Binding( 15 | get: { self.manager.currentPage }, 16 | set: { self.manager.scroll(to: $0) }) 17 | ``` 18 | */ 19 | @Published public var currentPage: Int 20 | 21 | public let pageTurnType: PageTurnType 22 | 23 | var anyCancellable: AnyCancellable? 24 | 25 | public init(startingPage: Int = 0, pageTurnType: PageTurnType) { 26 | currentPage = startingPage 27 | self.pageTurnType = pageTurnType 28 | } 29 | 30 | public func scroll(to page: Int, animated: Bool = true) { 31 | withAnimation(animated ? pageTurnAnimation : nil) { 32 | currentPage = page 33 | } 34 | } 35 | 36 | } 37 | 38 | protocol ElegantPagesManagerDirectAccess: PageTurnTypeDirectAccess { 39 | 40 | var manager: ElegantPagesManager { get } 41 | var pageTurnType: PageTurnType { get } 42 | 43 | } 44 | 45 | extension ElegantPagesManagerDirectAccess { 46 | 47 | var currentPage: Int { 48 | manager.currentPage 49 | } 50 | 51 | var pageTurnType: PageTurnType { 52 | manager.pageTurnType 53 | } 54 | 55 | } 56 | -------------------------------------------------------------------------------- /Sources/ElegantPages/Models/Public/PageTurnType.swift: -------------------------------------------------------------------------------- 1 | // Kevin Li - 6:07 PM - 6/23/20 2 | 3 | import SwiftUI 4 | 5 | public enum PageTurnType { 6 | 7 | case regular(pageTurnDelta: CGFloat) 8 | case earlyCutoff(config: EarlyCutOffConfiguration) 9 | 10 | } 11 | 12 | public struct EarlyCutOffConfiguration { 13 | 14 | public let scrollResistanceCutOff: CGFloat 15 | public let pageTurnCutOff: CGFloat 16 | public let pageTurnAnimation: Animation 17 | 18 | public init(scrollResistanceCutOff: CGFloat, pageTurnCutOff: CGFloat, pageTurnAnimation: Animation) { 19 | self.scrollResistanceCutOff = scrollResistanceCutOff 20 | self.pageTurnCutOff = pageTurnCutOff 21 | self.pageTurnAnimation = pageTurnAnimation 22 | } 23 | 24 | } 25 | 26 | public extension PageTurnType { 27 | 28 | static let regularDefault = Self.regular(pageTurnDelta: 0.3) 29 | static let earlyCutOffDefault = Self.earlyCutoff(config: .default) 30 | 31 | } 32 | 33 | public extension EarlyCutOffConfiguration { 34 | 35 | static let `default` = EarlyCutOffConfiguration( 36 | scrollResistanceCutOff: 40, 37 | pageTurnCutOff: 80, 38 | pageTurnAnimation: .spring(response: 0.4, dampingFraction: 0.95)) 39 | 40 | } 41 | 42 | 43 | protocol PageTurnTypeDirectAccess { 44 | 45 | var pageTurnType: PageTurnType { get } 46 | 47 | } 48 | 49 | extension PageTurnTypeDirectAccess { 50 | 51 | var pageTurnAnimation: Animation { 52 | switch pageTurnType { 53 | case .regular: 54 | return .easeInOut 55 | case let .earlyCutoff(config): 56 | return config.pageTurnAnimation 57 | } 58 | } 59 | 60 | } 61 | -------------------------------------------------------------------------------- /Sources/ElegantPages/Models/Public/PageViewBuilder.swift: -------------------------------------------------------------------------------- 1 | // Kevin Li - 6:01 PM - 6/23/20 2 | 3 | import SwiftUI 4 | 5 | // Kudos: https://github.com/fredyshox/PageView 6 | 7 | /** 8 | Container for composite pages 9 | */ 10 | public struct PageContainer: View where Content: View { 11 | 12 | let count: Int 13 | let content: Content 14 | 15 | public var body: some View { 16 | content 17 | } 18 | 19 | } 20 | 21 | /** 22 | Modified version of ViewBuilder, which does the counting of child views. 23 | Other than that it works the same way as the original 24 | */ 25 | @_functionBuilder 26 | public struct PageViewBuilder { 27 | 28 | public static func buildBlock(_ c: Content) -> PageContainer { 29 | return PageContainer(count: 1, content: c) 30 | } 31 | 32 | public static func buildBlock( 33 | _ c0: C0, 34 | _ c1: C1 35 | ) -> PageContainer> 36 | where 37 | C0 : View, 38 | C1 : View 39 | { 40 | let compositeView = ViewBuilder.buildBlock(c0, c1) 41 | return PageContainer(count: 2, content: compositeView) 42 | } 43 | 44 | public static func buildBlock( 45 | _ c0: C0, 46 | _ c1: C1, 47 | _ c2: C2 48 | ) -> PageContainer> 49 | where 50 | C0 : View, 51 | C1 : View, 52 | C2 : View 53 | { 54 | let compositeView = ViewBuilder.buildBlock(c0, c1, c2) 55 | return PageContainer(count: 3, content: compositeView) 56 | } 57 | 58 | public static func buildBlock( 59 | _ c0: C0, 60 | _ c1: C1, 61 | _ c2: C2, 62 | _ c3: C3 63 | ) -> PageContainer> 64 | where 65 | C0 : View, 66 | C1 : View, 67 | C2 : View, 68 | C3 : View 69 | { 70 | let compositeView = ViewBuilder.buildBlock(c0, c1, c2, c3) 71 | return PageContainer(count: 4, content: compositeView) 72 | } 73 | 74 | public static func buildBlock( 75 | _ c0: C0, 76 | _ c1: C1, 77 | _ c2: C2, 78 | _ c3: C3, 79 | _ c4: C4 80 | ) -> PageContainer> 81 | where 82 | C0 : View, 83 | C1 : View, 84 | C2 : View, 85 | C3 : View, 86 | C4 : View 87 | { 88 | let compositeView = ViewBuilder.buildBlock(c0, c1, c2, c3, c4) 89 | return PageContainer(count: 5, content: compositeView) 90 | } 91 | 92 | public static func buildBlock( 93 | _ c0: C0, 94 | _ c1: C1, 95 | _ c2: C2, 96 | _ c3: C3, 97 | _ c4: C4, 98 | _ c5: C5 99 | ) -> PageContainer> 100 | where 101 | C0 : View, 102 | C1 : View, 103 | C2 : View, 104 | C3 : View, 105 | C4 : View, 106 | C5 : View 107 | { 108 | let compositeView = ViewBuilder.buildBlock(c0, c1, c2, c3, c4, c5) 109 | return PageContainer(count: 6, content: compositeView) 110 | } 111 | 112 | public static func buildBlock( 113 | _ c0: C0, 114 | _ c1: C1, 115 | _ c2: C2, 116 | _ c3: C3, 117 | _ c4: C4, 118 | _ c5: C5, 119 | _ c6: C6 120 | ) -> PageContainer> 121 | where 122 | C0 : View, 123 | C1 : View, 124 | C2 : View, 125 | C3 : View, 126 | C4 : View, 127 | C5 : View, 128 | C6 : View 129 | { 130 | let compositeView = ViewBuilder.buildBlock(c0, c1, c2, c3, c4, c5, c6) 131 | return PageContainer(count: 7, content: compositeView) 132 | } 133 | 134 | public static func buildBlock( 135 | _ c0: C0, 136 | _ c1: C1, 137 | _ c2: C2, 138 | _ c3: C3, 139 | _ c4: C4, 140 | _ c5: C5, 141 | _ c6: C6, 142 | _ c7: C7 143 | ) -> PageContainer> 144 | where 145 | C0 : View, 146 | C1 : View, 147 | C2 : View, 148 | C3 : View, 149 | C4 : View, 150 | C5 : View, 151 | C6 : View, 152 | C7 : View 153 | { 154 | let compositeView = ViewBuilder.buildBlock(c0, c1, c2, c3, c4, c5, c6, c7) 155 | return PageContainer(count: 8, content: compositeView) 156 | } 157 | 158 | public static func buildBlock( 159 | _ c0: C0, 160 | _ c1: C1, 161 | _ c2: C2, 162 | _ c3: C3, 163 | _ c4: C4, 164 | _ c5: C5, 165 | _ c6: C6, 166 | _ c7: C7, 167 | _ c8: C8 168 | ) -> PageContainer> 169 | where 170 | C0 : View, 171 | C1 : View, 172 | C2 : View, 173 | C3 : View, 174 | C4 : View, 175 | C5 : View, 176 | C6 : View, 177 | C7 : View, 178 | C8 : View 179 | { 180 | let compositeView = ViewBuilder.buildBlock(c0, c1, c2, c3, c4, c5, c6, c7, c8) 181 | return PageContainer(count: 9, content: compositeView) 182 | } 183 | 184 | public static func buildBlock( 185 | _ c0: C0, 186 | _ c1: C1, 187 | _ c2: C2, 188 | _ c3: C3, 189 | _ c4: C4, 190 | _ c5: C5, 191 | _ c6: C6, 192 | _ c7: C7, 193 | _ c8: C8, 194 | _ c9: C9 195 | ) -> PageContainer> 196 | where 197 | C0 : View, 198 | C1 : View, 199 | C2 : View, 200 | C3 : View, 201 | C4 : View, 202 | C5 : View, 203 | C6 : View, 204 | C7 : View, 205 | C8 : View, 206 | C9 : View 207 | { 208 | let compositeView = ViewBuilder.buildBlock(c0, c1, c2, c3, c4, c5, c6, c7, c8, c9) 209 | return PageContainer(count: 10, content: compositeView) 210 | } 211 | 212 | } 213 | -------------------------------------------------------------------------------- /Sources/ElegantPages/Pages/Internal/ElegantPagesView.swift: -------------------------------------------------------------------------------- 1 | // Kevin Li - 6:41 PM - 6/23/20 2 | 3 | import SwiftUI 4 | 5 | struct ElegantPagesView: View, ElegantPagesManagerDirectAccess where Stack: View { 6 | 7 | @State private var translation: CGFloat = .zero 8 | @State private var isDragging = false 9 | @State private var isTurningPage = false 10 | 11 | @ObservedObject var manager: ElegantPagesManager 12 | 13 | let stackView: Stack 14 | let pageCount: Int 15 | let isHorizontal: Bool 16 | let bounces: Bool 17 | 18 | private var minDragDistance: CGFloat { 19 | switch pageTurnType { 20 | case .regular(let delta): 21 | return delta*screen.width / 5 22 | case .earlyCutoff(let config): 23 | return config.pageTurnCutOff / 5 24 | } 25 | } 26 | 27 | var body: some View { 28 | stackView 29 | .offset(offset) 30 | .contentShape(Rectangle()) 31 | .simultaneousGesture( 32 | DragGesture() 33 | .onChanged { value in 34 | let axisOffset = self.isHorizontal ? value.translation.width : value.translation.height 35 | let nonAxisOffset = self.isHorizontal ? value.translation.height : value.translation.width 36 | 37 | guard abs(nonAxisOffset) < abs(axisOffset) else { 38 | self.translation = .zero 39 | return 40 | } 41 | 42 | if abs(axisOffset) > self.minDragDistance { 43 | self.isDragging = true 44 | } 45 | 46 | withAnimation(self.pageTurnAnimation) { 47 | self.setTranslationForOffset(axisOffset) 48 | self.turnPageIfNeededForChangingOffset(axisOffset) 49 | } 50 | } 51 | .onEnded { value in 52 | guard self.isDragging else { return } 53 | 54 | let axisOffset = self.isHorizontal ? value.translation.width : value.translation.height 55 | 56 | self.isDragging = false 57 | 58 | withAnimation(self.pageTurnAnimation) { 59 | self.turnPageIfNeededForEndOffset(axisOffset) 60 | } 61 | }, 62 | including: isDragging ? .gesture : .all 63 | ) 64 | } 65 | 66 | } 67 | 68 | private extension ElegantPagesView { 69 | 70 | var offset: CGSize { 71 | if isHorizontal { 72 | return CGSize(width: horizontalScrollOffset, height: 0.0) 73 | } else { 74 | return CGSize(width: 0.0, height: verticalScrollOffset) 75 | } 76 | } 77 | 78 | var horizontalScrollOffset: CGFloat { 79 | horizontalOffset + properTranslation 80 | } 81 | 82 | var horizontalOffset: CGFloat { 83 | -CGFloat(currentPage) * screen.width 84 | } 85 | 86 | var verticalOffset: CGFloat { 87 | -CGFloat(currentPage) * screen.height 88 | } 89 | 90 | var verticalScrollOffset: CGFloat { 91 | verticalOffset + properTranslation 92 | } 93 | 94 | var properTranslation: CGFloat { 95 | guard !bounces else { return translation } 96 | 97 | if (currentPage == 0 && translation > 0) || 98 | (currentPage == pageCount-1 && translation < 0) { 99 | return 0 100 | } 101 | return translation 102 | } 103 | 104 | } 105 | 106 | private extension ElegantPagesView { 107 | 108 | private func setTranslationForOffset(_ offset: CGFloat) { 109 | switch pageTurnType { 110 | case .regular: 111 | translation = offset 112 | case let .earlyCutoff(config): 113 | translation = isTurningPage ? .zero : (offset / config.pageTurnCutOff) * config.scrollResistanceCutOff 114 | } 115 | } 116 | 117 | private func turnPageIfNeededForChangingOffset(_ offset: CGFloat) { 118 | switch pageTurnType { 119 | case .regular: 120 | return 121 | case let .earlyCutoff(config): 122 | guard !isTurningPage else { return } 123 | 124 | if offset > 0 && offset > config.pageTurnCutOff { 125 | guard currentPage != 0 else { return } 126 | 127 | scroll(direction: .backward) 128 | } else if offset < 0 && offset < -config.pageTurnCutOff { 129 | guard currentPage != pageCount-1 else { return } 130 | 131 | scroll(direction: .forward) 132 | } 133 | } 134 | } 135 | 136 | private func scroll(direction: ScrollDirection) { 137 | isTurningPage = true // Prevents user drag from continuing 138 | translation = .zero 139 | 140 | manager.currentPage = currentPage + direction.additiveFactor 141 | } 142 | 143 | private func turnPageIfNeededForEndOffset(_ offset: CGFloat) { 144 | translation = .zero 145 | 146 | switch pageTurnType { 147 | case let .regular(delta): 148 | let axisLength = isHorizontal ? screen.width : screen.height 149 | let dragDelta = offset / axisLength 150 | 151 | if abs(dragDelta) > delta { 152 | let properNewIndex = (dragDelta > 0 ? currentPage-1 : currentPage+1).clamped(to: 0...pageCount-1) 153 | if properNewIndex != currentPage { 154 | manager.currentPage = properNewIndex 155 | } 156 | } 157 | case .earlyCutoff: 158 | isTurningPage = false 159 | } 160 | } 161 | 162 | } 163 | -------------------------------------------------------------------------------- /Sources/ElegantPages/Pages/Public/ElegantHPages.swift: -------------------------------------------------------------------------------- 1 | // Kevin Li - 6:11 PM - 6/23/20 2 | 3 | import SwiftUI 4 | 5 | public struct ElegantHPages: View where Pages: View { 6 | 7 | public let manager: ElegantPagesManager 8 | public let bounces: Bool 9 | public let pages: PageContainer 10 | 11 | public init(manager: ElegantPagesManager, 12 | bounces: Bool = false, 13 | @PageViewBuilder builder: () -> PageContainer) { 14 | self.manager = manager 15 | self.bounces = bounces 16 | self.pages = builder() 17 | } 18 | 19 | public var body: some View { 20 | ElegantPagesView(manager: manager, 21 | stackView: HorizontalStack(pages: pages), 22 | pageCount: pages.count, 23 | isHorizontal: true, 24 | bounces: bounces) 25 | } 26 | 27 | } 28 | 29 | private struct HorizontalStack: View where Pages: View { 30 | 31 | let pages: Pages 32 | 33 | var body: some View { 34 | HStack(alignment: .center, spacing: 0) { 35 | pages 36 | .frame(width: screen.width, height: screen.height) 37 | } 38 | .frame(width: screen.width, height: screen.height, alignment: .leading) 39 | } 40 | 41 | } 42 | 43 | extension ElegantHPages { 44 | 45 | public func onPageChanged(_ callback: ((Int) -> Void)?) -> Self { 46 | manager.anyCancellable = manager.$currentPage 47 | .dropFirst() 48 | .sink { page in 49 | callback?(page) 50 | } 51 | return self 52 | } 53 | 54 | } 55 | -------------------------------------------------------------------------------- /Sources/ElegantPages/Pages/Public/ElegantVPages.swift: -------------------------------------------------------------------------------- 1 | // Kevin Li - 8:30 PM - 6/23/20 2 | 3 | import SwiftUI 4 | 5 | public struct ElegantVPages: View where Pages: View { 6 | 7 | public let manager: ElegantPagesManager 8 | public let bounces: Bool 9 | public let pages: PageContainer 10 | 11 | public init(manager: ElegantPagesManager, 12 | bounces: Bool = false, 13 | @PageViewBuilder builder: () -> PageContainer) { 14 | self.manager = manager 15 | self.bounces = bounces 16 | self.pages = builder() 17 | } 18 | 19 | public var body: some View { 20 | ElegantPagesView(manager: manager, 21 | stackView: VerticalStack(pages: pages), 22 | pageCount: pages.count, 23 | isHorizontal: false, 24 | bounces: bounces) 25 | } 26 | 27 | } 28 | 29 | private struct VerticalStack: View where Pages: View { 30 | 31 | let pages: Pages 32 | 33 | var body: some View { 34 | VStack(alignment: .center, spacing: 0) { 35 | pages 36 | .frame(width: screen.width, height: screen.height) 37 | } 38 | .frame(width: screen.width, height: screen.height, alignment: .top) 39 | } 40 | 41 | } 42 | 43 | extension ElegantVPages { 44 | 45 | public func onPageChanged(_ callback: ((Int) -> Void)?) -> Self { 46 | manager.anyCancellable = manager.$currentPage 47 | .dropFirst() 48 | .sink { page in 49 | callback?(page) 50 | } 51 | return self 52 | } 53 | 54 | } 55 | --------------------------------------------------------------------------------