└── Dynamic.playground ├── Contents.swift ├── contents.xcplayground ├── playground.xcworkspace ├── contents.xcworkspacedata └── xcuserdata │ └── matt.xcuserdatad │ └── UserInterfaceState.xcuserstate └── timeline.xctimeline /Dynamic.playground/Contents.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | import XCPlayground 3 | 4 | /** 5 | 6 | # Playground of Justice 7 | 8 | */ 9 | 10 | struct Item { 11 | var height: CGFloat 12 | } 13 | 14 | // Predictability is a good thing yo 15 | srand(123) 16 | 17 | func randomHeight() -> CGFloat { 18 | return CGFloat((rand() % 100) + 50) 19 | } 20 | 21 | class DynamicLayout: UICollectionViewLayout { 22 | let verticalPadding: CGFloat = 10.0 23 | var dynamicAnimator: UIDynamicAnimator? 24 | var latestDelta: CGFloat = 0.0 25 | var staticContentSize: CGSize = CGSizeZero 26 | 27 | override init() { 28 | super.init() 29 | dynamicAnimator = UIDynamicAnimator(collectionViewLayout: self) 30 | } 31 | 32 | required init?(coder aDecoder: NSCoder) { 33 | fatalError("init(coder:) has not been implemented") 34 | } 35 | 36 | override func prepareLayout() { 37 | super.prepareLayout() 38 | 39 | guard let collectionView = collectionView, 40 | let dataSource = collectionView.dataSource as? DataSource, 41 | let dynamicAnimator = dynamicAnimator else { return } 42 | 43 | let visibleRect = CGRectInset(CGRect(origin: collectionView.bounds.origin, size: collectionView.frame.size), 0, -100) 44 | let visiblePaths = indexPaths(visibleRect) 45 | var currentlyVisible: [NSIndexPath] = [] 46 | 47 | dynamicAnimator.behaviors.forEach { behavior in 48 | if let behavior = behavior as? UIAttachmentBehavior, 49 | let item = behavior.items.first as? UICollectionViewLayoutAttributes { 50 | if !visiblePaths.contains(item.indexPath) { 51 | dynamicAnimator.removeBehavior(behavior) 52 | } else { 53 | currentlyVisible.append(item.indexPath) 54 | } 55 | } 56 | } 57 | 58 | let newlyVisible = visiblePaths.filter { path in 59 | return !currentlyVisible.contains(path) 60 | } 61 | 62 | let staticAttributes: [UICollectionViewLayoutAttributes] = newlyVisible.map { path in 63 | let attributes = UICollectionViewLayoutAttributes(forCellWithIndexPath: path) 64 | let size = dataSource.cellSizes[path.item] 65 | let origin = dataSource.cellOrigins[path.item] 66 | attributes.frame = CGRect(origin: origin, size: size) 67 | 68 | return attributes 69 | } 70 | 71 | let touchLocation = collectionView.panGestureRecognizer.locationInView(collectionView) 72 | 73 | staticAttributes.forEach { attributes in 74 | let center = attributes.center 75 | let spring = UIAttachmentBehavior(item: attributes, attachedToAnchor: center) 76 | spring.length = 0.5 77 | spring.damping = 0.1 78 | spring.frequency = 1.5 79 | 80 | if (!CGPointEqualToPoint(CGPointZero, touchLocation)) { 81 | let yDistanceFromTouch = touchLocation.y - spring.anchorPoint.y 82 | let xDistanceFromTouch = touchLocation.x - spring.anchorPoint.x 83 | let scrollResistance = (yDistanceFromTouch + xDistanceFromTouch) / 1500.0 84 | var center = attributes.center 85 | if (latestDelta < 0) { 86 | center.y += max(latestDelta, latestDelta * scrollResistance); 87 | } else { 88 | center.y += min(latestDelta, latestDelta * scrollResistance); 89 | } 90 | attributes.center = center 91 | } 92 | 93 | dynamicAnimator.addBehavior(spring) 94 | } 95 | } 96 | 97 | override func shouldInvalidateLayoutForBoundsChange(newBounds: CGRect) -> Bool { 98 | guard let collectionView = collectionView, 99 | let dynamicAnimator = dynamicAnimator else { return false } 100 | 101 | let delta = newBounds.origin.y - collectionView.bounds.origin.y 102 | latestDelta = delta 103 | 104 | let touchLocation = collectionView.panGestureRecognizer.locationInView(collectionView) 105 | dynamicAnimator.behaviors.forEach { behavior in 106 | if let springBehaviour = behavior as? UIAttachmentBehavior, let item = springBehaviour.items.first { 107 | let yDistanceFromTouch = touchLocation.y - springBehaviour.anchorPoint.y 108 | let xDistanceFromTouch = touchLocation.x - springBehaviour.anchorPoint.x 109 | let scrollResistance = (yDistanceFromTouch + xDistanceFromTouch) / 1500.0 110 | var center = item.center 111 | if (delta < 0) { 112 | center.y += max(delta, delta*scrollResistance); 113 | } else { 114 | center.y += min(delta, delta*scrollResistance); 115 | } 116 | item.center = center 117 | dynamicAnimator.updateItemUsingCurrentState(item) 118 | } 119 | } 120 | return false 121 | } 122 | 123 | override func collectionViewContentSize() -> CGSize { 124 | if staticContentSize != CGSizeZero { 125 | return staticContentSize 126 | } 127 | 128 | guard let collectionView = collectionView, 129 | let dataSource: DataSource = collectionView.dataSource as? DataSource else { return CGSizeZero } 130 | var maxY: CGFloat = 0.0 131 | (0.. maxY { 136 | maxY = newMax 137 | } 138 | } 139 | // This needs to be calculated properly 140 | staticContentSize = CGSize(width: 320, height: maxY + 10) 141 | 142 | return staticContentSize 143 | } 144 | 145 | override func layoutAttributesForElementsInRect(rect: CGRect) -> [UICollectionViewLayoutAttributes]? { 146 | return dynamicAnimator?.itemsInRect(rect).map { 147 | ($0 as? UICollectionViewLayoutAttributes)! 148 | } 149 | } 150 | 151 | override func layoutAttributesForItemAtIndexPath(indexPath: NSIndexPath) -> UICollectionViewLayoutAttributes? { 152 | return dynamicAnimator?.layoutAttributesForCellAtIndexPath(indexPath) 153 | } 154 | 155 | 156 | func firstIndexPath(rect: CGRect) -> NSIndexPath { 157 | guard let dataSource = collectionView?.dataSource as? DataSource else { return NSIndexPath(forItem: 0, inSection: 0) } 158 | 159 | for (index, origin) in dataSource.cellOrigins.enumerate() { 160 | if origin.y >= CGRectGetMinY(rect) { 161 | return NSIndexPath(forItem: index, inSection: 0) 162 | } 163 | } 164 | 165 | return NSIndexPath(forItem: 0, inSection: 0) 166 | } 167 | 168 | func lastIndexPath(rect: CGRect) -> NSIndexPath { 169 | guard let dataSource = collectionView?.dataSource as? DataSource else { return NSIndexPath(forItem: 0, inSection: 0) } 170 | 171 | for (index, origin) in dataSource.cellOrigins.enumerate() { 172 | if origin.y >= CGRectGetMaxY(rect) { 173 | return NSIndexPath(forItem: index, inSection: 0) 174 | } 175 | } 176 | 177 | return NSIndexPath(forItem: dataSource.items.count - 1, inSection: 0) 178 | } 179 | 180 | func indexPaths(rect: CGRect) -> [NSIndexPath] { 181 | let min = firstIndexPath(rect).item 182 | let max = lastIndexPath(rect).item 183 | 184 | return (min...max).map { return NSIndexPath(forItem: $0, inSection: 0) } 185 | } 186 | 187 | } 188 | 189 | class DataSource: NSObject, UICollectionViewDataSource { 190 | let items = (0..<100).map {_ in 191 | return Item( 192 | height: randomHeight() 193 | ) 194 | } 195 | 196 | let cellOrigins: [CGPoint] 197 | let cellSizes: [CGSize] 198 | 199 | override init() { 200 | var tempOrigins = [CGPoint]() 201 | var tempSizes = [CGSize]() 202 | var leftHeight: CGFloat = 16.0 203 | var rightHeight: CGFloat = 16.0 204 | let padding: CGFloat = 32.0 205 | let leftOrigin: CGFloat = 16.0 206 | let rightOrigin: CGFloat = 200.0 207 | 208 | items.enumerate().forEach { index, event in 209 | var x: CGFloat = leftOrigin 210 | var y: CGFloat = 0.0 211 | let width: CGFloat = 150.0 212 | let height: CGFloat = event.height 213 | 214 | if rightHeight > leftHeight { 215 | y = leftHeight 216 | leftHeight += event.height + padding 217 | } else { 218 | x = rightOrigin 219 | y = rightHeight 220 | rightHeight += event.height + padding 221 | } 222 | 223 | tempOrigins.append(CGPoint(x: x, y: y)) 224 | tempSizes.append(CGSize(width: width, height: height)) 225 | } 226 | 227 | cellOrigins = tempOrigins 228 | cellSizes = tempSizes 229 | 230 | super.init() 231 | } 232 | 233 | func collectionView(collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { 234 | return items.count 235 | } 236 | 237 | func numberOfSectionsInCollectionView(collectionView: UICollectionView) -> Int { 238 | return 1 239 | } 240 | 241 | func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell { 242 | let cell = collectionView.dequeueReusableCellWithReuseIdentifier("cell", forIndexPath: indexPath) as! Cell 243 | cell.backgroundColor = UIColor.redColor() 244 | return cell 245 | } 246 | } 247 | 248 | class Cell: UICollectionViewCell { 249 | required init?(coder aDecoder: NSCoder) { 250 | super.init(coder: aDecoder) 251 | } 252 | 253 | override init(frame: CGRect) { 254 | super.init(frame: frame) 255 | } 256 | } 257 | 258 | let dataSource = DataSource() 259 | let layout = DynamicLayout() 260 | 261 | let foo = UICollectionViewController(collectionViewLayout: layout) 262 | foo.collectionView?.dataSource = dataSource 263 | foo.collectionView?.registerClass(Cell.self, forCellWithReuseIdentifier: "cell") 264 | foo.collectionView?.backgroundColor = UIColor.whiteColor() 265 | 266 | XCPlaygroundPage.currentPage.liveView = foo 267 | 268 | -------------------------------------------------------------------------------- /Dynamic.playground/contents.xcplayground: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /Dynamic.playground/playground.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Dynamic.playground/playground.xcworkspace/xcuserdata/matt.xcuserdatad/UserInterfaceState.xcuserstate: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/subjc/CollectionViewLayout/29d300c7793e4fbb42dceee1a7dc10963744e768/Dynamic.playground/playground.xcworkspace/xcuserdata/matt.xcuserdatad/UserInterfaceState.xcuserstate -------------------------------------------------------------------------------- /Dynamic.playground/timeline.xctimeline: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | --------------------------------------------------------------------------------