├── .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 |
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 |
--------------------------------------------------------------------------------