├── .gitignore ├── .swiftpm └── xcode │ └── package.xcworkspace │ └── contents.xcworkspacedata ├── Configs ├── ScrollStackController.plist └── ScrollStackControllerTests.plist ├── LICENSE ├── Package.swift ├── Podfile ├── README.md ├── Resources ├── Logo.sketch ├── architecture.png ├── custom_transition.gif ├── logo.png ├── scrollstack-dark.png └── scrollstack-light.png ├── ScrollStackController.podspec ├── ScrollStackController.xcodeproj ├── project.pbxproj ├── project.xcworkspace │ ├── contents.xcworkspacedata │ ├── xcshareddata │ │ └── IDEWorkspaceChecks.plist │ └── xcuserdata │ │ └── daniele.xcuserdatad │ │ ├── UserInterfaceState.xcuserstate │ │ └── xcdebugger │ │ └── Expressions.xcexplist ├── xcshareddata │ └── xcschemes │ │ └── ScrollStackController-iOS.xcscheme └── xcuserdata │ └── daniele.xcuserdatad │ ├── xcdebugger │ └── Breakpoints_v2.xcbkptlist │ └── xcschemes │ └── xcschememanagement.plist ├── ScrollStackControllerDemo ├── AppDelegate.swift ├── Assets.xcassets │ ├── AppIcon.appiconset │ │ └── Contents.json │ ├── Contents.json │ └── hotel.imageset │ │ ├── Contents.json │ │ └── hotel.png ├── Base.lproj │ ├── LaunchScreen.storyboard │ └── Main.storyboard ├── Child View Controllers │ ├── GalleryVC.swift │ ├── NotesVC.swift │ ├── PricingVC.swift │ ├── TagsVC.swift │ └── WelcomeVC.swift ├── Extension.swift ├── Info.plist ├── SceneDelegate.swift └── ViewController.swift ├── Sources └── ScrollStackController │ ├── ScrollStack.swift │ ├── ScrollStackRow.swift │ ├── ScrollStackSeparator.swift │ ├── ScrollStackViewController.swift │ └── Support │ ├── ScrollStack+Protocols.swift │ ├── ScrollStackRowAnimator.swift │ └── UIView+AutoLayout_Extensions.swift └── Tests └── LinuxMain.swift /.gitignore: -------------------------------------------------------------------------------- 1 | /Pods 2 | /Podfile.lock 3 | .DS_Store 4 | 5 | # Swift PM 6 | Package.resolved 7 | 8 | ## User settings 9 | xcuserdata/ 10 | 11 | ## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9) 12 | *.xcscmblueprint 13 | *.xccheckout 14 | 15 | ## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4) 16 | build/ 17 | DerivedData/ 18 | *.moved-aside 19 | *.pbxuser 20 | !default.pbxuser 21 | *.mode1v3 22 | !default.mode1v3 23 | *.mode2v3 24 | !default.mode2v3 25 | *.perspectivev3 26 | !default.perspectivev3 27 | 28 | ## Obj-C/Swift specific 29 | *.hmap 30 | 31 | ## App packaging 32 | *.ipa 33 | *.dSYM.zip 34 | *.dSYM 35 | 36 | ## Playgrounds 37 | timeline.xctimeline 38 | playground.xcworkspace 39 | 40 | # Swift Package Manager 41 | # 42 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 43 | # Packages/ 44 | # Package.pins 45 | # Package.resolved 46 | # *.xcodeproj 47 | # 48 | # Xcode automatically generates this directory with a .xcworkspacedata file and xcuserdata 49 | # hence it is not needed unless you have added a package configuration file to your project 50 | .swiftpm 51 | 52 | .build/ 53 | -------------------------------------------------------------------------------- /.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Configs/ScrollStackController.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | $(MARKETING_VERSION) 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | $(CURRENT_PROJECT_VERSION) 23 | NSHumanReadableCopyright 24 | Copyright © 2019 Daniele Margutti. All rights reserved. 25 | NSPrincipalClass 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /Configs/ScrollStackControllerTests.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | BNDL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1 23 | 24 | 25 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Daniele Margutti 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.5 2 | 3 | import PackageDescription 4 | 5 | let package = Package( 6 | name: "ScrollStackController", 7 | platforms: [.iOS(.v11)], 8 | products: [ 9 | .library( 10 | name: "ScrollStackController", 11 | targets: ["ScrollStackController"] 12 | ), 13 | ], 14 | targets: [ 15 | .target( 16 | name: "ScrollStackController", 17 | dependencies: [] 18 | ), 19 | ] 20 | ) 21 | -------------------------------------------------------------------------------- /Podfile: -------------------------------------------------------------------------------- 1 | # Uncomment the next line to define a global platform for your project 2 | platform :ios, '11.0' 3 | 4 | target 'ScrollStackController-iOS' do 5 | # Comment the next line if you don't want to use dynamic frameworks 6 | use_frameworks! 7 | 8 | # Pods for ScrollStackController-iOS 9 | 10 | target 'ScrollStackController-iOS Tests' do 11 | inherit! :search_paths 12 | # Pods for testing 13 | end 14 | 15 | end 16 | 17 | target 'ScrollStackController-tvOS' do 18 | # Comment the next line if you don't want to use dynamic frameworks 19 | use_frameworks! 20 | 21 | # Pods for ScrollStackController-tvOS 22 | 23 | target 'ScrollStackController-tvOS Tests' do 24 | inherit! :search_paths 25 | # Pods for testing 26 | end 27 | 28 | end 29 | 30 | target 'ScrollStackControllerDemo' do 31 | # Comment the next line if you don't want to use dynamic frameworks 32 | use_frameworks! 33 | 34 | # Pods for ScrollStackControllerDemo 35 | 36 | pod 'ScrollStackController', :path => './' 37 | 38 | end 39 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 | 4 | logo-library 5 | 6 |

7 | 8 | [![Swift](https://img.shields.io/badge/Swift-5.3_5.4_5.5_5.6-orange?style=flat-square)](https://img.shields.io/badge/Swift-5.3_5.4_5.5_5.6-Orange?style=flat-square) 9 | [![Platform](https://img.shields.io/badge/Platforms-iOS-4E4E4E.svg?colorA=28a745)](#installation) 10 | [![Swift Package Manager](https://img.shields.io/badge/Swift_Package_Manager-compatible-orange?style=flat-square)](https://img.shields.io/badge/Swift_Package_Manager-compatible-orange?style=flat-square) 11 | [![CocoaPods Compatible](https://img.shields.io/cocoapods/v/ScrollStackController.svg?style=flat-square)](https://img.shields.io/cocoapods/v/ScrollStackController.svg) 12 | 13 | 14 | Create complex scrollable layout using UIViewControllers or plain UIViews and simplify your code! 15 | 16 | ScrollStackController is a class you can use to create complex layouts using scrollable `UIStackView` but where each row is handled by a separate `UIViewController`; this allows you to keep a great separation of concerns. 17 | 18 | You can think of it as `UITableView` but with several differences: 19 | 20 | - **Each row can manage different `UIViewController` independently**: no more massive controllers, a much cleaner and maintainable architecture. 21 | - **You can still use plain `UIView` instances if need a lightweight solution**: this is especially useful when you are using ScrollStackController as layout-helper or your view don't have a complex logic and you can still use the main controller. 22 | - **Powered by AutoLayout since the beginning**; it uses a combination of `UIScrollView + UIStackView` to offer an animation friendly controller ideal for fixed and dynamic row sizing. 23 | - **You don't need to struggle yourself with view recycling**: suppose you have a layout composed by several different screens. There is no need of view recycling but it cause a more difficult managment of the layout. With a simpler and safer APIs set `ScrollStackView` is the ideal way to implement such layouts. 24 | 25 | 26 | | | Features Highlights | 27 | |--- |--------------------------------------------------------------------------------- | 28 | | 🕺 | Create complex layout without the boilerplate required by view recyling of `UICollectionView` or `UITableView`. | 29 | | 🧩 | Simplify your architecture by thinking each screen as a separate-indipendent `UIVIewController`. | 30 | | 🧩 | Support for lightweight mode to layout `UIView` without `UIViewController`. | 31 | | 🌈 | Animate show/hide and resize of rows easily even with custom animations! | 32 | | ⏱ | Compact code base, less than 1k LOC with no external dependencies. | 33 | | 🎯 | Easy to use and extensible APIs set. | 34 | | 🧬 | It uses standard UIKit components at its core. No magic, just a combination of `UIScrollView`+`UIStackView`. | 35 | | 🧨 | Support SwiftUI's View and autosizing based upon View's content | 36 | | 🐦 | Fully made in Swift 5 from Swift ❥ lovers | 37 | 38 | ## ❤️ Your Support 39 | 40 | *Hi fellow developer!* 41 | You know, maintaing and developing tools consumes resources and time. While I enjoy making them **your support is foundamental to allow me continue its development**. 42 | 43 | If you are using `ScrollStackController` or any other of my creations please consider the following options: 44 | 45 | - [**Make a donation with PayPal**](https://www.paypal.com/paypalme/danielemargutti/20) 46 | - [**Become a Sponsor**](https://github.com/sponsors/malcommac) 47 | - [Follow Me](https://github.com/malcommac) 48 | 49 | 50 | 51 | ## Table of Contents 52 | 53 | - [❤️ Your Support](#️-your-support) 54 | - [Table of Contents](#table-of-contents) 55 | - [When to use `ScrollStackController` and when not](#when-to-use-scrollstackcontroller-and-when-not) 56 | - [How to use it](#how-to-use-it) 57 | - [Adding Rows](#adding-rows) 58 | - [Removing / Replacing Rows](#removing--replacing-rows) 59 | - [Move Rows](#move-rows) 60 | - [Hide / Show Rows](#hide--show-rows) 61 | - [Hide / Show Rows with custom animations](#hide--show-rows-with-custom-animations) 62 | - [Reload Rows](#reload-rows) 63 | - [Sizing Rows](#sizing-rows) 64 | - [Fixed Row Size](#fixed-row-size) 65 | - [Fitting Layout Row Size](#fitting-layout-row-size) 66 | - [Collapsible Rows](#collapsible-rows) 67 | - [Working with dynamic UICollectionView/UITableView/UITextView](#working-with-dynamic-uicollectionviewuitableviewuitextview) 68 | - [Rows Separator](#rows-separator) 69 | - [Using plain UIViews instead of view controllers](#using-plain-uiviews-instead-of-view-controllers) 70 | - [Tap On Rows](#tap-on-rows) 71 | - [Get the row/controller](#get-the-rowcontroller) 72 | - [Set Row Insets](#set-row-insets) 73 | - [Change ScrollStack scrolling axis](#change-scrollstack-scrolling-axis) 74 | - [Subscribe to Row Events](#subscribe-to-row-events) 75 | - [System Requirements](#system-requirements) 76 | - [Example App](#example-app) 77 | - [Installation](#installation) 78 | - [Contributing](#contributing) 79 | - [Copyright \& Acknowledgements](#copyright--acknowledgements) 80 | 81 | 82 | 83 | ### When to use `ScrollStackController` and when not 84 | 85 | `ScrollStackController` is best used for shorter screens with an heterogeneous set of rows: in these cases you don't need to have view recycling. 86 | 87 | Thanks to autolayout you will get updates and animations for free. 88 | 89 | You can also manage each screen independently with a great separation of concerns; morehover unlike `UITableView` and `UICollectionView`, you can keep strong references to `UIViewController` (and its views) in an `ScrollStack` view and make changes to them at any point. 90 | 91 | `ScrollStackController` is not suitable in all situations. 92 | `ScrollStackController` lays out the entire UI at first time when your screen loads. 93 | If you have a long list of rows you may experience delays. 94 | 95 | So, `ScrollStackController` is generally not appropriate for screens that contain many views of the same type, all showing similar data (in these cases you should use `UITableView` or `UICollectionView`). 96 | 97 | ![Demo Project](https://media.giphy.com/media/fAi3hyXNalzd4SkVgI/giphy.gif) 98 | 99 | [↑ Back To Top](#index) 100 | 101 | 102 | 103 | ### How to use it 104 | 105 | The main class of the package is `ScrollStack`, a subclass of `UIScrollView`. It manages the layout of each row, animations and keep a strong reference to your rows. 106 | 107 | This is an overview of the architecture: 108 | 109 | ![](./Resources/architecture.png) 110 | 111 | - `ScrollStackController `: is a subclass of `UIViewController`. You would to use it and add as a child controller of your view controller. This allows you to manage any child-controllers related events for each row you will add to the stack controller. 112 | - `ScrollStack`: the view of the `ScrollStackController ` is a `ScrollStack`, a subclass of `UIScrollView` with an `UIStackView` which allows you to manage the layout of the stack. You can access to it via `scrollStack` property of the controller. 113 | - Each row is a `ScrollStackRow`, which is a subclass of `UIView`. Inside there are two views, the `contentView` (a reference to managed `UIViewController`'s `view`) and the `separatorView`. A row strongly reference managed view controller, so you don't need to keep a strong reference by your own. 114 | - Separator view are subclass of `ScrollStackSeparator` class. 115 | 116 | As we said, usually you don't want to intantiate a `ScrollStack` control directly but by using the `ScrollStackController` class. 117 | It's a view controller which allows you to get the child view controller's managment for free, so when you add/remove a row to the stack you will get the standard UIViewController events for free! 118 | 119 | This is an example of initialization in a view controller: 120 | 121 | ```swift 122 | class MyViewController: UIViewController { 123 | 124 | private var stackController = ScrollStackViewController() 125 | 126 | override func viewDidLoad() { 127 | super.viewDidLoad() 128 | 129 | stackController.view.frame = contentView.bounds 130 | contentView.addSubview(stackController.view) 131 | } 132 | 133 | } 134 | ``` 135 | 136 | Now you are ready to use the `ScrollStack` control inside the `stackController` class. 137 | `ScrollStack` have an extensible rich set of APIs to manage your layout: add, remove, move, hide or show your rows, including insets and separator management. 138 | 139 | Each row managed by `ScrollStack` is a subclass of `ScrollStackRow`: it strongly reference a parent `UIViewController` class where you content is placed. `UIViewController`'s `view` will be the `contentView` of the row itself. 140 | 141 | You don't need to handle lifecycle of your rows/view controller until they are part of the rows inside the stack. 142 | 143 | To get the list of rows of the stack you can use the `rows` property. 144 | 145 | ```swift 146 | // Standard methods 147 | let allRows = scrollStack.rows 148 | let isEmpty = scrollStack.isEmpty // true if it does not contains row 149 | let notHiddenRows = scrollStack.rows.filter { !$0.isHidden } 150 | 151 | // By Visibility 152 | let currentlyVisibleRows = scrollStack.visibleRows // only currently visible rows (partially or enterly) 153 | let enterlyVisibleRows = scrollStack.enterlyVisibleRows // only enterly visible rows into the stack 154 | 155 | // Shortcuts 156 | let firstRow = scrollStack.firstRow 157 | let lastRow = scrollStack.lastRow 158 | ``` 159 | 160 | Let's take a look below. 161 | 162 | [↑ Back To Top](#index) 163 | 164 | 165 | 166 | ### Adding Rows 167 | 168 | `ScrollStack` provides a comprehensive set of methods for managing rows, including inserting rows at the beginning and end, inserting rows above or below other rows. 169 | 170 | To add row you can use one the following methods: 171 | 172 | - `addRow(controller:at:animated:) -> ScrollStackRow?` 173 | - `addRows(controllers:at:animated:) -> [ScrollStackRow]?` 174 | 175 | Both of these methods takes as arguments: 176 | 177 | - `controller/s`: one or more `UIViewController` instances; each view of these controllers will be as a row of the stack inside a `ScrollStackRow` (a sort of cell). 178 | - `at`: specify the insertion point. It's an enum with the following options: `top` (at first index), `bottom` (append at the bottom of the list), `atIndex` (specific index), `after` or `below` (after/below a row which contain a specific `UIViewController`). 179 | - `animated`: if true insertion will be animated 180 | - `completion`: completion callback to call at the end of the operation. 181 | 182 | The following code add a rows with the view of each view controller passed: 183 | 184 | ```swift 185 | let welcomeVC = WelcomeVC.create() 186 | let tagsVC = TagsVC.create(delegate: self) 187 | let galleryVC = GalleryVC.create() 188 | 189 | stackView.addRows(controllers: [welcomeVC, notesVC, tagsVC, galleryVC], animated: false) 190 | ``` 191 | 192 | As you noticed there is not need to keep a strong reference to any view controller; they are automatically strong referenced by each row created to add them into the stack. 193 | 194 | [↑ Back To Top](#index) 195 | 196 | 197 | 198 | ### Removing / Replacing Rows 199 | 200 | A similar set of APIs are used to remove existing rows from the stack: 201 | 202 | - `removeAllRows(animated:)`: to remove all rows of the stack. 203 | - `removeRow(index:animated:) -> UIViewController?`: to remove a specific row at given index. It returns a reference to removed view controller. 204 | - `removeRows(indexes:animated:) -> [UIViewController]?`: to remove rows at specified indexes from the stack. Removed managed `UIViewController` instances are returned. 205 | - `replaceRow(index:withRow:animated:completion:)`: replace an existing row with a new row which manage new passed view controller. 206 | 207 | An example: 208 | 209 | ```swift 210 | let newVC: UIViewController = ... 211 | stackView.replaceRow(index: 1, withRow: newVC, animated: true) { 212 | print("Gallery controller is now in place!!") 213 | } 214 | ``` 215 | 216 | [↑ Back To Top](#index) 217 | 218 | 219 | 220 | ### Move Rows 221 | 222 | If you need to adjust the hierarchy of the stack by moving a row from a position to another you can use: 223 | 224 | - `moveRow(index:to:animated:completion:)`: move a row at passed inside to another index (both of indexes must be valid). 225 | 226 | The following method move the first row at a random position, by animating the transition: 227 | 228 | ```swift 229 | let randomDst = Int.random(in: 1.. 236 | 237 | ### Hide / Show Rows 238 | 239 | `ScrollStack` uses the power of `UIStackView`: you can show and hide rows easily with a gorgeous animation by using one of the following methods: 240 | 241 | - `setRowHidden(index:isHidden:animated:completion:)`: hide or show a row at index. 242 | - `setRowsHidden(indexes:isHidden:animated:completion:)`: hide or show multiple rows at specified indexes. 243 | 244 | Example: 245 | 246 | ```swift 247 | stackView.setRowsHidden(indexes: [0,1,2], isHidden: true, animated: true) 248 | ``` 249 | 250 | Keep in mind: when you hide a rows the row still part of the stack and it's not removed, just hidden! If you get the list of rows by calling `rows` property of the `ScrollStack` you still see it. 251 | 252 | [↑ Back To Top](#index) 253 | 254 | 255 | 256 | ### Hide / Show Rows with custom animations 257 | 258 | You can easily show or hide rows with any custom transition; your view controller just need to be conform to the `ScrollStackRowAnimatable` protocol. 259 | This protocol defines a set of animation infos (duration, delay, spring etc.) and two events you can override to perform actions: 260 | 261 | ```swift 262 | public protocol ScrollStackRowAnimatable { 263 | /// Animation main info. 264 | var animationInfo: ScrollStackAnimationInfo { get } 265 | 266 | /// Animation will start to hide or show the row. 267 | func willBeginAnimationTransition(toHide: Bool) 268 | 269 | /// Animation to hide/show the row did end. 270 | func didEndAnimationTransition(toHide: Bool) 271 | 272 | /// Animation transition. 273 | func animateTransition(toHide: Bool) 274 | } 275 | ``` 276 | 277 | So for example you can replicate the following animation: 278 | 279 | ![](./Resources/custom_transition.gif) 280 | 281 | by using the following code: 282 | 283 | ```swift 284 | extension WelcomeVC: ScrollStackRowAnimatable { 285 | public var animationInfo: ScrollStackAnimationInfo { 286 | return ScrollStackAnimationInfo(duration: 1, delay: 0, springDamping: 0.8) 287 | } 288 | 289 | public func animateTransition(toHide: Bool) { 290 | switch toHide { 291 | case true: 292 | self.view.transform = CGAffineTransform(translationX: -100, y: 0) 293 | self.view.alpha = 0 294 | 295 | case false: 296 | self.view.transform = .identity 297 | self.view.alpha = 1 298 | } 299 | } 300 | 301 | public func willBeginAnimationTransition(toHide: Bool) { 302 | if toHide == false { 303 | self.view.transform = CGAffineTransform(translationX: -100, y: 0) 304 | self.view.alpha = 0 305 | } 306 | } 307 | 308 | } 309 | ``` 310 | 311 | 312 | 313 | ### Reload Rows 314 | 315 | Reload rows method allows you to refresh the layout of the entire stack (using `layoutIfNeeded()`) while you have a chance to update a specific row's `contentView` (aka the view of the managed `UIViewController`). 316 | 317 | There are three methods: 318 | 319 | - `reloadRow(index:animated:completion:)`: reload a specific row at index. 320 | - `reloadRows(indexes:animated:completion:)`: reload a specific set of rows. 321 | - `reloadAllRows(animated:completion:)`: reload all rows. 322 | 323 | If your `UIViewController` implements `ScrollStackContainableController` protocol you will get notified inside the class about this request, so you have the opportunity to refresh your data: 324 | 325 | Example: 326 | 327 | ```swift 328 | class MyViewController: UIViewController { 329 | 330 | private let scrollStackController = ScrollStackController() 331 | 332 | @IBAction func someAction() { 333 | scrollStackController.scrollStack.reloadRow(0) 334 | } 335 | 336 | } 337 | 338 | // Your row 0 manages the GalleryVC, so in your GalleryVC implementation: 339 | 340 | class GalleryVC: UIViewController, ScrollStackContainableController { 341 | 342 | public func func reloadContentFromStackView(stackView: ScrollStack, row: ScrollStackRow, animated: Bool) { 343 | // update your UI 344 | } 345 | 346 | } 347 | ``` 348 | 349 | [↑ Back To Top](#index) 350 | 351 | 352 | 353 | ### Sizing Rows 354 | 355 | You can control the size of your `UIViewController` inside a row of a `ScrollStack` in two ways: 356 | 357 | - Creating contrains in your `UIViewController`'s view with Autolayout. 358 | - Implementing `ScrollStackContainableController` protocol in your `UIViewController` class and return a non `nil` value in `scrollStackRowSizeForAxis(:row:in:) -> ScrollStack.ControllerSize?` delegate method. 359 | 360 | In both case `ScrollStack` class will use only one dimension depending by the active scroll axis to layout the view controller content into the stack (if scroll axis is `horizontal` you can control only the `height` of the row, if it's `vertical` only the `width`. The other dimension will be the same of the scroll stack itself. 361 | 362 | Each of the following cases is covered inside the demo application: 363 | 364 | - Fixed row size in [GalleryVC](https://github.com/malcommac/ScrollStackController/blob/master/ScrollStackControllerDemo/Child%20View%20Controllers/GalleryVC.swift) 365 | - Collapsible / Expandable row in [TagsVC](https://github.com/malcommac/ScrollStackController/blob/master/ScrollStackControllerDemo/Child%20View%20Controllers/TagsVC.swift) 366 | - Growing row based on `UITextView`'s content in [NotesVC](https://github.com/malcommac/ScrollStackController/blob/master/ScrollStackControllerDemo/Child%20View%20Controllers/NotesVC.swift) 367 | - Growing row based on `UITableView`'s content in [PricingVC](https://github.com/malcommac/ScrollStackController/blob/master/ScrollStackControllerDemo/Child%20View%20Controllers/PricingVC.swift) 368 | 369 | [↑ Back To Top](#index) 370 | 371 | 372 | 373 | ### Fixed Row Size 374 | 375 | If your view controller has a fixed size you can just return it as follows: 376 | 377 | ```swift 378 | 379 | class GalleryVC: UIViewController, ScrollStackContainableController { 380 | 381 | public func scrollStackRowSizeForAxis(_ axis: NSLayoutConstraint.Axis, row: ScrollStackRow, in stackView: ScrollStack) -> ScrollStack.ControllerSize? { 382 | switch axis { 383 | case .horizontal: 384 | return .fixed(300) 385 | case .vertical: 386 | return .fixed(500) 387 | } 388 | } 389 | 390 | } 391 | 392 | ``` 393 | 394 | If your stack support single axis you can obivously avoid switch condition. 395 | When you will add this view controller in a scroll stack it will be sized as you requested (any height/width constraint already in place will be removed). 396 | 397 | [↑ Back To Top](#index) 398 | 399 | 400 | 401 | ### Fitting Layout Row Size 402 | 403 | Sometimes you may want to have the content view sized by fitting the contents of the view controller's view. In these cases you can use `. fitLayoutForAxis`. 404 | 405 | Example: 406 | 407 | ```swift 408 | public func scrollStackRowSizeForAxis(_ axis: NSLayoutConstraint.Axis, row: ScrollStackRow, in stackView: ScrollStack) -> ScrollStack.ControllerSize? { 409 | return .fitLayoutForAxis 410 | } 411 | ``` 412 | 413 | `ScrollStack` will use the `systemLayoutSizeFitting()` method on your view controller's view to get the best size to fit the content. 414 | 415 | [↑ Back To Top](#index) 416 | 417 | 418 | 419 | ### Collapsible Rows 420 | 421 | Sometimes you may want to create collapsible rows. 422 | These row can have different heights depending of a variable. 423 | 424 | In this case you just need to implement a `isExpanded: Bool` variable in your view controller and return a different height based on it. 425 | 426 | ```swift 427 | 428 | public class TagsVC: UIViewController, ScrollStackContainableController { 429 | 430 | public var isExpanded = false 431 | 432 | public func scrollStackRowSizeForAxis(_ axis: NSLayoutConstraint.Axis, row: ScrollStackRow, in stackView: ScrollStack) -> ScrollStack.ControllerSize? { 433 | return (isExpanded == false ? .fixed(170) : .fixed(170 + collectionView.contentSize.height + 20)) 434 | } 435 | } 436 | ``` 437 | 438 | In your main view controller you may call this: 439 | 440 | ```swift 441 | // get the first row which manages this controller 442 | let tagsRow = stackView.firstRowForControllerOfType(TagsVC.self) 443 | // or if you have already the instance you can get the row directly 444 | // let tagsRow = stackView.rowForController(tagsVCInstance) 445 | 446 | let tagsVCInstance = (tagsRow.controller as! TagsVC) 447 | tagsVCInstance.isExpanded = !tagsVCInstance.isExpanded 448 | 449 | stackView.reloadRow(tagsRow, animated: true) 450 | ``` 451 | 452 | And your rows will perform a great animation to resize its content. 453 | 454 | [↑ Back To Top](#index) 455 | 456 | 457 | 458 | ### Working with dynamic UICollectionView/UITableView/UITextView 459 | 460 | There are some special cases where you may need to resize the row according to the changing content in your view controller's view. 461 | 462 | Consider for example an `UIViewController` with a `UITableView` inside; you may want to show the entire table content's as it grown. 463 | In this case you need to make some further changes: 464 | 465 | - You need to return `.fitLayoutForAxis`. 466 | - In your view controller's view you need to create a reference to the height constraint of your table. 467 | - You need to create a constraint from the table to the bottom safe area of your view (this will be used by AL to grow the size of the view). 468 | 469 | Then you must override the `updateViewConstraints()` to change the value of the table's height constraint to the right value. 470 | 471 | This is the code: 472 | 473 | ```swift 474 | 475 | public class PricingVC: UIViewController, ScrollStackContainableController { 476 | 477 | public weak var delegate: PricingVCProtocol? 478 | 479 | @IBOutlet public var pricingTable: UITableView! 480 | @IBOutlet public var pricingTableHeightConstraint: NSLayoutConstraint! 481 | 482 | public func scrollStackRowSizeForAxis(_ axis: NSLayoutConstraint.Axis, row: ScrollStackRow, in stackView: ScrollStack) -> ScrollStack.ControllerSize? { 483 | return .fitLayoutForAxis 484 | } 485 | 486 | override public func updateViewConstraints() { 487 | pricingTableHeightConstraint.constant = pricingTable.contentSize.height // the size of the table as the size of its content 488 | view.height(constant: nil) // cancel any height constraint already in place in the view 489 | super.updateViewConstraints() 490 | } 491 | } 492 | ``` 493 | 494 | In this way as you add new value to the table the size of the row in stack view will grown. 495 | 496 | [↑ Back To Top](#index) 497 | 498 | 499 | 500 | ### Rows Separator 501 | 502 | Each row managed by `ScrollStack` is of a subview class of type `ScrollStackRow`. It has a strong referenced to managed `UIViewController` but also have a subview on bottom called `ScrollStackSeparator`. 503 | 504 | You can hide/show separators by using the following properties of the row: 505 | 506 | - `isSeparatorHidden`: to hide separator. 507 | - `separatorInsets`: to set the insets of the sepatator (by default is set to the same value used by `UITableView` instances) 508 | - `separatorView.color`: to change the color 509 | - `separatorView.thickness`: to se the thickness of the separator (1 by default). 510 | 511 | Moreover you can set these values directly on `ScrollStack` controller in order to have a default value for each new row. 512 | 513 | `ScrollStack` also have a property called `autoHideLastRowSeparator` to hide the last separator of the stack automatically. 514 | 515 | [↑ Back To Top](#index) 516 | 517 | 518 | 519 | ### Using plain UIViews instead of view controllers 520 | 521 | Since version 1.3.x ScrollStack can also be used to layout plain `UIView` instances which not belong to a parent view controllers. 522 | This is especially useful when you don't have a complex logic in your views and you want to use ScrollStack to make custom layout and keep your code lightweight. 523 | 524 | Using plain views is pretty easy; each row method supports both `UIView` or `UIViewController` as parameter. 525 | 526 | Since you are working with plain `UIView` instances in order to size it correctly you must set its `heightAnchor` or `widthAncor` (depending of your stack orientation) before adding it to the stack. 527 | As for controllers, `ScrollStack` keeps a strong reference to the managed view which is added as `contentView` of the parent `ScrollStackRow` instance as it happens for `UIViewController`'s `.view` property. 528 | 529 | This is a small example: 530 | 531 | ```swift 532 | let myCustomView = UIView(frame: .zero) 533 | myCustomView.backgroundColor = .green 534 | myCustomView.heightAnchor.constraint(equalToConstant: 300).isActive = true 535 | stackView.addRow(view: myCustomView) 536 | ``` 537 | 538 | 539 | 540 | ### Tap On Rows 541 | 542 | By default rows are not tappable but if you need to implement some sort of tap features like in `UITableView` you can add it by setting a default callback for `onTap` property on `ScrollStackRow` instances. 543 | 544 | For example: 545 | 546 | ```swift 547 | scrollStack.firstRow?.onTap = { row in 548 | // do something on tap 549 | } 550 | ``` 551 | 552 | Once you can set a tap handler you can also provide highlight color for tap. 553 | To do it you must implement `ScrollStackRowHighlightable` protocol in your row managed view controller. 554 | 555 | For example: 556 | 557 | ```swift 558 | class GalleryVC: UIViewController, ScrollStackRowHighlightable { 559 | 560 | public var isHighlightable: Bool { 561 | return true 562 | } 563 | 564 | func setIsHighlighted(_ isHighlighted: Bool) { 565 | self.view.backgroundColor = (isHighlighted ? .red : .white) 566 | } 567 | 568 | } 569 | 570 | ``` 571 | 572 | Transition between highlights state will be animated automatically. 573 | 574 | [↑ Back To Top](#index) 575 | 576 | 577 | 578 | ### Get the row/controller 579 | 580 | **Get the (first) row which manage a specific view controller type** 581 | You can get the first row which manage a specific view controller class using `firstRowForControllerOfType(:) -> ScrollStackRow?` function. 582 | 583 | ```swift 584 | let tagsVC = scrollStack.firstRowForControllerOfType(TagsVC.self) // TagsVC instance 585 | ``` 586 | 587 | **Get the row which manage a specific controller instance** 588 | To get the row associated with a specific controller you can use `rowForController()` function: 589 | 590 | ```swift 591 | let row = scrollStack.rowForController(tagsVC) // ScrollStackRow 592 | ``` 593 | 594 | 595 | 596 | ### Set Row Insets 597 | 598 | To set an insets for a specific row you can use `setRowInsets()` function: 599 | 600 | ```example 601 | let newInsets: UIEdgeInsets = ... 602 | scrollStack.setRowInsets(index: 0, insets: newInsets) 603 | ``` 604 | 605 | You can also use `setRowsInsets()` to set multiple rows. 606 | 607 | Moreover by setting `.rowInsets` in your `ScrollStack` class you can set a default insets value for new row added. 608 | 609 | 610 | 611 | ### Change ScrollStack scrolling axis 612 | 613 | In order to change the axis of scroll for your `ScrollStack` instances you can set the `axis` property to `horizontal` or `vertical. 614 | 615 | 616 | 617 | ### Subscribe to Row Events 618 | 619 | You can listen when a row is removed or added into the stack view by subscribing the `onChangeRow` property. 620 | 621 | ```swift 622 | scrollStackView.onChangeRow = { (row, isRemoved) in 623 | if isRemoved { 624 | print("Row at index \(row.index) was removed" 625 | } else { 626 | print("A new row is added at index: \(row.index). It manages \(type(of: row.controller))") 627 | } 628 | } 629 | ``` 630 | 631 | You can also subscribe events for events about row visibility state changes by setting the `stackDelegate`. Your destination object must therefore conforms to the `ScrollStackControllerDelegate ` protocol: 632 | 633 | Example: 634 | 635 | ```swift 636 | class ViewController: ScrollStackController, ScrollStackControllerDelegate { 637 | 638 | func viewDidLoad() { 639 | super.viewDidLoad() 640 | 641 | self.scrollStack.stackDelegate = self 642 | } 643 | 644 | func scrollStackDidScroll(_ stackView: ScrollStack, offset: CGPoint) { 645 | // Stack did scroll 646 | } 647 | 648 | 649 | func scrollStackDidEndScrollingAnimation(_ stackView: ScrollStack) { 650 | // Scrolling animation has ended 651 | } 652 | 653 | func scrollStackRowDidBecomeVisible(_ stackView: ScrollStack, row: ScrollStackRow, index: Int, state: ScrollStack.RowVisibility) { 654 | // Row did become partially or entirely visible. 655 | } 656 | 657 | func scrollStackRowDidBecomeHidden(_ stackView: ScrollStack, row: ScrollStackRow, index: Int, state: ScrollStack.RowVisibility) { 658 | // Row did become partially or entirely invisible. 659 | } 660 | 661 | func scrollStackDidUpdateLayout(_ stackView: ScrollStack) { 662 | // This function is called when layout is updated (added, removed, hide or show one or more rows). 663 | } 664 | 665 | func scrollStackContentSizeDidChange(_ stackView: ScrollStack, from oldValue: CGSize, to newValue: CGSize) { 666 | // This function is called when content size of the stack did change (remove/add, hide/show rows). 667 | } 668 | } 669 | ``` 670 | 671 | `ScrollStack.RowVisibility` is an enum with the following cases: 672 | 673 | - `partial`: row is partially visible. 674 | - `entire`: row is entirely visible. 675 | - `hidden`: row is invisible and hidden. 676 | - `offscreen`: row is not hidden but currently offscreen due to scroll position. 677 | 678 | [↑ Back To Top](#index) 679 | 680 | 681 | 682 | ### System Requirements 683 | 684 | - iOS 11+ 685 | - Xcode 10+ 686 | - Swift 5+ 687 | 688 | [↑ Back To Top](#index) 689 | 690 | 691 | 692 | ### Example App 693 | 694 | `ScrollStackController` comes with a demo application which show how easy you can create complex scrollable layoyut and some of the major features of the library. 695 | 696 | You should look at it in order to implement your own layout, create dynamically sized rows and dispatch events. 697 | 698 | [↑ Back To Top](#index) 699 | 700 | 701 | 702 | ### Installation 703 | 704 | `ScrollStackController` can be installed with CocoaPods by adding pod 'ScrollStackController' to your Podfile. 705 | 706 | ```ruby 707 | pod 'ScrollStackController' 708 | ``` 709 | 710 | It also supports `Swift Package Maneger` aka SPM in your `Package.swift`: 711 | 712 | ```sh 713 | import PackageDescription 714 | 715 | let package = Package(name: "YourPackage", 716 | dependencies: [ 717 | .Package(url: "https://github.com/malcommac/ScrollStackController.git", majorVersion: 0), 718 | ] 719 | ) 720 | ``` 721 | 722 | [↑ Back To Top](#index) 723 | 724 | 725 | 726 | **Consider ❤️ [support the development](#support) of this library!** 727 | 728 | ## Contributing 729 | 730 | - If you **need help** or you'd like to **ask a general question**, open an issue. 731 | - If you **found a bug**, open an issue. 732 | - If you **have a feature request**, open an issue. 733 | - If you **want to contribute**, submit a pull request. 734 | 735 | ## Copyright & Acknowledgements 736 | 737 | ScrollStackController is currently owned and maintained by Daniele Margutti. 738 | You can follow me on Twitter [@danielemargutti](http://twitter.com/danielemargutti). 739 | My web site is [https://www.danielemargutti.com](https://www.danielemargutti.com) 740 | 741 | This software is licensed under [MIT License](LICENSE.md). 742 | 743 | ***Follow me on:*** 744 | - 💼 [Linkedin](https://www.linkedin.com/in/danielemargutti/) 745 | - 🐦 [Twitter](https://twitter.com/danielemargutti) 746 | 747 | -------------------------------------------------------------------------------- /Resources/Logo.sketch: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/malcommac/ScrollStackController/8ada87d10a486a8222c005d38a68554af2161ca8/Resources/Logo.sketch -------------------------------------------------------------------------------- /Resources/architecture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/malcommac/ScrollStackController/8ada87d10a486a8222c005d38a68554af2161ca8/Resources/architecture.png -------------------------------------------------------------------------------- /Resources/custom_transition.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/malcommac/ScrollStackController/8ada87d10a486a8222c005d38a68554af2161ca8/Resources/custom_transition.gif -------------------------------------------------------------------------------- /Resources/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/malcommac/ScrollStackController/8ada87d10a486a8222c005d38a68554af2161ca8/Resources/logo.png -------------------------------------------------------------------------------- /Resources/scrollstack-dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/malcommac/ScrollStackController/8ada87d10a486a8222c005d38a68554af2161ca8/Resources/scrollstack-dark.png -------------------------------------------------------------------------------- /Resources/scrollstack-light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/malcommac/ScrollStackController/8ada87d10a486a8222c005d38a68554af2161ca8/Resources/scrollstack-light.png -------------------------------------------------------------------------------- /ScrollStackController.podspec: -------------------------------------------------------------------------------- 1 | Pod::Spec.new do |s| 2 | s.name = "ScrollStackController" 3 | s.version = "1.7.1" 4 | s.summary = "Create complex scrollable layout using UIViewController and simplify your code" 5 | s.homepage = "https://github.com/malcommac/ScrollStackController" 6 | s.license = { :type => "MIT", :file => "LICENSE" } 7 | s.author = { "Daniele Margutti" => "hello@danielemargutti.com" } 8 | s.social_media_url = "http://www.twitter.com/danielemargutti" 9 | s.ios.deployment_target = "11.0" 10 | s.source = { :git => "https://github.com/malcommac/ScrollStackController.git", :tag => s.version.to_s } 11 | s.frameworks = "Foundation", "UIKit" 12 | s.source_files = 'Sources/**/*.swift' 13 | s.swift_versions = ['5.0', '5.1', '5.3', '5.4', '5.5', '5.7', '5.8', '5.9', '5.10'] 14 | end 15 | -------------------------------------------------------------------------------- /ScrollStackController.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 47; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 52D6D9871BEFF229002C0205 /* ScrollStackController.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 52D6D97C1BEFF229002C0205 /* ScrollStackController.framework */; }; 11 | 6402E1F22347A8540087963C /* Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6402E1F12347A8540087963C /* Extension.swift */; }; 12 | 647C77B32348EA1600CAEB9F /* PricingVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 647C77B22348EA1600CAEB9F /* PricingVC.swift */; }; 13 | 6489C0612349C571003E5344 /* NotesVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6489C0602349C571003E5344 /* NotesVC.swift */; }; 14 | 649B1E9223B1251400BD6BFD /* ScrollStackRowAnimator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 649B1E9123B1251400BD6BFD /* ScrollStackRowAnimator.swift */; }; 15 | 649B1E9323B1251900BD6BFD /* ScrollStackRowAnimator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 649B1E9123B1251400BD6BFD /* ScrollStackRowAnimator.swift */; }; 16 | 64A8E8B32348CCCE00E893FB /* WelcomeVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64A8E8B02348CCCE00E893FB /* WelcomeVC.swift */; }; 17 | 64C02255234735A800A6D844 /* ScrollStackViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64C0224F234735A800A6D844 /* ScrollStackViewController.swift */; }; 18 | 64C02257234735A800A6D844 /* ScrollStack.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64C02250234735A800A6D844 /* ScrollStack.swift */; }; 19 | 64C02259234735A800A6D844 /* ScrollStackRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64C02251234735A800A6D844 /* ScrollStackRow.swift */; }; 20 | 64C0225B234735A800A6D844 /* ScrollStackSeparator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64C02252234735A800A6D844 /* ScrollStackSeparator.swift */; }; 21 | 64C0225D234735A800A6D844 /* UIView+AutoLayout_Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64C02253234735A800A6D844 /* UIView+AutoLayout_Extensions.swift */; }; 22 | 64C022682347360800A6D844 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64C022672347360800A6D844 /* AppDelegate.swift */; }; 23 | 64C0226A2347360800A6D844 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64C022692347360800A6D844 /* SceneDelegate.swift */; }; 24 | 64C0226C2347360800A6D844 /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64C0226B2347360800A6D844 /* ViewController.swift */; }; 25 | 64C0226F2347360800A6D844 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 64C0226D2347360800A6D844 /* Main.storyboard */; }; 26 | 64C022712347360900A6D844 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 64C022702347360900A6D844 /* Assets.xcassets */; }; 27 | 64C022742347360900A6D844 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 64C022722347360900A6D844 /* LaunchScreen.storyboard */; }; 28 | 64C0227D234753A100A6D844 /* GalleryVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64C0227C234753A100A6D844 /* GalleryVC.swift */; }; 29 | 64C0227E2347582D00A6D844 /* ScrollStack.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64C02250234735A800A6D844 /* ScrollStack.swift */; }; 30 | 64C0227F2347582D00A6D844 /* ScrollStackRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64C02251234735A800A6D844 /* ScrollStackRow.swift */; }; 31 | 64C022812347582D00A6D844 /* ScrollStackSeparator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64C02252234735A800A6D844 /* ScrollStackSeparator.swift */; }; 32 | 64C022822347582D00A6D844 /* ScrollStackViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64C0224F234735A800A6D844 /* ScrollStackViewController.swift */; }; 33 | 64C022832347582D00A6D844 /* UIView+AutoLayout_Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64C02253234735A800A6D844 /* UIView+AutoLayout_Extensions.swift */; }; 34 | 64C0228623475A0E00A6D844 /* ScrollStack+Protocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64C0228523475A0E00A6D844 /* ScrollStack+Protocols.swift */; }; 35 | 64C0228823475A0E00A6D844 /* ScrollStack+Protocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64C0228523475A0E00A6D844 /* ScrollStack+Protocols.swift */; }; 36 | 64C0228A2347834300A6D844 /* TagsVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64C022892347834300A6D844 /* TagsVC.swift */; }; 37 | /* End PBXBuildFile section */ 38 | 39 | /* Begin PBXContainerItemProxy section */ 40 | 52D6D9881BEFF229002C0205 /* PBXContainerItemProxy */ = { 41 | isa = PBXContainerItemProxy; 42 | containerPortal = 52D6D9731BEFF229002C0205 /* Project object */; 43 | proxyType = 1; 44 | remoteGlobalIDString = 52D6D97B1BEFF229002C0205; 45 | remoteInfo = ScrollStackController; 46 | }; 47 | /* End PBXContainerItemProxy section */ 48 | 49 | /* Begin PBXFileReference section */ 50 | 52D6D97C1BEFF229002C0205 /* ScrollStackController.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = ScrollStackController.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 51 | 52D6D9861BEFF229002C0205 /* ScrollStackController-iOS Tests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "ScrollStackController-iOS Tests.xctest"; sourceTree = BUILT_PRODUCTS_DIR; }; 52 | 6402E1F12347A8540087963C /* Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Extension.swift; sourceTree = ""; }; 53 | 647C77B22348EA1600CAEB9F /* PricingVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PricingVC.swift; sourceTree = ""; }; 54 | 6489C0602349C571003E5344 /* NotesVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotesVC.swift; sourceTree = ""; }; 55 | 649B1E9123B1251400BD6BFD /* ScrollStackRowAnimator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScrollStackRowAnimator.swift; sourceTree = ""; }; 56 | 64A8E8B02348CCCE00E893FB /* WelcomeVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WelcomeVC.swift; sourceTree = ""; }; 57 | 64C0224F234735A800A6D844 /* ScrollStackViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ScrollStackViewController.swift; sourceTree = ""; }; 58 | 64C02250234735A800A6D844 /* ScrollStack.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ScrollStack.swift; sourceTree = ""; }; 59 | 64C02251234735A800A6D844 /* ScrollStackRow.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ScrollStackRow.swift; sourceTree = ""; }; 60 | 64C02252234735A800A6D844 /* ScrollStackSeparator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ScrollStackSeparator.swift; sourceTree = ""; }; 61 | 64C02253234735A800A6D844 /* UIView+AutoLayout_Extensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UIView+AutoLayout_Extensions.swift"; sourceTree = ""; }; 62 | 64C022652347360800A6D844 /* ScrollStackControllerDemo.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = ScrollStackControllerDemo.app; sourceTree = BUILT_PRODUCTS_DIR; }; 63 | 64C022672347360800A6D844 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 64 | 64C022692347360800A6D844 /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; }; 65 | 64C0226B2347360800A6D844 /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; }; 66 | 64C0226E2347360800A6D844 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 67 | 64C022702347360900A6D844 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 68 | 64C022732347360900A6D844 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 69 | 64C022752347360900A6D844 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 70 | 64C0227C234753A100A6D844 /* GalleryVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GalleryVC.swift; sourceTree = ""; }; 71 | 64C0228523475A0E00A6D844 /* ScrollStack+Protocols.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ScrollStack+Protocols.swift"; sourceTree = ""; }; 72 | 64C022892347834300A6D844 /* TagsVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TagsVC.swift; sourceTree = ""; }; 73 | AD2FAA261CD0B6D800659CF4 /* ScrollStackController.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = ScrollStackController.plist; sourceTree = ""; }; 74 | AD2FAA281CD0B6E100659CF4 /* ScrollStackControllerTests.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = ScrollStackControllerTests.plist; sourceTree = ""; }; 75 | /* End PBXFileReference section */ 76 | 77 | /* Begin PBXFrameworksBuildPhase section */ 78 | 52D6D9781BEFF229002C0205 /* Frameworks */ = { 79 | isa = PBXFrameworksBuildPhase; 80 | buildActionMask = 2147483647; 81 | files = ( 82 | ); 83 | runOnlyForDeploymentPostprocessing = 0; 84 | }; 85 | 52D6D9831BEFF229002C0205 /* Frameworks */ = { 86 | isa = PBXFrameworksBuildPhase; 87 | buildActionMask = 2147483647; 88 | files = ( 89 | 52D6D9871BEFF229002C0205 /* ScrollStackController.framework in Frameworks */, 90 | ); 91 | runOnlyForDeploymentPostprocessing = 0; 92 | }; 93 | 64C022622347360800A6D844 /* Frameworks */ = { 94 | isa = PBXFrameworksBuildPhase; 95 | buildActionMask = 2147483647; 96 | files = ( 97 | ); 98 | runOnlyForDeploymentPostprocessing = 0; 99 | }; 100 | /* End PBXFrameworksBuildPhase section */ 101 | 102 | /* Begin PBXGroup section */ 103 | 52D6D9721BEFF229002C0205 = { 104 | isa = PBXGroup; 105 | children = ( 106 | 64C0224A2347358400A6D844 /* Sources */, 107 | 52D6D99C1BEFF38C002C0205 /* Configs */, 108 | 64C022662347360800A6D844 /* ScrollStackControllerDemo */, 109 | 52D6D97D1BEFF229002C0205 /* Products */, 110 | ); 111 | sourceTree = ""; 112 | }; 113 | 52D6D97D1BEFF229002C0205 /* Products */ = { 114 | isa = PBXGroup; 115 | children = ( 116 | 52D6D97C1BEFF229002C0205 /* ScrollStackController.framework */, 117 | 52D6D9861BEFF229002C0205 /* ScrollStackController-iOS Tests.xctest */, 118 | 64C022652347360800A6D844 /* ScrollStackControllerDemo.app */, 119 | ); 120 | name = Products; 121 | sourceTree = ""; 122 | }; 123 | 52D6D99C1BEFF38C002C0205 /* Configs */ = { 124 | isa = PBXGroup; 125 | children = ( 126 | DD7502721C68FC1B006590AF /* Frameworks */, 127 | DD7502731C68FC20006590AF /* Tests */, 128 | ); 129 | path = Configs; 130 | sourceTree = ""; 131 | }; 132 | 64A8E8AE2348933C00E893FB /* Support */ = { 133 | isa = PBXGroup; 134 | children = ( 135 | 64C0228523475A0E00A6D844 /* ScrollStack+Protocols.swift */, 136 | 64C02253234735A800A6D844 /* UIView+AutoLayout_Extensions.swift */, 137 | 649B1E9123B1251400BD6BFD /* ScrollStackRowAnimator.swift */, 138 | ); 139 | path = Support; 140 | sourceTree = ""; 141 | }; 142 | 64A8E8AF2348B02300E893FB /* Child View Controllers */ = { 143 | isa = PBXGroup; 144 | children = ( 145 | 64C0227C234753A100A6D844 /* GalleryVC.swift */, 146 | 64C022892347834300A6D844 /* TagsVC.swift */, 147 | 64A8E8B02348CCCE00E893FB /* WelcomeVC.swift */, 148 | 647C77B22348EA1600CAEB9F /* PricingVC.swift */, 149 | 6489C0602349C571003E5344 /* NotesVC.swift */, 150 | ); 151 | path = "Child View Controllers"; 152 | sourceTree = ""; 153 | }; 154 | 64C0224A2347358400A6D844 /* Sources */ = { 155 | isa = PBXGroup; 156 | children = ( 157 | 64C0224B2347358400A6D844 /* ScrollStackController */, 158 | ); 159 | path = Sources; 160 | sourceTree = SOURCE_ROOT; 161 | }; 162 | 64C0224B2347358400A6D844 /* ScrollStackController */ = { 163 | isa = PBXGroup; 164 | children = ( 165 | 64C0224F234735A800A6D844 /* ScrollStackViewController.swift */, 166 | 64C02250234735A800A6D844 /* ScrollStack.swift */, 167 | 64C02251234735A800A6D844 /* ScrollStackRow.swift */, 168 | 64C02252234735A800A6D844 /* ScrollStackSeparator.swift */, 169 | 64A8E8AE2348933C00E893FB /* Support */, 170 | ); 171 | path = ScrollStackController; 172 | sourceTree = ""; 173 | }; 174 | 64C022662347360800A6D844 /* ScrollStackControllerDemo */ = { 175 | isa = PBXGroup; 176 | children = ( 177 | 64C0226D2347360800A6D844 /* Main.storyboard */, 178 | 64C022672347360800A6D844 /* AppDelegate.swift */, 179 | 64C022692347360800A6D844 /* SceneDelegate.swift */, 180 | 64C0226B2347360800A6D844 /* ViewController.swift */, 181 | 6402E1F12347A8540087963C /* Extension.swift */, 182 | 64A8E8AF2348B02300E893FB /* Child View Controllers */, 183 | 64C022702347360900A6D844 /* Assets.xcassets */, 184 | 64C022722347360900A6D844 /* LaunchScreen.storyboard */, 185 | 64C022752347360900A6D844 /* Info.plist */, 186 | ); 187 | path = ScrollStackControllerDemo; 188 | sourceTree = ""; 189 | }; 190 | DD7502721C68FC1B006590AF /* Frameworks */ = { 191 | isa = PBXGroup; 192 | children = ( 193 | AD2FAA261CD0B6D800659CF4 /* ScrollStackController.plist */, 194 | ); 195 | name = Frameworks; 196 | sourceTree = ""; 197 | }; 198 | DD7502731C68FC20006590AF /* Tests */ = { 199 | isa = PBXGroup; 200 | children = ( 201 | AD2FAA281CD0B6E100659CF4 /* ScrollStackControllerTests.plist */, 202 | ); 203 | name = Tests; 204 | sourceTree = ""; 205 | }; 206 | /* End PBXGroup section */ 207 | 208 | /* Begin PBXHeadersBuildPhase section */ 209 | 52D6D9791BEFF229002C0205 /* Headers */ = { 210 | isa = PBXHeadersBuildPhase; 211 | buildActionMask = 2147483647; 212 | files = ( 213 | ); 214 | runOnlyForDeploymentPostprocessing = 0; 215 | }; 216 | /* End PBXHeadersBuildPhase section */ 217 | 218 | /* Begin PBXNativeTarget section */ 219 | 52D6D97B1BEFF229002C0205 /* ScrollStackController-iOS */ = { 220 | isa = PBXNativeTarget; 221 | buildConfigurationList = 52D6D9901BEFF229002C0205 /* Build configuration list for PBXNativeTarget "ScrollStackController-iOS" */; 222 | buildPhases = ( 223 | 52D6D9771BEFF229002C0205 /* Sources */, 224 | 52D6D9781BEFF229002C0205 /* Frameworks */, 225 | 52D6D9791BEFF229002C0205 /* Headers */, 226 | 52D6D97A1BEFF229002C0205 /* Resources */, 227 | ); 228 | buildRules = ( 229 | ); 230 | dependencies = ( 231 | ); 232 | name = "ScrollStackController-iOS"; 233 | productName = ScrollStackController; 234 | productReference = 52D6D97C1BEFF229002C0205 /* ScrollStackController.framework */; 235 | productType = "com.apple.product-type.framework"; 236 | }; 237 | 52D6D9851BEFF229002C0205 /* ScrollStackController-iOS Tests */ = { 238 | isa = PBXNativeTarget; 239 | buildConfigurationList = 52D6D9931BEFF229002C0205 /* Build configuration list for PBXNativeTarget "ScrollStackController-iOS Tests" */; 240 | buildPhases = ( 241 | 52D6D9821BEFF229002C0205 /* Sources */, 242 | 52D6D9831BEFF229002C0205 /* Frameworks */, 243 | 52D6D9841BEFF229002C0205 /* Resources */, 244 | ); 245 | buildRules = ( 246 | ); 247 | dependencies = ( 248 | 52D6D9891BEFF229002C0205 /* PBXTargetDependency */, 249 | ); 250 | name = "ScrollStackController-iOS Tests"; 251 | productName = ScrollStackControllerTests; 252 | productReference = 52D6D9861BEFF229002C0205 /* ScrollStackController-iOS Tests.xctest */; 253 | productType = "com.apple.product-type.bundle.unit-test"; 254 | }; 255 | 64C022642347360800A6D844 /* ScrollStackControllerDemo */ = { 256 | isa = PBXNativeTarget; 257 | buildConfigurationList = 64C022762347360900A6D844 /* Build configuration list for PBXNativeTarget "ScrollStackControllerDemo" */; 258 | buildPhases = ( 259 | 64C022612347360800A6D844 /* Sources */, 260 | 64C022622347360800A6D844 /* Frameworks */, 261 | 64C022632347360800A6D844 /* Resources */, 262 | ); 263 | buildRules = ( 264 | ); 265 | dependencies = ( 266 | ); 267 | name = ScrollStackControllerDemo; 268 | productName = ScrollStackControllerDemo; 269 | productReference = 64C022652347360800A6D844 /* ScrollStackControllerDemo.app */; 270 | productType = "com.apple.product-type.application"; 271 | }; 272 | /* End PBXNativeTarget section */ 273 | 274 | /* Begin PBXProject section */ 275 | 52D6D9731BEFF229002C0205 /* Project object */ = { 276 | isa = PBXProject; 277 | attributes = { 278 | LastSwiftUpdateCheck = 1100; 279 | LastUpgradeCheck = 1250; 280 | ORGANIZATIONNAME = ScrollStackController; 281 | TargetAttributes = { 282 | 52D6D97B1BEFF229002C0205 = { 283 | CreatedOnToolsVersion = 7.1; 284 | LastSwiftMigration = 1100; 285 | }; 286 | 52D6D9851BEFF229002C0205 = { 287 | CreatedOnToolsVersion = 7.1; 288 | LastSwiftMigration = 1020; 289 | }; 290 | 64C022642347360800A6D844 = { 291 | CreatedOnToolsVersion = 11.0; 292 | DevelopmentTeam = SXF6X3A7VN; 293 | ProvisioningStyle = Automatic; 294 | }; 295 | }; 296 | }; 297 | buildConfigurationList = 52D6D9761BEFF229002C0205 /* Build configuration list for PBXProject "ScrollStackController" */; 298 | compatibilityVersion = "Xcode 6.3"; 299 | developmentRegion = en; 300 | hasScannedForEncodings = 0; 301 | knownRegions = ( 302 | en, 303 | Base, 304 | ); 305 | mainGroup = 52D6D9721BEFF229002C0205; 306 | productRefGroup = 52D6D97D1BEFF229002C0205 /* Products */; 307 | projectDirPath = ""; 308 | projectRoot = ""; 309 | targets = ( 310 | 52D6D97B1BEFF229002C0205 /* ScrollStackController-iOS */, 311 | 52D6D9851BEFF229002C0205 /* ScrollStackController-iOS Tests */, 312 | 64C022642347360800A6D844 /* ScrollStackControllerDemo */, 313 | ); 314 | }; 315 | /* End PBXProject section */ 316 | 317 | /* Begin PBXResourcesBuildPhase section */ 318 | 52D6D97A1BEFF229002C0205 /* Resources */ = { 319 | isa = PBXResourcesBuildPhase; 320 | buildActionMask = 2147483647; 321 | files = ( 322 | ); 323 | runOnlyForDeploymentPostprocessing = 0; 324 | }; 325 | 52D6D9841BEFF229002C0205 /* Resources */ = { 326 | isa = PBXResourcesBuildPhase; 327 | buildActionMask = 2147483647; 328 | files = ( 329 | ); 330 | runOnlyForDeploymentPostprocessing = 0; 331 | }; 332 | 64C022632347360800A6D844 /* Resources */ = { 333 | isa = PBXResourcesBuildPhase; 334 | buildActionMask = 2147483647; 335 | files = ( 336 | 64C022742347360900A6D844 /* LaunchScreen.storyboard in Resources */, 337 | 64C022712347360900A6D844 /* Assets.xcassets in Resources */, 338 | 64C0226F2347360800A6D844 /* Main.storyboard in Resources */, 339 | ); 340 | runOnlyForDeploymentPostprocessing = 0; 341 | }; 342 | /* End PBXResourcesBuildPhase section */ 343 | 344 | /* Begin PBXSourcesBuildPhase section */ 345 | 52D6D9771BEFF229002C0205 /* Sources */ = { 346 | isa = PBXSourcesBuildPhase; 347 | buildActionMask = 2147483647; 348 | files = ( 349 | 64C0228623475A0E00A6D844 /* ScrollStack+Protocols.swift in Sources */, 350 | 64C02259234735A800A6D844 /* ScrollStackRow.swift in Sources */, 351 | 64C02255234735A800A6D844 /* ScrollStackViewController.swift in Sources */, 352 | 64C02257234735A800A6D844 /* ScrollStack.swift in Sources */, 353 | 649B1E9223B1251400BD6BFD /* ScrollStackRowAnimator.swift in Sources */, 354 | 64C0225D234735A800A6D844 /* UIView+AutoLayout_Extensions.swift in Sources */, 355 | 64C0225B234735A800A6D844 /* ScrollStackSeparator.swift in Sources */, 356 | ); 357 | runOnlyForDeploymentPostprocessing = 0; 358 | }; 359 | 52D6D9821BEFF229002C0205 /* Sources */ = { 360 | isa = PBXSourcesBuildPhase; 361 | buildActionMask = 2147483647; 362 | files = ( 363 | ); 364 | runOnlyForDeploymentPostprocessing = 0; 365 | }; 366 | 64C022612347360800A6D844 /* Sources */ = { 367 | isa = PBXSourcesBuildPhase; 368 | buildActionMask = 2147483647; 369 | files = ( 370 | 64C0228823475A0E00A6D844 /* ScrollStack+Protocols.swift in Sources */, 371 | 6489C0612349C571003E5344 /* NotesVC.swift in Sources */, 372 | 64C0227F2347582D00A6D844 /* ScrollStackRow.swift in Sources */, 373 | 64C0228A2347834300A6D844 /* TagsVC.swift in Sources */, 374 | 64C022822347582D00A6D844 /* ScrollStackViewController.swift in Sources */, 375 | 64A8E8B32348CCCE00E893FB /* WelcomeVC.swift in Sources */, 376 | 64C0226C2347360800A6D844 /* ViewController.swift in Sources */, 377 | 64C022812347582D00A6D844 /* ScrollStackSeparator.swift in Sources */, 378 | 64C0227E2347582D00A6D844 /* ScrollStack.swift in Sources */, 379 | 649B1E9323B1251900BD6BFD /* ScrollStackRowAnimator.swift in Sources */, 380 | 64C022682347360800A6D844 /* AppDelegate.swift in Sources */, 381 | 647C77B32348EA1600CAEB9F /* PricingVC.swift in Sources */, 382 | 6402E1F22347A8540087963C /* Extension.swift in Sources */, 383 | 64C022832347582D00A6D844 /* UIView+AutoLayout_Extensions.swift in Sources */, 384 | 64C0227D234753A100A6D844 /* GalleryVC.swift in Sources */, 385 | 64C0226A2347360800A6D844 /* SceneDelegate.swift in Sources */, 386 | ); 387 | runOnlyForDeploymentPostprocessing = 0; 388 | }; 389 | /* End PBXSourcesBuildPhase section */ 390 | 391 | /* Begin PBXTargetDependency section */ 392 | 52D6D9891BEFF229002C0205 /* PBXTargetDependency */ = { 393 | isa = PBXTargetDependency; 394 | target = 52D6D97B1BEFF229002C0205 /* ScrollStackController-iOS */; 395 | targetProxy = 52D6D9881BEFF229002C0205 /* PBXContainerItemProxy */; 396 | }; 397 | /* End PBXTargetDependency section */ 398 | 399 | /* Begin PBXVariantGroup section */ 400 | 64C0226D2347360800A6D844 /* Main.storyboard */ = { 401 | isa = PBXVariantGroup; 402 | children = ( 403 | 64C0226E2347360800A6D844 /* Base */, 404 | ); 405 | name = Main.storyboard; 406 | sourceTree = ""; 407 | }; 408 | 64C022722347360900A6D844 /* LaunchScreen.storyboard */ = { 409 | isa = PBXVariantGroup; 410 | children = ( 411 | 64C022732347360900A6D844 /* Base */, 412 | ); 413 | name = LaunchScreen.storyboard; 414 | sourceTree = ""; 415 | }; 416 | /* End PBXVariantGroup section */ 417 | 418 | /* Begin XCBuildConfiguration section */ 419 | 52D6D98E1BEFF229002C0205 /* Debug */ = { 420 | isa = XCBuildConfiguration; 421 | buildSettings = { 422 | ALWAYS_SEARCH_USER_PATHS = NO; 423 | CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; 424 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 425 | CLANG_CXX_LIBRARY = "libc++"; 426 | CLANG_ENABLE_MODULES = YES; 427 | CLANG_ENABLE_OBJC_ARC = YES; 428 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 429 | CLANG_WARN_BOOL_CONVERSION = YES; 430 | CLANG_WARN_COMMA = YES; 431 | CLANG_WARN_CONSTANT_CONVERSION = YES; 432 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 433 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 434 | CLANG_WARN_EMPTY_BODY = YES; 435 | CLANG_WARN_ENUM_CONVERSION = YES; 436 | CLANG_WARN_INFINITE_RECURSION = YES; 437 | CLANG_WARN_INT_CONVERSION = YES; 438 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 439 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 440 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 441 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 442 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 443 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 444 | CLANG_WARN_STRICT_PROTOTYPES = YES; 445 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 446 | CLANG_WARN_UNREACHABLE_CODE = YES; 447 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 448 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 449 | COPY_PHASE_STRIP = NO; 450 | CURRENT_PROJECT_VERSION = 1; 451 | DEBUG_INFORMATION_FORMAT = dwarf; 452 | ENABLE_STRICT_OBJC_MSGSEND = YES; 453 | ENABLE_TESTABILITY = YES; 454 | GCC_C_LANGUAGE_STANDARD = gnu99; 455 | GCC_DYNAMIC_NO_PIC = NO; 456 | GCC_NO_COMMON_BLOCKS = YES; 457 | GCC_OPTIMIZATION_LEVEL = 0; 458 | GCC_PREPROCESSOR_DEFINITIONS = ( 459 | "DEBUG=1", 460 | "$(inherited)", 461 | ); 462 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 463 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 464 | GCC_WARN_UNDECLARED_SELECTOR = YES; 465 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 466 | GCC_WARN_UNUSED_FUNCTION = YES; 467 | GCC_WARN_UNUSED_VARIABLE = YES; 468 | IPHONEOS_DEPLOYMENT_TARGET = 12.0; 469 | MTL_ENABLE_DEBUG_INFO = YES; 470 | ONLY_ACTIVE_ARCH = YES; 471 | SDKROOT = iphoneos; 472 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 473 | SWIFT_VERSION = 5.0; 474 | TARGETED_DEVICE_FAMILY = "1,2"; 475 | VERSIONING_SYSTEM = "apple-generic"; 476 | VERSION_INFO_PREFIX = ""; 477 | }; 478 | name = Debug; 479 | }; 480 | 52D6D98F1BEFF229002C0205 /* Release */ = { 481 | isa = XCBuildConfiguration; 482 | buildSettings = { 483 | ALWAYS_SEARCH_USER_PATHS = NO; 484 | CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; 485 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 486 | CLANG_CXX_LIBRARY = "libc++"; 487 | CLANG_ENABLE_MODULES = YES; 488 | CLANG_ENABLE_OBJC_ARC = YES; 489 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 490 | CLANG_WARN_BOOL_CONVERSION = YES; 491 | CLANG_WARN_COMMA = YES; 492 | CLANG_WARN_CONSTANT_CONVERSION = YES; 493 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 494 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 495 | CLANG_WARN_EMPTY_BODY = YES; 496 | CLANG_WARN_ENUM_CONVERSION = YES; 497 | CLANG_WARN_INFINITE_RECURSION = YES; 498 | CLANG_WARN_INT_CONVERSION = YES; 499 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 500 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 501 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 502 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 503 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 504 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 505 | CLANG_WARN_STRICT_PROTOTYPES = YES; 506 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 507 | CLANG_WARN_UNREACHABLE_CODE = YES; 508 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 509 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 510 | COPY_PHASE_STRIP = NO; 511 | CURRENT_PROJECT_VERSION = 1; 512 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 513 | ENABLE_NS_ASSERTIONS = NO; 514 | ENABLE_STRICT_OBJC_MSGSEND = YES; 515 | GCC_C_LANGUAGE_STANDARD = gnu99; 516 | GCC_NO_COMMON_BLOCKS = YES; 517 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 518 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 519 | GCC_WARN_UNDECLARED_SELECTOR = YES; 520 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 521 | GCC_WARN_UNUSED_FUNCTION = YES; 522 | GCC_WARN_UNUSED_VARIABLE = YES; 523 | IPHONEOS_DEPLOYMENT_TARGET = 12.0; 524 | MTL_ENABLE_DEBUG_INFO = NO; 525 | SDKROOT = iphoneos; 526 | SWIFT_VERSION = 5.0; 527 | TARGETED_DEVICE_FAMILY = "1,2"; 528 | VALIDATE_PRODUCT = YES; 529 | VERSIONING_SYSTEM = "apple-generic"; 530 | VERSION_INFO_PREFIX = ""; 531 | }; 532 | name = Release; 533 | }; 534 | 52D6D9911BEFF229002C0205 /* Debug */ = { 535 | isa = XCBuildConfiguration; 536 | buildSettings = { 537 | APPLICATION_EXTENSION_API_ONLY = YES; 538 | CLANG_ENABLE_MODULES = YES; 539 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = ""; 540 | CURRENT_PROJECT_VERSION = 0; 541 | DEFINES_MODULE = YES; 542 | DYLIB_COMPATIBILITY_VERSION = 1; 543 | DYLIB_CURRENT_VERSION = 1; 544 | DYLIB_INSTALL_NAME_BASE = "@rpath"; 545 | INFOPLIST_FILE = Configs/ScrollStackController.plist; 546 | INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; 547 | IPHONEOS_DEPLOYMENT_TARGET = 12.0; 548 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; 549 | MARKETING_VERSION = 1.3.2; 550 | ONLY_ACTIVE_ARCH = NO; 551 | PRODUCT_BUNDLE_IDENTIFIER = "com.ScrollStackController.ScrollStackController-iOS"; 552 | PRODUCT_NAME = ScrollStackController; 553 | SKIP_INSTALL = YES; 554 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 555 | SWIFT_VERSION = 5.0; 556 | }; 557 | name = Debug; 558 | }; 559 | 52D6D9921BEFF229002C0205 /* Release */ = { 560 | isa = XCBuildConfiguration; 561 | buildSettings = { 562 | APPLICATION_EXTENSION_API_ONLY = YES; 563 | CLANG_ENABLE_MODULES = YES; 564 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = ""; 565 | CURRENT_PROJECT_VERSION = 0; 566 | DEFINES_MODULE = YES; 567 | DYLIB_COMPATIBILITY_VERSION = 1; 568 | DYLIB_CURRENT_VERSION = 1; 569 | DYLIB_INSTALL_NAME_BASE = "@rpath"; 570 | INFOPLIST_FILE = Configs/ScrollStackController.plist; 571 | INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; 572 | IPHONEOS_DEPLOYMENT_TARGET = 12.0; 573 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; 574 | MARKETING_VERSION = 1.3.2; 575 | PRODUCT_BUNDLE_IDENTIFIER = "com.ScrollStackController.ScrollStackController-iOS"; 576 | PRODUCT_NAME = ScrollStackController; 577 | SKIP_INSTALL = YES; 578 | SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; 579 | SWIFT_VERSION = 5.0; 580 | }; 581 | name = Release; 582 | }; 583 | 52D6D9941BEFF229002C0205 /* Debug */ = { 584 | isa = XCBuildConfiguration; 585 | buildSettings = { 586 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 587 | CLANG_ENABLE_MODULES = YES; 588 | INFOPLIST_FILE = Configs/ScrollStackControllerTests.plist; 589 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; 590 | PRODUCT_BUNDLE_IDENTIFIER = "com.ScrollStackController.ScrollStackController-iOS-Tests"; 591 | PRODUCT_NAME = "$(TARGET_NAME)"; 592 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 593 | SWIFT_VERSION = 5.0; 594 | }; 595 | name = Debug; 596 | }; 597 | 52D6D9951BEFF229002C0205 /* Release */ = { 598 | isa = XCBuildConfiguration; 599 | buildSettings = { 600 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 601 | CLANG_ENABLE_MODULES = YES; 602 | INFOPLIST_FILE = Configs/ScrollStackControllerTests.plist; 603 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; 604 | PRODUCT_BUNDLE_IDENTIFIER = "com.ScrollStackController.ScrollStackController-iOS-Tests"; 605 | PRODUCT_NAME = "$(TARGET_NAME)"; 606 | SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; 607 | SWIFT_VERSION = 5.0; 608 | }; 609 | name = Release; 610 | }; 611 | 64C022772347360900A6D844 /* Debug */ = { 612 | isa = XCBuildConfiguration; 613 | buildSettings = { 614 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 615 | CLANG_ANALYZER_NONNULL = YES; 616 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 617 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 618 | CLANG_ENABLE_OBJC_WEAK = YES; 619 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 620 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 621 | CODE_SIGN_STYLE = Automatic; 622 | CURRENT_PROJECT_VERSION = 0; 623 | DEVELOPMENT_TEAM = SXF6X3A7VN; 624 | GCC_C_LANGUAGE_STANDARD = gnu11; 625 | INFOPLIST_FILE = ScrollStackControllerDemo/Info.plist; 626 | IPHONEOS_DEPLOYMENT_TARGET = 13.0; 627 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 628 | MARKETING_VERSION = 1.3.0; 629 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; 630 | MTL_FAST_MATH = YES; 631 | PRODUCT_BUNDLE_IDENTIFIER = com.danielemargutti.ScrollStackControllerDemo; 632 | PRODUCT_NAME = "$(TARGET_NAME)"; 633 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 634 | SWIFT_VERSION = 5.0; 635 | TARGETED_DEVICE_FAMILY = "1,2"; 636 | }; 637 | name = Debug; 638 | }; 639 | 64C022782347360900A6D844 /* Release */ = { 640 | isa = XCBuildConfiguration; 641 | buildSettings = { 642 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 643 | CLANG_ANALYZER_NONNULL = YES; 644 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 645 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 646 | CLANG_ENABLE_OBJC_WEAK = YES; 647 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 648 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 649 | CODE_SIGN_STYLE = Automatic; 650 | CURRENT_PROJECT_VERSION = 0; 651 | DEVELOPMENT_TEAM = SXF6X3A7VN; 652 | GCC_C_LANGUAGE_STANDARD = gnu11; 653 | INFOPLIST_FILE = ScrollStackControllerDemo/Info.plist; 654 | IPHONEOS_DEPLOYMENT_TARGET = 13.0; 655 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 656 | MARKETING_VERSION = 1.3.0; 657 | MTL_FAST_MATH = YES; 658 | PRODUCT_BUNDLE_IDENTIFIER = com.danielemargutti.ScrollStackControllerDemo; 659 | PRODUCT_NAME = "$(TARGET_NAME)"; 660 | SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; 661 | SWIFT_VERSION = 5.0; 662 | TARGETED_DEVICE_FAMILY = "1,2"; 663 | }; 664 | name = Release; 665 | }; 666 | /* End XCBuildConfiguration section */ 667 | 668 | /* Begin XCConfigurationList section */ 669 | 52D6D9761BEFF229002C0205 /* Build configuration list for PBXProject "ScrollStackController" */ = { 670 | isa = XCConfigurationList; 671 | buildConfigurations = ( 672 | 52D6D98E1BEFF229002C0205 /* Debug */, 673 | 52D6D98F1BEFF229002C0205 /* Release */, 674 | ); 675 | defaultConfigurationIsVisible = 0; 676 | defaultConfigurationName = Release; 677 | }; 678 | 52D6D9901BEFF229002C0205 /* Build configuration list for PBXNativeTarget "ScrollStackController-iOS" */ = { 679 | isa = XCConfigurationList; 680 | buildConfigurations = ( 681 | 52D6D9911BEFF229002C0205 /* Debug */, 682 | 52D6D9921BEFF229002C0205 /* Release */, 683 | ); 684 | defaultConfigurationIsVisible = 0; 685 | defaultConfigurationName = Release; 686 | }; 687 | 52D6D9931BEFF229002C0205 /* Build configuration list for PBXNativeTarget "ScrollStackController-iOS Tests" */ = { 688 | isa = XCConfigurationList; 689 | buildConfigurations = ( 690 | 52D6D9941BEFF229002C0205 /* Debug */, 691 | 52D6D9951BEFF229002C0205 /* Release */, 692 | ); 693 | defaultConfigurationIsVisible = 0; 694 | defaultConfigurationName = Release; 695 | }; 696 | 64C022762347360900A6D844 /* Build configuration list for PBXNativeTarget "ScrollStackControllerDemo" */ = { 697 | isa = XCConfigurationList; 698 | buildConfigurations = ( 699 | 64C022772347360900A6D844 /* Debug */, 700 | 64C022782347360900A6D844 /* Release */, 701 | ); 702 | defaultConfigurationIsVisible = 0; 703 | defaultConfigurationName = Release; 704 | }; 705 | /* End XCConfigurationList section */ 706 | }; 707 | rootObject = 52D6D9731BEFF229002C0205 /* Project object */; 708 | } 709 | -------------------------------------------------------------------------------- /ScrollStackController.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /ScrollStackController.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /ScrollStackController.xcodeproj/project.xcworkspace/xcuserdata/daniele.xcuserdatad/UserInterfaceState.xcuserstate: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/malcommac/ScrollStackController/8ada87d10a486a8222c005d38a68554af2161ca8/ScrollStackController.xcodeproj/project.xcworkspace/xcuserdata/daniele.xcuserdatad/UserInterfaceState.xcuserstate -------------------------------------------------------------------------------- /ScrollStackController.xcodeproj/project.xcworkspace/xcuserdata/daniele.xcuserdatad/xcdebugger/Expressions.xcexplist: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 7 | 8 | 10 | 11 | 12 | 13 | 15 | 16 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /ScrollStackController.xcodeproj/xcshareddata/xcschemes/ScrollStackController-iOS.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 31 | 32 | 38 | 39 | 40 | 41 | 43 | 49 | 50 | 51 | 52 | 53 | 63 | 64 | 70 | 71 | 72 | 73 | 79 | 80 | 86 | 87 | 88 | 89 | 91 | 92 | 95 | 96 | 97 | -------------------------------------------------------------------------------- /ScrollStackController.xcodeproj/xcuserdata/daniele.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | -------------------------------------------------------------------------------- /ScrollStackController.xcodeproj/xcuserdata/daniele.xcuserdatad/xcschemes/xcschememanagement.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | SchemeUserState 6 | 7 | ScrollStackController-iOS.xcscheme_^#shared#^_ 8 | 9 | orderHint 10 | 0 11 | 12 | ScrollStackControllerDemo.xcscheme_^#shared#^_ 13 | 14 | orderHint 15 | 1 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /ScrollStackControllerDemo/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // ScrollStackControllerDemo 4 | // 5 | // Created by Daniele Margutti on 04/10/2019. 6 | // Copyright © 2019 ScrollStackController. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | @UIApplicationMain 12 | class AppDelegate: UIResponder, UIApplicationDelegate { 13 | 14 | 15 | 16 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { 17 | // Override point for customization after application launch. 18 | return true 19 | } 20 | 21 | // MARK: UISceneSession Lifecycle 22 | 23 | func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration { 24 | // Called when a new scene session is being created. 25 | // Use this method to select a configuration to create the new scene with. 26 | return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role) 27 | } 28 | 29 | func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set) { 30 | // Called when the user discards a scene session. 31 | // If any sessions were discarded while the application was not running, this will be called shortly after application:didFinishLaunchingWithOptions. 32 | // Use this method to release any resources that were specific to the discarded scenes, as they will not return. 33 | } 34 | 35 | 36 | } 37 | 38 | -------------------------------------------------------------------------------- /ScrollStackControllerDemo/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 | "idiom" : "ios-marketing", 90 | "size" : "1024x1024", 91 | "scale" : "1x" 92 | } 93 | ], 94 | "info" : { 95 | "version" : 1, 96 | "author" : "xcode" 97 | } 98 | } -------------------------------------------------------------------------------- /ScrollStackControllerDemo/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /ScrollStackControllerDemo/Assets.xcassets/hotel.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "idiom" : "universal", 9 | "filename" : "hotel.png", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /ScrollStackControllerDemo/Assets.xcassets/hotel.imageset/hotel.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/malcommac/ScrollStackController/8ada87d10a486a8222c005d38a68554af2161ca8/ScrollStackControllerDemo/Assets.xcassets/hotel.imageset/hotel.png -------------------------------------------------------------------------------- /ScrollStackControllerDemo/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /ScrollStackControllerDemo/Child View Controllers/GalleryVC.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GalleryVC.swift 3 | // ScrollStackControllerDemo 4 | // 5 | // Created by Daniele Margutti on 04/10/2019. 6 | // Copyright © 2019 ScrollStackController. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | public class GalleryVC: UIViewController, ScrollStackContainableController { 12 | 13 | @IBOutlet public var collectionView: UICollectionView! 14 | @IBOutlet public var pageControl: UIPageControl! 15 | 16 | public var urls: [URL] = [ 17 | URL(string: "http://cdn.luxuo.com/2011/05/Aerial-view-luxury-Burj-Al-Arab.jpg")!, 18 | URL(string: "https://mediastream.jumeirah.com/webimage/heroactual//globalassets/global/hotels-and-resorts/dubai/burj-al-arab/rooms/new-royal-two-berdoom-suite/burj-al-arab-royal-suite-staircase-5-hero.jpg")!, 19 | URL(string: "https://mediastream.jumeirah.com/webimage/image1152x648//globalassets/global/hotels-and-resorts/dubai/burj-al-arab/rooms/new-sky-one-bedroom-suite/2019/burj-al-arab-jumeirah-sky-one-bedroom-suite-living-room-desktop.jpeg")!, 20 | URL(string: "https://q-xx.bstatic.com/xdata/images/hotel/max500/200178877.jpg?k=229a02237c3998ac6e8b11daae254113268e779e49ab2d18964f2e97bdc947a0&o=")! 21 | ] 22 | 23 | public static func create() -> GalleryVC { 24 | let storyboard = UIStoryboard(name: "Main", bundle: nil) 25 | let vc = storyboard.instantiateViewController(identifier: "GalleryVC") as! GalleryVC 26 | return vc 27 | } 28 | 29 | public func scrollStackRowSizeForAxis(_ axis: NSLayoutConstraint.Axis, row: ScrollStackRow, in stackView: ScrollStack) -> ScrollStack.ControllerSize? { 30 | return .fixed(300) 31 | } 32 | 33 | public override func viewDidAppear(_ animated: Bool) { 34 | super.viewDidAppear(animated) 35 | reloadData() 36 | } 37 | 38 | public func reloadContentFromStackView(stackView: ScrollStack, row: ScrollStackRow, animated: Bool) { 39 | 40 | } 41 | 42 | private func reloadData() { 43 | pageControl.numberOfPages = urls.count 44 | collectionView.reloadData() 45 | } 46 | 47 | } 48 | 49 | extension GalleryVC: UICollectionViewDataSource, UICollectionViewDelegateFlowLayout { 50 | 51 | public func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { 52 | return urls.count 53 | } 54 | 55 | public func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { 56 | let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "GalleryCell", for: indexPath) as! GalleryCell 57 | cell.url = urls[indexPath.item] 58 | return cell 59 | } 60 | 61 | public func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize { 62 | //let size = collectionView.bounds.size 63 | return CGSize(width: 310, height: collectionView.bounds.size.height) // fix here 64 | } 65 | 66 | public func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumInteritemSpacingForSectionAt section: Int) -> CGFloat { 67 | return 0 68 | } 69 | 70 | public func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumLineSpacingForSectionAt section: Int) -> CGFloat { 71 | return 0 72 | } 73 | 74 | public func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, insetForSectionAt section: Int) -> UIEdgeInsets { 75 | return .zero 76 | } 77 | 78 | 79 | public func collectionView(_ collectionView: UICollectionView, willDisplay cell: UICollectionViewCell, forItemAt indexPath: IndexPath) { 80 | pageControl.currentPage = indexPath.item 81 | } 82 | 83 | 84 | } 85 | 86 | public class GalleryCell: UICollectionViewCell { 87 | 88 | @IBOutlet public var imageView: UIImageView! 89 | 90 | private var dataTask: URLSessionTask? 91 | 92 | public var url: URL? { 93 | didSet { 94 | dataTask?.cancel() 95 | 96 | guard let url = url else { 97 | self.imageView.image = nil 98 | return 99 | } 100 | 101 | dataTask = URLSession.shared.dataTask(with: url) { (data, _, error) in 102 | let image = (data != nil ? UIImage(data: data!) : nil) 103 | DispatchQueue.main.async { 104 | self.imageView.image = image 105 | } 106 | } 107 | dataTask?.resume() 108 | } 109 | } 110 | 111 | public override func prepareForReuse() { 112 | super.prepareForReuse() 113 | self.url = nil 114 | } 115 | 116 | } 117 | -------------------------------------------------------------------------------- /ScrollStackControllerDemo/Child View Controllers/NotesVC.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NotesVC.swift 3 | // ScrollStackControllerDemo 4 | // 5 | // Created by Daniele Margutti on 06/10/2019. 6 | // Copyright © 2019 ScrollStackController. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | public protocol NotesVCProtocol { 12 | 13 | } 14 | 15 | public class NotesVC: UIViewController, ScrollStackContainableController { 16 | 17 | @IBOutlet public var textView: UITextView! 18 | @IBOutlet public var textViewHeightConstraint: NSLayoutConstraint! 19 | 20 | public static func create(delegate: NotesVCProtocol) -> NotesVC { 21 | let storyboard = UIStoryboard(name: "Main", bundle: nil) 22 | let vc = storyboard.instantiateViewController(identifier: "NotesVC") as! NotesVC 23 | return vc 24 | } 25 | 26 | public func scrollStackRowSizeForAxis(_ axis: NSLayoutConstraint.Axis, row: ScrollStackRow, in stackView: ScrollStack) -> ScrollStack.ControllerSize? { 27 | return .fitLayoutForAxis 28 | } 29 | 30 | 31 | override public func updateViewConstraints() { 32 | let fixedWidth = textView.frame.size.width 33 | let newSize = textView.sizeThatFits(CGSize(width: fixedWidth, height: CGFloat.greatestFiniteMagnitude)) 34 | self.textViewHeightConstraint.constant = newSize.height 35 | 36 | view.height(constant: nil) 37 | super.updateViewConstraints() 38 | } 39 | 40 | public func reloadContentFromStackView(stackView: ScrollStack, row: ScrollStackRow, animated: Bool) { 41 | 42 | } 43 | 44 | public override func viewDidLoad() { 45 | super.viewDidLoad() 46 | 47 | view.height(constant: nil) 48 | textView.isScrollEnabled = false 49 | textView.delegate = self 50 | } 51 | 52 | 53 | } 54 | 55 | extension NotesVC: UITextViewDelegate { 56 | 57 | public func textViewDidChange(_ textView: UITextView) { 58 | updateViewConstraints() 59 | } 60 | 61 | } 62 | -------------------------------------------------------------------------------- /ScrollStackControllerDemo/Child View Controllers/PricingVC.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PricingVC.swift 3 | // ScrollStackControllerDemo 4 | // 5 | // Created by Daniele Margutti on 05/10/2019. 6 | // Copyright © 2019 ScrollStackController. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | public protocol PricingVCProtocol: AnyObject { 12 | func addFee() 13 | } 14 | 15 | public class PricingVC: UIViewController, ScrollStackContainableController { 16 | 17 | public weak var delegate: PricingVCProtocol? 18 | 19 | @IBOutlet public var pricingTable: UITableView! 20 | @IBOutlet public var pricingTableHeightConstraint: NSLayoutConstraint! 21 | 22 | public var pricingTags: [PricingTag] = [ 23 | PricingTag(title: "Night fee", subtitle: "$750 x 3 nights", price: "$2,250.00"), 24 | PricingTag(title: "Hospitality fees", subtitle: "This fee covers services that come with the room", price: "$10.00"), 25 | PricingTag(title: "Property use taxes", subtitle: "Taxes the cost pays to rent their room", price: "$200.00") 26 | ] 27 | 28 | public static func create(delegate: PricingVCProtocol) -> PricingVC { 29 | let storyboard = UIStoryboard(name: "Main", bundle: nil) 30 | let vc = storyboard.instantiateViewController(identifier: "PricingVC") as! PricingVC 31 | vc.delegate = delegate 32 | return vc 33 | } 34 | 35 | public func scrollStackRowSizeForAxis(_ axis: NSLayoutConstraint.Axis, row: ScrollStackRow, in stackView: ScrollStack) -> ScrollStack.ControllerSize? { 36 | return .fitLayoutForAxis 37 | // let size = CGSize(width: stackView.bounds.size.width, height: 9000) 38 | // let best = self.view.systemLayoutSizeFitting(size, withHorizontalFittingPriority: .required, verticalFittingPriority: .defaultLow) 39 | // // NOTE: 40 | // // it's important to set both the height constraint and bottom safe constraints to safe area for tableview, 41 | // // otherwise growing does not work. 42 | // return best.height 43 | } 44 | 45 | override public func updateViewConstraints() { 46 | pricingTableHeightConstraint.constant = pricingTable.contentSize.height // the size of the table as the size of its content 47 | view.height(constant: nil) // cancel any height constraint already in place in the view 48 | super.updateViewConstraints() 49 | } 50 | 51 | public override func viewDidLoad() { 52 | super.viewDidLoad() 53 | 54 | pricingTable.rowHeight = UITableView.automaticDimension 55 | pricingTable.estimatedRowHeight = 60 56 | 57 | pricingTable.reloadData() 58 | pricingTable.sizeToFit() 59 | } 60 | 61 | public func addFee(_ otherFee: PricingTag) { 62 | pricingTags.append(otherFee) 63 | pricingTable.reloadData() 64 | updateViewConstraints() 65 | viewDidLayoutSubviews() 66 | } 67 | 68 | public func reloadContentFromStackView(stackView: ScrollStack, row: ScrollStackRow, animated: Bool) { 69 | 70 | } 71 | 72 | @IBAction public func addFee() { 73 | delegate?.addFee() 74 | } 75 | 76 | } 77 | 78 | extension PricingVC: UITableViewDataSource { 79 | 80 | public func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { 81 | return pricingTags.count 82 | } 83 | 84 | public func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { 85 | let cell = tableView.dequeueReusableCell(withIdentifier: "PricingCell", for: indexPath) as! PricingCell 86 | cell.priceTag = pricingTags[indexPath.row] 87 | return cell 88 | } 89 | 90 | } 91 | 92 | public struct PricingTag { 93 | public let title: String 94 | public let subtitle: String 95 | public let price: String 96 | 97 | public init(title: String, subtitle: String, price: String) { 98 | self.title = title 99 | self.subtitle = subtitle 100 | self.price = price 101 | } 102 | 103 | } 104 | 105 | public class PricingCell: UITableViewCell { 106 | @IBOutlet public var titleLabel: UILabel! 107 | @IBOutlet public var subtitleLabel: UILabel! 108 | @IBOutlet public var priceLabel: UILabel! 109 | 110 | public var priceTag: PricingTag? { 111 | didSet { 112 | titleLabel.text = priceTag?.title ?? "" 113 | subtitleLabel.text = priceTag?.subtitle ?? "" 114 | priceLabel.text = priceTag?.price ?? "" 115 | } 116 | } 117 | 118 | } 119 | -------------------------------------------------------------------------------- /ScrollStackControllerDemo/Child View Controllers/TagsVC.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TagsVC.swift 3 | // ScrollStackControllerDemo 4 | // 5 | // Created by Daniele Margutti on 04/10/2019. 6 | // Copyright © 2019 ScrollStackController. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | public protocol TagsVCProtocol: AnyObject { 12 | func toggleTags() 13 | } 14 | 15 | public class TagsVC: UIViewController, ScrollStackContainableController { 16 | 17 | @IBOutlet public var collectionView: UICollectionView! 18 | @IBOutlet public var toggleTagsButton: UIButton! 19 | 20 | private weak var delegate: TagsVCProtocol? 21 | 22 | private var tags: [String] = [ 23 | "swimming pool", 24 | "kitchen", 25 | "terrace", 26 | "bathtub", 27 | "A/C", 28 | "parking", 29 | "pet friendly", 30 | "relax spa", 31 | "private bathroom", 32 | "cafe" 33 | ] 34 | 35 | public var isExpanded = false { 36 | didSet { 37 | if isExpanded { 38 | collectionView.height(constant: collectionView.contentSize.height) 39 | } 40 | updateUI() 41 | } 42 | } 43 | 44 | public static func create(delegate: TagsVCProtocol) -> TagsVC { 45 | let storyboard = UIStoryboard(name: "Main", bundle: nil) 46 | let vc = storyboard.instantiateViewController(identifier: "TagsVC") as! TagsVC 47 | vc.delegate = delegate 48 | return vc 49 | } 50 | 51 | public func scrollStackRowSizeForAxis(_ axis: NSLayoutConstraint.Axis, row: ScrollStackRow, in stackView: ScrollStack) -> ScrollStack.ControllerSize? { 52 | collectionView.layoutIfNeeded() 53 | return (isExpanded == false ? .fixed(150) : .fixed(150 + collectionView.contentSize.height + 20)) 54 | } 55 | 56 | public func reloadContentFromStackView(stackView: ScrollStack, row: ScrollStackRow, animated: Bool) { 57 | 58 | } 59 | 60 | public override func viewDidLoad() { 61 | super.viewDidLoad() 62 | collectionView.reloadData() 63 | updateUI() 64 | } 65 | 66 | @IBAction public func toggleTags() { 67 | delegate?.toggleTags() 68 | } 69 | 70 | private func updateUI() { 71 | toggleTagsButton.setTitle( (isExpanded ? "Hide Tags" : "Show Tags"), for: .normal) 72 | } 73 | 74 | } 75 | 76 | extension TagsVC: UICollectionViewDataSource { 77 | 78 | public func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { 79 | return tags.count 80 | } 81 | 82 | public func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { 83 | let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "TagsCell", for: indexPath) as! TagsCell 84 | cell.labelCell.text = tags[indexPath.item] 85 | return cell 86 | } 87 | 88 | } 89 | 90 | public class TagsCell: UICollectionViewCell { 91 | 92 | @IBOutlet public var labelCell: UILabel! 93 | 94 | } 95 | -------------------------------------------------------------------------------- /ScrollStackControllerDemo/Child View Controllers/WelcomeVC.swift: -------------------------------------------------------------------------------- 1 | // 2 | // WelcomeVC.swift 3 | // ScrollStackController 4 | // 5 | // Created by Daniele Margutti on 05/10/2019. 6 | // Copyright © 2019 ScrollStackController. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | public class WelcomeVC: UIViewController, ScrollStackContainableController { 12 | 13 | public static func create() -> WelcomeVC { 14 | let storyboard = UIStoryboard(name: "Main", bundle: nil) 15 | let vc = storyboard.instantiateViewController(identifier: "WelcomeVC") as! WelcomeVC 16 | return vc 17 | } 18 | 19 | public func scrollStackRowSizeForAxis(_ axis: NSLayoutConstraint.Axis, row: ScrollStackRow, in stackView: ScrollStack) -> ScrollStack.ControllerSize? { 20 | // let size = CGSize(width: stackView.bounds.size.width, height: 9000) 21 | // let best = self.view.systemLayoutSizeFitting(size, withHorizontalFittingPriority: .required, verticalFittingPriority: .defaultLow) 22 | // return best.height 23 | return .fitLayoutForAxis 24 | } 25 | 26 | public func reloadContentFromStackView(stackView: ScrollStack, row: ScrollStackRow, animated: Bool) { 27 | 28 | } 29 | 30 | } 31 | 32 | extension WelcomeVC: ScrollStackRowAnimatable { 33 | 34 | public var animationInfo: ScrollStackAnimationInfo { 35 | return ScrollStackAnimationInfo(duration: 1, delay: 0, springDamping: 0.8) 36 | } 37 | 38 | public func animateTransition(toHide: Bool) { 39 | switch toHide { 40 | case true: 41 | self.view.transform = CGAffineTransform(translationX: -100, y: 0) 42 | self.view.alpha = 0 43 | 44 | case false: 45 | self.view.transform = .identity 46 | self.view.alpha = 1 47 | } 48 | } 49 | 50 | public func willBeginAnimationTransition(toHide: Bool) { 51 | if toHide == false { 52 | self.view.transform = CGAffineTransform(translationX: -100, y: 0) 53 | self.view.alpha = 0 54 | } 55 | } 56 | 57 | } 58 | -------------------------------------------------------------------------------- /ScrollStackControllerDemo/Extension.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Extension.swift 3 | // ScrollStackControllerDemo 4 | // 5 | // Created by Daniele Margutti on 04/10/2019. 6 | // Copyright © 2019 ScrollStackController. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | extension UIColor { 12 | 13 | static func random(hue: CGFloat = CGFloat.random(in: 0...1), 14 | saturation: CGFloat = CGFloat.random(in: 0.5...1), // from 0.5 to 1.0 to stay away from white 15 | brightness: CGFloat = CGFloat.random(in: 0.5...1), // from 0.5 to 1.0 to stay away from black 16 | alpha: CGFloat = 1) -> UIColor { 17 | return UIColor(hue: hue, saturation: saturation, brightness: brightness, alpha: alpha) 18 | } 19 | 20 | } 21 | -------------------------------------------------------------------------------- /ScrollStackControllerDemo/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 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 17 | CFBundleShortVersionString 18 | $(MARKETING_VERSION) 19 | CFBundleVersion 20 | $(CURRENT_PROJECT_VERSION) 21 | LSRequiresIPhoneOS 22 | 23 | NSAppTransportSecurity 24 | 25 | NSAllowsArbitraryLoads 26 | 27 | 28 | UIApplicationSceneManifest 29 | 30 | UIApplicationSupportsMultipleScenes 31 | 32 | UISceneConfigurations 33 | 34 | UIWindowSceneSessionRoleApplication 35 | 36 | 37 | UISceneConfigurationName 38 | Default Configuration 39 | UISceneDelegateClassName 40 | $(PRODUCT_MODULE_NAME).SceneDelegate 41 | UISceneStoryboardFile 42 | Main 43 | 44 | 45 | 46 | 47 | UILaunchStoryboardName 48 | LaunchScreen 49 | UIMainStoryboardFile 50 | Main 51 | UIRequiredDeviceCapabilities 52 | 53 | armv7 54 | 55 | UISupportedInterfaceOrientations 56 | 57 | UIInterfaceOrientationPortrait 58 | UIInterfaceOrientationLandscapeLeft 59 | UIInterfaceOrientationLandscapeRight 60 | 61 | UISupportedInterfaceOrientations~ipad 62 | 63 | UIInterfaceOrientationPortrait 64 | UIInterfaceOrientationPortraitUpsideDown 65 | UIInterfaceOrientationLandscapeLeft 66 | UIInterfaceOrientationLandscapeRight 67 | 68 | UIUserInterfaceStyle 69 | Light 70 | 71 | 72 | -------------------------------------------------------------------------------- /ScrollStackControllerDemo/SceneDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SceneDelegate.swift 3 | // ScrollStackControllerDemo 4 | // 5 | // Created by Daniele Margutti on 04/10/2019. 6 | // Copyright © 2019 ScrollStackController. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class SceneDelegate: UIResponder, UIWindowSceneDelegate { 12 | 13 | var window: UIWindow? 14 | 15 | 16 | func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { 17 | // Use this method to optionally configure and attach the UIWindow `window` to the provided UIWindowScene `scene`. 18 | // If using a storyboard, the `window` property will automatically be initialized and attached to the scene. 19 | // This delegate does not imply the connecting scene or session are new (see `application:configurationForConnectingSceneSession` instead). 20 | guard let _ = (scene as? UIWindowScene) else { return } 21 | } 22 | 23 | func sceneDidDisconnect(_ scene: UIScene) { 24 | // Called as the scene is being released by the system. 25 | // This occurs shortly after the scene enters the background, or when its session is discarded. 26 | // Release any resources associated with this scene that can be re-created the next time the scene connects. 27 | // The scene may re-connect later, as its session was not neccessarily discarded (see `application:didDiscardSceneSessions` instead). 28 | } 29 | 30 | func sceneDidBecomeActive(_ scene: UIScene) { 31 | // Called when the scene has moved from an inactive state to an active state. 32 | // Use this method to restart any tasks that were paused (or not yet started) when the scene was inactive. 33 | } 34 | 35 | func sceneWillResignActive(_ scene: UIScene) { 36 | // Called when the scene will move from an active state to an inactive state. 37 | // This may occur due to temporary interruptions (ex. an incoming phone call). 38 | } 39 | 40 | func sceneWillEnterForeground(_ scene: UIScene) { 41 | // Called as the scene transitions from the background to the foreground. 42 | // Use this method to undo the changes made on entering the background. 43 | } 44 | 45 | func sceneDidEnterBackground(_ scene: UIScene) { 46 | // Called as the scene transitions from the foreground to the background. 47 | // Use this method to save data, release shared resources, and store enough scene-specific state information 48 | // to restore the scene back to its current state. 49 | } 50 | 51 | 52 | } 53 | 54 | -------------------------------------------------------------------------------- /ScrollStackControllerDemo/ViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewController.swift 3 | // ScrollStackControllerDemo 4 | // 5 | // Created by Daniele Margutti on 04/10/2019. 6 | // Copyright © 2019 ScrollStackController. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class ViewController: UIViewController, ScrollStackControllerDelegate { 12 | 13 | 14 | @IBOutlet public var contentView: UIView! 15 | 16 | private var stackController = ScrollStackViewController() 17 | 18 | public var stackView: ScrollStack { 19 | return stackController.scrollStack 20 | } 21 | 22 | 23 | private var tagsVC: TagsVC! 24 | private var welcomeVC: WelcomeVC! 25 | private var galleryVC: GalleryVC! 26 | private var pricingVC: PricingVC! 27 | private var notesVC: NotesVC! 28 | 29 | override func viewDidLoad() { 30 | super.viewDidLoad() 31 | } 32 | 33 | func scrollStackRowDidUpdateLayout(_ stackView: ScrollStack) { 34 | debugPrint("New content insets \(stackView.contentSize.height)") 35 | } 36 | 37 | override func viewDidAppear(_ animated: Bool) { 38 | super.viewDidAppear(animated) 39 | 40 | stackController.view.frame = contentView.bounds 41 | contentView.addSubview(stackController.view) 42 | 43 | 44 | stackView.stackDelegate = self 45 | // Prepare content 46 | 47 | welcomeVC = WelcomeVC.create() 48 | tagsVC = TagsVC.create(delegate: self) 49 | galleryVC = GalleryVC.create() 50 | pricingVC = PricingVC.create(delegate: self) 51 | notesVC = NotesVC.create(delegate: self) 52 | 53 | /*stackView.isSeparatorHidden = false 54 | stackView.separatorColor = .red 55 | stackView.separatorThickness = 3 56 | stackView.autoHideLastRowSeparator = true 57 | */ 58 | 59 | /* 60 | Plain UIView example 61 | let plainView = UIView(frame: .zero) 62 | plainView.backgroundColor = .green 63 | plainView.heightAnchor.constraint(equalToConstant: 300).isActive = true 64 | stackView.addRow(view: plainView) 65 | */ 66 | stackView.addRows(controllers: [welcomeVC, notesVC, tagsVC, galleryVC, pricingVC], animated: false) 67 | } 68 | 69 | @IBAction public func addNewRow() { 70 | let galleryVC = GalleryVC.create() 71 | stackView.scrollToTop() 72 | stackView.addRow(controller: galleryVC, at: .top, animated: true) 73 | } 74 | 75 | @IBAction public func hideOrShowRandomRow() { 76 | //let randomRow = Int.random(in: 0.. Void)? 224 | 225 | /// Innert stack view. 226 | public let stackView = UIStackView() 227 | 228 | /// Constraints to manage the main axis set. 229 | private var axisConstraint: NSLayoutConstraint? 230 | 231 | // MARK: Initialization 232 | 233 | public init() { 234 | super.init(frame: .zero) 235 | setupUI() 236 | } 237 | 238 | public required init?(coder: NSCoder) { 239 | fatalError("Initialization from IB not supported yet!") 240 | } 241 | 242 | // MARK: - Set Rows 243 | 244 | /// Remove all existing rows and put in place the new list based upon passed controllers. 245 | /// 246 | /// - Parameter controllers: controllers to set. 247 | @discardableResult 248 | open func setRows(controllers: [UIViewController]) -> [ScrollStackRow] { 249 | removeAllRows(animated: false) 250 | return addRows(controllers: controllers) 251 | } 252 | 253 | /// Remove all existing rows and put in place the new list based upon passed views. 254 | /// 255 | /// - Parameter views: views to set. 256 | @discardableResult 257 | open func setRows(views: [UIView]) -> [ScrollStackRow] { 258 | removeAllRows(animated: false) 259 | return addRows(views: views) 260 | } 261 | 262 | // MARK: - Insert Rows 263 | 264 | /// Insert a new to manage passed view without associated controller. 265 | /// 266 | /// - Parameters: 267 | /// - view: view to add. It will be added as contentView of the row. 268 | /// - location: location inside the stack of the new row. 269 | /// - animated: `true` to animate operation, by default is `false`. 270 | /// - completion: completion: optional completion callback to call at the end of insertion. 271 | @discardableResult 272 | open func addRow(view: UIView, at location: InsertLocation = .bottom, animated: Bool = false, completion: (() -> Void)? = nil) -> ScrollStackRow? { 273 | guard let index = indexForLocation(location) else { 274 | return nil 275 | } 276 | 277 | return createRowForView(view, insertAt: index, animated: animated, completion: completion) 278 | } 279 | 280 | /// Add new rows for each passed view. 281 | /// 282 | /// - Parameter controllers: controllers to add as rows. 283 | /// - Parameter location: location inside the stack of the new row. 284 | /// - Parameter animated: `true` to animate operatio, by default is `false`. 285 | @discardableResult 286 | open func addRows(views: [UIView], at location: InsertLocation = .bottom, animated: Bool = false) -> [ScrollStackRow] { 287 | enumerateItems(views, insertAt: location) { 288 | addRow(view: $0, at: location, animated: animated) 289 | } 290 | } 291 | 292 | 293 | /// Insert a new row to manage passed controller instance. 294 | /// 295 | /// - Parameter controller: controller to manage; it's `view` will be added as contentView of the row. 296 | /// - Parameter location: location inside the stack of the new row. 297 | /// - Parameter animated: `true` to animate operation, by default is `false`. 298 | /// - Parameter completion: optional completion callback to call at the end of insertion. 299 | @discardableResult 300 | open func addRow(controller: UIViewController, at location: InsertLocation = .bottom, animated: Bool = false, completion: (() -> Void)? = nil) -> ScrollStackRow? { 301 | guard let index = indexForLocation(location) else { 302 | return nil 303 | } 304 | 305 | return createRowForController(controller, insertAt: index, animated: animated, completion: completion) 306 | } 307 | 308 | /// Add new rows for each passed controllers. 309 | /// 310 | /// - Parameter controllers: controllers to add as rows. 311 | /// - Parameter location: location inside the stack of the new row. 312 | /// - Parameter animated: `true` to animate operatio, by default is `false`. 313 | @discardableResult 314 | open func addRows(controllers: [UIViewController], at location: InsertLocation = .bottom, animated: Bool = false) -> [ScrollStackRow] { 315 | enumerateItems(controllers, insertAt: location) { 316 | addRow(controller: $0, at: location, animated: animated) 317 | } 318 | } 319 | 320 | // MARK: - Reload Rows 321 | 322 | /// Perform a reload method by updating any constraint of the stack view's row. 323 | /// If row's managed controller implements `ScrollStackContainableController` it also call 324 | /// the reload event. 325 | /// 326 | /// - Parameter index: index of the row to reload. 327 | /// - Parameter animated: `true` to animate reload (any constraint change). 328 | /// - Parameter completion: optional completion callback to call. 329 | open func reloadRow(index: Int, animated: Bool = false, completion: (() -> Void)? = nil) { 330 | reloadRows(indexes: [index], animated: animated, completion: completion) 331 | } 332 | 333 | /// Perform a reload method on multiple rows. 334 | /// 335 | /// - Parameter indexes: indexes of the rows to reload. 336 | /// - Parameter animated: `true` to animate reload (any constraint change). 337 | /// - Parameter completion: optional completion callback to call. 338 | open func reloadRows(indexes: [Int], animated: Bool = false, completion: (() -> Void)? = nil) { 339 | let selectedRows = safeRowsAtIndexes(indexes) 340 | reloadRows(selectedRows, animated: animated, completion: completion) 341 | } 342 | 343 | /// Reload all rows of the stack view. 344 | /// 345 | /// - Parameter animated: `true` to animate reload (any constraint change). 346 | /// - Parameter completion: optional completion callback to call. 347 | open func reloadAllRows(animated: Bool = false, completion: (() -> Void)? = nil) { 348 | reloadRows(rows, animated: animated, completion: completion) 349 | } 350 | 351 | // MARK: - Remove Rows 352 | 353 | /// Remove all rows currently in place into the stack. 354 | /// 355 | /// - Parameter animated: `true` to perform animated removeal, by default is `false`. 356 | open func removeAllRows(animated: Bool = false) { 357 | rows.forEach { 358 | removeRowFromStackView($0, animated: animated) 359 | } 360 | } 361 | 362 | /// Remove specified row. 363 | /// 364 | /// - Parameter row: row instance to remove. 365 | /// - Parameter animated: `true` to perform animation to remove item, by default is `false`. 366 | @discardableResult 367 | open func removeRow(index: Int, animated: Bool = false) -> UIViewController? { 368 | guard let row = safeRowAtIndex(index) else { 369 | return nil 370 | } 371 | return removeRowFromStackView(row, animated: animated) 372 | } 373 | 374 | /// Remove passed rows. 375 | /// 376 | /// - Parameter rowIndexes: indexes of the row to remove. 377 | /// - Parameter animated: `true` to animate the removeal, by default is `false`. 378 | @discardableResult 379 | open func removeRows(indexes rowIndexes: [Int], animated: Bool = false) -> [UIViewController]? { 380 | return rowIndexes.compactMap { 381 | return removeRowFromStackView(safeRowAtIndex($0), animated: animated) 382 | } 383 | } 384 | 385 | /// Replace an existing row with another new row which manage passed view. 386 | /// 387 | /// - Parameters: 388 | /// - sourceIndex: row to replace. 389 | /// - view: view to use as `contentView` of the row. 390 | /// - animated: `true` to animate the transition. 391 | /// - completion: optional callback called at the end of the transition. 392 | open func replaceRow(index sourceIndex: Int, withRow view: UIView, animated: Bool = false, completion: (() -> Void)? = nil) { 393 | doReplaceRow(index: sourceIndex, createRow: { (index, animated) -> ScrollStackRow in 394 | return self.createRowForView(view, insertAt: index, animated: animated) 395 | }, animated: animated, completion: completion) 396 | } 397 | 398 | /// Replace an existing row with another new row which manage passed controller. 399 | /// 400 | /// - Parameter row: row to replace. 401 | /// - Parameter controller: view controller to replace. 402 | /// - Parameter animated: `true` to animate the transition. 403 | /// - Parameter completion: optional callback called at the end of the transition. 404 | open func replaceRow(index sourceIndex: Int, withRow controller: UIViewController, animated: Bool = false, completion: (() -> Void)? = nil) { 405 | doReplaceRow(index: sourceIndex, createRow: { (index, animated) in 406 | return self.createRowForController(controller, insertAt: sourceIndex, animated: false) 407 | }, animated: animated, completion: completion) 408 | } 409 | 410 | 411 | /// Move the row at given index to another index. 412 | /// If one of the indexes is not valid nothing is made. 413 | /// 414 | /// - Parameter sourceIndex: source index. 415 | /// - Parameter destIndex: destination index. 416 | /// - Parameter animated: `true` to animate the transition. 417 | /// - Parameter completion: optional callback called at the end of the transition. 418 | open func moveRow(index sourceIndex: Int, to destIndex: Int, animated: Bool = false, completion: (() -> Void)? = nil) { 419 | guard sourceIndex >= 0, sourceIndex < rows.count, destIndex < rows.count else { 420 | return 421 | } 422 | 423 | let sourceRow = rows[sourceIndex] 424 | 425 | func executeMoveRow() { 426 | if sourceRow == stackView.arrangedSubviews.first { 427 | sourceRow.removeFromSuperview() 428 | } 429 | stackView.insertArrangedSubview(sourceRow, at: destIndex) 430 | postInsertRow(sourceRow, animated: false) 431 | stackView.setNeedsLayout() 432 | } 433 | 434 | guard animated else { 435 | executeMoveRow() 436 | completion?() 437 | return 438 | } 439 | 440 | UIView.execute(executeMoveRow, completion: completion) 441 | } 442 | 443 | // MARK: Show/Hide Rows 444 | 445 | /// Hide/Show row from the stack. 446 | /// Row is always on stack and it's returned from the `rows` property. 447 | /// 448 | /// - Parameter rowIndex: target row index. 449 | /// - Parameter isHidden: `true` to hide the row, `false` to make it visible. 450 | /// - Parameter animated: `true` to perform animated transition. 451 | /// - Parameter completion: completion callback called once the operation did finish. 452 | open func setRowHidden(index rowIndex: Int, isHidden: Bool, animated: Bool, completion: (() -> Void)? = nil) { 453 | guard let row = safeRowAtIndex(rowIndex) else { 454 | return 455 | } 456 | 457 | guard animated else { 458 | row.isHidden = isHidden 459 | return 460 | } 461 | 462 | guard row.isHidden != isHidden else { 463 | return 464 | } 465 | 466 | let coordinator = ScrollStackRowAnimator(row: row, toHidden: isHidden, internalHandler: { 467 | row.isHidden = isHidden 468 | row.layoutIfNeeded() 469 | }) 470 | coordinator.execute() 471 | } 472 | 473 | /// Hide/Show selected rows. 474 | /// Rows is always on stack and it's returned from the `rows` property. 475 | /// 476 | /// - Parameter rowIndexes: indexes of the row to hide or show. 477 | /// - Parameter isHidden: `true` to hide the row, `false` to make it visible. 478 | /// - Parameter animated: `true` to perform animated transition. 479 | /// - Parameter completion: completion callback called once the operation did finish. 480 | open func setRowsHidden(indexes rowIndexes: [Int], isHidden: Bool, animated: Bool, completion: (() -> Void)? = nil) { 481 | rowIndexes.forEach { 482 | setRowHidden(index: $0, isHidden: isHidden, animated: animated) 483 | } 484 | } 485 | 486 | // MARK: - Row Appearance 487 | 488 | /// Return the first row which manages a controller of given type. 489 | /// 490 | /// - Parameter type: type of controller to get 491 | open func firstRowForControllerOfType(_ type: T.Type) -> ScrollStackRow? { 492 | return rows.first { 493 | if let _ = $0.controller as? T { 494 | return true 495 | } 496 | return false 497 | } 498 | } 499 | 500 | /// Return the row associated with passed `UIView` instance and its index into the `rows` array. 501 | /// 502 | /// - Parameter view: target view (the `contentView` of the associated `ScrollStackRow` instance). 503 | open func rowForView(_ view: UIView) -> (index: Int, cell: ScrollStackRow)? { 504 | guard let index = rows.firstIndex(where: { 505 | $0.contentView == view 506 | }) else { 507 | return nil 508 | } 509 | 510 | return (index, rows[index]) 511 | } 512 | 513 | /// Return the row associated with passed `UIViewController` instance and its index into the `rows` array. 514 | /// 515 | /// - Parameter controller: target controller. 516 | open func rowForController(_ controller: UIViewController) -> (index: Int, cell: ScrollStackRow)? { 517 | guard let index = rows.firstIndex(where: { 518 | $0.controller === controller 519 | }) else { 520 | return nil 521 | } 522 | 523 | return (index, rows[index]) 524 | } 525 | 526 | /// Return `true` if controller is inside the stackview as a row. 527 | /// 528 | /// - Parameter controller: controller to check. 529 | open func containsRowForController(_ controller: UIViewController) -> Bool { 530 | return rowForController(controller)?.index != nil 531 | } 532 | 533 | /// Return the index of the row. 534 | /// It return `nil` if row is not part of the stack. 535 | /// 536 | /// - Parameter row: row to search for. 537 | open func indexOfRow(_ row: ScrollStackRow) -> Int? { 538 | return rows.firstIndex(of: row) 539 | } 540 | 541 | /// Set the insets of the row's content related to parent row cell. 542 | /// 543 | /// - Parameter row: target row. 544 | /// - Parameter insets: new insets. 545 | open func setRowInsets(index rowIndex: Int, insets: UIEdgeInsets) { 546 | safeRowAtIndex(rowIndex)?.rowInsets = insets 547 | } 548 | 549 | /// Set the insets of the row's content related to the parent row cell. 550 | /// 551 | /// - Parameter row: target rows. 552 | /// - Parameter insets: new insets. 553 | open func setRowsInsets(indexes rowIndexes: [Int], insets: UIEdgeInsets) { 554 | rowIndexes.forEach { 555 | setRowInsets(index: $0, insets: insets) 556 | } 557 | } 558 | 559 | /// Set the padding of the row's content related to parent row cell. 560 | /// 561 | /// - Parameter row: target row. 562 | /// - Parameter padding: new insets. 563 | open func setRowPadding(index rowIndex: Int, padding: UIEdgeInsets) { 564 | safeRowAtIndex(rowIndex)?.rowPadding = padding 565 | } 566 | 567 | /// Set the padding of the row's content related to the parent row cell. 568 | /// 569 | /// - Parameter row: target rows. 570 | /// - Parameter insets: new padding. 571 | open func setRowPadding(indexes rowIndexes: [Int], padding: UIEdgeInsets) { 572 | rowIndexes.forEach { 573 | setRowPadding(index: $0, padding: padding) 574 | } 575 | } 576 | 577 | /// Return the visibility status of a row. 578 | /// 579 | /// - Parameter index: index of the row to check. 580 | open func isRowVisible(index: Int) -> RowVisibility { 581 | guard let row = safeRowAtIndex(index), row.isHidden == false else { 582 | return .hidden 583 | } 584 | 585 | return rowVisibilityType(row: row) 586 | } 587 | 588 | /// Return `true` if row is currently hidden. 589 | /// 590 | /// - Parameter row: row to check. 591 | open func isRowHidden(index: Int) -> Bool { 592 | return safeRowAtIndex(index)?.isHidden ?? false 593 | } 594 | 595 | // MARK: - Scroll 596 | 597 | /// Scroll to the passed row. 598 | /// 599 | /// - Parameter rowIndex: index of the row to make visible. 600 | /// - Parameter location: visibility of the row, location of the center point. 601 | /// - Parameter animated: `true` to perform animated transition. 602 | open func scrollToRow(index rowIndex: Int, at position: ScrollPosition = .automatic, animated: Bool = true) { 603 | guard let row = safeRowAtIndex(rowIndex) else { 604 | return 605 | } 606 | 607 | let rowFrame = convert(row.frame, to: self) 608 | 609 | if case .automatic = position { 610 | scrollRectToVisible(rowFrame, animated: animated) 611 | return 612 | } 613 | 614 | let offset = adjustedOffsetForFrame(rowFrame, toScrollAt: position) 615 | setContentOffset(offset, animated: animated) 616 | } 617 | 618 | /// Scroll to top. 619 | /// - Parameter animated: `true` to perform animated transition. 620 | open func scrollToTop(animated: Bool = true) { 621 | let topOffset = CGPoint(x: 0, y: -contentInset.top) 622 | setContentOffset(topOffset, animated: animated) 623 | } 624 | 625 | /// Scroll to bottom. 626 | /// - Parameter animated: `true` to perform animated transition. 627 | open func scrollToBottom(animated: Bool = true) { 628 | let bottomOffset = CGPoint(x: 0, y: contentSize.height - bounds.size.height + contentInset.bottom) 629 | if bottomOffset.y > 0 { 630 | setContentOffset(bottomOffset, animated: animated) 631 | } 632 | } 633 | 634 | /// Invert axis of scroll. 635 | /// 636 | /// - Parameter animated: `true` to animate operation. 637 | /// - Parameter completion: completion callback. 638 | open func toggleAxis(animated: Bool = false, completion: (() -> Void)? = nil) { 639 | UIView.execute(animated: animated, { 640 | self.axis = (self.axis == .horizontal ? .vertical : .horizontal) 641 | }, completion: completion) 642 | } 643 | 644 | // MARK: - Private Functions 645 | 646 | private func doReplaceRow(index sourceIndex: Int, createRow handler: @escaping ((Int, Bool) -> ScrollStackRow), animated: Bool, completion: (() -> Void)? = nil) { 647 | guard sourceIndex >= 0, sourceIndex < rows.count else { 648 | return 649 | } 650 | 651 | let sourceRow = rows[sourceIndex] 652 | guard animated else { 653 | removeRow(index: sourceRow.index!) 654 | _ = handler(sourceIndex, false) 655 | return 656 | } 657 | 658 | stackView.setNeedsLayout() 659 | 660 | UIView.execute({ 661 | sourceRow.isHidden = true 662 | }) { 663 | let newRow = handler(sourceIndex, false) 664 | newRow.isHidden = true 665 | UIView.execute({ 666 | newRow.isHidden = false 667 | }, completion: completion) 668 | } 669 | } 670 | 671 | /// Enumerate items to insert into the correct order based upon the location of destination. 672 | /// 673 | /// - Parameters: 674 | /// - list: list to enumerate. 675 | /// - location: insert location. 676 | /// - callback: callback to call on enumrate. 677 | private func enumerateItems(_ list: [T], insertAt location: InsertLocation, callback: ((T) -> ScrollStackRow?)) -> [ScrollStackRow] { 678 | switch location { 679 | case .top: 680 | return list.reversed().compactMap(callback).reversed() // double reversed() is to avoid strange behaviour when additing rows on tops. 681 | 682 | default: 683 | return list.compactMap(callback) 684 | 685 | } 686 | } 687 | 688 | /// Return the destination index for passed location. `nil` if index is not valid. 689 | /// 690 | /// - Parameter location: location. 691 | private func indexForLocation(_ location: InsertLocation) -> Int? { 692 | switch location { 693 | case .top: 694 | return 0 695 | 696 | case .bottom: 697 | return rows.count 698 | 699 | case .atIndex(let index): 700 | return index 701 | 702 | case .after(let controller): 703 | guard let index = rowForController(controller)?.index else { 704 | return nil 705 | } 706 | return ((index + 1) >= rows.count ? rows.count : (index + 1)) 707 | 708 | case .afterView(let view): 709 | guard let index = rowForView(view)?.index else { 710 | return nil 711 | } 712 | return ((index + 1) >= rows.count ? rows.count : (index + 1)) 713 | 714 | case .before(let controller): 715 | guard let index = rowForController(controller)?.index else { 716 | return nil 717 | } 718 | return index 719 | 720 | case .beforeView(let view): 721 | guard let index = rowForView(view)?.index else { 722 | return nil 723 | } 724 | return index 725 | } 726 | } 727 | 728 | /// Initial configuration of the control. 729 | private func setupUI() { 730 | backgroundColor = .white 731 | 732 | // Create stack view and add it to the scrollview 733 | stackView.translatesAutoresizingMaskIntoConstraints = false 734 | stackView.axis = .vertical 735 | backgroundColor = .white 736 | addSubview(stackView) 737 | 738 | // Configure constraints for stackview 739 | NSLayoutConstraint.activate([ 740 | stackView.topAnchor.constraint(equalTo: topAnchor), 741 | stackView.bottomAnchor.constraint(equalTo: bottomAnchor), 742 | stackView.leadingAnchor.constraint(equalTo: leadingAnchor), 743 | stackView.trailingAnchor.constraint(equalTo: trailingAnchor) 744 | ]) 745 | 746 | didChangeAxis(axis) 747 | } 748 | 749 | /// Reload selected rows of the stackview. 750 | /// 751 | /// - Parameter rows: rows to reload. 752 | /// - Parameter animated: `true` to animate reload. 753 | /// - Parameter completion: completion callback to call at the end of the reload. 754 | private func reloadRows(_ rows: [ScrollStackRow], animated: Bool = false, completion: (() -> Void)? = nil) { 755 | guard rows.isEmpty == false else { 756 | return 757 | } 758 | 759 | rows.forEach { 760 | ($0.controller as? ScrollStackContainableController)?.reloadContentFromStackView(stackView: self, row: $0, animated: animated) 761 | $0.askForCutomizedSizeOfContentView(animated: animated) 762 | } 763 | 764 | UIView.execute(animated: animated, { 765 | self.layoutIfNeeded() 766 | }, completion: completion) 767 | } 768 | 769 | /// Get the row at specified index; if index is invalid `nil` is returned. 770 | /// 771 | /// - Parameter index: index of the row to get. 772 | private func safeRowAtIndex(_ index: Int) -> ScrollStackRow? { 773 | return safeRowsAtIndexes([index]).first 774 | } 775 | 776 | /// Get the rows at specified indexes, invalid indexes are ignored. 777 | /// 778 | /// - Parameter indexes: indexes of the rows to get. 779 | private func safeRowsAtIndexes(_ indexes: [Int]) -> [ScrollStackRow] { 780 | return indexes.compactMap { index in 781 | guard index >= 0, index < rows.count else { 782 | return nil 783 | } 784 | return rows[index] 785 | } 786 | } 787 | 788 | /// Get the row visibility type for a specific row. 789 | /// 790 | /// - Parameter row: row to get. 791 | private func rowVisibilityType(row: ScrollStackRow) -> RowVisibility { 792 | let rowFrame = convert(row.frame, to: self) 793 | guard bounds.intersects(rowFrame) else { 794 | return .offscreen 795 | } 796 | 797 | if bounds.contains(rowFrame) { 798 | return .entire 799 | } else { 800 | let intersection = bounds.intersection(rowFrame) 801 | let intersectionPercentage = ((intersection.width * intersection.height) / (rowFrame.width * rowFrame.height)) * 100 802 | return .partial(percentage: intersectionPercentage) 803 | } 804 | } 805 | 806 | /// Remove passed row from stack view. 807 | /// 808 | /// - Parameter row: row to remove. 809 | /// - Parameter animated: `true` to perform animated transition. 810 | @discardableResult 811 | private func removeRowFromStackView(_ row: ScrollStackRow?, animated: Bool = false) -> UIViewController? { 812 | guard let row = row else { 813 | return nil 814 | } 815 | 816 | // Animate visibility 817 | let removedController = row.controller 818 | animateCellVisibility(row, animated: animated, hide: true, completion: { [weak self] in 819 | guard let self = self else { return } 820 | 821 | self.onChangeRow?(row, true) 822 | 823 | row.removeFromStackView() 824 | 825 | // When removing a cell the cell above is the only cell whose separator visibility 826 | // will be affected, so we need to update its visibility. 827 | self.updateRowsSeparatorVisibility() 828 | 829 | // Remove from the status 830 | self.prevVisibilityState.removeValue(forKey: row) 831 | }) 832 | 833 | return removedController 834 | } 835 | 836 | /// Create a new row to handle passed view and insert it at specified index. 837 | /// 838 | /// - Parameters: 839 | /// - view: view to use as `contentView` of the row. 840 | /// - index: position of the new row with controller's view. 841 | /// - animated: `true` to animate transition. 842 | /// - completion: completion callback called when operation is finished. 843 | @discardableResult 844 | private func createRowForView(_ view: UIView, insertAt index: Int, animated: Bool, completion: (() -> Void)? = nil) -> ScrollStackRow { 845 | // Identify any other cell with the same controller 846 | let cellToRemove = rowForView(view)?.cell 847 | 848 | // Create the new container cell for this view. 849 | let newRow = ScrollStackRow(view: view, stackView: self) 850 | return createRow(newRow, at: index, cellToRemove: cellToRemove, animated: animated, completion: completion) 851 | } 852 | 853 | /// Create a new row to handle passed controller and insert it at specified index. 854 | /// 855 | /// - Parameter controller: controller to manage. 856 | /// - Parameter index: position of the new row with controller's view. 857 | /// - Parameter animated: `true` to animate transition. 858 | /// - Parameter completion: completion callback called when operation is finished. 859 | @discardableResult 860 | private func createRowForController(_ controller: UIViewController, insertAt index: Int, animated: Bool, completion: (() -> Void)? = nil) -> ScrollStackRow { 861 | // Identify any other cell with the same controller to remove 862 | let cellToRemove = rowForController(controller)?.cell 863 | 864 | // Create the new container cell for this controller's view 865 | let newRow = ScrollStackRow(controller: controller, stackView: self) 866 | return createRow(newRow, at: index, cellToRemove: cellToRemove, animated: animated, completion: completion) 867 | } 868 | 869 | private var rowVisibilityChangesDispatchWorkItem: DispatchWorkItem? 870 | 871 | /// Private implementation to add new row. 872 | private func createRow(_ newRow: ScrollStackRow, at index: Int, 873 | cellToRemove: ScrollStackRow?, 874 | animated: Bool, completion: (() -> Void)? = nil) -> ScrollStackRow { 875 | onChangeRow?(newRow, false) 876 | stackView.insertArrangedSubview(newRow, at: index) 877 | 878 | // Remove any duplicate cell with the same view 879 | removeRowFromStackView(cellToRemove) 880 | 881 | postInsertRow(newRow, animated: animated, completion: completion) 882 | 883 | if animated { 884 | UIView.execute({ 885 | self.layoutIfNeeded() 886 | }, completion: nil) 887 | } 888 | 889 | if rowVisibilityChangesDispatchWorkItem == nil { 890 | 891 | rowVisibilityChangesDispatchWorkItem = DispatchWorkItem(block: { [weak self] in 892 | if let stackDelegate = self?.stackDelegate { 893 | self?.dispatchRowsVisibilityChangesTo(stackDelegate) 894 | } 895 | 896 | self?.rowVisibilityChangesDispatchWorkItem = nil 897 | }) 898 | 899 | /// Schedule a single `dispatchRowsVisibilityChangesTo(_:)` call. 900 | /// 901 | /// In this way, when rows are created inside a for-loop, the delegate is called only once after the `ScrollStack` has been fully laid out. 902 | DispatchQueue.main.async(execute: rowVisibilityChangesDispatchWorkItem!) 903 | } 904 | 905 | return newRow 906 | } 907 | 908 | private func postInsertRow(_ row: ScrollStackRow, animated: Bool, completion: (() -> Void)? = nil) { 909 | updateRowsSeparatorVisibility() // update visibility of the separators 910 | animateCellVisibility(row, animated: animated, hide: false, completion: completion) // Animate visibility of the cell 911 | } 912 | 913 | /// Update the separator visibility. 914 | /// 915 | /// - Parameter row: row target. 916 | private func updateRowsSeparatorVisibility() { 917 | let rows = stackView.arrangedSubviews as? [ScrollStackRow] ?? [] 918 | for (idx, row) in rows.enumerated() { 919 | row.separatorView.isHidden = (idx == rows.last?.index ? true : row.isSeparatorHidden) 920 | } 921 | } 922 | 923 | /// Return the row before a given row, if exists. 924 | /// 925 | /// - Parameter row: row to check. 926 | private func rowBeforeRow(_ row: ScrollStackRow) -> ScrollStackRow? { 927 | guard let index = stackView.arrangedSubviews.firstIndex(of: row), index > 0 else { 928 | return nil 929 | } 930 | return stackView.arrangedSubviews[index - 1] as? ScrollStackRow 931 | } 932 | 933 | // MARK: - Row Animated Transitions 934 | 935 | private func animateCellVisibility(_ cell: ScrollStackRow, animated: Bool, hide: Bool, completion: (() -> Void)? = nil) { 936 | if hide { 937 | animateCellToInvisibleState(cell, animated: animated, hide: hide, completion: completion) 938 | } else { 939 | animateCellToVisibleState(cell, animated: animated, hide: hide, completion: completion) 940 | } 941 | } 942 | 943 | /// Animate transition of the cell to visible state. 944 | private func animateCellToVisibleState(_ row: ScrollStackRow, animated: Bool, hide: Bool, completion: (() -> Void)? = nil) { 945 | guard animated else { 946 | row.alpha = 1.0 947 | row.isHidden = false 948 | completion?() 949 | return 950 | } 951 | 952 | row.alpha = 0.0 953 | layoutIfNeeded() 954 | UIView.execute({ 955 | row.alpha = 1.0 956 | }, completion: completion) 957 | } 958 | 959 | /// Animate transition of the cell to invisibile state. 960 | private func animateCellToInvisibleState(_ row: ScrollStackRow, animated: Bool, hide: Bool, completion: (() -> Void)? = nil) { 961 | UIView.execute(animated: animated, { 962 | row.isHidden = true 963 | }, completion: completion) 964 | } 965 | 966 | // MARK: - Axis Change Events 967 | 968 | /// Update the constraint due to axis change of the stack view. 969 | /// 970 | /// - Parameter axis: new axis. 971 | private func didChangeAxis(_ axis: NSLayoutConstraint.Axis) { 972 | didUpdateStackViewAxisTo(axis) 973 | didReflectAxisChangeToRows(axis) 974 | } 975 | 976 | private func didUpdateStackViewAxisTo(_ axis: NSLayoutConstraint.Axis) { 977 | axisConstraint?.isActive = false 978 | switch axis { 979 | case .vertical: 980 | axisConstraint = stackView.widthAnchor.constraint(equalTo: widthAnchor) 981 | 982 | case .horizontal: 983 | axisConstraint = stackView.heightAnchor.constraint(equalTo: heightAnchor) 984 | 985 | @unknown default: 986 | break 987 | 988 | } 989 | 990 | rows.forEach { 991 | $0.layoutUI() 992 | } 993 | 994 | axisConstraint?.isActive = true 995 | } 996 | 997 | private func didReflectAxisChangeToRows(_ axis: NSLayoutConstraint.Axis) { 998 | rows.forEach { 999 | $0.separatorAxis = (axis == .horizontal ? .vertical : .horizontal) 1000 | } 1001 | } 1002 | 1003 | private func dispatchRowsVisibilityChangesTo(_ delegate: ScrollStackControllerDelegate) { 1004 | rows.enumerated().forEach { (idx, row) in 1005 | let current = isRowVisible(index: idx) 1006 | let previous = prevVisibilityState[row] 1007 | 1008 | switch (previous, current) { 1009 | case (.offscreen, .partial), // row will become visible 1010 | (nil, .entire), 1011 | (nil, .partial), 1012 | (.partial, .entire), 1013 | (.hidden, .partial), 1014 | (.hidden, .entire): 1015 | delegate.scrollStackRowDidBecomeVisible(self, row: row, index: idx, state: current) 1016 | 1017 | case (.partial, .offscreen), // row will become invisible 1018 | (.entire, .partial), 1019 | (.partial, .hidden), 1020 | (.entire, .hidden): 1021 | delegate.scrollStackRowDidBecomeHidden(self, row: row, index: idx, state: current) 1022 | 1023 | default: 1024 | break 1025 | } 1026 | 1027 | // store previous state 1028 | prevVisibilityState[row] = current 1029 | } 1030 | } 1031 | 1032 | // MARK: - Private Scroll 1033 | 1034 | private func adjustedOffsetForFrame(_ frame: CGRect, toScrollAt position: ScrollPosition) -> CGPoint { 1035 | var adjustedPoint: CGPoint = frame.origin 1036 | 1037 | switch position { 1038 | case .middle: 1039 | if axis == .horizontal { 1040 | adjustedPoint.x = frame.origin.x - ((bounds.size.width - frame.size.width) / 2.0) 1041 | } else { 1042 | adjustedPoint.y = frame.origin.y - (bounds.size.height - frame.size.height) 1043 | } 1044 | 1045 | case .final: 1046 | if axis == .horizontal { 1047 | adjustedPoint.x = frame.origin.x - (bounds.size.width - frame.size.width) 1048 | } else { 1049 | adjustedPoint.y = frame.origin.y - (bounds.size.height - frame.size.height) 1050 | } 1051 | 1052 | case .initial: 1053 | if axis == .horizontal { 1054 | adjustedPoint.x = frame.origin.x 1055 | } else { 1056 | adjustedPoint.y = frame.origin.y 1057 | } 1058 | 1059 | case .automatic: 1060 | break 1061 | 1062 | } 1063 | 1064 | if axis == .horizontal { 1065 | adjustedPoint.x = max(adjustedPoint.x, 0) 1066 | 1067 | let reachedOffsetx = adjustedPoint.x + self.bounds.size.width 1068 | if reachedOffsetx > self.contentSize.width { 1069 | adjustedPoint.x -= (reachedOffsetx - self.contentSize.width) 1070 | } 1071 | } else { 1072 | adjustedPoint.y = max(adjustedPoint.y, 0) 1073 | 1074 | let reachedOffsetY = adjustedPoint.y + self.bounds.size.height 1075 | if reachedOffsetY > self.contentSize.height { 1076 | adjustedPoint.y -= (reachedOffsetY - self.contentSize.height) 1077 | } 1078 | } 1079 | 1080 | return adjustedPoint 1081 | } 1082 | 1083 | // MARK: UIScrollViewDelegate 1084 | 1085 | public func scrollViewDidScroll(_ scrollView: UIScrollView) { 1086 | guard let stackDelegate = stackDelegate else { 1087 | return 1088 | } 1089 | stackDelegate.scrollStackDidScroll(self, offset: contentOffset) 1090 | 1091 | dispatchRowsVisibilityChangesTo(stackDelegate) 1092 | } 1093 | 1094 | public func scrollViewDidEndScrollingAnimation(_ scrollView: UIScrollView) { 1095 | guard let stackDelegate = stackDelegate else { 1096 | return 1097 | } 1098 | 1099 | stackDelegate.scrollStackDidEndScrollingAnimation(self) 1100 | } 1101 | 1102 | open override func layoutSubviews() { 1103 | super.layoutSubviews() 1104 | 1105 | guard let stackDelegate = stackDelegate else { 1106 | return 1107 | } 1108 | 1109 | stackDelegate.scrollStackDidUpdateLayout(self) 1110 | 1111 | if cachedContentSize != self.contentSize { 1112 | stackDelegate.scrollStackContentSizeDidChange(self, from: cachedContentSize, to: contentSize) 1113 | } 1114 | cachedContentSize = self.contentSize 1115 | } 1116 | 1117 | } 1118 | -------------------------------------------------------------------------------- /Sources/ScrollStackController/ScrollStackRow.swift: -------------------------------------------------------------------------------- 1 | /* 2 | * ScrollStackController 3 | * Create complex scrollable layout using UIViewController and simplify your code 4 | * 5 | * Created by: Daniele Margutti 6 | * Email: hello@danielemargutti.com 7 | * Web: http://www.danielemargutti.com 8 | * Twitter: @danielemargutti 9 | * 10 | * Copyright © 2019 Daniele Margutti 11 | * 12 | * 13 | * Permission is hereby granted, free of charge, to any person obtaining a copy 14 | * of this software and associated documentation files (the "Software"), to deal 15 | * in the Software without restriction, including without limitation the rights 16 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 17 | * copies of the Software, and to permit persons to whom the Software is 18 | * furnished to do so, subject to the following conditions: 19 | * 20 | * The above copyright notice and this permission notice shall be included in 21 | * all copies or substantial portions of the Software. 22 | * 23 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 24 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 25 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 26 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 27 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 28 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 29 | * THE SOFTWARE. 30 | * 31 | */ 32 | 33 | import UIKit 34 | 35 | // MARK: - ScrollStackRow 36 | 37 | open class ScrollStackRow: UIView, UIGestureRecognizerDelegate { 38 | 39 | // MARK: - Private Properties 40 | 41 | /// Weak reference to the parent stack view. 42 | private weak var stackView: ScrollStack? 43 | 44 | /// Tap gesture recognizer. 45 | private lazy var onTapGestureRecognizer: UITapGestureRecognizer = { 46 | let gesture = UITapGestureRecognizer() 47 | 48 | gesture.addTarget(self, action: #selector(handleTap(_:))) 49 | gesture.delegate = self 50 | addGestureRecognizer(gesture) 51 | gesture.isEnabled = false 52 | 53 | return gesture 54 | }() 55 | 56 | /// Constraints to handle separator's insets changes. 57 | private var separatorConstraints: ConstraintsHolder? 58 | 59 | /// Constraints to handle content's view padding changes. 60 | private var paddingConstraints: ConstraintsHolder? 61 | 62 | /// Location of the separator view. 63 | /// It's automatically managed when you change the axis of the parent stackview. 64 | internal var separatorAxis: NSLayoutConstraint.Axis = .horizontal { 65 | didSet { 66 | guard separatorAxis != oldValue else { 67 | return 68 | } 69 | didUpdateSeparatorViewContraintsIfNeeded() 70 | didUpdateSeparatorAxis() 71 | didUpdateSeparatorInsets() 72 | layoutIfNeeded() 73 | } 74 | } 75 | 76 | // MARK: - Public Properties 77 | 78 | /// Return the index of the row into the parent stack. 79 | public var index: Int? { 80 | return self.stackView?.indexOfRow(self) 81 | } 82 | 83 | /// Row highlight color. 84 | open var rowHighlightColor = ScrollStack.defaultRowColor 85 | 86 | /// Row background color. 87 | open var rowBackgroundColor = ScrollStack.defaultRowHighlightColor { 88 | didSet { 89 | backgroundColor = rowBackgroundColor 90 | } 91 | } 92 | 93 | /// Callback called when a tap is performed on row. 94 | /// By default row is not tappable. 95 | public var onTap: ((ScrollStackRow) -> Void)? { 96 | didSet { 97 | onTapGestureRecognizer.isEnabled = (onTap != nil) 98 | } 99 | } 100 | 101 | /// Parent controller. 102 | /// This value maybe `nil` if you use just `view` and not controller as row. 103 | /// 104 | /// NOTE: 105 | /// This value is strongly retained so you don't need to 106 | /// save it anywhere in your parent controller in order to avoid releases. 107 | public private(set) var controller: UIViewController? 108 | 109 | /// Content view controller (if controller is used) or just the view addded. 110 | /// 111 | /// NOTE: This value is strongly retained. 112 | public private(set) var contentView: UIView? 113 | 114 | // MARK: - Manage Separator 115 | 116 | /// Separator view object. 117 | public let separatorView = ScrollStackSeparator() 118 | 119 | /// Specifies the default insets for cell's separator. 120 | /// By default the value applied is inerithed from the separator's insets configuration of the 121 | /// parent stackview at the time of the creation of the cell. 122 | /// You can however assign a custom insets for each separator. 123 | open var separatorInsets: UIEdgeInsets = .zero { 124 | didSet { 125 | didUpdateSeparatorInsets() 126 | } 127 | } 128 | 129 | open var isSeparatorHidden: Bool { 130 | didSet { 131 | separatorView.isHidden = isSeparatorHidden 132 | } 133 | } 134 | 135 | // MARK: Private Properties 136 | 137 | @objc private func handleTap(_ tapGestureRecognizer: UITapGestureRecognizer) { 138 | guard contentView?.isUserInteractionEnabled ?? false else { 139 | return 140 | } 141 | onTap?(self) 142 | } 143 | 144 | open var rowInsets: UIEdgeInsets { 145 | get { 146 | return layoutMargins 147 | } 148 | set { 149 | layoutMargins = newValue 150 | } 151 | } 152 | 153 | open var rowPadding: UIEdgeInsets { 154 | didSet { 155 | paddingConstraints?.updateInsets(rowPadding) 156 | } 157 | } 158 | 159 | open override var isHidden: Bool { 160 | didSet { 161 | guard isHidden != oldValue else { 162 | return 163 | } 164 | separatorView.alpha = (isHidden ? 0 : 1) 165 | } 166 | } 167 | 168 | // MARK: - Initialization 169 | 170 | public required init?(coder: NSCoder) { 171 | fatalError("init(coder:) has not been implemented") 172 | } 173 | 174 | internal init(view: UIView, stackView: ScrollStack) { 175 | self.stackView = stackView 176 | self.controller = nil 177 | self.contentView = view 178 | self.rowPadding = stackView.rowPadding 179 | self.isSeparatorHidden = stackView.isSeparatorHidden 180 | 181 | super.init(frame: .zero) 182 | 183 | setupPostInit() 184 | } 185 | 186 | internal init(controller: UIViewController, stackView: ScrollStack) { 187 | self.stackView = stackView 188 | self.controller = controller 189 | self.contentView = controller.view 190 | self.rowPadding = stackView.rowPadding 191 | self.isSeparatorHidden = stackView.isSeparatorHidden 192 | 193 | super.init(frame: .zero) 194 | 195 | setupPostInit() 196 | } 197 | 198 | // MARK: - Setup UI 199 | 200 | public func removeFromStackView() { 201 | contentView?.removeFromSuperview() 202 | contentView = nil 203 | controller = nil 204 | 205 | stackView?.stackView.removeArrangedSubview(self) 206 | removeFromSuperview() 207 | } 208 | 209 | private func setupPostInit() { 210 | guard let contentView = contentView else { 211 | return 212 | } 213 | 214 | clipsToBounds = true 215 | insetsLayoutMarginsFromSafeArea = false 216 | contentView.translatesAutoresizingMaskIntoConstraints = false 217 | self.translatesAutoresizingMaskIntoConstraints = false 218 | 219 | layoutUI() 220 | } 221 | 222 | internal func layoutUI() { 223 | guard let contentView = contentView else { 224 | return 225 | } 226 | 227 | contentView.removeFromSuperview() 228 | 229 | addSubview(contentView) 230 | addSubview(separatorView) 231 | 232 | didUpdateContentViewContraints() 233 | didUpdateSeparatorViewContraintsIfNeeded() 234 | didUpdateSeparatorAxis() 235 | 236 | applyParentStackAttributes() 237 | 238 | separatorView.isHidden = isSeparatorHidden 239 | setNeedsUpdateConstraints() 240 | } 241 | 242 | open override func updateConstraints() { 243 | // called the event to update the height of the row. 244 | askForCutomizedSizeOfContentView(animated: false) 245 | 246 | super.updateConstraints() 247 | } 248 | 249 | private func applyParentStackAttributes() { 250 | guard let stackView = self.stackView else { 251 | return 252 | } 253 | 254 | rowInsets = stackView.rowInsets 255 | rowPadding = stackView.rowPadding 256 | rowBackgroundColor = stackView.rowBackgroundColor 257 | rowHighlightColor = stackView.rowHighlightColor 258 | 259 | separatorAxis = (stackView.axis == .horizontal ? .vertical : .horizontal) 260 | separatorInsets = stackView.separatorInsets 261 | 262 | separatorView.color = stackView.separatorColor 263 | separatorView.thickness = stackView.separatorThickness 264 | isSeparatorHidden = stackView.hideSeparators 265 | } 266 | 267 | // MARK: - Manage Separator 268 | 269 | private func didUpdateContentViewContraints() { 270 | guard let contentView = contentView else { 271 | return 272 | } 273 | 274 | let bottomConstraint = contentView.bottomAnchor.constraint(equalTo: layoutMarginsGuide.bottomAnchor, constant: rowPadding.bottom) 275 | bottomConstraint.priority = UILayoutPriority(rawValue: UILayoutPriority.required.rawValue - 1) 276 | 277 | paddingConstraints = ConstraintsHolder( 278 | top: contentView.topAnchor.constraint(equalTo: layoutMarginsGuide.topAnchor, constant: rowPadding.top), 279 | bottom: bottomConstraint, 280 | left: contentView.leadingAnchor.constraint(equalTo: layoutMarginsGuide.leadingAnchor, constant: rowPadding.left), 281 | right: contentView.trailingAnchor.constraint(equalTo: layoutMarginsGuide.trailingAnchor, constant: rowPadding.right) 282 | ) 283 | 284 | paddingConstraints?.activateAll() 285 | } 286 | 287 | private func didUpdateSeparatorViewContraintsIfNeeded() { 288 | if separatorConstraints == nil { 289 | separatorConstraints = ConstraintsHolder( 290 | top: separatorView.topAnchor.constraint(equalTo: topAnchor), 291 | bottom: separatorView.bottomAnchor.constraint(equalTo: bottomAnchor), 292 | left: separatorView.leadingAnchor.constraint(equalTo: leadingAnchor), 293 | right: separatorView.trailingAnchor.constraint(equalTo: trailingAnchor) 294 | ) 295 | } 296 | } 297 | 298 | private func didUpdateSeparatorAxis() { 299 | separatorConstraints?.top?.isActive = (separatorAxis == .vertical) 300 | separatorConstraints?.bottom?.isActive = true 301 | separatorConstraints?.left?.isActive = (separatorAxis == .horizontal) 302 | separatorConstraints?.right?.isActive = true 303 | } 304 | 305 | private func didUpdateSeparatorInsets() { 306 | separatorConstraints?.top?.constant = separatorInsets.top 307 | separatorConstraints?.bottom?.constant = (separatorAxis == .horizontal ? 0 : -separatorInsets.bottom) 308 | separatorConstraints?.left?.constant = separatorInsets.left 309 | separatorConstraints?.right?.constant = (separatorAxis == .vertical ? 0 : -separatorInsets.right) 310 | } 311 | 312 | // MARK: - Sizing the Controller 313 | 314 | internal func askForCutomizedSizeOfContentView(animated: Bool) { 315 | guard let customizableController = controller as? ScrollStackContainableController else { 316 | return // ignore, it's not implemented, use autolayout. 317 | } 318 | 319 | let currentAxis = stackView!.axis 320 | guard let bestSize = customizableController.scrollStackRowSizeForAxis(currentAxis, row: self, in: self.stackView!) else { 321 | // ignore, use autolayout in place for content view or if you have used views instead of controllers. 322 | // you need to set the heightAnchor in this case! 323 | return 324 | } 325 | 326 | switch bestSize { 327 | case .fixed(let value): 328 | setupRowToFixedValue(value) 329 | 330 | case .fitLayoutForAxis: 331 | setupRowSizeToFitLayout() 332 | } 333 | } 334 | 335 | private func setupRowToFixedValue(_ value: CGFloat) { 336 | guard let stackView = stackView, let contentView = contentView else { return } 337 | 338 | if stackView.axis == .vertical { 339 | contentView.width(constant: nil) 340 | contentView.height(constant: value) 341 | } else { 342 | contentView.width(constant: value) 343 | contentView.height(constant: nil) 344 | } 345 | } 346 | 347 | private func removeFixedDimensionConstraintsIfNeeded(_ contentView: UIView) { 348 | let fixedDimensions = contentView.constraints.filter({ 349 | $0.firstAttribute == .height || $0.firstAttribute == .width 350 | }) 351 | contentView.removeConstraints(fixedDimensions) 352 | } 353 | 354 | private func setupRowSizeToFitLayout() { 355 | guard let stackView = stackView, let contentView = contentView else { return } 356 | 357 | // If user changed the way of how the controller's view is resized 358 | // (ie from fixed height to auto-dimension) we should need to remove 359 | // attached constraints about height/width in order to leave the viww to 360 | // auto resize according to its content. 361 | removeFixedDimensionConstraintsIfNeeded(contentView) 362 | 363 | var bestSize: CGSize! 364 | if stackView.axis == .vertical { 365 | let maxAllowedSize = CGSize(width: contentView.bounds.width, height: 0) 366 | bestSize = contentView.systemLayoutSizeFitting( 367 | maxAllowedSize, 368 | withHorizontalFittingPriority: .required, 369 | verticalFittingPriority: .fittingSizeLevel 370 | ) 371 | } else { 372 | let maxAllowedSize = CGSize(width: 0, height: contentView.bounds.height) 373 | bestSize = contentView.systemLayoutSizeFitting( 374 | maxAllowedSize, 375 | withHorizontalFittingPriority: .fittingSizeLevel, 376 | verticalFittingPriority: .required 377 | ) 378 | } 379 | 380 | setupRowToFixedValue(bestSize.height) 381 | } 382 | 383 | 384 | // MARK: - Handle Touch 385 | 386 | public func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldReceive touch: UITouch) -> Bool { 387 | guard let view = gestureRecognizer.view else { 388 | return false 389 | } 390 | 391 | let location = touch.location(in: view) 392 | var hitView = view.hitTest(location, with: nil) 393 | 394 | // Traverse the chain of superviews looking for any UIControls. 395 | while hitView != view && hitView != nil { 396 | if hitView is UIControl { 397 | // Ensure UIControls get the touches instead of the tap gesture. 398 | return false 399 | } 400 | hitView = hitView?.superview 401 | } 402 | 403 | return true 404 | } 405 | 406 | open override func touchesBegan(_ touches: Set, with event: UIEvent?) { 407 | super.touchesBegan(touches, with: event) 408 | guard contentView?.isUserInteractionEnabled ?? false else { 409 | return 410 | } 411 | 412 | if let contentView = contentView as? ScrollStackRowHighlightable, 413 | contentView.isHighlightable { 414 | contentView.setIsHighlighted(true) 415 | } 416 | } 417 | 418 | open override func touchesMoved(_ touches: Set, with event: UIEvent?) { 419 | super.touchesMoved(touches, with: event) 420 | guard contentView?.isUserInteractionEnabled ?? false, let touch = touches.first else { 421 | return 422 | } 423 | 424 | let locationInSelf = touch.location(in: self) 425 | 426 | if let contentView = contentView as? ScrollStackRowHighlightable, 427 | contentView.isHighlightable { 428 | let isPointInsideCell = point(inside: locationInSelf, with: event) 429 | contentView.setIsHighlighted(isPointInsideCell) 430 | } 431 | } 432 | 433 | open override func touchesCancelled(_ touches: Set, with event: UIEvent?) { 434 | super.touchesCancelled(touches, with: event) 435 | guard contentView?.isUserInteractionEnabled ?? false else { 436 | return 437 | } 438 | 439 | if let contentView = contentView as? ScrollStackRowHighlightable, 440 | contentView.isHighlightable { 441 | contentView.setIsHighlighted(false) 442 | } 443 | } 444 | 445 | open override func touchesEnded(_ touches: Set, with event: UIEvent?) { 446 | super.touchesEnded(touches, with: event) 447 | guard contentView?.isUserInteractionEnabled ?? false else { 448 | return 449 | } 450 | 451 | if let contentView = contentView as? ScrollStackRowHighlightable, 452 | contentView.isHighlightable { 453 | contentView.setIsHighlighted(false) 454 | } 455 | } 456 | 457 | } 458 | 459 | // MARK: - ConstraintsHolder 460 | 461 | fileprivate class ConstraintsHolder { 462 | var top: NSLayoutConstraint? 463 | var left: NSLayoutConstraint? 464 | var bottom: NSLayoutConstraint? 465 | var right: NSLayoutConstraint? 466 | 467 | init(top: NSLayoutConstraint?, bottom: NSLayoutConstraint?, 468 | left: NSLayoutConstraint?, right: NSLayoutConstraint?) { 469 | self.top = top 470 | self.bottom = bottom 471 | self.left = left 472 | self.right = right 473 | } 474 | 475 | func activateAll() { 476 | [top, left, bottom, right].forEach { $0?.isActive = true } 477 | } 478 | 479 | func updateInsets(_ insets: UIEdgeInsets) { 480 | top?.constant = insets.top 481 | bottom?.constant = insets.bottom 482 | left?.constant = insets.left 483 | right?.constant = insets.right 484 | } 485 | 486 | } 487 | -------------------------------------------------------------------------------- /Sources/ScrollStackController/ScrollStackSeparator.swift: -------------------------------------------------------------------------------- 1 | /* 2 | * ScrollStackController 3 | * Create complex scrollable layout using UIViewController and simplify your code 4 | * 5 | * Created by: Daniele Margutti 6 | * Email: hello@danielemargutti.com 7 | * Web: http://www.danielemargutti.com 8 | * Twitter: @danielemargutti 9 | * 10 | * Copyright © 2019 Daniele Margutti 11 | * 12 | * 13 | * Permission is hereby granted, free of charge, to any person obtaining a copy 14 | * of this software and associated documentation files (the "Software"), to deal 15 | * in the Software without restriction, including without limitation the rights 16 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 17 | * copies of the Software, and to permit persons to whom the Software is 18 | * furnished to do so, subject to the following conditions: 19 | * 20 | * The above copyright notice and this permission notice shall be included in 21 | * all copies or substantial portions of the Software. 22 | * 23 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 24 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 25 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 26 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 27 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 28 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 29 | * THE SOFTWARE. 30 | * 31 | */ 32 | 33 | import UIKit 34 | 35 | public final class ScrollStackSeparator: UIView { 36 | 37 | internal init() { 38 | super.init(frame: .zero) 39 | setupUI() 40 | } 41 | 42 | required init?(coder: NSCoder) { 43 | fatalError("init(coder:) has not been implemented") 44 | } 45 | 46 | private func setupUI() { 47 | translatesAutoresizingMaskIntoConstraints = false 48 | 49 | setContentHuggingPriority(.defaultLow, for: .horizontal) 50 | setContentHuggingPriority(.defaultLow, for: .vertical) 51 | setContentCompressionResistancePriority(.defaultLow, for: .horizontal) 52 | setContentCompressionResistancePriority(.defaultLow, for: .vertical) 53 | } 54 | 55 | public override var intrinsicContentSize: CGSize { 56 | return CGSize(width: thickness, height: thickness) 57 | } 58 | 59 | public var color: UIColor { 60 | get { 61 | return backgroundColor ?? .clear 62 | } 63 | set { 64 | backgroundColor = newValue 65 | } 66 | } 67 | 68 | public var thickness: CGFloat = 1 { 69 | didSet { 70 | invalidateIntrinsicContentSize() 71 | } 72 | } 73 | 74 | } 75 | -------------------------------------------------------------------------------- /Sources/ScrollStackController/ScrollStackViewController.swift: -------------------------------------------------------------------------------- 1 | /* 2 | * ScrollStackController 3 | * Create complex scrollable layout using UIViewController and simplify your code 4 | * 5 | * Created by: Daniele Margutti 6 | * Email: hello@danielemargutti.com 7 | * Web: http://www.danielemargutti.com 8 | * Twitter: @danielemargutti 9 | * 10 | * Copyright © 2019 Daniele Margutti 11 | * 12 | * 13 | * Permission is hereby granted, free of charge, to any person obtaining a copy 14 | * of this software and associated documentation files (the "Software"), to deal 15 | * in the Software without restriction, including without limitation the rights 16 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 17 | * copies of the Software, and to permit persons to whom the Software is 18 | * furnished to do so, subject to the following conditions: 19 | * 20 | * The above copyright notice and this permission notice shall be included in 21 | * all copies or substantial portions of the Software. 22 | * 23 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 24 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 25 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 26 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 27 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 28 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 29 | * THE SOFTWARE. 30 | * 31 | */ 32 | 33 | import UIKit 34 | 35 | open class ScrollStackViewController: UIViewController { 36 | 37 | // MARK: Public Properties 38 | 39 | /// Inner stack view control. 40 | public let scrollStack = ScrollStack() 41 | 42 | /// Displays the scroll indicators momentarily. 43 | open var automaticallyFlashScrollIndicators = false 44 | 45 | // MARK: Init 46 | 47 | public init() { 48 | super.init(nibName: nil, bundle: nil) 49 | } 50 | 51 | public required init?(coder: NSCoder) { 52 | super.init(coder: coder) 53 | } 54 | 55 | // MARK: View Lifecycle 56 | 57 | open override func loadView() { 58 | view = scrollStack 59 | // monitor remove or add of a row to manage the view controller's hierarchy 60 | scrollStack.onChangeRow = { [weak self] (row, isRemoved) in 61 | guard let self = self else { 62 | return 63 | } 64 | self.didAddOrRemoveRow(row, isRemoved: isRemoved) 65 | } 66 | } 67 | 68 | open override func viewDidAppear(_ animated: Bool) { 69 | super.viewDidAppear(animated) 70 | 71 | if automaticallyFlashScrollIndicators { 72 | scrollStack.flashScrollIndicators() 73 | } 74 | } 75 | 76 | // MARK: - Private Functions 77 | 78 | private func didAddOrRemoveRow(_ row: ScrollStackRow, isRemoved: Bool) { 79 | guard let controller = row.controller else { 80 | return 81 | } 82 | 83 | if isRemoved { 84 | controller.removeFromParent() 85 | controller.didMove(toParent: nil) 86 | 87 | } else { 88 | self.addChild(controller) 89 | controller.didMove(toParent: self) 90 | } 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /Sources/ScrollStackController/Support/ScrollStack+Protocols.swift: -------------------------------------------------------------------------------- 1 | /* 2 | * ScrollStackController 3 | * Create complex scrollable layout using UIViewController and simplify your code 4 | * 5 | * Created by: Daniele Margutti 6 | * Email: hello@danielemargutti.com 7 | * Web: http://www.danielemargutti.com 8 | * Twitter: @danielemargutti 9 | * 10 | * Copyright © 2019 Daniele Margutti 11 | * 12 | * 13 | * Permission is hereby granted, free of charge, to any person obtaining a copy 14 | * of this software and associated documentation files (the "Software"), to deal 15 | * in the Software without restriction, including without limitation the rights 16 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 17 | * copies of the Software, and to permit persons to whom the Software is 18 | * furnished to do so, subject to the following conditions: 19 | * 20 | * The above copyright notice and this permission notice shall be included in 21 | * all copies or substantial portions of the Software. 22 | * 23 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 24 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 25 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 26 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 27 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 28 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 29 | * THE SOFTWARE. 30 | * 31 | */ 32 | 33 | import UIKit 34 | 35 | // MARK: - ScrollStackContainableController 36 | 37 | /// You can implement the following protocol in your view controller in order 38 | /// to specify explictely (without using autolayout constraints) the best size (width/height depending 39 | /// by the axis) of the controller when inside a scroll stack view. 40 | public protocol ScrollStackContainableController: UIViewController { 41 | 42 | /// If you implement this protocol you can manage the size of the controller 43 | /// when is placed inside a `ScrollStackView`. 44 | /// This method is also called when scroll stack change the orientation. 45 | /// You can return `nil` to leave the opportunity to change the size to the 46 | /// controller's view constraints. 47 | /// By default it returns `nil`. 48 | /// 49 | /// - Parameter axis: axis of the stackview. 50 | /// - Parameter row: row where the controller is placed. 51 | /// - Parameter stackView: stackview where the row is placed. 52 | func scrollStackRowSizeForAxis(_ axis: NSLayoutConstraint.Axis, row: ScrollStackRow, in stackView: ScrollStack) -> ScrollStack.ControllerSize? 53 | 54 | /// Method is called when you call a `reloadRow` function on a row where this controller is contained in. 55 | func reloadContentFromStackView(stackView: ScrollStack, row: ScrollStackRow, animated: Bool) 56 | 57 | } 58 | 59 | // MARK: - ScrollStackControllerDelegate 60 | 61 | /// You can implement the following delegate to receive events about row visibility changes during scroll of the stack. 62 | /// NOTE: No events are currently sent at the time of add/remove/move. A PR about is is accepted :-) 63 | public protocol ScrollStackControllerDelegate: AnyObject { 64 | 65 | /// Tells the delegate when the user scrolls the content view within the receiver. 66 | /// 67 | /// - Parameter stackView: target stack view. 68 | /// - Parameter offset: current scroll offset. 69 | func scrollStackDidScroll(_ stackView: ScrollStack, offset: CGPoint) 70 | 71 | /// Tells the delegate when a scrolling animation in the scroll view concludes. 72 | /// 73 | /// - Parameter stackView: The ScrollStack object that’s performing the scrolling animation. 74 | func scrollStackDidEndScrollingAnimation(_ stackView: ScrollStack) 75 | 76 | /// Row did become partially or entirely visible. 77 | /// 78 | /// - Parameter row: target row. 79 | /// - Parameter index: index of the row. 80 | /// - Parameter state: state of the row. 81 | func scrollStackRowDidBecomeVisible(_ stackView: ScrollStack, row: ScrollStackRow, index: Int, state: ScrollStack.RowVisibility) 82 | 83 | /// Row did become partially or entirely invisible. 84 | /// 85 | /// - Parameter row: target row. 86 | /// - Parameter index: index of the row. 87 | /// - Parameter state: state of the row. 88 | func scrollStackRowDidBecomeHidden(_ stackView: ScrollStack, row: ScrollStackRow, index: Int, state: ScrollStack.RowVisibility) 89 | 90 | 91 | /// This function is called when layout is updated (added, removed, hide or show one or more rows). 92 | /// - Parameter stackView: target stack view. 93 | func scrollStackDidUpdateLayout(_ stackView: ScrollStack) 94 | 95 | /// This function is called when content size of the stack did change (remove/add, hide/show rows). 96 | /// 97 | /// - Parameters: 98 | /// - stackView: target stack view 99 | /// - oldValue: old content size. 100 | /// - newValue: new content size. 101 | func scrollStackContentSizeDidChange(_ stackView: ScrollStack, from oldValue: CGSize, to newValue: CGSize) 102 | 103 | } 104 | 105 | // MARK: - ScrollStackRowHighlightable 106 | 107 | /// Indicates that a row into the stackview should be highlighted when the user touches it. 108 | public protocol ScrollStackRowHighlightable { 109 | 110 | /// Checked when the user touches down on a row to determine if the row should be highlighted. 111 | /// 112 | /// The default implementation of this method always returns `true`. 113 | var isHighlightable: Bool { get } 114 | 115 | /// Called when the highlighted state of the row changes. 116 | /// Override this method to provide custom highlighting behavior for the row. 117 | /// 118 | /// The default implementation of this method changes the background color of the row to the `rowHighlightColor`. 119 | func setIsHighlighted(_ isHighlighted: Bool) 120 | 121 | } 122 | 123 | extension ScrollStackRowHighlightable where Self: UIView { 124 | 125 | public var isHighlightable: Bool { 126 | return true 127 | } 128 | 129 | public func setIsHighlighted(_ isHighlighted: Bool) { 130 | guard let row = superview as? ScrollStackRow else { 131 | return 132 | } 133 | row.backgroundColor = (isHighlighted ? row.rowHighlightColor : row.rowBackgroundColor) 134 | } 135 | 136 | } 137 | 138 | 139 | // MARK: - ScrollStack 140 | 141 | public extension ScrollStack { 142 | 143 | /// Define the controller size. 144 | /// - `fixed`: fixed size in points. 145 | /// - `fitLayoutForAxis`: attempt to size the controller to fits its content set with autolayout. 146 | enum ControllerSize { 147 | case fixed(CGFloat) 148 | case fitLayoutForAxis 149 | } 150 | 151 | /// Insertion of the new row. 152 | /// - `top`: insert row at the top of the stack. 153 | /// - `bottom`: append the row at the end of the stack rows. 154 | /// - `atIndex`: insert at specified index. If index is invalid nothing happens. 155 | /// - `after`: insert after the location of specified row. 156 | /// - `before`: insert before the location of the specified row. 157 | enum InsertLocation { 158 | case top 159 | case bottom 160 | case atIndex(Int) 161 | case afterView(UIView) 162 | case beforeView(UIView) 163 | case after(UIViewController) 164 | case before(UIViewController) 165 | } 166 | 167 | /// Scrolling position 168 | /// - `middle`: row is in the middle x/y of the container when possible. 169 | /// - `final`: row left/top side is aligned to the left/top anchor of the container when possible. 170 | /// - `final`: row right/top side is aligned to the right/top anchor of the container when possible. 171 | /// - `automatic`: row is aligned automatically. 172 | enum ScrollPosition { 173 | case middle 174 | case final 175 | case initial 176 | case automatic 177 | } 178 | 179 | /// Row visibility 180 | /// - `partial`: row is partially visible. 181 | /// - `entire`: row is entirely visible. 182 | /// - `hidden`: row is invisible and hidden. 183 | /// - `offscreen`: row is not hidden but currently offscreen due to scroll position. 184 | /// - `removed`: row is removed manually. 185 | enum RowVisibility: Equatable { 186 | case hidden 187 | case partial(percentage: Double) 188 | case entire 189 | case offscreen 190 | case removed 191 | 192 | /// Return if row is visible. 193 | public var isVisible: Bool { 194 | switch self { 195 | case .hidden, .offscreen, .removed: 196 | return false 197 | default: 198 | return true 199 | } 200 | } 201 | } 202 | 203 | } 204 | -------------------------------------------------------------------------------- /Sources/ScrollStackController/Support/ScrollStackRowAnimator.swift: -------------------------------------------------------------------------------- 1 | /* 2 | * ScrollStackController 3 | * Create complex scrollable layout using UIViewController and simplify your code 4 | * 5 | * Created by: Daniele Margutti 6 | * Email: hello@danielemargutti.com 7 | * Web: http://www.danielemargutti.com 8 | * Twitter: @danielemargutti 9 | * 10 | * Copyright © 2019 Daniele Margutti 11 | * 12 | * 13 | * Permission is hereby granted, free of charge, to any person obtaining a copy 14 | * of this software and associated documentation files (the "Software"), to deal 15 | * in the Software without restriction, including without limitation the rights 16 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 17 | * copies of the Software, and to permit persons to whom the Software is 18 | * furnished to do so, subject to the following conditions: 19 | * 20 | * The above copyright notice and this permission notice shall be included in 21 | * all copies or substantial portions of the Software. 22 | * 23 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 24 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 25 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 26 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 27 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 28 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 29 | * THE SOFTWARE. 30 | * 31 | */ 32 | 33 | import UIKit 34 | 35 | // MARK: - ScrollStackRowAnimatable 36 | 37 | public protocol ScrollStackRowAnimatable { 38 | 39 | /// Animation main info. 40 | var animationInfo: ScrollStackAnimationInfo { get } 41 | 42 | /// Animation will start to hide or show the row. 43 | /// - Parameter toHide: hide or show transition. 44 | func willBeginAnimationTransition(toHide: Bool) 45 | 46 | /// Animation to hide/show the row did end. 47 | /// - Parameter toHide: hide or show transition. 48 | func didEndAnimationTransition(toHide: Bool) 49 | 50 | /// Animation transition. 51 | /// - Parameter toHide: hide or show transition. 52 | func animateTransition(toHide: Bool) 53 | 54 | } 55 | 56 | // MARK: - ScrollStackRowAnimatable Extension 57 | 58 | public extension ScrollStackRowAnimatable where Self: UIViewController { 59 | 60 | var animationInfo: ScrollStackAnimationInfo { 61 | return ScrollStackAnimationInfo() 62 | } 63 | 64 | func animateTransition(toHide: Bool) { 65 | 66 | } 67 | 68 | func willBeginAnimationTransition(toHide: Bool) { 69 | 70 | } 71 | 72 | func didEndAnimationTransition(toHide: Bool) { 73 | 74 | } 75 | 76 | } 77 | 78 | 79 | // MARK: - ScrollStackAnimationInfo 80 | 81 | public struct ScrollStackAnimationInfo { 82 | 83 | /// Duration of the animation. By default is set to `0.25`. 84 | var duration: TimeInterval 85 | 86 | /// Delay before start animation. 87 | var delay: TimeInterval 88 | 89 | /// The springDamping value used to determine the amount of `bounce`. 90 | /// Default Value is `0.8`. 91 | var springDamping: CGFloat 92 | 93 | public init(duration: TimeInterval = 0.25, delay: TimeInterval = 0, springDamping: CGFloat = 0.8) { 94 | self.duration = duration 95 | self.delay = delay 96 | self.springDamping = springDamping 97 | } 98 | 99 | } 100 | 101 | // MARK: - ScrollStackRowAnimator 102 | 103 | internal class ScrollStackRowAnimator { 104 | 105 | /// Row to animate. 106 | private let targetRow: ScrollStackRow 107 | 108 | /// Final state after animation, hidden or not. 109 | private let toHidden: Bool 110 | 111 | /// Animation handler, used to perform actions for animation in `ScrollStack`. 112 | private let internalHandler: () -> Void 113 | 114 | /// Completion handler. 115 | private let completion: ((Bool) -> Void)? 116 | 117 | /// Target row if animatable. 118 | private var animatableRow: ScrollStackRowAnimatable? { 119 | return (targetRow.controller as? ScrollStackRowAnimatable) ?? (targetRow.contentView as? ScrollStackRowAnimatable) 120 | } 121 | 122 | // MARK: - Initialization 123 | 124 | init(row: ScrollStackRow, toHidden: Bool, 125 | internalHandler: @escaping () -> Void, completion: ((Bool) -> Void)? = nil) { 126 | self.targetRow = row 127 | self.toHidden = toHidden 128 | self.internalHandler = internalHandler 129 | self.completion = completion 130 | } 131 | 132 | /// Execute animation. 133 | func execute() { 134 | animatableRow?.willBeginAnimationTransition(toHide: toHidden) 135 | 136 | let duration = animatableRow?.animationInfo.duration ?? 0.25 137 | UIView.animate(withDuration: duration, 138 | delay: 0, 139 | usingSpringWithDamping: animatableRow?.animationInfo.springDamping ?? 1, 140 | initialSpringVelocity: 0, 141 | options: [.curveEaseInOut, .allowUserInteraction, .beginFromCurrentState], 142 | animations: { 143 | self.animatableRow?.animateTransition(toHide: self.toHidden) 144 | self.internalHandler() 145 | }) { finished in 146 | self.animatableRow?.didEndAnimationTransition(toHide: self.toHidden) 147 | self.completion?(finished) 148 | } 149 | } 150 | 151 | } 152 | -------------------------------------------------------------------------------- /Sources/ScrollStackController/Support/UIView+AutoLayout_Extensions.swift: -------------------------------------------------------------------------------- 1 | /* 2 | * ScrollStackController 3 | * Create complex scrollable layout using UIViewController and simplify your code 4 | * 5 | * Created by: Daniele Margutti 6 | * Email: hello@danielemargutti.com 7 | * Web: http://www.danielemargutti.com 8 | * Twitter: @danielemargutti 9 | * 10 | * Copyright © 2019 Daniele Margutti 11 | * 12 | * 13 | * Permission is hereby granted, free of charge, to any person obtaining a copy 14 | * of this software and associated documentation files (the "Software"), to deal 15 | * in the Software without restriction, including without limitation the rights 16 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 17 | * copies of the Software, and to permit persons to whom the Software is 18 | * furnished to do so, subject to the following conditions: 19 | * 20 | * The above copyright notice and this permission notice shall be included in 21 | * all copies or substantial portions of the Software. 22 | * 23 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 24 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 25 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 26 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 27 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 28 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 29 | * THE SOFTWARE. 30 | * 31 | */ 32 | 33 | import UIKit 34 | 35 | extension UIView { 36 | 37 | public func height(constant: CGFloat?) { 38 | setConstraint(value: constant, attribute: .height) 39 | } 40 | 41 | public func width(constant: CGFloat?) { 42 | setConstraint(value: constant, attribute: .width) 43 | } 44 | 45 | private func removeConstraint(attribute: NSLayoutConstraint.Attribute) { 46 | constraints.forEach { 47 | if $0.firstAttribute == attribute { 48 | removeConstraint($0) 49 | } 50 | } 51 | } 52 | 53 | private func setConstraint(value: CGFloat?, attribute: NSLayoutConstraint.Attribute) { 54 | removeConstraint(attribute: attribute) 55 | if let value = value { 56 | let constraint = 57 | NSLayoutConstraint(item: self, 58 | attribute: attribute, 59 | relatedBy: NSLayoutConstraint.Relation.equal, 60 | toItem: nil, 61 | attribute: NSLayoutConstraint.Attribute.notAnAttribute, 62 | multiplier: 1, 63 | constant: value) 64 | self.addConstraint(constraint) 65 | } 66 | } 67 | 68 | public static func execute(animated: Bool = true, _ callback: @escaping (() -> Void), completion: (() -> Void)? = nil) { 69 | guard animated else { 70 | callback() 71 | completion?() 72 | return 73 | } 74 | 75 | UIView.animate(withDuration: 0.3, animations: callback) { isFinished in 76 | if isFinished { 77 | completion?() 78 | } 79 | } 80 | } 81 | 82 | } 83 | -------------------------------------------------------------------------------- /Tests/LinuxMain.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import ScrollStackControllerTests 3 | 4 | XCTMain([ 5 | testCase(ScrollStackControllerTests.allTests), 6 | ]) 7 | --------------------------------------------------------------------------------