├── .gitignore ├── LICENSE ├── README.md ├── ScrollingAlbum.xcodeproj ├── project.pbxproj ├── project.xcworkspace │ └── contents.xcworkspacedata └── xcuserdata │ └── RippleArc.xcuserdatad │ └── xcschemes │ └── xcschememanagement.plist └── ScrollingAlbum ├── AlbumViewController.swift ├── AppDelegate.swift ├── Assets.xcassets └── AppIcon.appiconset │ └── Contents.json ├── FlowLayout ├── CellAnimationMeasurement.swift ├── CellBasicMeasurement.swift ├── CellPassiveMeasurement.swift ├── FlowLayoutInvalidateBehavior.swift ├── Layout │ ├── HDFlowLayout.swift │ ├── ThumbnailMasterFlowLayout.swift │ └── ThumbnailSlaveFlowLayout.swift ├── Manager │ ├── AcoordionAnimationManager.swift │ └── FlowLayoutSyncManager.swift └── ThumbnailFlowLayoutDraggingBehavior.swift ├── Info.plist ├── helper └── UIImageEx.swift ├── model └── PhotoModel.swift ├── samples ├── barton_nature_area_bridge.JPG ├── barton_nature_area_bridge_hd.JPG ├── barton_nature_lake.JPG ├── barton_nature_lake_hd.JPG ├── barton_nature_leeve.JPG ├── barton_nature_leeve_hd.JPG ├── barton_nature_swan.JPG ├── barton_nature_swan_hd.JPG ├── bird_hills_nature_foliage.JPG ├── bird_hills_nature_foliage_hd.JPG ├── bird_hills_nature_sunset.JPG ├── bird_hills_nature_sunset_hd.JPG ├── bird_hills_nature_tree.JPG ├── bird_hills_nature_tree_hd.JPG ├── horizontal_strip.png ├── horizontal_strip_hd.png ├── huron_river.JPG ├── huron_river_hd.JPG ├── leslie_park.png ├── leslie_park_hd.png ├── vertical_strip.png ├── vertical_strip_hd.png ├── willowtree_apartment_sunset.jpg ├── willowtree_apartment_sunset_hd.jpg ├── winsor_skyline.png └── winsor_skyline_hd.png └── view ├── Base.lproj └── Main.storyboard ├── CellConfiguratedCollectionView.swift └── cell ├── HDCollectionViewCell.swift ├── ReusableView.swift ├── ThumbnailCollectionViewCell.swift └── UICollectionViewEx.swift /.gitignore: -------------------------------------------------------------------------------- 1 | ScrollingAlbum.xcodeproj/xcuserdata/RippleArc.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 RippleArc llc 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # What to learn 2 | :mortar_board: `UICollectionView` is extremely flexible in building customized content thanks to its powerful `UICollectionViewFlowLayout`. We are using it to build the main interface of the native iOS `Photos`app. 3 | 4 | Check out the `Photos` native app on iOS, you will find out the main interface is composed of two horizontal scroll views. The bigger one displays the full resolution copies while the smaller displays the thumbnails. The scrolling motion on either one of them is reflected on the other. Look closer as you scroll the photos, the bigger scroll view renders the photo with parallax effect, and the same image also presents the accordion animation effect in the smaller scroll view. 5 | 6 | # What to build 7 | :construction_worker: In this lab, we will build the main interface of `Photos` using two `UICollectionViews`. To be more specific, we will customize the **UICollectionViewFlowLayout** associated with each `UICollectionView` to decide the `size` and `center` of the items that are visible on the screen. The changes on these attributes create the `parallax` and `accordion` animation. 8 | 9 | # How to learn 10 | :microscope: This [GitHub Page](https://ripplearc.github.io/iOS-UI-Scrollling-Album/) explains the details of the lab. 11 | 12 | Drawing 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /ScrollingAlbum.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 48; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 993936551FEF60D700BF26AB /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 993936541FEF60D700BF26AB /* AppDelegate.swift */; }; 11 | 993936571FEF60D700BF26AB /* AlbumViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 993936561FEF60D700BF26AB /* AlbumViewController.swift */; }; 12 | 9939365A1FEF60D700BF26AB /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 993936581FEF60D700BF26AB /* Main.storyboard */; }; 13 | 9939365C1FEF60D700BF26AB /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 9939365B1FEF60D700BF26AB /* Assets.xcassets */; }; 14 | 9939369B1FEFF5E000BF26AB /* PhotoModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 993936991FEFF5E000BF26AB /* PhotoModel.swift */; }; 15 | 993936B41FF002EF00BF26AB /* HDCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 993936B31FF002EF00BF26AB /* HDCollectionViewCell.swift */; }; 16 | 993936B61FF0033D00BF26AB /* ThumbnailCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 993936B51FF0033D00BF26AB /* ThumbnailCollectionViewCell.swift */; }; 17 | 993936E31FF00F9300BF26AB /* UIImageEx.swift in Sources */ = {isa = PBXBuildFile; fileRef = 993936E21FF00F9300BF26AB /* UIImageEx.swift */; }; 18 | 994789E11FF0B9770041CC04 /* bird_hills_nature_sunset_hd.JPG in Resources */ = {isa = PBXBuildFile; fileRef = 994789C71FF0B9740041CC04 /* bird_hills_nature_sunset_hd.JPG */; }; 19 | 994789E21FF0B9770041CC04 /* barton_nature_lake.JPG in Resources */ = {isa = PBXBuildFile; fileRef = 994789C81FF0B9740041CC04 /* barton_nature_lake.JPG */; }; 20 | 994789E31FF0B9770041CC04 /* vertical_strip_hd.png in Resources */ = {isa = PBXBuildFile; fileRef = 994789C91FF0B9740041CC04 /* vertical_strip_hd.png */; }; 21 | 994789E41FF0B9770041CC04 /* winsor_skyline.png in Resources */ = {isa = PBXBuildFile; fileRef = 994789CA1FF0B9740041CC04 /* winsor_skyline.png */; }; 22 | 994789E51FF0B9770041CC04 /* barton_nature_swan_hd.JPG in Resources */ = {isa = PBXBuildFile; fileRef = 994789CB1FF0B9740041CC04 /* barton_nature_swan_hd.JPG */; }; 23 | 994789E61FF0B9770041CC04 /* bird_hills_nature_foliage_hd.JPG in Resources */ = {isa = PBXBuildFile; fileRef = 994789CC1FF0B9740041CC04 /* bird_hills_nature_foliage_hd.JPG */; }; 24 | 994789E71FF0B9770041CC04 /* bird_hills_nature_sunset.JPG in Resources */ = {isa = PBXBuildFile; fileRef = 994789CD1FF0B9750041CC04 /* bird_hills_nature_sunset.JPG */; }; 25 | 994789E81FF0B9770041CC04 /* leslie_park.png in Resources */ = {isa = PBXBuildFile; fileRef = 994789CE1FF0B9750041CC04 /* leslie_park.png */; }; 26 | 994789E91FF0B9770041CC04 /* willowtree_apartment_sunset.jpg in Resources */ = {isa = PBXBuildFile; fileRef = 994789CF1FF0B9750041CC04 /* willowtree_apartment_sunset.jpg */; }; 27 | 994789EA1FF0B9770041CC04 /* barton_nature_leeve_hd.JPG in Resources */ = {isa = PBXBuildFile; fileRef = 994789D01FF0B9750041CC04 /* barton_nature_leeve_hd.JPG */; }; 28 | 994789EB1FF0B9770041CC04 /* horizontal_strip_hd.png in Resources */ = {isa = PBXBuildFile; fileRef = 994789D11FF0B9750041CC04 /* horizontal_strip_hd.png */; }; 29 | 994789EC1FF0B9770041CC04 /* bird_hills_nature_foliage.JPG in Resources */ = {isa = PBXBuildFile; fileRef = 994789D21FF0B9750041CC04 /* bird_hills_nature_foliage.JPG */; }; 30 | 994789ED1FF0B9770041CC04 /* barton_nature_swan.JPG in Resources */ = {isa = PBXBuildFile; fileRef = 994789D31FF0B9750041CC04 /* barton_nature_swan.JPG */; }; 31 | 994789EE1FF0B9770041CC04 /* winsor_skyline_hd.png in Resources */ = {isa = PBXBuildFile; fileRef = 994789D41FF0B9750041CC04 /* winsor_skyline_hd.png */; }; 32 | 994789EF1FF0B9770041CC04 /* huron_river_hd.JPG in Resources */ = {isa = PBXBuildFile; fileRef = 994789D51FF0B9750041CC04 /* huron_river_hd.JPG */; }; 33 | 994789F01FF0B9770041CC04 /* barton_nature_area_bridge.JPG in Resources */ = {isa = PBXBuildFile; fileRef = 994789D61FF0B9760041CC04 /* barton_nature_area_bridge.JPG */; }; 34 | 994789F11FF0B9770041CC04 /* barton_nature_lake_hd.JPG in Resources */ = {isa = PBXBuildFile; fileRef = 994789D71FF0B9760041CC04 /* barton_nature_lake_hd.JPG */; }; 35 | 994789F21FF0B9770041CC04 /* leslie_park_hd.png in Resources */ = {isa = PBXBuildFile; fileRef = 994789D81FF0B9760041CC04 /* leslie_park_hd.png */; }; 36 | 994789F31FF0B9770041CC04 /* bird_hills_nature_tree.JPG in Resources */ = {isa = PBXBuildFile; fileRef = 994789D91FF0B9760041CC04 /* bird_hills_nature_tree.JPG */; }; 37 | 994789F41FF0B9770041CC04 /* barton_nature_leeve.JPG in Resources */ = {isa = PBXBuildFile; fileRef = 994789DA1FF0B9760041CC04 /* barton_nature_leeve.JPG */; }; 38 | 994789F51FF0B9770041CC04 /* horizontal_strip.png in Resources */ = {isa = PBXBuildFile; fileRef = 994789DB1FF0B9760041CC04 /* horizontal_strip.png */; }; 39 | 994789F61FF0B9770041CC04 /* bird_hills_nature_tree_hd.JPG in Resources */ = {isa = PBXBuildFile; fileRef = 994789DC1FF0B9770041CC04 /* bird_hills_nature_tree_hd.JPG */; }; 40 | 994789F71FF0B9770041CC04 /* barton_nature_area_bridge_hd.JPG in Resources */ = {isa = PBXBuildFile; fileRef = 994789DD1FF0B9770041CC04 /* barton_nature_area_bridge_hd.JPG */; }; 41 | 994789F81FF0B9770041CC04 /* willowtree_apartment_sunset_hd.jpg in Resources */ = {isa = PBXBuildFile; fileRef = 994789DE1FF0B9770041CC04 /* willowtree_apartment_sunset_hd.jpg */; }; 42 | 994789F91FF0B9770041CC04 /* huron_river.JPG in Resources */ = {isa = PBXBuildFile; fileRef = 994789DF1FF0B9770041CC04 /* huron_river.JPG */; }; 43 | 994789FA1FF0B9770041CC04 /* vertical_strip.png in Resources */ = {isa = PBXBuildFile; fileRef = 994789E01FF0B9770041CC04 /* vertical_strip.png */; }; 44 | 994789FF1FF1437E0041CC04 /* HDFlowLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = 994789FE1FF1437E0041CC04 /* HDFlowLayout.swift */; }; 45 | 99478A011FF14EBA0041CC04 /* CellConfiguratedCollectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 99478A001FF14EBA0041CC04 /* CellConfiguratedCollectionView.swift */; }; 46 | 99478A031FF1718B0041CC04 /* CellBasicMeasurement.swift in Sources */ = {isa = PBXBuildFile; fileRef = 99478A021FF1718A0041CC04 /* CellBasicMeasurement.swift */; }; 47 | 99478A051FF19CBC0041CC04 /* ThumbnailMasterFlowLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = 99478A041FF19CBC0041CC04 /* ThumbnailMasterFlowLayout.swift */; }; 48 | 99478A091FF19DBA0041CC04 /* AcoordionAnimationManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 99478A081FF19DBA0041CC04 /* AcoordionAnimationManager.swift */; }; 49 | 99478A0B1FF1D22C0041CC04 /* CellAnimationMeasurement.swift in Sources */ = {isa = PBXBuildFile; fileRef = 99478A0A1FF1D22C0041CC04 /* CellAnimationMeasurement.swift */; }; 50 | 994DEAA01FF5EDB7007BA7B2 /* FlowLayoutSyncManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 994DEA9F1FF5EDB7007BA7B2 /* FlowLayoutSyncManager.swift */; }; 51 | 994DEAA21FF6B11E007BA7B2 /* CellPassiveMeasurement.swift in Sources */ = {isa = PBXBuildFile; fileRef = 994DEAA11FF6B11E007BA7B2 /* CellPassiveMeasurement.swift */; }; 52 | 994DEAA41FF6C746007BA7B2 /* ThumbnailSlaveFlowLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = 994DEAA31FF6C746007BA7B2 /* ThumbnailSlaveFlowLayout.swift */; }; 53 | 994DEAA61FF7E50E007BA7B2 /* FlowLayoutInvalidateBehavior.swift in Sources */ = {isa = PBXBuildFile; fileRef = 994DEAA51FF7E50E007BA7B2 /* FlowLayoutInvalidateBehavior.swift */; }; 54 | 9969E09A1FF1F404000D5D1E /* ThumbnailFlowLayoutDraggingBehavior.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9969E0991FF1F404000D5D1E /* ThumbnailFlowLayoutDraggingBehavior.swift */; }; 55 | 99BFDEA81FF03FEB00589553 /* ReusableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 99BFDEA71FF03FEB00589553 /* ReusableView.swift */; }; 56 | 99BFDEAA1FF0409100589553 /* UICollectionViewEx.swift in Sources */ = {isa = PBXBuildFile; fileRef = 99BFDEA91FF0409100589553 /* UICollectionViewEx.swift */; }; 57 | /* End PBXBuildFile section */ 58 | 59 | /* Begin PBXFileReference section */ 60 | 993936511FEF60D700BF26AB /* ScrollingAlbum.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = ScrollingAlbum.app; sourceTree = BUILT_PRODUCTS_DIR; }; 61 | 993936541FEF60D700BF26AB /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 62 | 993936561FEF60D700BF26AB /* AlbumViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlbumViewController.swift; sourceTree = ""; }; 63 | 993936591FEF60D700BF26AB /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 64 | 9939365B1FEF60D700BF26AB /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 65 | 993936601FEF60D700BF26AB /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 66 | 993936991FEFF5E000BF26AB /* PhotoModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PhotoModel.swift; sourceTree = ""; }; 67 | 993936B31FF002EF00BF26AB /* HDCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HDCollectionViewCell.swift; sourceTree = ""; }; 68 | 993936B51FF0033D00BF26AB /* ThumbnailCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThumbnailCollectionViewCell.swift; sourceTree = ""; }; 69 | 993936E21FF00F9300BF26AB /* UIImageEx.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIImageEx.swift; sourceTree = ""; }; 70 | 994789C71FF0B9740041CC04 /* bird_hills_nature_sunset_hd.JPG */ = {isa = PBXFileReference; lastKnownFileType = image.jpeg; path = bird_hills_nature_sunset_hd.JPG; sourceTree = ""; }; 71 | 994789C81FF0B9740041CC04 /* barton_nature_lake.JPG */ = {isa = PBXFileReference; lastKnownFileType = image.jpeg; path = barton_nature_lake.JPG; sourceTree = ""; }; 72 | 994789C91FF0B9740041CC04 /* vertical_strip_hd.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = vertical_strip_hd.png; sourceTree = ""; }; 73 | 994789CA1FF0B9740041CC04 /* winsor_skyline.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = winsor_skyline.png; sourceTree = ""; }; 74 | 994789CB1FF0B9740041CC04 /* barton_nature_swan_hd.JPG */ = {isa = PBXFileReference; lastKnownFileType = image.jpeg; path = barton_nature_swan_hd.JPG; sourceTree = ""; }; 75 | 994789CC1FF0B9740041CC04 /* bird_hills_nature_foliage_hd.JPG */ = {isa = PBXFileReference; lastKnownFileType = image.jpeg; path = bird_hills_nature_foliage_hd.JPG; sourceTree = ""; }; 76 | 994789CD1FF0B9750041CC04 /* bird_hills_nature_sunset.JPG */ = {isa = PBXFileReference; lastKnownFileType = image.jpeg; path = bird_hills_nature_sunset.JPG; sourceTree = ""; }; 77 | 994789CE1FF0B9750041CC04 /* leslie_park.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = leslie_park.png; sourceTree = ""; }; 78 | 994789CF1FF0B9750041CC04 /* willowtree_apartment_sunset.jpg */ = {isa = PBXFileReference; lastKnownFileType = image.jpeg; path = willowtree_apartment_sunset.jpg; sourceTree = ""; }; 79 | 994789D01FF0B9750041CC04 /* barton_nature_leeve_hd.JPG */ = {isa = PBXFileReference; lastKnownFileType = image.jpeg; path = barton_nature_leeve_hd.JPG; sourceTree = ""; }; 80 | 994789D11FF0B9750041CC04 /* horizontal_strip_hd.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = horizontal_strip_hd.png; sourceTree = ""; }; 81 | 994789D21FF0B9750041CC04 /* bird_hills_nature_foliage.JPG */ = {isa = PBXFileReference; lastKnownFileType = image.jpeg; path = bird_hills_nature_foliage.JPG; sourceTree = ""; }; 82 | 994789D31FF0B9750041CC04 /* barton_nature_swan.JPG */ = {isa = PBXFileReference; lastKnownFileType = image.jpeg; path = barton_nature_swan.JPG; sourceTree = ""; }; 83 | 994789D41FF0B9750041CC04 /* winsor_skyline_hd.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = winsor_skyline_hd.png; sourceTree = ""; }; 84 | 994789D51FF0B9750041CC04 /* huron_river_hd.JPG */ = {isa = PBXFileReference; lastKnownFileType = image.jpeg; path = huron_river_hd.JPG; sourceTree = ""; }; 85 | 994789D61FF0B9760041CC04 /* barton_nature_area_bridge.JPG */ = {isa = PBXFileReference; lastKnownFileType = image.jpeg; path = barton_nature_area_bridge.JPG; sourceTree = ""; }; 86 | 994789D71FF0B9760041CC04 /* barton_nature_lake_hd.JPG */ = {isa = PBXFileReference; lastKnownFileType = image.jpeg; path = barton_nature_lake_hd.JPG; sourceTree = ""; }; 87 | 994789D81FF0B9760041CC04 /* leslie_park_hd.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = leslie_park_hd.png; sourceTree = ""; }; 88 | 994789D91FF0B9760041CC04 /* bird_hills_nature_tree.JPG */ = {isa = PBXFileReference; lastKnownFileType = image.jpeg; path = bird_hills_nature_tree.JPG; sourceTree = ""; }; 89 | 994789DA1FF0B9760041CC04 /* barton_nature_leeve.JPG */ = {isa = PBXFileReference; lastKnownFileType = image.jpeg; path = barton_nature_leeve.JPG; sourceTree = ""; }; 90 | 994789DB1FF0B9760041CC04 /* horizontal_strip.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = horizontal_strip.png; sourceTree = ""; }; 91 | 994789DC1FF0B9770041CC04 /* bird_hills_nature_tree_hd.JPG */ = {isa = PBXFileReference; lastKnownFileType = image.jpeg; path = bird_hills_nature_tree_hd.JPG; sourceTree = ""; }; 92 | 994789DD1FF0B9770041CC04 /* barton_nature_area_bridge_hd.JPG */ = {isa = PBXFileReference; lastKnownFileType = image.jpeg; path = barton_nature_area_bridge_hd.JPG; sourceTree = ""; }; 93 | 994789DE1FF0B9770041CC04 /* willowtree_apartment_sunset_hd.jpg */ = {isa = PBXFileReference; lastKnownFileType = image.jpeg; path = willowtree_apartment_sunset_hd.jpg; sourceTree = ""; }; 94 | 994789DF1FF0B9770041CC04 /* huron_river.JPG */ = {isa = PBXFileReference; lastKnownFileType = image.jpeg; path = huron_river.JPG; sourceTree = ""; }; 95 | 994789E01FF0B9770041CC04 /* vertical_strip.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = vertical_strip.png; sourceTree = ""; }; 96 | 994789FE1FF1437E0041CC04 /* HDFlowLayout.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HDFlowLayout.swift; sourceTree = ""; }; 97 | 99478A001FF14EBA0041CC04 /* CellConfiguratedCollectionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CellConfiguratedCollectionView.swift; sourceTree = ""; }; 98 | 99478A021FF1718A0041CC04 /* CellBasicMeasurement.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CellBasicMeasurement.swift; sourceTree = ""; }; 99 | 99478A041FF19CBC0041CC04 /* ThumbnailMasterFlowLayout.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThumbnailMasterFlowLayout.swift; sourceTree = ""; }; 100 | 99478A081FF19DBA0041CC04 /* AcoordionAnimationManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AcoordionAnimationManager.swift; sourceTree = ""; }; 101 | 99478A0A1FF1D22C0041CC04 /* CellAnimationMeasurement.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CellAnimationMeasurement.swift; sourceTree = ""; }; 102 | 994DEA9F1FF5EDB7007BA7B2 /* FlowLayoutSyncManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FlowLayoutSyncManager.swift; sourceTree = ""; }; 103 | 994DEAA11FF6B11E007BA7B2 /* CellPassiveMeasurement.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CellPassiveMeasurement.swift; sourceTree = ""; }; 104 | 994DEAA31FF6C746007BA7B2 /* ThumbnailSlaveFlowLayout.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThumbnailSlaveFlowLayout.swift; sourceTree = ""; }; 105 | 994DEAA51FF7E50E007BA7B2 /* FlowLayoutInvalidateBehavior.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FlowLayoutInvalidateBehavior.swift; sourceTree = ""; }; 106 | 9969E0991FF1F404000D5D1E /* ThumbnailFlowLayoutDraggingBehavior.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThumbnailFlowLayoutDraggingBehavior.swift; sourceTree = ""; }; 107 | 99BFDEA71FF03FEB00589553 /* ReusableView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReusableView.swift; sourceTree = ""; }; 108 | 99BFDEA91FF0409100589553 /* UICollectionViewEx.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UICollectionViewEx.swift; sourceTree = ""; }; 109 | /* End PBXFileReference section */ 110 | 111 | /* Begin PBXFrameworksBuildPhase section */ 112 | 9939364E1FEF60D700BF26AB /* Frameworks */ = { 113 | isa = PBXFrameworksBuildPhase; 114 | buildActionMask = 2147483647; 115 | files = ( 116 | ); 117 | runOnlyForDeploymentPostprocessing = 0; 118 | }; 119 | /* End PBXFrameworksBuildPhase section */ 120 | 121 | /* Begin PBXGroup section */ 122 | 993936481FEF60D700BF26AB = { 123 | isa = PBXGroup; 124 | children = ( 125 | 993936531FEF60D700BF26AB /* ScrollingAlbum */, 126 | 993936521FEF60D700BF26AB /* Products */, 127 | ); 128 | sourceTree = ""; 129 | }; 130 | 993936521FEF60D700BF26AB /* Products */ = { 131 | isa = PBXGroup; 132 | children = ( 133 | 993936511FEF60D700BF26AB /* ScrollingAlbum.app */, 134 | ); 135 | name = Products; 136 | sourceTree = ""; 137 | }; 138 | 993936531FEF60D700BF26AB /* ScrollingAlbum */ = { 139 | isa = PBXGroup; 140 | children = ( 141 | 994789FB1FF142D80041CC04 /* FlowLayout */, 142 | 993936E11FF00F2000BF26AB /* helper */, 143 | 9939369D1FEFF9AC00BF26AB /* view */, 144 | 993936661FEF623200BF26AB /* model */, 145 | 993936541FEF60D700BF26AB /* AppDelegate.swift */, 146 | 993936561FEF60D700BF26AB /* AlbumViewController.swift */, 147 | 9939365B1FEF60D700BF26AB /* Assets.xcassets */, 148 | 993936601FEF60D700BF26AB /* Info.plist */, 149 | 993936671FEFF56100BF26AB /* samples */, 150 | ); 151 | path = ScrollingAlbum; 152 | sourceTree = ""; 153 | }; 154 | 993936661FEF623200BF26AB /* model */ = { 155 | isa = PBXGroup; 156 | children = ( 157 | 993936991FEFF5E000BF26AB /* PhotoModel.swift */, 158 | ); 159 | path = model; 160 | sourceTree = ""; 161 | }; 162 | 993936671FEFF56100BF26AB /* samples */ = { 163 | isa = PBXGroup; 164 | children = ( 165 | 994789DD1FF0B9770041CC04 /* barton_nature_area_bridge_hd.JPG */, 166 | 994789D61FF0B9760041CC04 /* barton_nature_area_bridge.JPG */, 167 | 994789D71FF0B9760041CC04 /* barton_nature_lake_hd.JPG */, 168 | 994789C81FF0B9740041CC04 /* barton_nature_lake.JPG */, 169 | 994789D01FF0B9750041CC04 /* barton_nature_leeve_hd.JPG */, 170 | 994789DA1FF0B9760041CC04 /* barton_nature_leeve.JPG */, 171 | 994789CB1FF0B9740041CC04 /* barton_nature_swan_hd.JPG */, 172 | 994789D31FF0B9750041CC04 /* barton_nature_swan.JPG */, 173 | 994789CC1FF0B9740041CC04 /* bird_hills_nature_foliage_hd.JPG */, 174 | 994789D21FF0B9750041CC04 /* bird_hills_nature_foliage.JPG */, 175 | 994789C71FF0B9740041CC04 /* bird_hills_nature_sunset_hd.JPG */, 176 | 994789CD1FF0B9750041CC04 /* bird_hills_nature_sunset.JPG */, 177 | 994789DC1FF0B9770041CC04 /* bird_hills_nature_tree_hd.JPG */, 178 | 994789D91FF0B9760041CC04 /* bird_hills_nature_tree.JPG */, 179 | 994789D11FF0B9750041CC04 /* horizontal_strip_hd.png */, 180 | 994789DB1FF0B9760041CC04 /* horizontal_strip.png */, 181 | 994789D51FF0B9750041CC04 /* huron_river_hd.JPG */, 182 | 994789DF1FF0B9770041CC04 /* huron_river.JPG */, 183 | 994789D81FF0B9760041CC04 /* leslie_park_hd.png */, 184 | 994789CE1FF0B9750041CC04 /* leslie_park.png */, 185 | 994789C91FF0B9740041CC04 /* vertical_strip_hd.png */, 186 | 994789E01FF0B9770041CC04 /* vertical_strip.png */, 187 | 994789DE1FF0B9770041CC04 /* willowtree_apartment_sunset_hd.jpg */, 188 | 994789CF1FF0B9750041CC04 /* willowtree_apartment_sunset.jpg */, 189 | 994789D41FF0B9750041CC04 /* winsor_skyline_hd.png */, 190 | 994789CA1FF0B9740041CC04 /* winsor_skyline.png */, 191 | ); 192 | path = samples; 193 | sourceTree = ""; 194 | }; 195 | 9939369D1FEFF9AC00BF26AB /* view */ = { 196 | isa = PBXGroup; 197 | children = ( 198 | 993936B21FF002B800BF26AB /* cell */, 199 | 993936581FEF60D700BF26AB /* Main.storyboard */, 200 | 99478A001FF14EBA0041CC04 /* CellConfiguratedCollectionView.swift */, 201 | ); 202 | path = view; 203 | sourceTree = ""; 204 | }; 205 | 993936B21FF002B800BF26AB /* cell */ = { 206 | isa = PBXGroup; 207 | children = ( 208 | 99BFDEA71FF03FEB00589553 /* ReusableView.swift */, 209 | 99BFDEA91FF0409100589553 /* UICollectionViewEx.swift */, 210 | 993936B31FF002EF00BF26AB /* HDCollectionViewCell.swift */, 211 | 993936B51FF0033D00BF26AB /* ThumbnailCollectionViewCell.swift */, 212 | ); 213 | path = cell; 214 | sourceTree = ""; 215 | }; 216 | 993936E11FF00F2000BF26AB /* helper */ = { 217 | isa = PBXGroup; 218 | children = ( 219 | 993936E21FF00F9300BF26AB /* UIImageEx.swift */, 220 | ); 221 | path = helper; 222 | sourceTree = ""; 223 | }; 224 | 994789FB1FF142D80041CC04 /* FlowLayout */ = { 225 | isa = PBXGroup; 226 | children = ( 227 | 994DEAA51FF7E50E007BA7B2 /* FlowLayoutInvalidateBehavior.swift */, 228 | 9969E0991FF1F404000D5D1E /* ThumbnailFlowLayoutDraggingBehavior.swift */, 229 | 99478A021FF1718A0041CC04 /* CellBasicMeasurement.swift */, 230 | 99478A0A1FF1D22C0041CC04 /* CellAnimationMeasurement.swift */, 231 | 994DEAA11FF6B11E007BA7B2 /* CellPassiveMeasurement.swift */, 232 | 99478A061FF19D4B0041CC04 /* Manager */, 233 | 994789FD1FF143440041CC04 /* Layout */, 234 | ); 235 | path = FlowLayout; 236 | sourceTree = ""; 237 | }; 238 | 994789FD1FF143440041CC04 /* Layout */ = { 239 | isa = PBXGroup; 240 | children = ( 241 | 994789FE1FF1437E0041CC04 /* HDFlowLayout.swift */, 242 | 99478A041FF19CBC0041CC04 /* ThumbnailMasterFlowLayout.swift */, 243 | 994DEAA31FF6C746007BA7B2 /* ThumbnailSlaveFlowLayout.swift */, 244 | ); 245 | path = Layout; 246 | sourceTree = ""; 247 | }; 248 | 99478A061FF19D4B0041CC04 /* Manager */ = { 249 | isa = PBXGroup; 250 | children = ( 251 | 99478A081FF19DBA0041CC04 /* AcoordionAnimationManager.swift */, 252 | 994DEA9F1FF5EDB7007BA7B2 /* FlowLayoutSyncManager.swift */, 253 | ); 254 | path = Manager; 255 | sourceTree = ""; 256 | }; 257 | /* End PBXGroup section */ 258 | 259 | /* Begin PBXNativeTarget section */ 260 | 993936501FEF60D700BF26AB /* ScrollingAlbum */ = { 261 | isa = PBXNativeTarget; 262 | buildConfigurationList = 993936631FEF60D700BF26AB /* Build configuration list for PBXNativeTarget "ScrollingAlbum" */; 263 | buildPhases = ( 264 | 9939364D1FEF60D700BF26AB /* Sources */, 265 | 9939364E1FEF60D700BF26AB /* Frameworks */, 266 | 9939364F1FEF60D700BF26AB /* Resources */, 267 | ); 268 | buildRules = ( 269 | ); 270 | dependencies = ( 271 | ); 272 | name = ScrollingAlbum; 273 | productName = ScrollingAlbum; 274 | productReference = 993936511FEF60D700BF26AB /* ScrollingAlbum.app */; 275 | productType = "com.apple.product-type.application"; 276 | }; 277 | /* End PBXNativeTarget section */ 278 | 279 | /* Begin PBXProject section */ 280 | 993936491FEF60D700BF26AB /* Project object */ = { 281 | isa = PBXProject; 282 | attributes = { 283 | LastSwiftUpdateCheck = 0920; 284 | LastUpgradeCheck = 0920; 285 | ORGANIZATIONNAME = RippleArc; 286 | TargetAttributes = { 287 | 993936501FEF60D700BF26AB = { 288 | CreatedOnToolsVersion = 9.2; 289 | ProvisioningStyle = Automatic; 290 | }; 291 | }; 292 | }; 293 | buildConfigurationList = 9939364C1FEF60D700BF26AB /* Build configuration list for PBXProject "ScrollingAlbum" */; 294 | compatibilityVersion = "Xcode 8.0"; 295 | developmentRegion = en; 296 | hasScannedForEncodings = 0; 297 | knownRegions = ( 298 | en, 299 | Base, 300 | ); 301 | mainGroup = 993936481FEF60D700BF26AB; 302 | productRefGroup = 993936521FEF60D700BF26AB /* Products */; 303 | projectDirPath = ""; 304 | projectRoot = ""; 305 | targets = ( 306 | 993936501FEF60D700BF26AB /* ScrollingAlbum */, 307 | ); 308 | }; 309 | /* End PBXProject section */ 310 | 311 | /* Begin PBXResourcesBuildPhase section */ 312 | 9939364F1FEF60D700BF26AB /* Resources */ = { 313 | isa = PBXResourcesBuildPhase; 314 | buildActionMask = 2147483647; 315 | files = ( 316 | 994789EB1FF0B9770041CC04 /* horizontal_strip_hd.png in Resources */, 317 | 994789ED1FF0B9770041CC04 /* barton_nature_swan.JPG in Resources */, 318 | 994789E51FF0B9770041CC04 /* barton_nature_swan_hd.JPG in Resources */, 319 | 994789F21FF0B9770041CC04 /* leslie_park_hd.png in Resources */, 320 | 994789FA1FF0B9770041CC04 /* vertical_strip.png in Resources */, 321 | 994789EF1FF0B9770041CC04 /* huron_river_hd.JPG in Resources */, 322 | 994789F11FF0B9770041CC04 /* barton_nature_lake_hd.JPG in Resources */, 323 | 994789EE1FF0B9770041CC04 /* winsor_skyline_hd.png in Resources */, 324 | 994789F71FF0B9770041CC04 /* barton_nature_area_bridge_hd.JPG in Resources */, 325 | 9939365C1FEF60D700BF26AB /* Assets.xcassets in Resources */, 326 | 994789F91FF0B9770041CC04 /* huron_river.JPG in Resources */, 327 | 994789F61FF0B9770041CC04 /* bird_hills_nature_tree_hd.JPG in Resources */, 328 | 994789F51FF0B9770041CC04 /* horizontal_strip.png in Resources */, 329 | 994789F01FF0B9770041CC04 /* barton_nature_area_bridge.JPG in Resources */, 330 | 994789E21FF0B9770041CC04 /* barton_nature_lake.JPG in Resources */, 331 | 994789E91FF0B9770041CC04 /* willowtree_apartment_sunset.jpg in Resources */, 332 | 994789EA1FF0B9770041CC04 /* barton_nature_leeve_hd.JPG in Resources */, 333 | 994789E31FF0B9770041CC04 /* vertical_strip_hd.png in Resources */, 334 | 994789E61FF0B9770041CC04 /* bird_hills_nature_foliage_hd.JPG in Resources */, 335 | 994789F41FF0B9770041CC04 /* barton_nature_leeve.JPG in Resources */, 336 | 994789E41FF0B9770041CC04 /* winsor_skyline.png in Resources */, 337 | 994789E71FF0B9770041CC04 /* bird_hills_nature_sunset.JPG in Resources */, 338 | 994789F81FF0B9770041CC04 /* willowtree_apartment_sunset_hd.jpg in Resources */, 339 | 994789E81FF0B9770041CC04 /* leslie_park.png in Resources */, 340 | 994789E11FF0B9770041CC04 /* bird_hills_nature_sunset_hd.JPG in Resources */, 341 | 994789EC1FF0B9770041CC04 /* bird_hills_nature_foliage.JPG in Resources */, 342 | 9939365A1FEF60D700BF26AB /* Main.storyboard in Resources */, 343 | 994789F31FF0B9770041CC04 /* bird_hills_nature_tree.JPG in Resources */, 344 | ); 345 | runOnlyForDeploymentPostprocessing = 0; 346 | }; 347 | /* End PBXResourcesBuildPhase section */ 348 | 349 | /* Begin PBXSourcesBuildPhase section */ 350 | 9939364D1FEF60D700BF26AB /* Sources */ = { 351 | isa = PBXSourcesBuildPhase; 352 | buildActionMask = 2147483647; 353 | files = ( 354 | 99478A031FF1718B0041CC04 /* CellBasicMeasurement.swift in Sources */, 355 | 99478A091FF19DBA0041CC04 /* AcoordionAnimationManager.swift in Sources */, 356 | 9939369B1FEFF5E000BF26AB /* PhotoModel.swift in Sources */, 357 | 993936571FEF60D700BF26AB /* AlbumViewController.swift in Sources */, 358 | 994DEAA41FF6C746007BA7B2 /* ThumbnailSlaveFlowLayout.swift in Sources */, 359 | 994DEAA61FF7E50E007BA7B2 /* FlowLayoutInvalidateBehavior.swift in Sources */, 360 | 993936551FEF60D700BF26AB /* AppDelegate.swift in Sources */, 361 | 99478A051FF19CBC0041CC04 /* ThumbnailMasterFlowLayout.swift in Sources */, 362 | 99478A011FF14EBA0041CC04 /* CellConfiguratedCollectionView.swift in Sources */, 363 | 99478A0B1FF1D22C0041CC04 /* CellAnimationMeasurement.swift in Sources */, 364 | 993936B41FF002EF00BF26AB /* HDCollectionViewCell.swift in Sources */, 365 | 993936B61FF0033D00BF26AB /* ThumbnailCollectionViewCell.swift in Sources */, 366 | 99BFDEAA1FF0409100589553 /* UICollectionViewEx.swift in Sources */, 367 | 994DEAA21FF6B11E007BA7B2 /* CellPassiveMeasurement.swift in Sources */, 368 | 9969E09A1FF1F404000D5D1E /* ThumbnailFlowLayoutDraggingBehavior.swift in Sources */, 369 | 994DEAA01FF5EDB7007BA7B2 /* FlowLayoutSyncManager.swift in Sources */, 370 | 994789FF1FF1437E0041CC04 /* HDFlowLayout.swift in Sources */, 371 | 99BFDEA81FF03FEB00589553 /* ReusableView.swift in Sources */, 372 | 993936E31FF00F9300BF26AB /* UIImageEx.swift in Sources */, 373 | ); 374 | runOnlyForDeploymentPostprocessing = 0; 375 | }; 376 | /* End PBXSourcesBuildPhase section */ 377 | 378 | /* Begin PBXVariantGroup section */ 379 | 993936581FEF60D700BF26AB /* Main.storyboard */ = { 380 | isa = PBXVariantGroup; 381 | children = ( 382 | 993936591FEF60D700BF26AB /* Base */, 383 | ); 384 | name = Main.storyboard; 385 | sourceTree = ""; 386 | }; 387 | /* End PBXVariantGroup section */ 388 | 389 | /* Begin XCBuildConfiguration section */ 390 | 993936611FEF60D700BF26AB /* Debug */ = { 391 | isa = XCBuildConfiguration; 392 | buildSettings = { 393 | ALWAYS_SEARCH_USER_PATHS = NO; 394 | CLANG_ANALYZER_NONNULL = YES; 395 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 396 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 397 | CLANG_CXX_LIBRARY = "libc++"; 398 | CLANG_ENABLE_MODULES = YES; 399 | CLANG_ENABLE_OBJC_ARC = YES; 400 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 401 | CLANG_WARN_BOOL_CONVERSION = YES; 402 | CLANG_WARN_COMMA = YES; 403 | CLANG_WARN_CONSTANT_CONVERSION = YES; 404 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 405 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 406 | CLANG_WARN_EMPTY_BODY = YES; 407 | CLANG_WARN_ENUM_CONVERSION = YES; 408 | CLANG_WARN_INFINITE_RECURSION = YES; 409 | CLANG_WARN_INT_CONVERSION = YES; 410 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 411 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 412 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 413 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 414 | CLANG_WARN_STRICT_PROTOTYPES = YES; 415 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 416 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 417 | CLANG_WARN_UNREACHABLE_CODE = YES; 418 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 419 | CODE_SIGN_IDENTITY = "iPhone Developer"; 420 | COPY_PHASE_STRIP = NO; 421 | DEBUG_INFORMATION_FORMAT = dwarf; 422 | ENABLE_STRICT_OBJC_MSGSEND = YES; 423 | ENABLE_TESTABILITY = YES; 424 | GCC_C_LANGUAGE_STANDARD = gnu11; 425 | GCC_DYNAMIC_NO_PIC = NO; 426 | GCC_NO_COMMON_BLOCKS = YES; 427 | GCC_OPTIMIZATION_LEVEL = 0; 428 | GCC_PREPROCESSOR_DEFINITIONS = ( 429 | "DEBUG=1", 430 | "$(inherited)", 431 | ); 432 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 433 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 434 | GCC_WARN_UNDECLARED_SELECTOR = YES; 435 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 436 | GCC_WARN_UNUSED_FUNCTION = YES; 437 | GCC_WARN_UNUSED_VARIABLE = YES; 438 | IPHONEOS_DEPLOYMENT_TARGET = 11.2; 439 | MTL_ENABLE_DEBUG_INFO = YES; 440 | ONLY_ACTIVE_ARCH = YES; 441 | SDKROOT = iphoneos; 442 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 443 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 444 | }; 445 | name = Debug; 446 | }; 447 | 993936621FEF60D700BF26AB /* Release */ = { 448 | isa = XCBuildConfiguration; 449 | buildSettings = { 450 | ALWAYS_SEARCH_USER_PATHS = NO; 451 | CLANG_ANALYZER_NONNULL = YES; 452 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 453 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 454 | CLANG_CXX_LIBRARY = "libc++"; 455 | CLANG_ENABLE_MODULES = YES; 456 | CLANG_ENABLE_OBJC_ARC = YES; 457 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 458 | CLANG_WARN_BOOL_CONVERSION = YES; 459 | CLANG_WARN_COMMA = YES; 460 | CLANG_WARN_CONSTANT_CONVERSION = YES; 461 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 462 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 463 | CLANG_WARN_EMPTY_BODY = YES; 464 | CLANG_WARN_ENUM_CONVERSION = YES; 465 | CLANG_WARN_INFINITE_RECURSION = YES; 466 | CLANG_WARN_INT_CONVERSION = YES; 467 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 468 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 469 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 470 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 471 | CLANG_WARN_STRICT_PROTOTYPES = YES; 472 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 473 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 474 | CLANG_WARN_UNREACHABLE_CODE = YES; 475 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 476 | CODE_SIGN_IDENTITY = "iPhone Developer"; 477 | COPY_PHASE_STRIP = NO; 478 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 479 | ENABLE_NS_ASSERTIONS = NO; 480 | ENABLE_STRICT_OBJC_MSGSEND = YES; 481 | GCC_C_LANGUAGE_STANDARD = gnu11; 482 | GCC_NO_COMMON_BLOCKS = YES; 483 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 484 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 485 | GCC_WARN_UNDECLARED_SELECTOR = YES; 486 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 487 | GCC_WARN_UNUSED_FUNCTION = YES; 488 | GCC_WARN_UNUSED_VARIABLE = YES; 489 | IPHONEOS_DEPLOYMENT_TARGET = 11.2; 490 | MTL_ENABLE_DEBUG_INFO = NO; 491 | SDKROOT = iphoneos; 492 | SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; 493 | VALIDATE_PRODUCT = YES; 494 | }; 495 | name = Release; 496 | }; 497 | 993936641FEF60D700BF26AB /* Debug */ = { 498 | isa = XCBuildConfiguration; 499 | buildSettings = { 500 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 501 | CODE_SIGN_STYLE = Automatic; 502 | DEVELOPMENT_TEAM = GGKT557D86; 503 | INFOPLIST_FILE = ScrollingAlbum/Info.plist; 504 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 505 | PRODUCT_BUNDLE_IDENTIFIER = RippleArc.ScrollingAlbum; 506 | PRODUCT_NAME = "$(TARGET_NAME)"; 507 | SWIFT_VERSION = 4.0; 508 | TARGETED_DEVICE_FAMILY = "1,2"; 509 | }; 510 | name = Debug; 511 | }; 512 | 993936651FEF60D700BF26AB /* Release */ = { 513 | isa = XCBuildConfiguration; 514 | buildSettings = { 515 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 516 | CODE_SIGN_STYLE = Automatic; 517 | DEVELOPMENT_TEAM = GGKT557D86; 518 | INFOPLIST_FILE = ScrollingAlbum/Info.plist; 519 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 520 | PRODUCT_BUNDLE_IDENTIFIER = RippleArc.ScrollingAlbum; 521 | PRODUCT_NAME = "$(TARGET_NAME)"; 522 | SWIFT_VERSION = 4.0; 523 | TARGETED_DEVICE_FAMILY = "1,2"; 524 | }; 525 | name = Release; 526 | }; 527 | /* End XCBuildConfiguration section */ 528 | 529 | /* Begin XCConfigurationList section */ 530 | 9939364C1FEF60D700BF26AB /* Build configuration list for PBXProject "ScrollingAlbum" */ = { 531 | isa = XCConfigurationList; 532 | buildConfigurations = ( 533 | 993936611FEF60D700BF26AB /* Debug */, 534 | 993936621FEF60D700BF26AB /* Release */, 535 | ); 536 | defaultConfigurationIsVisible = 0; 537 | defaultConfigurationName = Release; 538 | }; 539 | 993936631FEF60D700BF26AB /* Build configuration list for PBXNativeTarget "ScrollingAlbum" */ = { 540 | isa = XCConfigurationList; 541 | buildConfigurations = ( 542 | 993936641FEF60D700BF26AB /* Debug */, 543 | 993936651FEF60D700BF26AB /* Release */, 544 | ); 545 | defaultConfigurationIsVisible = 0; 546 | defaultConfigurationName = Release; 547 | }; 548 | /* End XCConfigurationList section */ 549 | }; 550 | rootObject = 993936491FEF60D700BF26AB /* Project object */; 551 | } 552 | -------------------------------------------------------------------------------- /ScrollingAlbum.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /ScrollingAlbum.xcodeproj/xcuserdata/RippleArc.xcuserdatad/xcschemes/xcschememanagement.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | SchemeUserState 6 | 7 | ScrollingAlbum.xcscheme 8 | 9 | orderHint 10 | 0 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /ScrollingAlbum/AlbumViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewController.swift 3 | // ScrollingAlbum 4 | // 5 | // Created by RippleArc on 12/23/17. 6 | // Copyright © 2017 RippleArc. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class AlbumViewController: UIViewController { 12 | // if enabled, will show a middle vertical line for debugging and indices of the photos 13 | var debug: Bool = false 14 | @IBOutlet weak var assistantMiddleLine: UIView! 15 | 16 | // dependency injection 17 | var hdPhotoModel: PhotoModel! 18 | var thumbnailPhotoModel: PhotoModel! 19 | var flowLayoutSyncManager: FlowLayoutSyncManager! 20 | 21 | var hdCollectionViewRatio: CGFloat = 0 22 | var thumbnailCollectionViewThinnestRatio: CGFloat = 0 23 | var thumbnailCollectionViewThickestRatio: CGFloat = 0 24 | let thumbnailMaximumWidth:CGFloat = 160 25 | 26 | @IBOutlet weak var hdCollectionView: CellConfiguratedCollectionView! 27 | @IBOutlet weak var thumbnailCollectionView: CellConfiguratedCollectionView! 28 | override func viewDidLoad() { 29 | super.viewDidLoad() 30 | assistantMiddleLine.isHidden = !debug 31 | 32 | setupHDCollectionView() 33 | setupThumbnailCollectionView() 34 | flowLayoutSyncManager.register(hdCollectionView) 35 | flowLayoutSyncManager.register(thumbnailCollectionView) 36 | } 37 | 38 | override func viewDidAppear(_ animated: Bool) { 39 | if let layout = thumbnailCollectionView.collectionViewLayout as? ThumbnailFlowLayoutDraggingBehavior { 40 | layout.unfoldCurrentCell() 41 | } 42 | } 43 | 44 | override func viewDidLayoutSubviews() { 45 | setupHDCollectionViewMeasurement() 46 | hdCollectionView.collectionViewLayout.invalidateLayout() 47 | setupThumbnailCollectionViewMeasurement() 48 | thumbnailCollectionView.collectionViewLayout.invalidateLayout() 49 | } 50 | 51 | fileprivate func setupHDCollectionView() { 52 | hdCollectionView!.cellSize = self 53 | hdCollectionView.dataSource = self 54 | hdCollectionView.delegate = self 55 | hdCollectionView!.isPagingEnabled = true 56 | hdCollectionView!.decelerationRate = UIScrollViewDecelerationRateNormal; 57 | let layout = HDFlowLayout() 58 | layout.flowLayoutSyncManager = flowLayoutSyncManager 59 | hdCollectionView!.collectionViewLayout = layout 60 | } 61 | 62 | fileprivate func setupThumbnailCollectionView() { 63 | thumbnailCollectionView!.dataSource = self 64 | thumbnailCollectionView!.delegate = self 65 | thumbnailCollectionView!.cellSize = self 66 | thumbnailCollectionView!.alwaysBounceHorizontal = true 67 | thumbnailCollectionView!.collectionViewLayout = ThumbnailSlaveFlowLayout() 68 | } 69 | 70 | fileprivate func setupHDCollectionViewMeasurement() { 71 | hdCollectionView.cellFullSpacing = 100 72 | hdCollectionView.cellNormalWidth = hdCollectionView!.bounds.size.width - hdCollectionView.cellFullSpacing 73 | hdCollectionView.cellMaximumWidth = hdCollectionView!.bounds.size.width 74 | hdCollectionView.cellNormalSpacing = 0 75 | hdCollectionView.cellHeight = hdCollectionView.bounds.size.height 76 | hdCollectionViewRatio = hdCollectionView.frame.size.height / hdCollectionView.frame.size.width 77 | if var layout = hdCollectionView.collectionViewLayout as? FlowLayoutInvalidateBehavior { 78 | layout.shouldLayoutEverything = true 79 | } 80 | } 81 | 82 | fileprivate func setupThumbnailCollectionViewMeasurement() { 83 | thumbnailCollectionView.cellNormalWidth = 30 84 | thumbnailCollectionView.cellFullSpacing = 15 85 | thumbnailCollectionView.cellNormalSpacing = 2 86 | thumbnailCollectionView.cellHeight = thumbnailCollectionView.frame.size.height 87 | thumbnailCollectionView.cellMaximumWidth = thumbnailMaximumWidth 88 | thumbnailCollectionViewThinnestRatio = thumbnailCollectionView.cellHeight / thumbnailCollectionView.cellNormalWidth 89 | thumbnailCollectionViewThickestRatio = thumbnailCollectionView.cellHeight / thumbnailMaximumWidth 90 | if var layout = hdCollectionView.collectionViewLayout as? FlowLayoutInvalidateBehavior { 91 | layout.shouldLayoutEverything = true 92 | } 93 | } 94 | } 95 | 96 | //MARK: UICollectionViewDataSource 97 | 98 | extension AlbumViewController: UICollectionViewDataSource { 99 | func numberOfSections(in collectionView: UICollectionView) -> Int { 100 | return 1 101 | } 102 | 103 | func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { 104 | switch collectionView { 105 | case hdCollectionView: 106 | return hdPhotoModel.numberOfPhotos() 107 | case thumbnailCollectionView: 108 | return thumbnailPhotoModel.numberOfPhotos() 109 | default: 110 | return 0 111 | } 112 | } 113 | 114 | func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { 115 | switch collectionView { 116 | case thumbnailCollectionView: 117 | let cell = collectionView.dequeueReusableCell(for: indexPath) as ThumbnailCollectionViewCell 118 | if let image = thumbnailPhotoModel.photo(at: indexPath.row, debug:debug), 119 | let size = self.collectionView(thumbnailCollectionView, sizeForItemAt: indexPath) { 120 | cell.photoViewWidthConstraint.constant = size.width 121 | cell.clipsToBounds = true 122 | cell.photoView?.contentMode = .scaleAspectFill 123 | cell.photoView?.image = image 124 | } 125 | 126 | return cell 127 | case hdCollectionView: 128 | let cell = collectionView.dequeueReusableCell(for: indexPath) as HDCollectionViewCell 129 | if let image = hdPhotoModel.photo(at: indexPath.item, debug:debug), 130 | let size = self.collectionView(hdCollectionView, sizeForItemAt: indexPath) { 131 | cell.photoViewWidthConstraint.constant = size.width 132 | cell.photoViewHeightConstraint.constant = size.height 133 | cell.clipsToBounds = true 134 | cell.photoView?.contentMode = .scaleAspectFill 135 | cell.photoView?.image = image 136 | } 137 | 138 | return cell 139 | default: 140 | return UICollectionViewCell() 141 | } 142 | } 143 | } 144 | 145 | //MARK:- CollectionView Delegate 146 | extension AlbumViewController: UICollectionViewDelegate { 147 | func scrollViewWillBeginDragging(_ scrollView: UIScrollView) { 148 | if let collectionView = scrollView as? UICollectionView { 149 | flowLayoutSyncManager.masterCollectionView = collectionView 150 | if let layout = collectionView.collectionViewLayout as? ThumbnailFlowLayoutDraggingBehavior { 151 | layout.foldCurrentCell() 152 | } 153 | } 154 | } 155 | 156 | func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) { 157 | if let collectionView = scrollView as? UICollectionView, 158 | let layout = collectionView.collectionViewLayout as? ThumbnailFlowLayoutDraggingBehavior{ 159 | layout.unfoldCurrentCell() 160 | } 161 | } 162 | 163 | func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) { 164 | if !decelerate, 165 | let collectionView = scrollView as? UICollectionView, 166 | let layout = collectionView.collectionViewLayout as? ThumbnailFlowLayoutDraggingBehavior{ 167 | layout.unfoldCurrentCell() 168 | } 169 | } 170 | } 171 | 172 | //MARK: - CollectionViewCellSize Protocol 173 | extension AlbumViewController: CollectionViewCellSize { 174 | func collectionView(_ collectionView: UICollectionView, sizeForItemAt indexPath: IndexPath) -> CGSize? { 175 | switch collectionView { 176 | case hdCollectionView: 177 | if let size = hdPhotoModel.photoSize(at: indexPath.row) { 178 | return cellSize(forHDImage: size) 179 | } 180 | case thumbnailCollectionView: 181 | if let size = thumbnailPhotoModel.photoSize(at: indexPath.row) { 182 | return cellSize(forThumbImage: size) 183 | } 184 | default: 185 | return nil 186 | } 187 | return nil 188 | } 189 | 190 | fileprivate func cellSize(forHDImage size: CGSize) -> CGSize? { 191 | let ratio = size.height / size.width 192 | if (ratio < hdCollectionViewRatio) { 193 | return CGSize(width: hdCollectionView.frame.size.width, height: hdCollectionView.frame.size.width * ratio) 194 | } else { 195 | return CGSize(width: hdCollectionView.frame.size.height / ratio, height: hdCollectionView.frame.size.height) 196 | } 197 | } 198 | 199 | fileprivate func cellSize(forThumbImage size: CGSize) -> CGSize? { 200 | let ratio = size.height / size.width 201 | if (ratio > thumbnailCollectionViewThinnestRatio) { 202 | return CGSize(width: thumbnailCollectionView.cellNormalWidth, height: thumbnailCollectionView.cellHeight) 203 | } else if ratio < thumbnailCollectionViewThickestRatio { 204 | return CGSize(width: thumbnailCollectionView.cellMaximumWidth, height: thumbnailCollectionView.cellHeight) 205 | } else { 206 | return CGSize(width: thumbnailCollectionView.frame.size.height / ratio, height: thumbnailCollectionView.frame.size.height) 207 | } 208 | } 209 | } 210 | -------------------------------------------------------------------------------- /ScrollingAlbum/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // ScrollingAlbum 4 | // 5 | // Created by RippleArc on 12/23/17. 6 | // Copyright © 2017 RippleArc. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | @UIApplicationMain 12 | class AppDelegate: UIResponder, UIApplicationDelegate { 13 | 14 | var window: UIWindow? 15 | 16 | 17 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool { 18 | 19 | let albumViewController = 20 | window!.rootViewController as! AlbumViewController 21 | 22 | // dependency inject the models 23 | albumViewController.hdPhotoModel = PhotoCollection(photos: [ "barton_nature_area_bridge_hd.JPG", "barton_nature_lake_hd.JPG", "barton_nature_swan_hd.JPG", "bird_hills_nature_tree_hd.JPG", "bird_hills_nature_sunset_hd.JPG", "huron_river_hd.JPG", "bird_hills_nature_foliage_hd.JPG","leslie_park_hd.png", "willowtree_apartment_sunset_hd.jpg", "vertical_strip_hd.png", "winsor_skyline_hd.png","barton_nature_leeve_hd.JPG"]) 24 | 25 | albumViewController.thumbnailPhotoModel = PhotoCollection(photos: [ "barton_nature_area_bridge.JPG", "barton_nature_lake.JPG", "barton_nature_swan.JPG", "bird_hills_nature_tree.JPG", "bird_hills_nature_sunset.JPG", "huron_river.JPG", "bird_hills_nature_foliage.JPG","leslie_park", "willowtree_apartment_sunset.jpg", "vertical_strip.png", "winsor_skyline.png", "barton_nature_leeve.JPG" 26 | ]) 27 | 28 | albumViewController.flowLayoutSyncManager = FlowLayoutSyncManager() 29 | albumViewController.debug = false 30 | return true 31 | } 32 | 33 | func applicationWillResignActive(_ application: UIApplication) { 34 | // Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state. 35 | // Use this method to pause ongoing tasks, disable timers, and invalidate graphics rendering callbacks. Games should use this method to pause the game. 36 | } 37 | 38 | func applicationDidEnterBackground(_ application: UIApplication) { 39 | // Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later. 40 | // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits. 41 | } 42 | 43 | func applicationWillEnterForeground(_ application: UIApplication) { 44 | // Called as part of the transition from the background to the active state; here you can undo many of the changes made on entering the background. 45 | } 46 | 47 | func applicationDidBecomeActive(_ application: UIApplication) { 48 | // Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface. 49 | } 50 | 51 | func applicationWillTerminate(_ application: UIApplication) { 52 | // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:. 53 | } 54 | 55 | 56 | } 57 | 58 | -------------------------------------------------------------------------------- /ScrollingAlbum/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "size" : "20x20", 6 | "scale" : "2x" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "size" : "20x20", 11 | "scale" : "3x" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "size" : "29x29", 16 | "scale" : "2x" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "size" : "29x29", 21 | "scale" : "3x" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "size" : "40x40", 26 | "scale" : "2x" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "size" : "40x40", 31 | "scale" : "3x" 32 | }, 33 | { 34 | "idiom" : "iphone", 35 | "size" : "60x60", 36 | "scale" : "2x" 37 | }, 38 | { 39 | "idiom" : "iphone", 40 | "size" : "60x60", 41 | "scale" : "3x" 42 | }, 43 | { 44 | "idiom" : "ipad", 45 | "size" : "20x20", 46 | "scale" : "1x" 47 | }, 48 | { 49 | "idiom" : "ipad", 50 | "size" : "20x20", 51 | "scale" : "2x" 52 | }, 53 | { 54 | "idiom" : "ipad", 55 | "size" : "29x29", 56 | "scale" : "1x" 57 | }, 58 | { 59 | "idiom" : "ipad", 60 | "size" : "29x29", 61 | "scale" : "2x" 62 | }, 63 | { 64 | "idiom" : "ipad", 65 | "size" : "40x40", 66 | "scale" : "1x" 67 | }, 68 | { 69 | "idiom" : "ipad", 70 | "size" : "40x40", 71 | "scale" : "2x" 72 | }, 73 | { 74 | "idiom" : "ipad", 75 | "size" : "76x76", 76 | "scale" : "1x" 77 | }, 78 | { 79 | "idiom" : "ipad", 80 | "size" : "76x76", 81 | "scale" : "2x" 82 | }, 83 | { 84 | "idiom" : "ipad", 85 | "size" : "83.5x83.5", 86 | "scale" : "2x" 87 | } 88 | ], 89 | "info" : { 90 | "version" : 1, 91 | "author" : "xcode" 92 | } 93 | } -------------------------------------------------------------------------------- /ScrollingAlbum/FlowLayout/CellAnimationMeasurement.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CellAnimationMeasurement.swift 3 | // ScrollingAlbum 4 | // 5 | // Created by RippleArc on 12/25/17. 6 | // Copyright © 2017 RippleArc. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | protocol CellAnimationMeasurement { 12 | var animatedCellIndex: Int { get set} 13 | var originalInsetAndContentOffset: (CGFloat, CGFloat) { get set} 14 | var animatedCellType: AnimatedCellType { get set } 15 | } 16 | -------------------------------------------------------------------------------- /ScrollingAlbum/FlowLayout/CellBasicMeasurement.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CellBasicConfiguration.swift 3 | // ScrollingAlbum 4 | // 5 | // Created by RippleArc on 12/25/17. 6 | // Copyright © 2017 RippleArc. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | protocol CellBasicMeasurement: class { 12 | var currentCellIndex: Int { get } 13 | var cellMaximumWidth: CGFloat { get } 14 | } 15 | 16 | extension CellBasicMeasurement where Self: UICollectionViewLayout { 17 | var cellMaximumWidth: CGFloat { 18 | guard let collectionView = collectionView as? CellConfiguratedCollectionView else { return 0 } 19 | return collectionView.cellMaximumWidth 20 | } 21 | 22 | func cellFullWidth(for indexPath: IndexPath) -> CGFloat { 23 | guard let collectionView = collectionView as? CellConfiguratedCollectionView else { return 0 } 24 | return collectionView.cellSize(for: indexPath)?.width ?? 0 25 | } 26 | 27 | var cellFullSpacing: CGFloat { 28 | guard let collectionView = collectionView as? CellConfiguratedCollectionView else { return 0 } 29 | return collectionView.cellFullSpacing 30 | } 31 | 32 | var cellNormalWidth: CGFloat { 33 | guard let collectionView = collectionView as? CellConfiguratedCollectionView else { return 0 } 34 | return collectionView.cellNormalWidth 35 | } 36 | 37 | var cellNormalSpacing: CGFloat { 38 | guard let collectionView = collectionView as? CellConfiguratedCollectionView else { return 0 } 39 | return collectionView.cellNormalSpacing 40 | } 41 | 42 | var cellNormalWidthAndSpacing: CGFloat { 43 | return cellNormalWidth + cellNormalSpacing 44 | } 45 | 46 | var cellNormalSize: CGSize { 47 | guard let collectionView = collectionView as? CellConfiguratedCollectionView else { return CGSize.zero } 48 | return CGSize(width:collectionView.cellNormalWidth, height:collectionView.cellHeight) 49 | } 50 | 51 | var cellHeight: CGFloat { 52 | guard let collectionView = collectionView as? CellConfiguratedCollectionView else { return 0 } 53 | return collectionView.cellHeight 54 | } 55 | 56 | var cellCount: Int { 57 | return collectionView!.dataSource!.collectionView(collectionView!, numberOfItemsInSection: 0) 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /ScrollingAlbum/FlowLayout/CellPassiveMeasurement.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CellPassiveMeasurement.swift 3 | // ScrollingAlbum 4 | // 5 | // Created by RippleArc on 12/29/17. 6 | // Copyright © 2017 RippleArc. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | protocol CellPassiveMeasurement { 12 | var puppetCellIndex: Int { get set } 13 | var puppetFractionComplete: CGFloat { get set } 14 | var unitStepOfPuppet: CGFloat { get } 15 | } 16 | -------------------------------------------------------------------------------- /ScrollingAlbum/FlowLayout/FlowLayoutInvalidateBehavior.swift: -------------------------------------------------------------------------------- 1 | // 2 | // flowLayoutInvalidateBehavior.swift 3 | // ScrollingAlbum 4 | // 5 | // Created by RippleArc on 12/30/17. 6 | // Copyright © 2017 RippleArc. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | protocol FlowLayoutInvalidateBehavior { 12 | var shouldLayoutEverything: Bool { get set } 13 | } 14 | 15 | -------------------------------------------------------------------------------- /ScrollingAlbum/FlowLayout/Layout/HDFlowLayout.swift: -------------------------------------------------------------------------------- 1 | // 2 | // HDFlowLayout.swift 3 | // ScrollingAlbum 4 | // 5 | // Created by RippleArc on 12/25/17. 6 | // Copyright © 2017 RippleArc. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class HDFlowLayout: UICollectionViewFlowLayout, CellBasicMeasurement, FlowLayoutInvalidateBehavior { 12 | 13 | fileprivate func cellIndex(at offset: CGFloat) -> Int { 14 | return Int(offset / cellMaximumWidth) 15 | } 16 | 17 | fileprivate var currentOffset: CGFloat { 18 | return (collectionView!.contentOffset.x + collectionView!.contentInset.left) 19 | } 20 | 21 | var currentCellIndex: Int { 22 | return min(cellCount - 1, Int(currentOffset / cellMaximumWidth)) 23 | } 24 | 25 | fileprivate var currentFractionComplete: CGFloat { 26 | let relativeOffset = currentOffset / cellMaximumWidth 27 | return modf(relativeOffset).1 28 | } 29 | 30 | // MARK: - Stored Property 31 | fileprivate var cellEstimatedCenterPoints: [CGPoint] = [] 32 | fileprivate var cellEstimatedFrames: [CGRect] = [] 33 | var shouldLayoutEverything = true 34 | let minimumPhotoWidth: CGFloat = 40 35 | 36 | // MARK: - Injection 37 | var flowLayoutSyncManager: FlowLayoutSync! 38 | } 39 | 40 | 41 | //MARK: - UICollectionViewFlowLayout Override 42 | extension HDFlowLayout { 43 | override func prepare() { 44 | guard shouldLayoutEverything else { return } 45 | 46 | cellEstimatedCenterPoints = [] 47 | cellEstimatedFrames = [] 48 | for itemIndex in 0 ..< cellCount { 49 | var cellCenter: CGPoint = CGPoint(x: 0, y: 0) 50 | cellCenter.y = collectionView!.frame.size.height / 2.0 51 | cellCenter.x = cellMaximumWidth * CGFloat(itemIndex) + cellMaximumWidth / 2.0 52 | cellEstimatedCenterPoints.append(cellCenter) 53 | cellEstimatedFrames.append(CGRect.init(origin: CGPoint.init(x: cellMaximumWidth * CGFloat(itemIndex), y: 0), size: CGSize.init(width: cellMaximumWidth, height: cellHeight))) 54 | } 55 | shouldLayoutEverything = false 56 | } 57 | 58 | override var collectionViewContentSize: CGSize { 59 | let contentWidth = cellMaximumWidth * CGFloat(cellCount) 60 | let contentHeight = cellHeight 61 | return CGSize(width: contentWidth, height: contentHeight) 62 | } 63 | 64 | override func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? { 65 | guard let attributes = super.layoutAttributesForItem(at: indexPath) else { 66 | return nil 67 | } 68 | 69 | if let collectionView = collectionView as? CellConfiguratedCollectionView, 70 | let cellSize = collectionView.cellSize(for: indexPath) { 71 | switch indexPath.item { 72 | case currentCellIndex: 73 | attributes.size = CGSize(width: max(minimumPhotoWidth, cellSize.width - cellFullSpacing * currentFractionComplete), height: cellSize.height) 74 | 75 | case currentCellIndex + 1: 76 | attributes.size = CGSize(width: max(minimumPhotoWidth, cellSize.width - cellFullSpacing * (1-currentFractionComplete)), height: cellSize.height) 77 | 78 | default: 79 | attributes.size = CGSize(width: cellMaximumWidth, height: cellHeight) 80 | } 81 | attributes.center = cellEstimatedCenterPoints[indexPath.row] 82 | } 83 | 84 | return attributes 85 | } 86 | 87 | override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? { 88 | 89 | flowLayoutSyncManager.didMove(collectionView!, to: IndexPath(item:currentCellIndex, section:0), with: currentFractionComplete) 90 | 91 | var allAttributes: [UICollectionViewLayoutAttributes] = [] 92 | for itemIndex in 0 ..< cellCount { 93 | if rect.intersects(cellEstimatedFrames[itemIndex]) { 94 | let indexPath = IndexPath(item: itemIndex, section: 0) 95 | let attributes = layoutAttributesForItem(at: indexPath)! 96 | allAttributes.append(attributes) 97 | } 98 | } 99 | return allAttributes 100 | } 101 | } 102 | 103 | //MARK: - Invalidate Context 104 | 105 | extension HDFlowLayout { 106 | override func shouldInvalidateLayout(forBoundsChange newBounds: CGRect) -> Bool { 107 | return true 108 | } 109 | 110 | override func invalidationContext(forBoundsChange newBounds: CGRect) -> UICollectionViewLayoutInvalidationContext { 111 | let context = super.invalidationContext(forBoundsChange: newBounds) 112 | 113 | if newBounds.size != collectionView!.bounds.size { 114 | shouldLayoutEverything = true 115 | } 116 | return context 117 | } 118 | 119 | override func invalidateLayout(with context: UICollectionViewLayoutInvalidationContext) { 120 | if context.invalidateEverything || context.invalidateDataSourceCounts { 121 | shouldLayoutEverything = true 122 | } 123 | super.invalidateLayout(with: context) 124 | } 125 | } 126 | 127 | -------------------------------------------------------------------------------- /ScrollingAlbum/FlowLayout/Layout/ThumbnailMasterFlowLayout.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ThumbnailMasterFlowLayout.swift 3 | // ScrollingAlbum 4 | // 5 | // Created by RippleArc on 12/25/17. 6 | // Copyright © 2017 RippleArc. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | enum AnimatedCellType { 12 | case folding 13 | case unfolding 14 | } 15 | 16 | class ThumbnailMasterFlowLayout: UICollectionViewFlowLayout, CellBasicMeasurement, CellAnimationMeasurement { 17 | 18 | fileprivate var normalCenterPoints: [CGPoint] = [] 19 | var shouldLayoutEverything = true 20 | 21 | var animatedCellType: AnimatedCellType = .folding 22 | 23 | // Dependency Injection 24 | var accordionAnimationManager: AccordionAnimation! 25 | var flowLayoutSyncManager: FlowLayoutSync! 26 | 27 | var animatedCellIndex: Int = 0 28 | 29 | var originalInsetAndContentOffset: (CGFloat, CGFloat) = (0, 0) { 30 | didSet { 31 | if animatedCellType == .unfolding { 32 | unfoldingCenterOffset = originalInsetAndContentOffset.0 + originalInsetAndContentOffset.1 + cellNormalWidthAndSpacing / 2 - normalCenterPoints[currentCellIndex].x 33 | } 34 | } 35 | } 36 | 37 | var currentCellIndex: Int { 38 | return min(cellCount-1, Int(currentOffset / cellNormalWidthAndSpacing)) 39 | } 40 | 41 | fileprivate var unfoldingCenterOffset: CGFloat = 0 42 | } 43 | 44 | //MARK: - ThumbnailFlowLayout 45 | extension ThumbnailMasterFlowLayout: ThumbnailFlowLayoutDraggingBehavior { 46 | func foldCurrentCell() { 47 | startAnimation(of: .folding) 48 | } 49 | 50 | func unfoldCurrentCell() { 51 | startAnimation(of: .unfolding) 52 | } 53 | 54 | fileprivate func startAnimation(of type:AnimatedCellType) { 55 | accordionAnimationManager.startAnimation(collectionView!, animationType: type, cellLength: cellFullWidth(for: animatedCellIndexPath), onProgress: { [weak self] _ in 56 | if let strongSelf = self { 57 | strongSelf.onAnimationUpdate(of: type) 58 | } 59 | }) 60 | } 61 | 62 | fileprivate func onAnimationUpdate(of type: AnimatedCellType) { 63 | invalidateLayout() 64 | 65 | setContentInset() 66 | if type == .unfolding { 67 | setContentOffset() 68 | } 69 | } 70 | } 71 | 72 | //MARK: - UICollectionViewFlowLayout Override 73 | extension ThumbnailMasterFlowLayout { 74 | override func prepare() { 75 | guard shouldLayoutEverything else { return } 76 | 77 | normalCenterPoints = [] 78 | for itemIndex in 0 ..< cellCount { 79 | var cellCenter: CGPoint = CGPoint.zero 80 | cellCenter.y = cellHeight / 2.0 81 | cellCenter.x = CGFloat(itemIndex) * cellNormalWidthAndSpacing + cellNormalSpacing + cellNormalWidth / 2.0 82 | normalCenterPoints.append(cellCenter) 83 | } 84 | shouldLayoutEverything = false 85 | } 86 | 87 | override var collectionViewContentSize: CGSize { 88 | let contentWidth = 2 * adjacentSpacingOfAnimatedCell 89 | + animatedCellSize.width 90 | + fmax(0.0, CGFloat(cellCount - 1)) * cellNormalWidthAndSpacing 91 | 92 | return CGSize(width: contentWidth, height: cellHeight) 93 | } 94 | 95 | override func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? { 96 | guard let attributes = super.layoutAttributesForItem(at: indexPath) else { 97 | return nil 98 | } 99 | 100 | if indexPath.item < animatedCellIndex { 101 | attributes.size = cellNormalSize 102 | attributes.center = normalCenterPoints[indexPath.item] 103 | } else if indexPath.item > animatedCellIndex { 104 | attributes.size = cellNormalSize 105 | attributes.center = centerAfterAnimatedCell(for: indexPath) 106 | } else { 107 | attributes.size = animatedCellSize 108 | attributes.center = animatedCellCenter 109 | } 110 | 111 | return attributes 112 | } 113 | 114 | override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? { 115 | flowLayoutSyncManager.didMove(collectionView!, to: IndexPath(item:currentCellIndex, section:0), with: 0) 116 | 117 | var allAttributes: [UICollectionViewLayoutAttributes] = [] 118 | for itemIndex in 0 ..< cellCount { 119 | if rect.contains(normalCenterPoints[itemIndex]) { 120 | let indexPath = IndexPath(item: itemIndex, section: 0) 121 | let attributes = layoutAttributesForItem(at: indexPath)! 122 | allAttributes.append(attributes) 123 | } 124 | } 125 | 126 | return allAttributes 127 | } 128 | 129 | fileprivate func setContentInset() { 130 | collectionView!.contentInset.left = symmetricContentInset 131 | collectionView!.contentInset.right = symmetricContentInset 132 | } 133 | 134 | fileprivate func setContentOffset() { 135 | if accordionAnimationManager.progress() < 1, 136 | accordionAnimationManager.progress() > 0 { 137 | let insetOffset = symmetricContentInset - originalInsetAndContentOffset.0 138 | var cellCenterOffset: CGFloat = 0 139 | if animatedCellType == .unfolding { 140 | cellCenterOffset = unfoldingCenterOffset * accordionAnimationManager.progress() 141 | } 142 | collectionView!.contentOffset.x = originalInsetAndContentOffset.1 - insetOffset - cellCenterOffset 143 | } 144 | } 145 | } 146 | 147 | //MARK: - Invalidate Context 148 | extension ThumbnailMasterFlowLayout { 149 | override func shouldInvalidateLayout(forBoundsChange newBounds: CGRect) -> Bool { 150 | return true 151 | } 152 | 153 | override func invalidationContext(forBoundsChange newBounds: CGRect) -> UICollectionViewLayoutInvalidationContext { 154 | let context = super.invalidationContext(forBoundsChange: newBounds) 155 | 156 | if newBounds.size != collectionView!.bounds.size { 157 | shouldLayoutEverything = true 158 | } 159 | return context 160 | } 161 | 162 | override func invalidateLayout(with context: UICollectionViewLayoutInvalidationContext) { 163 | if context.invalidateEverything || context.invalidateDataSourceCounts { 164 | shouldLayoutEverything = true 165 | } 166 | super.invalidateLayout(with: context) 167 | } 168 | } 169 | 170 | // MARK: - Computed Property 171 | 172 | extension ThumbnailMasterFlowLayout { 173 | 174 | fileprivate var symmetricContentInset: CGFloat{ 175 | return collectionView!.superview!.frame.size.width / 2.0 176 | - adjacentSpacingOfAnimatedCell 177 | - animatedCellSize.width / 2 178 | 179 | } 180 | 181 | fileprivate var currentOffset: CGFloat { 182 | return (collectionView!.contentOffset.x + collectionView!.contentInset.left + cellNormalWidthAndSpacing / 2) 183 | } 184 | } 185 | 186 | // MARK: - Cell Center and Size 187 | 188 | extension ThumbnailMasterFlowLayout { 189 | 190 | fileprivate var animatedCellIndexPath: IndexPath { 191 | return IndexPath(item: animatedCellIndex, section: 0) 192 | } 193 | 194 | fileprivate func centerAfterAnimatedCell(for indexPath: IndexPath) -> CGPoint { 195 | guard indexPath.item > animatedCellIndexPath.item else { return CGPoint.zero } 196 | return CGPoint(x: animatedCellCenter.x 197 | + animatedCellSize.width / 2.0 198 | + adjacentSpacingOfAnimatedCell 199 | + cellNormalWidthAndSpacing * fmax(0, CGFloat(indexPath.item - animatedCellIndex - 1)) 200 | + cellNormalWidth / 2, 201 | y: cellHeight / 2) 202 | } 203 | 204 | fileprivate var animatedCellCenter: CGPoint { 205 | return CGPoint(x: CGFloat(animatedCellIndex) * cellNormalWidthAndSpacing + adjacentSpacingOfAnimatedCell + animatedCellSize.width / 2,y: cellHeight / 2) 206 | } 207 | 208 | fileprivate var adjacentSpacingOfAnimatedCell: CGFloat { 209 | return (cellFullSpacing - cellNormalSpacing) * accordionAnimationManager.progress() + cellNormalSpacing 210 | } 211 | 212 | fileprivate var animatedCellSize: CGSize { 213 | return CGSize(width: (cellFullWidth(for: animatedCellIndexPath) - cellNormalWidth) * accordionAnimationManager.progress() + cellNormalWidth, height: cellHeight) 214 | } 215 | } 216 | -------------------------------------------------------------------------------- /ScrollingAlbum/FlowLayout/Layout/ThumbnailSlaveFlowLayout.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ThumbnailSlaveFlowLayout.swift 3 | // ScrollingAlbum 4 | // 5 | // Created by RippleArc on 12/29/17. 6 | // Copyright © 2017 RippleArc. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class ThumbnailSlaveFlowLayout: UICollectionViewFlowLayout, CellBasicMeasurement, CellPassiveMeasurement { 12 | 13 | //MARK: - CellBasicMeasurement 14 | var currentCellIndex: Int = 0 15 | 16 | //MARK: - CellPassiveMeasurement 17 | var puppetCellIndex: Int = 0 18 | var puppetFractionComplete: CGFloat = 0 19 | var unitStepOfPuppet: CGFloat { 20 | return cellNormalWidth + cellNormalSpacing 21 | } 22 | 23 | //MARK: - Stored Property 24 | fileprivate var estimatedCenterPoints: [CGPoint] = [] 25 | var shouldLayoutEverything = true 26 | 27 | // MARK: - Layout Overrides 28 | override func prepare() { 29 | estimatedCenterPoints = [] 30 | for itemIndex in 0 ..< cellCount { 31 | var cellCenter: CGPoint = CGPoint.zero 32 | cellCenter.y = cellHeight / 2.0 33 | cellCenter.x = CGFloat(itemIndex) * cellNormalWidthAndSpacing + cellNormalSpacing + cellNormalWidth / 2.0 34 | estimatedCenterPoints.append(cellCenter) 35 | } 36 | shouldLayoutEverything = false 37 | } 38 | 39 | override var collectionViewContentSize: CGSize { 40 | let contentWidth = 2 * cellFullSpacing 41 | + focusedCellSize.width 42 | + nextFocusedCellSize.width 43 | + fmax(0.0, CGFloat(cellCount - 2)) * cellNormalWidthAndSpacing 44 | + cellNormalSpacing 45 | 46 | return CGSize(width: contentWidth, height: cellHeight) 47 | } 48 | 49 | override func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? { 50 | guard let attributes = super.layoutAttributesForItem(at: indexPath) else { 51 | return nil 52 | } 53 | 54 | if indexPath.item < puppetCellIndex { 55 | attributes.size = cellNormalSize 56 | attributes.center = estimatedCenterPoints[indexPath.item] 57 | } else if indexPath.item > puppetCellIndex + 1 { 58 | attributes.size = cellNormalSize 59 | attributes.center = centerAfterNextFocusedCell(for: indexPath) 60 | } else if indexPath.item == puppetCellIndex { 61 | attributes.size = focusedCellSize 62 | attributes.center = focusedCellCenter 63 | } else if indexPath.item == puppetCellIndex + 1 { 64 | attributes.size = nextFocusedCellSize 65 | attributes.center = nextFocusedCellCenter 66 | } 67 | 68 | return attributes 69 | } 70 | 71 | override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? { 72 | setCollectionViewInset() 73 | 74 | var allAttributes: [UICollectionViewLayoutAttributes] = [] 75 | for itemIndex in 0 ..< cellCount { 76 | if rect.contains(estimatedCenterPoints[itemIndex]) || 77 | rect.contains(estimatedCenterPoints[itemIndex]){ 78 | let indexPath = IndexPath(item: itemIndex, section: 0) 79 | let attributes = layoutAttributesForItem(at: indexPath)! 80 | allAttributes.append(attributes) 81 | } 82 | } 83 | return allAttributes 84 | } 85 | } 86 | 87 | //MARK: - Invalidate Context 88 | extension ThumbnailSlaveFlowLayout { 89 | override func shouldInvalidateLayout(forBoundsChange newBounds: CGRect) -> Bool { 90 | return true 91 | } 92 | 93 | override func invalidationContext(forBoundsChange newBounds: CGRect) -> UICollectionViewLayoutInvalidationContext { 94 | let context = super.invalidationContext(forBoundsChange: newBounds) 95 | 96 | if newBounds.size != collectionView!.bounds.size { 97 | shouldLayoutEverything = true 98 | } 99 | return context 100 | } 101 | 102 | override func invalidateLayout(with context: UICollectionViewLayoutInvalidationContext) { 103 | if context.invalidateEverything || context.invalidateDataSourceCounts { 104 | shouldLayoutEverything = true 105 | } 106 | super.invalidateLayout(with: context) 107 | } 108 | 109 | fileprivate func centerAfterNextFocusedCell(for indexPath: IndexPath) -> CGPoint { 110 | guard (indexPath.item > next(to: currentIndexPath).item) else { return CGPoint.zero } 111 | return CGPoint(x: nextFocusedCellCenter.x 112 | + nextFocusedCellSize.width / 2 113 | + rightSpacingOfNextFocusedCell 114 | + cellNormalWidthAndSpacing * CGFloat(indexPath.item - puppetCellIndex - 2) 115 | + cellNormalWidth / 2, 116 | y: nextFocusedCellCenter.y) 117 | } 118 | 119 | fileprivate func setCollectionViewInset() { 120 | let inset = collectionView!.superview!.frame.size.width / 2.0 121 | - cellFullSpacing 122 | - (focusedCellSize.width + nextFocusedCellSize.width - cellNormalWidth) / 2 123 | collectionView!.contentInset.left = inset 124 | collectionView!.contentInset.right = inset 125 | } 126 | } 127 | 128 | 129 | // MARK: - Helper 130 | 131 | extension ThumbnailSlaveFlowLayout { 132 | 133 | fileprivate var currentIndexPath: IndexPath { 134 | return IndexPath(item:puppetCellIndex, section: 0) 135 | } 136 | 137 | fileprivate var leftSpacingOfFocusedCell: CGFloat { 138 | return (cellFullSpacing - cellNormalSpacing) * (1 - puppetFractionComplete) + cellNormalSpacing 139 | } 140 | 141 | fileprivate var rightSpacingOfNextFocusedCell: CGFloat { 142 | return cellFullSpacing + cellNormalSpacing - leftSpacingOfFocusedCell 143 | } 144 | 145 | fileprivate var focusedCellSize: CGSize { 146 | if puppetFractionComplete < 0 { 147 | return CGSize(width: cellFullWidth(for:currentIndexPath), height: cellHeight) 148 | } else { 149 | return CGSize(width: (cellFullWidth(for:currentIndexPath) - cellNormalWidth) * (1 - puppetFractionComplete) + cellNormalWidth, height:cellHeight) 150 | } 151 | } 152 | 153 | fileprivate var focusedCellCenter: CGPoint { 154 | if puppetFractionComplete < 0 { 155 | return CGPoint(x: cellFullSpacing + cellFullWidth(for:currentIndexPath) / 2, y: cellHeight / 2) 156 | } else { 157 | return CGPoint(x: CGFloat(puppetCellIndex) * cellNormalWidthAndSpacing 158 | + leftSpacingOfFocusedCell 159 | + focusedCellSize.width / 2, y: cellHeight / 2) 160 | } 161 | } 162 | 163 | fileprivate var nextFocusedCellSize: CGSize { 164 | return CGSize(width: (cellFullWidth(for:next(to: currentIndexPath)) - cellNormalWidth) * puppetFractionComplete + cellNormalWidth, height: cellHeight) 165 | } 166 | 167 | fileprivate var nextFocusedCellCenter: CGPoint { 168 | return CGPoint(x: focusedCellCenter.x 169 | + focusedCellSize.width / 2 170 | + nextFocusedCellSize.width / 2 171 | + cellFullSpacing, y: cellHeight / 2) 172 | } 173 | 174 | fileprivate func prev(to indexPath: IndexPath) -> IndexPath{ 175 | if indexPath.item > 0 { 176 | return IndexPath(item: indexPath.item - 1, section: 0) 177 | } else { 178 | return indexPath 179 | } 180 | } 181 | 182 | fileprivate func next(to indexPath: IndexPath) -> IndexPath{ 183 | return IndexPath(item: indexPath.item + 1, section: 0) 184 | } 185 | } 186 | -------------------------------------------------------------------------------- /ScrollingAlbum/FlowLayout/Manager/AcoordionAnimationManager.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FlowLayoutAnimationProgressManager.swift 3 | // ScrollingAlbum 4 | // 5 | // Created by RippleArc on 12/25/17. 6 | // Copyright © 2017 RippleArc. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | typealias timerHandler = (Timer) -> Void 12 | 13 | protocol AccordionAnimation { 14 | mutating func startAnimation(_ collectionView: UICollectionView, animationType: AnimatedCellType, cellLength: CGFloat, onProgress: @escaping timerHandler) 15 | func progress() -> CGFloat 16 | } 17 | 18 | struct AcoordionAnimationManager: AccordionAnimation { 19 | var animationMinimumDuration: TimeInterval = 0.25 20 | var cellBaseLength:CGFloat = 80 21 | 22 | fileprivate var timer: Timer? 23 | fileprivate var animationType: AnimatedCellType = .folding 24 | fileprivate var animationStartTime: TimeInterval = 0 25 | fileprivate var cellLength: CGFloat = 0 26 | fileprivate var timeInterval: TimeInterval = 0.02 27 | 28 | mutating func startAnimation(_ collectionView: UICollectionView, animationType: AnimatedCellType, cellLength: CGFloat, onProgress block: @escaping timerHandler) { 29 | 30 | self.animationType = animationType 31 | self.cellLength = cellLength 32 | self.animationStartTime = Date().timeIntervalSince1970 33 | 34 | setupCollectionViewForAnimation(collectionView) 35 | 36 | startTimer(onProgress:block) 37 | } 38 | 39 | func progress() -> CGFloat { 40 | let currentTime = Date().timeIntervalSince1970 41 | switch animationType { 42 | case .folding: 43 | return foldingAnimationProgress(currentTime) 44 | case .unfolding: 45 | return unfoldingAnimationProgress(currentTime) 46 | } 47 | } 48 | 49 | fileprivate func setupCollectionViewForAnimation(_ collectionView: UICollectionView) { 50 | if let basicMeasurement = collectionView.collectionViewLayout as? CellBasicMeasurement, 51 | var animationMeasurement = collectionView.collectionViewLayout as? CellAnimationMeasurement { 52 | animationMeasurement.animatedCellType = animationType 53 | animationMeasurement.originalInsetAndContentOffset = (collectionView.contentInset.left , collectionView.contentOffset.x) 54 | 55 | animationMeasurement.animatedCellIndex = basicMeasurement.currentCellIndex 56 | } 57 | } 58 | 59 | mutating fileprivate func startTimer(onProgress: @escaping timerHandler) { 60 | timer?.invalidate() 61 | timer = Timer.scheduledTimer(withTimeInterval: timeInterval, repeats: true, block: onProgress) 62 | if let timer = timer { 63 | RunLoop.main.add(timer, forMode: .commonModes) 64 | } 65 | } 66 | 67 | fileprivate func endTimer() { 68 | timer?.invalidate() 69 | } 70 | } 71 | 72 | //MARK: - Helper 73 | extension AcoordionAnimationManager { 74 | 75 | fileprivate var animationDuration: TimeInterval { 76 | return fmax(animationMinimumDuration, TimeInterval(cellLength / cellBaseLength) * animationMinimumDuration) 77 | } 78 | 79 | fileprivate func unfoldingAnimationProgress(_ currentTime: TimeInterval) -> CGFloat { 80 | if animationStartTime == 0 { 81 | return 0 82 | } else if currentTime >= animationStartTime + animationDuration { 83 | endTimer() 84 | return 1 85 | } else { 86 | return CGFloat((currentTime - animationStartTime) / animationDuration) 87 | } 88 | } 89 | 90 | fileprivate func foldingAnimationProgress(_ currentTime: TimeInterval) -> CGFloat { 91 | if animationStartTime == 0 { 92 | return 1 93 | } else if currentTime >= animationStartTime + animationDuration { 94 | endTimer() 95 | return 0 96 | } else { 97 | return 1 - CGFloat((currentTime - animationStartTime) / animationDuration) 98 | } 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /ScrollingAlbum/FlowLayout/Manager/FlowLayoutSyncManager.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FlowLayoutSyncProtocol.swift 3 | // ScrollingAlbum 4 | // 5 | // Created by RippleArc on 12/28/17. 6 | // Copyright © 2017 RippleArc. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | protocol FlowLayoutSync { 12 | func register(_ collectionView: UICollectionView) 13 | var masterCollectionView: UICollectionView? { get set } 14 | func didMove(_ collectionView: UICollectionView, to indexPath:IndexPath, with fractionComplete: CGFloat) 15 | } 16 | 17 | class FlowLayoutSyncManager: FlowLayoutSync { 18 | 19 | var collectionViews = Set() 20 | var exSlave: CellConfiguratedCollectionView? 21 | 22 | func didMove(_ collectionView: UICollectionView, to indexPath: IndexPath, with fractionComplete: CGFloat) { 23 | if isHdMaster, 24 | let slave = slaveCollectionView { 25 | setThumbnailContentOffset(slave, indexPath, fractionComplete) 26 | } else if !isHdMaster, 27 | let slave = slaveCollectionView { 28 | setHDContentOffset(slave, indexPath) 29 | } 30 | } 31 | 32 | var masterCollectionView: UICollectionView? { 33 | didSet { 34 | guard !isSlaveNotChanged else { return } 35 | if (isHdMaster) { 36 | switchThumbnailToSlave() 37 | } else { 38 | switchThumbnailToMaster() 39 | } 40 | } 41 | } 42 | 43 | func register(_ collectionView: UICollectionView) { 44 | guard let collectionView = collectionView as? CellConfiguratedCollectionView else { return } 45 | collectionViews.insert(collectionView) 46 | } 47 | } 48 | 49 | 50 | // MARK: - Helper 51 | extension FlowLayoutSyncManager { 52 | 53 | fileprivate func switchThumbnailToSlave() { 54 | slaveCollectionView?.collectionViewLayout = ThumbnailSlaveFlowLayout() 55 | } 56 | 57 | fileprivate func switchThumbnailToMaster() { 58 | let newLayout = ThumbnailMasterFlowLayout() 59 | newLayout.flowLayoutSyncManager = self 60 | newLayout.accordionAnimationManager = AcoordionAnimationManager() 61 | 62 | if let oldLayout = masterCollectionView?.collectionViewLayout as? CellPassiveMeasurement, 63 | let originalContentOffset = masterCollectionView?.contentOffset { 64 | newLayout.animatedCellIndex = oldLayout.puppetCellIndex 65 | masterCollectionView?.setCollectionViewLayout(newLayout, animated: false) 66 | masterCollectionView?.setContentOffset(originalContentOffset, animated: false) 67 | } 68 | } 69 | 70 | fileprivate func setHDContentOffset(_ slave: UICollectionView, _ indexPath: IndexPath) { 71 | if let slaveMeasurement = slave.collectionViewLayout as? CellBasicMeasurement { 72 | let slaveContentOffset = slaveMeasurement.cellMaximumWidth * (CGFloat(indexPath.item)) 73 | slave.setContentOffset((CGPoint(x: slaveContentOffset - slave.contentInset.left, y:0)), animated: false) 74 | } 75 | } 76 | 77 | fileprivate func setThumbnailContentOffset(_ slave: CellConfiguratedCollectionView, _ indexPath: IndexPath, _ fractionComplete: CGFloat) { 78 | var slaveContentOffset:CGFloat = 0 79 | 80 | if let cellSize = slave.cellSize(for: indexPath), 81 | var slaveLayout = slave.collectionViewLayout as? CellPassiveMeasurement { 82 | if fractionComplete < 0 { 83 | slaveContentOffset = cellSize.width * fractionComplete 84 | } else { 85 | slaveContentOffset = slaveLayout.unitStepOfPuppet * (CGFloat(indexPath.item) + fractionComplete) 86 | slaveLayout.puppetCellIndex = indexPath.item 87 | slaveLayout.puppetFractionComplete = fractionComplete 88 | } 89 | slave.setContentOffset(CGPoint(x: slaveContentOffset - slave.contentInset.left, y: 0), animated: false) 90 | } 91 | } 92 | 93 | // MARK: - Computed Property 94 | 95 | fileprivate var isSlaveNotChanged: Bool { 96 | if let slave = slaveCollectionView, 97 | slave != exSlave { 98 | exSlave = slave 99 | return false 100 | } else { 101 | return true 102 | } 103 | } 104 | 105 | fileprivate var isHdMaster: Bool { 106 | guard let slave = slaveCollectionView else { return false } 107 | return !(slave.collectionViewLayout is HDFlowLayout) 108 | } 109 | 110 | fileprivate var slaveCollectionView: CellConfiguratedCollectionView? { 111 | guard let master = masterCollectionView else { return nil } 112 | for view in collectionViews { 113 | if view != master{ 114 | return view 115 | } 116 | } 117 | return nil 118 | } 119 | } 120 | 121 | -------------------------------------------------------------------------------- /ScrollingAlbum/FlowLayout/ThumbnailFlowLayoutDraggingBehavior.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ThumbnailFlowLayout.swift 3 | // ScrollingAlbum 4 | // 5 | // Created by RippleArc on 12/25/17. 6 | // Copyright © 2017 RippleArc. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | protocol ThumbnailFlowLayoutDraggingBehavior { 12 | func foldCurrentCell() 13 | func unfoldCurrentCell() 14 | } 15 | -------------------------------------------------------------------------------- /ScrollingAlbum/Info.plist: -------------------------------------------------------------------------------- 1 | 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 | APPL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | LSRequiresIPhoneOS 22 | 23 | UILaunchStoryboardName 24 | LaunchScreen 25 | UIMainStoryboardFile 26 | Main 27 | UIRequiredDeviceCapabilities 28 | 29 | armv7 30 | 31 | UISupportedInterfaceOrientations 32 | 33 | UIInterfaceOrientationPortrait 34 | UIInterfaceOrientationLandscapeLeft 35 | UIInterfaceOrientationLandscapeRight 36 | 37 | UISupportedInterfaceOrientations~ipad 38 | 39 | UIInterfaceOrientationPortrait 40 | UIInterfaceOrientationPortraitUpsideDown 41 | UIInterfaceOrientationLandscapeLeft 42 | UIInterfaceOrientationLandscapeRight 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /ScrollingAlbum/helper/UIImageEx.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIImage.swift 3 | // ScrollingAlbum 4 | // 5 | // Created by RippleArc on 12/24/17. 6 | // Copyright © 2017 RippleArc. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | extension UIImage { 12 | 13 | class func stamp(image:UIImage, with index:String) -> UIImage { 14 | let imageView: UIImageView = UIImageView.init(image: image) 15 | imageView.frame = CGRect(x: 0, y: 0, width: image.size.width, height: image.size.height) 16 | imageView.translatesAutoresizingMaskIntoConstraints = false 17 | 18 | imageView.addSubview(labelStamp(from: index, frame: CGRect(x: image.size.width*0.375, y: image.size.height*0.375, width: image.size.width/4.0, height: image.size.height/4.0))) 19 | return UIImage.imageWithImageView(imageView: imageView) 20 | } 21 | 22 | class func labelStamp(from index:String, frame:CGRect) -> UILabel { 23 | let labelView: UILabel = UILabel.init(frame: frame) 24 | let sizeOfFont = frame.size.width > 200 ? 320 : 32 25 | labelView.font = UIFont(name: "HelveticaNeue", size: CGFloat(sizeOfFont) ) 26 | labelView.text = index 27 | labelView.textColor = UIColor.black 28 | labelView.textAlignment = .center 29 | labelView.backgroundColor = UIColor.white 30 | 31 | return labelView 32 | } 33 | 34 | class func imageWithImageView(imageView: UIImageView) -> UIImage { 35 | UIGraphicsBeginImageContextWithOptions(imageView.bounds.size, false, 0.0) 36 | imageView.layer.render(in: UIGraphicsGetCurrentContext()!) 37 | guard let img = UIGraphicsGetImageFromCurrentImageContext() else { return UIImage()} 38 | UIGraphicsEndImageContext() 39 | return img 40 | } 41 | } 42 | 43 | -------------------------------------------------------------------------------- /ScrollingAlbum/model/PhotoModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // HDPhotoDataController.swift 3 | // PhotoScroller 4 | // 5 | // Created by RippleArc on 10/24/17. 6 | // Copyright © 2017 RippleArc. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | protocol PhotoModel { 12 | func numberOfPhotos() -> Int 13 | func photoName(at index:Int) -> String? 14 | mutating func photoSize(at index: Int) -> CGSize? 15 | func photo(at index:Int, debug: Bool) -> UIImage? 16 | func photo(at index:Int) -> UIImage? 17 | } 18 | 19 | struct PhotoCollection: PhotoModel { 20 | 21 | let photoNames: [String] 22 | var photoSizes: [String:CGSize] = [:] 23 | init(photos: [String]) { 24 | photoNames = photos 25 | } 26 | 27 | func numberOfPhotos() -> Int { 28 | return photoNames.count 29 | } 30 | 31 | func photoName(at index:Int) -> String? { 32 | guard index < photoNames.count else { 33 | return nil 34 | } 35 | return photoNames[index] 36 | } 37 | 38 | mutating func photoSize(at index: Int) -> CGSize? { 39 | guard let name = photoName(at:index), name != "" else { 40 | return nil 41 | } 42 | if let size = photoSizes[name] { 43 | return size 44 | } else { 45 | photoSizes[name] = photo(at: index)?.size 46 | return photoSizes[name] 47 | } 48 | } 49 | 50 | func photo(at index:Int) -> UIImage? { 51 | return photo(at: index, debug: false) 52 | } 53 | 54 | func photo(at index:Int, debug: Bool) -> UIImage? { 55 | guard let name = photoName(at:index), name != "" else { 56 | return nil 57 | } 58 | if debug { 59 | return UIImage.stamp(image: UIImage(named:name)!, with: "\(index)") 60 | 61 | } else { 62 | return UIImage(named:name) 63 | } 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /ScrollingAlbum/samples/barton_nature_area_bridge.JPG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/suyangdong/ScrollingAlbum/9f3815e8748e94eee5bcbc3005b127f1f6754935/ScrollingAlbum/samples/barton_nature_area_bridge.JPG -------------------------------------------------------------------------------- /ScrollingAlbum/samples/barton_nature_area_bridge_hd.JPG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/suyangdong/ScrollingAlbum/9f3815e8748e94eee5bcbc3005b127f1f6754935/ScrollingAlbum/samples/barton_nature_area_bridge_hd.JPG -------------------------------------------------------------------------------- /ScrollingAlbum/samples/barton_nature_lake.JPG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/suyangdong/ScrollingAlbum/9f3815e8748e94eee5bcbc3005b127f1f6754935/ScrollingAlbum/samples/barton_nature_lake.JPG -------------------------------------------------------------------------------- /ScrollingAlbum/samples/barton_nature_lake_hd.JPG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/suyangdong/ScrollingAlbum/9f3815e8748e94eee5bcbc3005b127f1f6754935/ScrollingAlbum/samples/barton_nature_lake_hd.JPG -------------------------------------------------------------------------------- /ScrollingAlbum/samples/barton_nature_leeve.JPG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/suyangdong/ScrollingAlbum/9f3815e8748e94eee5bcbc3005b127f1f6754935/ScrollingAlbum/samples/barton_nature_leeve.JPG -------------------------------------------------------------------------------- /ScrollingAlbum/samples/barton_nature_leeve_hd.JPG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/suyangdong/ScrollingAlbum/9f3815e8748e94eee5bcbc3005b127f1f6754935/ScrollingAlbum/samples/barton_nature_leeve_hd.JPG -------------------------------------------------------------------------------- /ScrollingAlbum/samples/barton_nature_swan.JPG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/suyangdong/ScrollingAlbum/9f3815e8748e94eee5bcbc3005b127f1f6754935/ScrollingAlbum/samples/barton_nature_swan.JPG -------------------------------------------------------------------------------- /ScrollingAlbum/samples/barton_nature_swan_hd.JPG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/suyangdong/ScrollingAlbum/9f3815e8748e94eee5bcbc3005b127f1f6754935/ScrollingAlbum/samples/barton_nature_swan_hd.JPG -------------------------------------------------------------------------------- /ScrollingAlbum/samples/bird_hills_nature_foliage.JPG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/suyangdong/ScrollingAlbum/9f3815e8748e94eee5bcbc3005b127f1f6754935/ScrollingAlbum/samples/bird_hills_nature_foliage.JPG -------------------------------------------------------------------------------- /ScrollingAlbum/samples/bird_hills_nature_foliage_hd.JPG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/suyangdong/ScrollingAlbum/9f3815e8748e94eee5bcbc3005b127f1f6754935/ScrollingAlbum/samples/bird_hills_nature_foliage_hd.JPG -------------------------------------------------------------------------------- /ScrollingAlbum/samples/bird_hills_nature_sunset.JPG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/suyangdong/ScrollingAlbum/9f3815e8748e94eee5bcbc3005b127f1f6754935/ScrollingAlbum/samples/bird_hills_nature_sunset.JPG -------------------------------------------------------------------------------- /ScrollingAlbum/samples/bird_hills_nature_sunset_hd.JPG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/suyangdong/ScrollingAlbum/9f3815e8748e94eee5bcbc3005b127f1f6754935/ScrollingAlbum/samples/bird_hills_nature_sunset_hd.JPG -------------------------------------------------------------------------------- /ScrollingAlbum/samples/bird_hills_nature_tree.JPG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/suyangdong/ScrollingAlbum/9f3815e8748e94eee5bcbc3005b127f1f6754935/ScrollingAlbum/samples/bird_hills_nature_tree.JPG -------------------------------------------------------------------------------- /ScrollingAlbum/samples/bird_hills_nature_tree_hd.JPG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/suyangdong/ScrollingAlbum/9f3815e8748e94eee5bcbc3005b127f1f6754935/ScrollingAlbum/samples/bird_hills_nature_tree_hd.JPG -------------------------------------------------------------------------------- /ScrollingAlbum/samples/horizontal_strip.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/suyangdong/ScrollingAlbum/9f3815e8748e94eee5bcbc3005b127f1f6754935/ScrollingAlbum/samples/horizontal_strip.png -------------------------------------------------------------------------------- /ScrollingAlbum/samples/horizontal_strip_hd.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/suyangdong/ScrollingAlbum/9f3815e8748e94eee5bcbc3005b127f1f6754935/ScrollingAlbum/samples/horizontal_strip_hd.png -------------------------------------------------------------------------------- /ScrollingAlbum/samples/huron_river.JPG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/suyangdong/ScrollingAlbum/9f3815e8748e94eee5bcbc3005b127f1f6754935/ScrollingAlbum/samples/huron_river.JPG -------------------------------------------------------------------------------- /ScrollingAlbum/samples/huron_river_hd.JPG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/suyangdong/ScrollingAlbum/9f3815e8748e94eee5bcbc3005b127f1f6754935/ScrollingAlbum/samples/huron_river_hd.JPG -------------------------------------------------------------------------------- /ScrollingAlbum/samples/leslie_park.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/suyangdong/ScrollingAlbum/9f3815e8748e94eee5bcbc3005b127f1f6754935/ScrollingAlbum/samples/leslie_park.png -------------------------------------------------------------------------------- /ScrollingAlbum/samples/leslie_park_hd.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/suyangdong/ScrollingAlbum/9f3815e8748e94eee5bcbc3005b127f1f6754935/ScrollingAlbum/samples/leslie_park_hd.png -------------------------------------------------------------------------------- /ScrollingAlbum/samples/vertical_strip.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/suyangdong/ScrollingAlbum/9f3815e8748e94eee5bcbc3005b127f1f6754935/ScrollingAlbum/samples/vertical_strip.png -------------------------------------------------------------------------------- /ScrollingAlbum/samples/vertical_strip_hd.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/suyangdong/ScrollingAlbum/9f3815e8748e94eee5bcbc3005b127f1f6754935/ScrollingAlbum/samples/vertical_strip_hd.png -------------------------------------------------------------------------------- /ScrollingAlbum/samples/willowtree_apartment_sunset.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/suyangdong/ScrollingAlbum/9f3815e8748e94eee5bcbc3005b127f1f6754935/ScrollingAlbum/samples/willowtree_apartment_sunset.jpg -------------------------------------------------------------------------------- /ScrollingAlbum/samples/willowtree_apartment_sunset_hd.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/suyangdong/ScrollingAlbum/9f3815e8748e94eee5bcbc3005b127f1f6754935/ScrollingAlbum/samples/willowtree_apartment_sunset_hd.jpg -------------------------------------------------------------------------------- /ScrollingAlbum/samples/winsor_skyline.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/suyangdong/ScrollingAlbum/9f3815e8748e94eee5bcbc3005b127f1f6754935/ScrollingAlbum/samples/winsor_skyline.png -------------------------------------------------------------------------------- /ScrollingAlbum/samples/winsor_skyline_hd.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/suyangdong/ScrollingAlbum/9f3815e8748e94eee5bcbc3005b127f1f6754935/ScrollingAlbum/samples/winsor_skyline_hd.png -------------------------------------------------------------------------------- /ScrollingAlbum/view/Base.lproj/Main.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 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | -------------------------------------------------------------------------------- /ScrollingAlbum/view/CellConfiguratedCollectionView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CellConfiguratedCollectionView.swift 3 | // ScrollingAlbum 4 | // 5 | // Created by RippleArc on 12/25/17. 6 | // Copyright © 2017 RippleArc. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | protocol CellConfiguration { 12 | var cellMaximumWidth: CGFloat! { get set } 13 | var cellNormalWidth: CGFloat! { get set } 14 | var cellFullSpacing: CGFloat! { get set } 15 | var cellNormalSpacing: CGFloat! { get set } 16 | var cellHeight: CGFloat! { get set } 17 | func cellSize(for indexPath:IndexPath) -> CGSize? 18 | } 19 | 20 | protocol CollectionViewCellSize: AnyObject { 21 | func collectionView(_ collectionView: UICollectionView, sizeForItemAt indexPath: IndexPath) -> CGSize? 22 | } 23 | 24 | class CellConfiguratedCollectionView: UICollectionView, CellConfiguration{ 25 | weak var cellSize: CollectionViewCellSize? 26 | func cellSize(for indexPath: IndexPath) -> CGSize? { 27 | return cellSize?.collectionView(self, sizeForItemAt:indexPath) 28 | } 29 | var cellMaximumWidth: CGFloat! 30 | var cellNormalWidth: CGFloat! 31 | var cellFullSpacing: CGFloat! 32 | var cellNormalSpacing: CGFloat! 33 | var cellHeight: CGFloat! 34 | } 35 | -------------------------------------------------------------------------------- /ScrollingAlbum/view/cell/HDCollectionViewCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // HDCollectionViewCell.swift 3 | // ScrollingAlbum 4 | // 5 | // Created by RippleArc on 12/24/17. 6 | // Copyright © 2017 RippleArc. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class HDCollectionViewCell: UICollectionViewCell { 12 | @IBOutlet weak var photoView: UIImageView! 13 | @IBOutlet weak var photoViewHeightConstraint: NSLayoutConstraint! 14 | @IBOutlet weak var photoViewWidthConstraint: NSLayoutConstraint! 15 | } 16 | -------------------------------------------------------------------------------- /ScrollingAlbum/view/cell/ReusableView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ReusableView.swift 3 | // ScrollingAlbum 4 | // 5 | // Created by RippleArc on 12/24/17. 6 | // Copyright © 2017 RippleArc. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | protocol ReusableView: class {} 12 | 13 | extension ReusableView where Self: UIView { 14 | static var reuseIdentifier: String { 15 | return String(describing: self) 16 | } 17 | } 18 | 19 | extension HDCollectionViewCell: ReusableView {} 20 | extension ThumbnailCollectionViewCell: ReusableView {} 21 | -------------------------------------------------------------------------------- /ScrollingAlbum/view/cell/ThumbnailCollectionViewCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ThumbnailCollectionViewCell.swift 3 | // ScrollingAlbum 4 | // 5 | // Created by RippleArc on 12/24/17. 6 | // Copyright © 2017 RippleArc. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class ThumbnailCollectionViewCell: UICollectionViewCell { 12 | @IBOutlet weak var photoView: UIImageView! 13 | 14 | @IBOutlet weak var photoViewWidthConstraint: NSLayoutConstraint! 15 | } 16 | -------------------------------------------------------------------------------- /ScrollingAlbum/view/cell/UICollectionViewEx.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UICollectionViewEx.swift 3 | // ScrollingAlbum 4 | // 5 | // Created by RippleArc on 12/24/17. 6 | // Copyright © 2017 RippleArc. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | extension UICollectionView { 12 | func dequeueReusableCell(for indexPath: IndexPath) -> T where T: ReusableView { 13 | guard let cell = dequeueReusableCell(withReuseIdentifier: T.reuseIdentifier, for: indexPath) as? T else { 14 | fatalError("Could not dequeue cell with identifier \(T.reuseIdentifier)") 15 | } 16 | return cell 17 | } 18 | } 19 | --------------------------------------------------------------------------------