├── .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 |
5 |
6 |
7 |
8 | [](https://img.shields.io/badge/Swift-5.3_5.4_5.5_5.6-Orange?style=flat-square)
9 | [](#installation)
10 | [](https://img.shields.io/badge/Swift_Package_Manager-compatible-orange?style=flat-square)
11 | [](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 | 
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 | 
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 | 
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 |
--------------------------------------------------------------------------------