├── .gitignore
├── .swift-version
├── LICENSE.md
├── README.md
├── Screenshots
├── cards_stack.gif
├── events_feed_stack.gif
├── item_title_custom.jpg
├── item_title_none.jpg
├── item_title_standard.jpg
├── paging_stack.gif
├── stack_of_stacks.gif
└── two_stacks.gif
├── StackFlowView.podspec
├── StackFlowView
├── StackFlow.swift
├── StackFlowView.swift
└── StackItemView.swift
└── StackFlowViewDemo
├── StackFlowViewDemo.xcodeproj
├── project.pbxproj
└── project.xcworkspace
│ └── contents.xcworkspacedata
└── StackFlowViewDemo
├── Controllers
└── ViewController.swift
├── Extensions
├── Foundation+Extensions.swift
├── SwiftLibrary+Extentions.swift
└── UIKit+Extensions.swift
├── Misc
├── AppDelegate.swift
├── Defines.swift
└── Info.plist
├── Res
├── Assets.xcassets
│ ├── AppIcon.appiconset
│ │ └── Contents.json
│ ├── Contents.json
│ ├── back.imageset
│ │ ├── Contents.json
│ │ ├── back-1.png
│ │ ├── back-2.png
│ │ └── back.png
│ └── fwd.imageset
│ │ ├── Contents.json
│ │ ├── fwd-1.png
│ │ ├── fwd-2.png
│ │ └── fwd.png
└── UI
│ └── bg.jpg
├── Utils
└── Utils.swift
└── Views
├── CustomStackView
└── CustomStackFlowView.swift
├── DemoItemView
├── DemoItemView.swift
└── DemoItemView.xib
└── Storyboards
└── Base.lproj
├── LaunchScreen.storyboard
└── Main.storyboard
/.gitignore:
--------------------------------------------------------------------------------
1 | *xcuser*
--------------------------------------------------------------------------------
/.swift-version:
--------------------------------------------------------------------------------
1 | 4.0
2 |
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2017 0xNSHuman
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
StackFlowView
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 | 📥 Build custom UI flow using stack order 📤
11 |
12 |
13 | 🔗 Enforce sequential user interaction 🔗
14 |
15 |
16 | 🗂 Focus user attention on one flow step at a time 🗂
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 | ## How does it work?
32 |
33 | **StackFlowView** is a high-level view capable of hosting a collection of custom `UIView`s. Which is, well, not unique behaviour.. The special thing about it though, is that it *enforces stack flow* behaviour (as the name suggests), which means:
34 | 1. Only the last view in stack allows user interaction. There is no way to affect past or future state of the UI flow;
35 |
36 | 2. No view properties can be pre-determined until the moment before putting one into stack (**push** action). This way, every next stack item considers previous state and can be adjusted to reflect particular flow step;
37 |
38 | 3. It is not possible to go **N** items back without dismissing/destroying those items (**pop** action). This way, going back in time and changing state enforces subsequent flow steps to be revisited.
39 |
40 | > During development, various state-dependent UX cases were kept in mind. For example, this solution perfectly works for all kinds of dynamic input forms where every next set of options depends on previous choices made by user.
41 |
42 | ## Installation
43 | ### CocoaPods
44 | 1. Add `pod 'StackFlowView'` to your `Podfile`;
45 | 2. Run `pod install` or `pod update` in Terminal;
46 | 3. Re-open your project using `.xcworkspace`, put `import StackFlowView` in the swift files you plan to use stack flow in (or use bridging in Obj-C projects);
47 | 4. Rebuild and enjoy.
48 |
49 | ### Old School Way
50 | Drop folder with `.swift` source files to your project and you're done.
51 |
52 | ## Usage
53 |
54 | ### Creation
55 |
56 | Creating `StackFlowView` takes a few lines of code. Basically, you need to:
57 | - Initialize it with any frame (not necessery);
58 | - Add it to superview;
59 | - Set delegate;
60 | - Optionally set up constraints if you want to enjoy autolayout-ready behaviour;
61 |
62 | ```Swift
63 | let stackView = StackFlowView() // StackFlowView(frame: ...)
64 | stackView.delegate = self
65 |
66 | view.addSubview(stackView)
67 |
68 | /* — Optional constraints — */
69 |
70 | ([
71 | NSLayoutConstraint(item: stackView, attribute: .top, relatedBy: .equal, toItem: view, attribute: .top, multiplier: 1.0, constant: 0),
72 | NSLayoutConstraint(item: stackView, attribute: .bottom, relatedBy: .equal, toItem: view, attribute: .bottom, multiplier: 1.0, constant: 0),
73 | NSLayoutConstraint(item: stackView, attribute: .leading, relatedBy: .equal, toItem: view, attribute: .leading, multiplier: 1.0, constant: 0),
74 | NSLayoutConstraint(item: stackView, attribute: .trailing, relatedBy: .equal, toItem: view, attribute: .trailing, multiplier: 1.0, constant: 0)
75 | ]).forEach { $0.isActive = true }
76 |
77 | view.setNeedsLayout()
78 | view.layoutIfNeeded()
79 | ```
80 |
81 | ### Customization
82 |
83 | There are some nice options to define desired behaviour of your stack flow, including its direction of growth, the way it separates items, gestures user can use to move back and forth, and more. Please see the comments below, as well as property references in Xcode.
84 |
85 | ```Swift
86 | // How big should padding next to the stack's last item be?
87 | stackView.headPadding = 0
88 |
89 | // Which direction should new items be pushed in?
90 | stackView.growthDirection = .down
91 |
92 | // Separate by lines, padding or nothing at all?
93 | stackView.separationStyle = .line(thikness: 2.0, color: .black)
94 | // .padding(size: 20.0)
95 | // .none
96 |
97 | // If you want your stack gradually fade away, you can pick any of the styles, or combine them!
98 | stackView.fadingStyle = .combined(styles:
99 | [
100 | .tint(color: .white, preLastAlpha: 0.9, alphaDecrement: 0.1),
101 | .gradientMask(effectDistance: stackView.bounds.height * 0.7)
102 | ]
103 | ) // Or just .none
104 |
105 | // You can swipe up-down or left-right to control your flow, and/or also tap inactive stack area to pop any number of items (depends on where you tap)
106 | stackView.userNavigationOptions = [.swipe, .tap]
107 |
108 | // Fast hops or sloooooow animation?
109 | stackView.transitionDuration = 0.25
110 |
111 | // Set to false if you don't need automatic safe area detection/adoption
112 | stackView.isSeekingSafeArea = true
113 |
114 | // Set to false to turn off stretch-out behaviour for your content items during autolayout updates
115 | stackView.isAutoresizingItems = true
116 | ```
117 |
118 | ### Push and Pop items
119 |
120 | > **NOTE**: There is no views reusability mechanism in current version, so whatever you push to `StackFlowView` increments memory usage until you pop it. Therefore, the weak place of this library is a large number of flow steps. It's in TODO list to address this feature.
121 |
122 | #### Push
123 |
124 | There is only one straight-forward to use method to push your view into Stack Flow, but it lets you customize things to the extent you want.
125 |
126 | You can stick to using the same style for all items, or use custom approach for each one.
127 |
128 | ###### Just push the view itself
129 |
130 | Send the view to stack without showing anything else.
131 |
132 |
133 |
134 |
135 |
136 | ```Swift
137 | stackView.push(myCustomView)
138 | ```
139 |
140 | ###### Push with header bar (standard appearance)
141 |
142 | Display item's title along with standard-looking buttons navigation, which is a good alternative to gestures in case you need it (for example, as an Accessibility option for users).
143 |
144 |
145 |
146 |
147 |
148 | ```Swift
149 | stackView.push(myCustomView, title: "Step ♦️")
150 | ```
151 |
152 | ###### Push with customised header bar
153 |
154 | Define custom appearance for the item's container, including its header bar background color, title font and color, and navigation buttons appearance.
155 |
156 |
157 |
158 |
159 |
160 | ```Swift
161 | // Define custom top bar appearance
162 |
163 | let topBarAppearance: StackItemAppearance.TopBar = {
164 | let popButtonAppearance: StackItemAppearance.TopBar.Button
165 | let pushButtonAppearance: StackItemAppearance.TopBar.Button
166 |
167 | // You can use images or attributed text for navigation buttons
168 |
169 | let preferIconsOverText = false
170 |
171 | if preferIconsOverText { // Use icons
172 | popButtonAppearance = StackItemAppearance.TopBar.Button(icon: UIImage(named: "back")!)
173 | pushButtonAppearance = StackItemAppearance.TopBar.Button(icon: UIImage(named: "forth")!)
174 | } else { // Use text
175 | let popButtonTitle = NSAttributedString(string: "♦️⬅️", attributes: [.foregroundColor : UIColor.blue])
176 | popButtonAppearance = StackItemAppearance.TopBar.Button(title: popButtonTitle)
177 |
178 | let pushButtonTitle = NSAttributedString(string: "➡️💎", attributes: [.foregroundColor : UIColor.blue])
179 | pushButtonAppearance = StackItemAppearance.TopBar.Button(title: pushButtonTitle)
180 | }
181 |
182 | let customBarAppearance = StackItemAppearance.TopBar(backgroundColor: Utils.randomPastelColor(), titleFont: .italicSystemFont(ofSize: 17.0), titleTextColor: .white, popButtonIdentity: popButtonAppearance, pushButtonIdentity: pushButtonAppearance)
183 |
184 | return customBarAppearance
185 | }()
186 |
187 | // Set appearence for the whole item, including previously created top bar appearance
188 |
189 | let customAppearance = StackItemAppearance(backgroundColor: Utils.randomPastelColor(), topBarAppearance: topBarAppearance)
190 |
191 | // Push it all to the stack!
192 |
193 | stackView.push(myCustomView, title: "Step ♦️", customAppearance: customAppearance)
194 | ```
195 |
196 | #### Pop
197 |
198 | Pop N items from stack by calling one of the `pop(_:)` method variations.
199 |
200 | ###### Pop one item
201 |
202 | ```Swift
203 | stackView.pop()
204 | ```
205 |
206 | ###### Pop multiple items
207 |
208 | ```Swift
209 | stackView.pop(numberOfItems)
210 | ```
211 |
212 | ### Delegate methods
213 |
214 | `StackFlowDelegate` protocol enables control over stack flow by the object implementing it. For example, it delivers **push** and **pop** intention events triggered by user gestures, and lets you decide if StackFlowView should proceed or ignore this action. It also reports about the corresponding actions that are upcoming or just passed.
215 |
216 | ```Swift
217 | func stackFlowViewDidRequestPop(_ stackView: StackFlowView, numberOfItems: Int) {
218 | log(message: "Requested to go \(numberOfItems) steps back", from: self)
219 | stackView.pop(numberOfItems)
220 | }
221 |
222 | func stackFlowViewDidRequestPush(_ stackView: StackFlowView) {
223 | log(message: "Requested next item", from: self)
224 | goToNextStep()
225 | }
226 |
227 | func stackFlowViewWillPop(_ stackView: StackFlowView) {
228 | log(message: "About to go one item back", from: self)
229 | }
230 |
231 | func stackFlowViewDidPop(_ stackView: StackFlowView) {
232 | log(message: "Went one item back", from: self)
233 | }
234 |
235 | func stackFlowView(_ stackView: StackFlowView, willPush view: UIView) {
236 | log(message: "About to to go to the next step", from: self)
237 | }
238 |
239 | func stackFlowView(_ stackView: StackFlowView, didPush view: UIView) {
240 | log(message: "Went to next step with view: \(view)", from: self)
241 | }
242 | ```
243 |
244 | ## [Optional] Simplest flow logic example
245 |
246 | ```Swift
247 | class MyFlowController: UIViewController {
248 | // MARK: - Flow definition -
249 |
250 | enum MyFlowStep: Int {
251 | case none = -1
252 | case one = 0, two, three, four
253 |
254 | static var count: Int { return 4 }
255 |
256 | var title: String {
257 | switch self {
258 | default:
259 | return "Step \(shortSymbol)"
260 | }
261 | }
262 |
263 | var shortSymbol: String {
264 | switch self {
265 | case .one:
266 | return "♦️"
267 |
268 | case .two:
269 | return "♠️"
270 |
271 | case .three:
272 | return "💎"
273 |
274 | case .four:
275 | return "🔮"
276 |
277 | case .none:
278 | return "❌"
279 | }
280 | }
281 |
282 | var prevStep: FlowStep? {
283 | let prevValue = rawValue - 1
284 | return prevValue >= 0 ? FlowStep(rawValue: prevValue) : nil
285 | }
286 |
287 | var nextStep: FlowStep? {
288 | let nextValue = rawValue + 1
289 | return nextValue < FlowStep.count ? FlowStep(rawValue: nextValue) : nil
290 | }
291 | }
292 |
293 | // MARK: - Properties -
294 |
295 | private let stackView = StackFlowView()
296 |
297 | /* — Whenever this property is set, you can prepare the next view to push — */
298 |
299 | private var currentStep: MyFlowStep = .none {
300 | didSet {
301 | // Get identity of the current step
302 |
303 | let itemTitle = currentStep.title
304 |
305 | // You can optionall use bounding steps' identity for something like setting custom navigation buttons
306 |
307 | let prevItemSymbol = currentStep.prevStep?.shortSymbol
308 | let nextItemSymbol = currentStep.nextStep?.shortSymbol
309 |
310 | // Here you should construct your custom UIView considering purposes of this particular step
311 |
312 | let itemView = stepView(for: currentStep)
313 |
314 | // Now you can push your custom view using superclass `push()` method!
315 |
316 | stackView.push(itemView, title: itemTitle)
317 | }
318 | }
319 |
320 | // MARK: - View constructor -
321 |
322 | private func stepView(for step: MyFlowStep) -> UIView {
323 | let stepView: UIView
324 |
325 | // Note this `safeSize` property of StackFlowView. You should use it to get info about its available content area, not blocked by any views outside of safe area
326 |
327 | let safeStackFlowViewWidth = stackView.safeSize.width
328 |
329 | // Build custom view for any given step
330 |
331 | switch step {
332 | case .one:
333 | stepView = UIView(frame: CGRect(x: 0, y: 0, width: safeStackFlowViewWidth, height: 100.0))
334 |
335 | case .two:
336 | stepView = UIView(frame: CGRect(x: 0, y: 0, width: safeStackFlowViewWidth, height: 200.0))
337 |
338 | default:
339 | stepView = UIView(frame: CGRect(x: 0, y: 0, width: safeStackFlowViewWidth, height: 300.0))
340 | }
341 |
342 | return stepView
343 | }
344 | }
345 | ```
346 |
347 | ## TODO
348 |
349 | - [ ] Think about views reusability mechanism
350 |
351 | ## License
352 | StackFlowView is released under an MIT license. See the [LICENSE](https://github.com/0xNSHuman/StackFlowView/blob/master/LICENSE.md) file.
353 |
--------------------------------------------------------------------------------
/Screenshots/cards_stack.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/0xNSHuman/StackFlowView/60032156606bab7b24dc935348b2afce49902df1/Screenshots/cards_stack.gif
--------------------------------------------------------------------------------
/Screenshots/events_feed_stack.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/0xNSHuman/StackFlowView/60032156606bab7b24dc935348b2afce49902df1/Screenshots/events_feed_stack.gif
--------------------------------------------------------------------------------
/Screenshots/item_title_custom.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/0xNSHuman/StackFlowView/60032156606bab7b24dc935348b2afce49902df1/Screenshots/item_title_custom.jpg
--------------------------------------------------------------------------------
/Screenshots/item_title_none.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/0xNSHuman/StackFlowView/60032156606bab7b24dc935348b2afce49902df1/Screenshots/item_title_none.jpg
--------------------------------------------------------------------------------
/Screenshots/item_title_standard.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/0xNSHuman/StackFlowView/60032156606bab7b24dc935348b2afce49902df1/Screenshots/item_title_standard.jpg
--------------------------------------------------------------------------------
/Screenshots/paging_stack.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/0xNSHuman/StackFlowView/60032156606bab7b24dc935348b2afce49902df1/Screenshots/paging_stack.gif
--------------------------------------------------------------------------------
/Screenshots/stack_of_stacks.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/0xNSHuman/StackFlowView/60032156606bab7b24dc935348b2afce49902df1/Screenshots/stack_of_stacks.gif
--------------------------------------------------------------------------------
/Screenshots/two_stacks.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/0xNSHuman/StackFlowView/60032156606bab7b24dc935348b2afce49902df1/Screenshots/two_stacks.gif
--------------------------------------------------------------------------------
/StackFlowView.podspec:
--------------------------------------------------------------------------------
1 | Pod::Spec.new do |s|
2 |
3 | s.name = "StackFlowView"
4 | s.version = "1.0.2"
5 | s.summary = "Enforcing stack order for custom UI elements"
6 |
7 | s.homepage = "https://github.com/0xNSHuman/StackFlowView"
8 | s.screenshots = "https://raw.githubusercontent.com/0xNSHuman/StackFlowView/master/Screenshots/two_stacks.gif", "https://github.com/0xNSHuman/StackFlowView/raw/master/Screenshots/cards_stack.gif", "https://raw.githubusercontent.com/0xNSHuman/StackFlowView/master/Screenshots/stack_of_stacks.gif"
9 |
10 | s.license = { :type => "MIT", :file => "LICENSE.md" }
11 |
12 | s.platform = :ios
13 | s.ios.deployment_target = "8.0"
14 |
15 | s.source = { :git => "https://github.com/0xNSHuman/StackFlowView.git", :tag => "v1.0.2" }
16 |
17 | s.source_files = "StackFlowView", "StackFlowView/**/*.swift"
18 |
19 | s.frameworks = "UIKit"
20 |
21 | s.requires_arc = true
22 | s.pod_target_xcconfig = { 'OTHER_SWIFT_FLAGS[config=Debug]' => '-D DEBUG' }
23 |
24 | end
25 |
--------------------------------------------------------------------------------
/StackFlowView/StackFlow.swift:
--------------------------------------------------------------------------------
1 | //
2 | // StackFlow.swift
3 | // Created by 0xNSHuman on 11/12/2017.
4 |
5 | /*
6 | The MIT License (MIT)
7 | Copyright © 2017 0xNSHuman
8 | Permission is hereby granted, free of charge, to any person obtaining a copy
9 | of this software and associated documentation files (the "Software"), to deal
10 | in the Software without restriction, including without limitation the rights
11 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
12 | copies of the Software, and to permit persons to whom the Software is
13 | furnished to do so, subject to the following conditions:
14 | The above copyright notice and this permission notice shall be included in all
15 | copies or substantial portions of the Software.
16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 | SOFTWARE.
23 | */
24 |
25 | import Foundation
26 | import UIKit
27 |
28 | // MARK: - Stack item appearance -
29 |
30 | /// Appearance description, typically assigned to newly pushed stack flow item when you want to customize its look.
31 |
32 | public struct StackItemAppearance {
33 | static var defaultPreset: StackItemAppearance {
34 | let popButtonTitle = NSAttributedString(string: "prev", attributes: [.foregroundColor : UIColor.blue])
35 | let pushButtonTitle = NSAttributedString(string: "next", attributes: [.foregroundColor : UIColor.blue])
36 |
37 | let topBarAppearance = StackItemAppearance.TopBar(backgroundColor: .white, titleFont: UIFont.systemFont(ofSize: UIFont.systemFontSize), titleTextColor: .darkGray, popButtonIdentity: TopBar.Button(title: popButtonTitle), pushButtonIdentity: TopBar.Button(title: pushButtonTitle))
38 |
39 | return StackItemAppearance(backgroundColor: .clear, topBarAppearance: topBarAppearance)
40 | }
41 |
42 | /// Appearance description for stack flow item's top (navigation) bar.
43 |
44 | public struct TopBar {
45 | public struct Button {
46 | let icon: UIImage?
47 | let title: NSAttributedString?
48 |
49 | public init(icon: UIImage) {
50 | self.icon = icon
51 | self.title = nil
52 | }
53 |
54 | public init(title: NSAttributedString) {
55 | self.title = title
56 | self.icon = nil
57 | }
58 | }
59 |
60 | let backgroundColor: UIColor
61 | let titleFont: UIFont
62 | let titleTextColor: UIColor
63 | let popButtonIdentity: Button
64 | let pushButtonIdentity: Button
65 |
66 | public init(backgroundColor: UIColor, titleFont: UIFont, titleTextColor: UIColor, popButtonIdentity: Button, pushButtonIdentity: Button) {
67 |
68 | self.backgroundColor = backgroundColor
69 | self.titleFont = titleFont
70 | self.titleTextColor = titleTextColor
71 | self.popButtonIdentity = popButtonIdentity
72 | self.pushButtonIdentity = pushButtonIdentity
73 | }
74 | }
75 |
76 | let backgroundColor: UIColor
77 | let topBarAppearance: TopBar
78 |
79 | public init(backgroundColor: UIColor, topBarAppearance: TopBar? = nil) {
80 | self.backgroundColor = backgroundColor
81 | self.topBarAppearance = topBarAppearance ?? StackItemAppearance.defaultPreset.topBarAppearance
82 | }
83 | }
84 |
85 | // MARK: - Internal notifications -
86 |
87 | /// Internally used events wrapper. Intended for inter-module use only, at least with its current implementation.
88 |
89 | struct StackFlowNotification {
90 | // MARK: - Errors -
91 |
92 | enum StackFlowNotificationError: Error {
93 | case parsingFailed
94 | }
95 |
96 | // MARK: - Notification names -
97 |
98 | enum Name: String {
99 | case itemPushed = "stackFlowViewItemPushed"
100 | case itemPopped = "stackFlowViewItemPopped"
101 | }
102 |
103 | // MARK: - Stored values -
104 |
105 | private let obj: Notification
106 | let name: Name
107 | let stackFlowView: StackFlowView
108 |
109 | // MARK: - Initializers -
110 |
111 | private init(name: Name, stackView: StackFlowView) {
112 | self.name = name
113 | self.stackFlowView = stackView
114 | self.obj = Notification(name: Notification.Name(rawValue: name.rawValue), object: stackView, userInfo: nil)
115 | }
116 |
117 | init(plainNotification: Notification) throws {
118 | guard let name = Name(rawValue: plainNotification.name.rawValue), let stackView = plainNotification.object as? StackFlowView else {
119 | throw StackFlowNotificationError.parsingFailed
120 | }
121 |
122 | self.name = name
123 | self.stackFlowView = stackView
124 | self.obj = plainNotification
125 | }
126 |
127 | // MARK: - Post -
128 |
129 | static func post(name: Name, stackView: StackFlowView) {
130 | let notification = StackFlowNotification(name: name, stackView: stackView)
131 | NotificationCenter.default.post(notification.obj)
132 | }
133 |
134 | // MARK: - Subscribe -
135 |
136 | static func observe(name: Name, by observer: Any, selector: Selector) {
137 | NotificationCenter.default.addObserver(observer, selector: selector, name: Notification.Name(rawValue: name.rawValue), object: nil)
138 | }
139 |
140 | static func forget(observer: Any) {
141 | NotificationCenter.default.removeObserver(observer)
142 | }
143 | }
144 |
--------------------------------------------------------------------------------
/StackFlowView/StackFlowView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // StackFlowView.swift
3 | // Created by 0xNSHuman on 05/12/2017.
4 |
5 | /*
6 | The MIT License (MIT)
7 | Copyright © 2017 0xNSHuman
8 | Permission is hereby granted, free of charge, to any person obtaining a copy
9 | of this software and associated documentation files (the "Software"), to deal
10 | in the Software without restriction, including without limitation the rights
11 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
12 | copies of the Software, and to permit persons to whom the Software is
13 | furnished to do so, subject to the following conditions:
14 | The above copyright notice and this permission notice shall be included in all
15 | copies or substantial portions of the Software.
16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 | SOFTWARE.
23 | */
24 |
25 | import UIKit
26 |
27 | /// Protocol used by Stack Flow View consumers to control the flow itself.
28 |
29 | public protocol StackFlowDelegate {
30 | func stackFlowViewDidRequestPop(_ stackView: StackFlowView, numberOfItems: Int)
31 | func stackFlowViewDidRequestPush(_ stackView: StackFlowView)
32 |
33 | func stackFlowViewWillPop(_ stackView: StackFlowView)
34 | func stackFlowViewDidPop(_ stackView: StackFlowView)
35 |
36 | func stackFlowView(_ stackView: StackFlowView, willPush view: UIView)
37 | func stackFlowView(_ stackView: StackFlowView, didPush view: UIView)
38 | }
39 |
40 | /// View that enables visual stack behaviour for subviews it contains. It can be used to stack up any custom UI elements in strict predefined order, or just randomly, in any of the four directions: up, down, left, right. There are a lot of customization options that make a number of different common UI patterns to be set up.
41 |
42 | open class StackFlowView: UIView, StackItemViewDelegate {
43 | // MARK: - Types -
44 |
45 | typealias StackSeparatorView = UIView
46 |
47 | public enum Direction {
48 | case up, down, left, right
49 |
50 | public var isVertical: Bool { return self == .up || self == .down }
51 | }
52 |
53 | public enum SeparationStyle {
54 | case none
55 | case line(thikness: CGFloat, color: UIColor)
56 | case padding(size: CGFloat)
57 | }
58 |
59 | public enum FadingStyle {
60 | case none
61 | case tint(color: UIColor, preLastAlpha: CGFloat, alphaDecrement: CGFloat?)
62 | case gradientMask(effectDistance: CGFloat)
63 | indirect case combined(styles: [FadingStyle])
64 | }
65 |
66 | public struct NavigationOptions: OptionSet {
67 | public let rawValue: Int
68 |
69 | public static let swipe = NavigationOptions(rawValue: 1 << 1)
70 | public static let tap = NavigationOptions(rawValue: 1 << 2)
71 |
72 | public static let none: [NavigationOptions] = []
73 | public static let all: [NavigationOptions] = [.swipe, .tap]
74 |
75 | public init(rawValue: Int) {
76 | self.rawValue = rawValue
77 | }
78 | }
79 |
80 | // MARK: - Delegate -
81 |
82 | public var delegate: StackFlowDelegate? = nil
83 |
84 | // MARK: - Subviews -
85 |
86 | private let contentContainer = UIView()
87 | private var items: [StackItemView] = []
88 | private var separators: [StackItemView : StackSeparatorView] = [:]
89 |
90 | var lastItem: StackItemView? {
91 | return items.last
92 | }
93 |
94 | // MARK: - Public accessors -
95 |
96 | public var numberOfItems: Int {
97 | return items.count
98 | }
99 |
100 | public var lastItemContent: UIView? {
101 | return lastItem?.contentView
102 | }
103 |
104 | /// This property is calculated considering only part of stack flow view that stays inside safe area of its superview. It's highly recommended to always use this property for newly pushed item side definition, to avoid broken layout caused by dynamically shrinked safe area.
105 |
106 | public var safeSize: CGSize {
107 | let width = contentContainer.bounds.width
108 | let height = contentContainer.bounds.height
109 |
110 | let size = CGSize(width: width, height: height)
111 | return size
112 | }
113 |
114 | // MARK: - Appearance -
115 |
116 | open override var frame: CGRect {
117 | didSet {
118 | super.frame = frame
119 | }
120 | }
121 |
122 | /// Whether or not stack flow view should stretch/shrink its items' width (for vertical stacks) or height (for horizontal stacks) to fill the area perpendicular to stack's direction. For example: should it make vertical stack items wider after rotatin from portrait to landscape orientation.
123 |
124 | public var isAutoresizingItems: Bool = true
125 |
126 | /// Whether or not stack flow view should do its best to stay inside superview's safe area. For example: adjust content to avoid being covered by status bar, navigation bar, iPhone X notch, etc.
127 |
128 | public var isSeekingSafeArea: Bool = true {
129 | didSet {
130 | resetContainerConstraints()
131 | layoutIfNeeded()
132 | }
133 | }
134 |
135 | /// Head of stack is the last item pushed. Depending on the direction of stack growth, this property adds padding on top, bottom, left or right side of the stack, right next to its head item.
136 |
137 | public var headPadding: CGFloat = 0.0 {
138 | didSet {
139 | resetContainerConstraints()
140 | layoutIfNeeded()
141 | }
142 | }
143 |
144 | /// The direction stack pushes its items to. For example: .down direction will always keep the last item sticked to the bottom of stack flow view, and all previous elements will be shifted upper side.
145 |
146 | public var growthDirection: Direction = .down {
147 | didSet {
148 | resetChildrenLayoutConstraints()
149 | layoutIfNeeded()
150 | }
151 | }
152 |
153 | /// The way stack items are separated visually.
154 |
155 | public var separationStyle: SeparationStyle = .line(thikness: 1.0, color: .white) {
156 | didSet {
157 | resetChildrenLayoutConstraints()
158 | layoutIfNeeded()
159 | }
160 | }
161 |
162 | /// The way stack item fades out previously pushed elements, focusing on the latest one. Multiple options can be combined and used simultaneously, or none of them, resulting in fade-out functionality turned off.
163 |
164 | public var fadingStyle: FadingStyle = .combined(styles: [.tint(color: .black, preLastAlpha: 0.9, alphaDecrement: nil), .gradientMask(effectDistance: 500)]) {
165 | didSet {
166 | resetFadingSettings()
167 | }
168 | }
169 |
170 | /// Active built-in navigation options. For example: swipe up or down controls flow of vertical stack, or tapping any of the inactive items pops the stack until it becomes active.
171 |
172 | public var userNavigationOptions: NavigationOptions = [.swipe, .tap]
173 |
174 | /// Duration of pop/push transitions
175 |
176 | public var transitionDuration: Double = 0.25
177 |
178 | // MARK: - Meta resolvers -
179 |
180 | func doesOwn(item: StackItemView) -> Bool {
181 | return (items.index(of: item) ?? nil) != nil
182 | }
183 |
184 | private var separationSizeAndColor: (CGFloat, UIColor) {
185 | switch separationStyle {
186 | case .padding(let size):
187 | return (size, .clear)
188 |
189 | case .line(let thikness, let color):
190 | return (thikness, color)
191 |
192 | case .none:
193 | return (0, .clear)
194 | }
195 | }
196 |
197 | // MARK: - Initializers -
198 |
199 | public override init(frame: CGRect) {
200 | super.init(frame: frame)
201 | setUp()
202 | }
203 |
204 | required public init?(coder aDecoder: NSCoder) {
205 | fatalError("init(coder:) has not been implemented")
206 | }
207 |
208 | // MARK: - Life cycle -
209 |
210 | open override func layoutSubviews() {
211 | super.layoutSubviews()
212 |
213 | resetFadingSettings()
214 | resetChildrenLayoutConstraints()
215 |
216 | layoutIfNeeded()
217 | }
218 |
219 | // MARK: - Setup -
220 |
221 | private func setUp() {
222 | backgroundColor = .clear
223 | contentContainer.backgroundColor = .clear
224 | contentContainer.frame = bounds
225 |
226 | contentContainer.translatesAutoresizingMaskIntoConstraints = false
227 | contentContainer.clipsToBounds = true
228 |
229 | addSubview(contentContainer)
230 |
231 | resetContainerConstraints()
232 | layoutIfNeeded()
233 |
234 | // Add navigation gestures
235 |
236 | let swipeGestures = [
237 | UISwipeGestureRecognizer(target: self, action: #selector(swipeOccured(_:))),
238 | UISwipeGestureRecognizer(target: self, action: #selector(swipeOccured(_:))),
239 | UISwipeGestureRecognizer(target: self, action: #selector(swipeOccured(_:))),
240 | UISwipeGestureRecognizer(target: self, action: #selector(swipeOccured(_:)))
241 | ]
242 |
243 | swipeGestures.forEach {
244 | let directions: [UISwipeGestureRecognizerDirection] = [.up, .down, .left, .right]
245 | $0.direction = directions[swipeGestures.index(of: $0)!]
246 | contentContainer.addGestureRecognizer($0)
247 | }
248 |
249 | let tapGesture = UITapGestureRecognizer(target: self, action: #selector(tapOccured(_:)))
250 | contentContainer.addGestureRecognizer(tapGesture)
251 | }
252 |
253 | // MARK: - Items lifecycle -
254 |
255 | /// Pushes given view with custom content to stack. Optionally, navigation bar can be presented once title is provided. Also, said navigation bar's appearance can be customized.
256 |
257 | /// - parameter view: The view with custom content to push
258 | /// - parameter title: Optional title to display along with navigation nar
259 | /// - parameter customAppearance: Optional customization of the presented navigation bar
260 |
261 | public func push(_ view: UIView, title: String? = nil, customAppearance: StackItemAppearance? = nil) {
262 | let stackItemView = StackItemView(contentView: view, title: title, customAppearance: customAppearance)
263 | stackItemView.delegate = self
264 |
265 | delegate?.stackFlowView(self, willPush: view)
266 |
267 | items.append(stackItemView)
268 |
269 | prepareAndPush(stackItemView)
270 |
271 | delegate?.stackFlowView(self, didPush: view)
272 | }
273 |
274 | /// Pops given number of items out of stack flow view.
275 |
276 | /// - parameter numberOfItems: Number of items to pop
277 |
278 | public func pop(_ numberOfItems: Int = 1) {
279 | for _ in 0 ..< numberOfItems {
280 | if let lastChild = items.last {
281 | delegate?.stackFlowViewWillPop(self)
282 |
283 | items.removeLast()
284 |
285 | popAndCleanUp(lastChild)
286 |
287 | delegate?.stackFlowViewDidPop(self)
288 | }
289 | }
290 | }
291 |
292 | /// Pops all the items out of stack flow view.
293 |
294 | public func clean() {
295 | pop(items.count)
296 | }
297 |
298 | // MARK: - StackItemViewDelegate -
299 |
300 | func stackItemRequestedPop(_ stackItemView: StackItemView) {
301 | delegate?.stackFlowViewDidRequestPop(self, numberOfItems: 1)
302 | }
303 |
304 | func stackItemRequestedPush(_ stackItemView: StackItemView) {
305 | delegate?.stackFlowViewDidRequestPush(self)
306 | }
307 |
308 | // MARK: - Transitions -
309 |
310 | private func popAndCleanUp(_ item: StackItemView) {
311 | UIView.animate(withDuration: transitionDuration * 0.5, animations: {
312 | item.alpha = 0.0
313 | self.separators[item]?.alpha = 0.0
314 | }) { (_) in
315 | item.removeFromSuperview()
316 |
317 | self.separators[item]?.removeFromSuperview()
318 | self.separators.removeValue(forKey: item)
319 |
320 | self.resetChildrenLayoutConstraints()
321 |
322 | UIView.animate(withDuration: self.transitionDuration * 0.5) {
323 | self.resetFadingSettings()
324 | self.layoutIfNeeded()
325 | }
326 |
327 | StackFlowNotification.post(name: .itemPopped, stackView: self)
328 | }
329 | }
330 |
331 | private func prepareAndPush(_ item: StackItemView) {
332 | removeChildrenLayoutConstraints()
333 |
334 | var separator: StackSeparatorView? = nil
335 |
336 | // TODO: Revisit this `isFirstInStack` property thing, I'm not fully satisfied with this approach (@0xNSHuman)
337 |
338 | if items.first == item {
339 | item.isFirstInStack = true
340 | } else {
341 | item.isFirstInStack = false
342 |
343 | separator = StackSeparatorView()
344 | separator?.backgroundColor = separationSizeAndColor.1
345 |
346 | separators[item] = separator
347 | }
348 |
349 | // Initial position to kick transition from
350 |
351 | switch growthDirection {
352 | case .up:
353 | item.center = CGPoint(x: contentContainer.bounds.width / 2, y: -(item.bounds.height / 2))
354 | case .down:
355 | separator?.frame = CGRect(x: 0, y: contentContainer.bounds.height + (separator?.bounds.height ?? 0.0) / 2, width: contentContainer.bounds.width, height: separationSizeAndColor.0)
356 |
357 | if let separator = separator {
358 | item.center = CGPoint(x: contentContainer.bounds.width / 2, y: separator.center.y + separationSizeAndColor.0)
359 | } else {
360 | item.center = CGPoint(x: contentContainer.bounds.width / 2, y: contentContainer.bounds.height + item.bounds.height / 2)
361 | }
362 |
363 | case .left:
364 | item.center = CGPoint(x: -(item.bounds.width / 2), y: contentContainer.bounds.height / 2)
365 | case .right:
366 | item.center = CGPoint(x: contentContainer.bounds.width + item.bounds.width / 2, y: contentContainer.bounds.height / 2)
367 | }
368 |
369 | if let separator = separator {
370 | contentContainer.addSubview(separator)
371 | }
372 |
373 | contentContainer.addSubview(item)
374 | applyChildrenLayoutConstraints()
375 |
376 | UIView.animate(withDuration: transitionDuration) {
377 | self.resetFadingSettings()
378 | self.layoutIfNeeded()
379 | }
380 |
381 | StackFlowNotification.post(name: .itemPushed, stackView: self)
382 | }
383 |
384 | // MARK: - Items fading -
385 |
386 | private func resetFadingSettings() {
387 | guard let lastItem = items.last else { return }
388 |
389 | // Lock up all the elements but the last in stack
390 |
391 | items.forEach { $0.isUserInteractionEnabled = false }
392 | lastItem.isUserInteractionEnabled = true
393 |
394 | // Clean up previous isolation setup
395 |
396 | lastItem.tintView.isHidden = true
397 | lastItem.alpha = 1.0
398 |
399 | let tintResetAction: () -> () = {
400 | for i in 0 ..< self.items.count - 1 {
401 | self.items[i].tintView.isHidden = true
402 | self.items[i].alpha = 1.0
403 | }
404 | }
405 |
406 | let gradientResetAction: () -> () = {
407 | self.contentContainer.layer.mask = nil
408 | }
409 |
410 | if case .gradientMask(_) = fadingStyle {
411 | tintResetAction()
412 | } else if case .tint(_, _, _) = fadingStyle {
413 | gradientResetAction()
414 | } else if case .combined(let styles) = fadingStyle {
415 | if !styles.contains(where: { (style) -> Bool in
416 | if case .tint(_, _, _) = style {
417 | return true
418 | } else {
419 | return false
420 | }
421 | }) {
422 | tintResetAction()
423 | }
424 |
425 | if !styles.contains(where: { (style) -> Bool in
426 | if case .gradientMask(_) = style {
427 | return true
428 | } else {
429 | return false
430 | }
431 | }) {
432 | gradientResetAction()
433 | }
434 | } else if case .none = fadingStyle {
435 | tintResetAction()
436 | gradientResetAction()
437 | }
438 |
439 | // Apply new isolation
440 |
441 | func applyStyle(_ style: FadingStyle) {
442 | switch style {
443 | case .gradientMask(let effectDistance):
444 | let itemDistance = (growthDirection.isVertical ? lastItem.bounds.height : lastItem.bounds.width)
445 | let containerDistance = growthDirection.isVertical ? contentContainer.bounds.height : contentContainer.bounds.width
446 |
447 | let relativeItemEnd = itemDistance / containerDistance
448 | let relativeEffectEnd = relativeItemEnd + (effectDistance / containerDistance)
449 |
450 | let gradientLayer = CAGradientLayer()
451 | gradientLayer.frame = contentContainer.bounds
452 | gradientLayer.colors = [UIColor.black.cgColor, UIColor.clear.cgColor, UIColor.clear.cgColor]
453 | gradientLayer.locations = [relativeItemEnd as NSNumber, relativeEffectEnd as NSNumber, 1.0]
454 |
455 | switch growthDirection {
456 | case .up:
457 | gradientLayer.startPoint = CGPoint(x: 0.5, y: 0.0)
458 | gradientLayer.endPoint = CGPoint(x: 0.5, y: 1.0)
459 |
460 | case .down:
461 | gradientLayer.startPoint = CGPoint(x: 0.5, y: 1.0)
462 | gradientLayer.endPoint = CGPoint(x: 0.5, y: 0.0)
463 |
464 | case .left:
465 | gradientLayer.startPoint = CGPoint(x: 0.0, y: 0.5)
466 | gradientLayer.endPoint = CGPoint(x: 1.0, y: 0.5)
467 |
468 | case .right:
469 | gradientLayer.startPoint = CGPoint(x: 1.0, y: 0.5)
470 | gradientLayer.endPoint = CGPoint(x: 0.0, y: 0.5)
471 | }
472 |
473 | contentContainer.layer.mask = gradientLayer
474 |
475 | case .tint(let color, let alpha, let alphaDecrement):
476 | // The decision to use stored subview instead of sublayer here is made because CALayer lacks ability to be autolayouted on iOS
477 |
478 | for i in 0 ..< items.count - 1 {
479 | let item = items[i]
480 | let tintView = item.tintView
481 | tintView.backgroundColor = color
482 |
483 | var itemAlpha: CGFloat
484 | if let decrement = alphaDecrement {
485 | itemAlpha = alpha - decrement * CGFloat(items.count - i - 1)
486 | itemAlpha = itemAlpha > 0.0 ? itemAlpha : 0.0
487 | } else {
488 | itemAlpha = alpha
489 | }
490 |
491 | item.alpha = itemAlpha
492 | }
493 |
494 | case .combined(let styles):
495 | for style in styles {
496 | applyStyle(style)
497 | }
498 |
499 | break
500 |
501 | case .none:
502 | break
503 | }
504 | }
505 |
506 | applyStyle(fadingStyle)
507 | }
508 |
509 | // MARK: - Layout -
510 |
511 | private func resetContainerConstraints() {
512 | constraints.forEach {
513 | // Only relationships with superview!
514 |
515 | guard ($0.firstItem as? UIView == contentContainer) || ($0.secondItem as? UIView == contentContainer) else { return }
516 |
517 | $0.isActive = false
518 | }
519 |
520 | contentContainer.removeConstraints(contentContainer.constraints)
521 |
522 | var containerConstraints: [NSLayoutConstraint]
523 |
524 | // Here we're trying to consider any potential safe area insets. Although, I believe it's responsibility on developer using this view to position it properly, that's still nice to be backed by this little nice feature.
525 |
526 | if isSeekingSafeArea, #available(iOS 11, *) {
527 | let guide = safeAreaLayoutGuide
528 |
529 | containerConstraints = [
530 | contentContainer.topAnchor.constraint(equalTo: guide.topAnchor, constant: growthDirection == .up ? headPadding : 0.0),
531 | contentContainer.bottomAnchor.constraint(equalTo: guide.bottomAnchor, constant: growthDirection == .down ? -headPadding : 0.0),
532 | contentContainer.leadingAnchor.constraint(equalTo: guide.leadingAnchor, constant: growthDirection == .left ? headPadding : 0.0),
533 | contentContainer.trailingAnchor.constraint(equalTo: guide.trailingAnchor, constant: growthDirection == .right ? -headPadding : 0.0)
534 | ]
535 | } else {
536 | containerConstraints = [
537 | NSLayoutConstraint(item: self, attribute: .top, relatedBy: .equal, toItem: contentContainer, attribute: .top, multiplier: 1.0, constant: growthDirection == .up ? headPadding : 0.0),
538 | NSLayoutConstraint(item: self, attribute: .bottom, relatedBy: .equal, toItem: contentContainer, attribute: .bottom, multiplier: 1.0, constant: growthDirection == .down ? headPadding : 0.0),
539 | NSLayoutConstraint(item: self, attribute: .leading, relatedBy: .equal, toItem: contentContainer, attribute: .leading, multiplier: 1.0, constant: growthDirection == .left ? headPadding : 0.0),
540 | NSLayoutConstraint(item: self, attribute: .trailing, relatedBy: .equal, toItem: contentContainer, attribute: .trailing, multiplier: 1.0, constant: growthDirection == .right ? headPadding : 0.0)
541 | ]
542 | }
543 |
544 | containerConstraints.forEach { $0.isActive = true }
545 | layoutIfNeeded()
546 | }
547 |
548 | private func resetChildrenLayoutConstraints() {
549 | removeChildrenLayoutConstraints()
550 | applyChildrenLayoutConstraints()
551 | }
552 |
553 | private func removeChildrenLayoutConstraints() {
554 | contentContainer.constraints.forEach {
555 | if $0.firstItem is StackItemView || $0.secondItem is StackItemView {
556 | $0.isActive = false
557 | contentContainer.removeConstraint($0)
558 | }
559 | }
560 | }
561 |
562 | private func applyChildrenLayoutConstraints() {
563 | items.forEach {
564 | let childConstraints = layout(for: $0)
565 | childConstraints.forEach { $0.isActive = true }
566 | }
567 | }
568 |
569 | private func layout(for child: StackItemView) -> [NSLayoutConstraint] {
570 | child.translatesAutoresizingMaskIntoConstraints = false
571 |
572 | // Get info about items relative position and separation between them
573 |
574 | let isFirstChild = child == items.first
575 | let isLastChild = child == items.last
576 |
577 | let prevChild: StackItemView?
578 | let separator: StackSeparatorView?
579 | let padding: CGFloat?
580 |
581 | if !isFirstChild {
582 | prevChild = items[items.index(of: child)! - 1]
583 | separator = separators[child]!
584 |
585 | separator?.translatesAutoresizingMaskIntoConstraints = false
586 |
587 | switch separationStyle {
588 | case .padding(let height):
589 | padding = height
590 | separator?.backgroundColor = .clear
591 |
592 | case .line(let width, let color):
593 | padding = width
594 | separator?.backgroundColor = color
595 |
596 | case .none:
597 | padding = 0
598 | separator?.backgroundColor = .clear
599 | }
600 | } else {
601 | prevChild = nil
602 | separator = nil
603 | padding = nil
604 | }
605 |
606 | // Set up necessary constraints
607 |
608 | var constraints: [NSLayoutConstraint] = []
609 |
610 | if growthDirection.isVertical {
611 | constraints = [
612 | // Center child content
613 |
614 | NSLayoutConstraint(item: child, attribute: .centerX, relatedBy: .equal, toItem: contentContainer, attribute: .centerX, multiplier: 1.0, constant: 0.0),
615 | ] + (!isFirstChild ? [
616 | // Stick separator to container bounds
617 |
618 | NSLayoutConstraint(item: separator!, attribute: .leading, relatedBy: .equal, toItem: contentContainer, attribute: .leading, multiplier: 1.0, constant: 0.0),
619 | NSLayoutConstraint(item: separator!, attribute: .trailing, relatedBy: .equal, toItem: contentContainer, attribute: .trailing, multiplier: 1.0, constant: 0.0),
620 | NSLayoutConstraint(item: separator!, attribute: .height, relatedBy: .equal, toItem: nil, attribute: .notAnAttribute, multiplier: 1.0, constant: padding!)
621 | ] : [])
622 | } else {
623 | constraints = [
624 | // Center child content
625 |
626 | NSLayoutConstraint(item: child, attribute: .centerY, relatedBy: .equal, toItem: contentContainer, attribute: .centerY, multiplier: 1.0, constant: 0.0)
627 | ] + (!isFirstChild ? [
628 | // Stick separator to container bounds
629 |
630 | NSLayoutConstraint(item: separator!, attribute: .top, relatedBy: .equal, toItem: contentContainer, attribute: .top, multiplier: 1.0, constant: 0.0),
631 | NSLayoutConstraint(item: separator!, attribute: .bottom, relatedBy: .equal, toItem: contentContainer, attribute: .bottom, multiplier: 1.0, constant: 0.0),
632 | NSLayoutConstraint(item: separator!, attribute: .width, relatedBy: .equal, toItem: nil, attribute: .notAnAttribute, multiplier: 1.0, constant: padding!)
633 | ] : [])
634 | }
635 |
636 | if isAutoresizingItems {
637 | let autoResizeAttribute: NSLayoutAttribute = growthDirection.isVertical ? .width : .height
638 | constraints.append(NSLayoutConstraint(item: child, attribute: autoResizeAttribute, relatedBy: .equal, toItem: contentContainer, attribute: autoResizeAttribute, multiplier: 1.0, constant: 0.0))
639 | }
640 |
641 | // Constraints depending on stack growth direction
642 |
643 | var relAttrConstBuffer: (UIView, NSLayoutAttribute, CGFloat)
644 |
645 | switch growthDirection {
646 | case .up:
647 | if isLastChild {
648 | relAttrConstBuffer = (contentContainer, .top, 0.0)
649 | let topC = NSLayoutConstraint(item: child, attribute: .top, relatedBy: .equal, toItem: relAttrConstBuffer.0, attribute: relAttrConstBuffer.1, multiplier: 1.0, constant: relAttrConstBuffer.2)
650 |
651 | constraints.append(topC)
652 | }
653 |
654 | if !isFirstChild {
655 | let separatorConstraints = [
656 | NSLayoutConstraint(item: separator!, attribute: .bottom, relatedBy: .equal, toItem: prevChild, attribute: .top, multiplier: 1.0, constant: 0.0),
657 | NSLayoutConstraint(item: separator!, attribute: .top, relatedBy: .equal, toItem: child, attribute: .bottom, multiplier: 1.0, constant: 0.0)
658 | ]
659 |
660 | constraints.append(contentsOf: separatorConstraints)
661 | }
662 |
663 | case .down:
664 | if isLastChild {
665 | relAttrConstBuffer = (contentContainer, .bottom, 0.0)
666 | let bottomC = NSLayoutConstraint(item: child, attribute: .bottom, relatedBy: .equal, toItem: relAttrConstBuffer.0, attribute: relAttrConstBuffer.1, multiplier: 1.0, constant: relAttrConstBuffer.2)
667 |
668 | constraints.append(bottomC)
669 | }
670 |
671 | if !isFirstChild {
672 | let separatorConstraints = [
673 | NSLayoutConstraint(item: separator!, attribute: .top, relatedBy: .equal, toItem: prevChild, attribute: .bottom, multiplier: 1.0, constant: 0.0),
674 | NSLayoutConstraint(item: separator!, attribute: .bottom, relatedBy: .equal, toItem: child, attribute: .top, multiplier: 1.0, constant: 0.0)
675 | ]
676 |
677 | constraints.append(contentsOf: separatorConstraints)
678 | }
679 |
680 | case .left:
681 | if isLastChild {
682 | relAttrConstBuffer = (contentContainer, .leading, 0.0)
683 | let leadingC = NSLayoutConstraint(item: child, attribute: .leading, relatedBy: .equal, toItem: relAttrConstBuffer.0, attribute: relAttrConstBuffer.1, multiplier: 1.0, constant: relAttrConstBuffer.2)
684 |
685 | constraints.append(leadingC)
686 | }
687 |
688 | if !isFirstChild {
689 | let separatorConstraints = [
690 | NSLayoutConstraint(item: separator!, attribute: .leading, relatedBy: .equal, toItem: child, attribute: .trailing, multiplier: 1.0, constant: 0.0),
691 | NSLayoutConstraint(item: separator!, attribute: .trailing, relatedBy: .equal, toItem: prevChild, attribute: .leading, multiplier: 1.0, constant: 0.0)
692 | ]
693 |
694 | constraints.append(contentsOf: separatorConstraints)
695 | }
696 |
697 | case .right:
698 | if isLastChild {
699 | relAttrConstBuffer = (contentContainer, .trailing, 0.0)
700 | let trailingC = NSLayoutConstraint(item: child, attribute: .trailing, relatedBy: .equal, toItem: relAttrConstBuffer.0, attribute: relAttrConstBuffer.1, multiplier: 1.0, constant: relAttrConstBuffer.2)
701 |
702 | constraints.append(trailingC)
703 | }
704 |
705 | if !isFirstChild {
706 | let separatorConstraints = [
707 | NSLayoutConstraint(item: separator!, attribute: .leading, relatedBy: .equal, toItem: prevChild, attribute: .trailing, multiplier: 1.0, constant: 0.0),
708 | NSLayoutConstraint(item: separator!, attribute: .trailing, relatedBy: .equal, toItem: child, attribute: .leading, multiplier: 1.0, constant: 0.0)
709 | ]
710 |
711 | constraints.append(contentsOf: separatorConstraints)
712 | }
713 | }
714 |
715 | return constraints
716 | }
717 |
718 | // MARK: - Navigation controls -
719 |
720 | @objc func swipeOccured(_ swipe: UISwipeGestureRecognizer) {
721 | guard userNavigationOptions.contains(.swipe) else { return }
722 |
723 | switch swipe.direction {
724 | case .up:
725 | if growthDirection == .down {
726 | delegate?.stackFlowViewDidRequestPush(self)
727 | } else if growthDirection == .up {
728 | delegate?.stackFlowViewDidRequestPop(self, numberOfItems: 1)
729 | }
730 |
731 | case .down:
732 | if growthDirection == .down {
733 | delegate?.stackFlowViewDidRequestPop(self, numberOfItems: 1)
734 | } else if growthDirection == .up {
735 | delegate?.stackFlowViewDidRequestPush(self)
736 | }
737 |
738 | case .left:
739 | if growthDirection == .left {
740 | delegate?.stackFlowViewDidRequestPop(self, numberOfItems: 1)
741 | } else if growthDirection == .right {
742 | delegate?.stackFlowViewDidRequestPush(self)
743 | }
744 |
745 | case .right:
746 | if growthDirection == .left {
747 | delegate?.stackFlowViewDidRequestPush(self)
748 | } else if growthDirection == .right {
749 | delegate?.stackFlowViewDidRequestPop(self, numberOfItems: 1)
750 | }
751 |
752 | default:
753 | break
754 | }
755 | }
756 |
757 | @objc func tapOccured(_ swipe: UITapGestureRecognizer) {
758 | guard userNavigationOptions.contains(.tap) else { return }
759 |
760 | let tapPoint = swipe.location(in: contentContainer)
761 |
762 | if let tappedItem = itemAtLocation(tapPoint), let itemIndex = items.index(of: tappedItem) {
763 | delegate?.stackFlowViewDidRequestPop(self, numberOfItems: (items.count - 1) - itemIndex)
764 | }
765 | }
766 |
767 | // MARK: - Resolvers -
768 |
769 | /// Location of stack item in stack view container coordinates
770 |
771 | private func itemAtLocation(_ point: CGPoint) -> StackItemView? {
772 | var item: StackItemView? = nil
773 |
774 | items.forEach {
775 | if $0.frame.contains(point) {
776 | item = $0
777 | }
778 | }
779 |
780 | return item
781 | }
782 | }
783 |
--------------------------------------------------------------------------------
/StackFlowView/StackItemView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // StackItemView.swift
3 | // Created by 0xNSHuman on 05/12/2017.
4 |
5 | /*
6 | The MIT License (MIT)
7 | Copyright © 2017 0xNSHuman
8 | Permission is hereby granted, free of charge, to any person obtaining a copy
9 | of this software and associated documentation files (the "Software"), to deal
10 | in the Software without restriction, including without limitation the rights
11 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
12 | copies of the Software, and to permit persons to whom the Software is
13 | furnished to do so, subject to the following conditions:
14 | The above copyright notice and this permission notice shall be included in all
15 | copies or substantial portions of the Software.
16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 | SOFTWARE.
23 | */
24 |
25 | import UIKit
26 |
27 | /// View that is typically presented on top of the stack flow item's content, containing given title, as well as navigation buttons to control stack flow.
28 |
29 | class StackItemViewHeader: UIView {
30 | // MARK: - Constants -
31 |
32 | static let defaultHeight: CGFloat = 44.0
33 | static let margin: CGFloat = 20.0
34 |
35 | // MARK: - Stack Item reference -
36 |
37 | private weak var stackItem: StackItemView?
38 |
39 | // MARK: - Subviews -
40 |
41 | let titleLabel = UILabel()
42 |
43 | var popButton: UIButton? = nil {
44 | didSet {
45 | oldValue?.removeFromSuperview()
46 | if let button = popButton { addSubview(button) }
47 |
48 | popButton?.addTarget(stackItem, action: #selector(StackItemView.popTapped), for: .touchUpInside)
49 |
50 | if let _ = popButton { applyAppearance() }
51 | resetConstraints()
52 | }
53 | }
54 |
55 | var pushButton: UIButton? = nil {
56 | didSet {
57 | oldValue?.removeFromSuperview()
58 | if let button = pushButton { addSubview(button) }
59 |
60 | pushButton?.addTarget(stackItem, action: #selector(StackItemView.pushTapped), for: .touchUpInside)
61 |
62 | if let _ = pushButton { applyAppearance() }
63 | resetConstraints()
64 | }
65 | }
66 |
67 | // MARK: - Appearance -
68 |
69 | override var frame: CGRect {
70 | didSet {
71 | super.frame = frame
72 | }
73 | }
74 |
75 | var appearance: StackItemAppearance.TopBar = StackItemAppearance.defaultPreset.topBarAppearance {
76 | didSet {
77 | applyAppearance()
78 | }
79 | }
80 |
81 | // MARK: - Initializers -
82 |
83 | convenience init(title: String) {
84 | self.init(frame: .zero, title: title)
85 | }
86 |
87 | required init(frame: CGRect, title: String) {
88 | super.init(frame: frame)
89 |
90 | addSubview(titleLabel)
91 | titleLabel.text = title
92 | setUp()
93 | }
94 |
95 | required init?(coder aDecoder: NSCoder) {
96 | fatalError("init(coder:) has not been implemented")
97 | }
98 |
99 | // MARK: - Life cycle -
100 |
101 | override func layoutSubviews() {
102 | super.layoutSubviews()
103 | }
104 |
105 | override func willMove(toSuperview newSuperview: UIView?) {
106 | super.willMove(toSuperview: newSuperview)
107 |
108 | guard let stackItem = newSuperview as? StackItemView else {
109 | return
110 | }
111 |
112 | self.stackItem = stackItem
113 | }
114 |
115 | // MARK: - Setup -
116 |
117 | private func setUp() {
118 | titleLabel.textAlignment = .center
119 | resetConstraints()
120 | }
121 |
122 | private func applyAppearance() {
123 | backgroundColor = appearance.backgroundColor
124 | titleLabel.textColor = appearance.titleTextColor
125 | titleLabel.font = appearance.titleFont
126 |
127 | var buttonWidth: CGFloat = 0
128 |
129 | if let popTitle = appearance.popButtonIdentity.title {
130 | popButton?.setAttributedTitle(popTitle, for: .normal)
131 |
132 | buttonWidth = {
133 | return (popButton?.titleLabel?.sizeThatFits(CGSize(width: bounds.width, height: bounds.height)).width ?? 0) + (StackItemViewHeader.margin * 2)
134 | }()
135 | } else if let popImage = appearance.popButtonIdentity.icon {
136 | popButton?.setImage(popImage, for: .normal)
137 |
138 | buttonWidth = {
139 | let imageRatio = popImage.size.width / popImage.size.height
140 | return (bounds.height * imageRatio) + (StackItemViewHeader.margin * 2)
141 | }()
142 | }
143 |
144 | popButton?.frame = CGRect(origin: .zero, size: CGSize(width: buttonWidth, height: bounds.height))
145 |
146 | if let pushTitle = appearance.pushButtonIdentity.title {
147 | pushButton?.setAttributedTitle(pushTitle, for: .normal)
148 |
149 | buttonWidth = {
150 | return (pushButton?.titleLabel?.sizeThatFits(CGSize(width: bounds.width, height: bounds.height)).width ?? 0) + (StackItemViewHeader.margin * 2)
151 | }()
152 | } else if let pushImage = appearance.pushButtonIdentity.icon {
153 | pushButton?.setImage(pushImage, for: .normal)
154 |
155 | buttonWidth = {
156 | let imageRatio = pushImage.size.width / pushImage.size.height
157 | return (bounds.height * imageRatio) + (StackItemViewHeader.margin * 2)
158 | }()
159 | }
160 |
161 | pushButton?.frame = CGRect(origin: .zero, size: CGSize(width: buttonWidth, height: bounds.height))
162 | }
163 |
164 | private func resetConstraints() {
165 | translatesAutoresizingMaskIntoConstraints = false
166 |
167 | constraints.forEach {
168 | guard $0.firstAttribute != .height && $0.firstAttribute != .width else { return }
169 |
170 | $0.isActive = false
171 | removeConstraint($0)
172 | }
173 |
174 | titleLabel.translatesAutoresizingMaskIntoConstraints = false
175 | let longestButtonWidth = max(popButton?.bounds.width ?? 0.0, pushButton?.bounds.width ?? 0.0)
176 |
177 | var newConstraints = [
178 | NSLayoutConstraint(item: titleLabel, attribute: .leading, relatedBy: .equal, toItem: self, attribute: .leading, multiplier: 1.0, constant: longestButtonWidth),
179 | NSLayoutConstraint(item: titleLabel, attribute: .trailing, relatedBy: .equal, toItem: self, attribute: .trailing, multiplier: 1.0, constant: -(longestButtonWidth)),
180 | NSLayoutConstraint(item: titleLabel, attribute: .top, relatedBy: .equal, toItem: self, attribute: .top, multiplier: 1.0, constant: 0.0),
181 | NSLayoutConstraint(item: titleLabel, attribute: .bottom, relatedBy: .equal, toItem: self, attribute: .bottom, multiplier: 1.0, constant: 0.0)
182 | ]
183 |
184 | if let popButton = popButton {
185 | popButton.translatesAutoresizingMaskIntoConstraints = false
186 |
187 | let buttonConstraints = [
188 | NSLayoutConstraint(item: popButton, attribute: .leading, relatedBy: .equal, toItem: self, attribute: .leading, multiplier: 1.0, constant: 0.0),
189 | NSLayoutConstraint(item: popButton, attribute: .trailing, relatedBy: .equal, toItem: titleLabel, attribute: .leading, multiplier: 1.0, constant: 0.0),
190 | NSLayoutConstraint(item: popButton, attribute: .top, relatedBy: .equal, toItem: self, attribute: .top, multiplier: 1.0, constant: 0.0),
191 | NSLayoutConstraint(item: popButton, attribute: .bottom, relatedBy: .equal, toItem: self, attribute: .bottom, multiplier: 1.0, constant: 0.0)
192 | ]
193 |
194 | newConstraints.append(contentsOf: buttonConstraints)
195 | }
196 |
197 | if let pushButton = pushButton {
198 | pushButton.translatesAutoresizingMaskIntoConstraints = false
199 |
200 | let buttonConstraints = [
201 | NSLayoutConstraint(item: pushButton, attribute: .leading, relatedBy: .equal, toItem: titleLabel, attribute: .trailing, multiplier: 1.0, constant: 0.0),
202 | NSLayoutConstraint(item: pushButton, attribute: .trailing, relatedBy: .equal, toItem: self, attribute: .trailing, multiplier: 1.0, constant: 0.0),
203 | NSLayoutConstraint(item: pushButton, attribute: .top, relatedBy: .equal, toItem: self, attribute: .top, multiplier: 1.0, constant: 0.0),
204 | NSLayoutConstraint(item: pushButton, attribute: .bottom, relatedBy: .equal, toItem: self, attribute: .bottom, multiplier: 1.0, constant: 0.0)
205 | ]
206 |
207 | newConstraints.append(contentsOf: buttonConstraints)
208 | }
209 |
210 | newConstraints.forEach { $0.isActive = true }
211 |
212 | setNeedsLayout()
213 | layoutIfNeeded()
214 | }
215 | }
216 |
217 | /// Internally used protocol for delivering events from particular items to containing stack flow view.
218 |
219 | protocol StackItemViewDelegate {
220 | func stackItemRequestedPop(_ stackItemView: StackItemView)
221 | func stackItemRequestedPush(_ stackItemView: StackItemView)
222 | }
223 |
224 | /// Item view containing provided custom UI content along with wrapped up logic that enables pushing/popping this content into/out of stack flow view.
225 |
226 | class StackItemView: UIView {
227 | // MARK: - Types -
228 |
229 | typealias TintView = UIView
230 |
231 | // MARK: - Subviews -
232 |
233 | private var headerView: StackItemViewHeader? = nil
234 | private var contentContainer = UIView()
235 | private(set) var contentView: UIView?
236 |
237 | var tintView = TintView()
238 |
239 | // MARK: - Other Properties -
240 |
241 | var delegate: StackItemViewDelegate? = nil
242 |
243 | // MARK: - Appearance -
244 |
245 | override var frame: CGRect {
246 | didSet {
247 | super.frame = frame
248 | }
249 | }
250 |
251 | var appearance: StackItemAppearance = StackItemAppearance.defaultPreset {
252 | didSet {
253 | applyAppearance()
254 | }
255 | }
256 |
257 | // MARK: Meta
258 |
259 | var isFirstInStack: Bool = true {
260 | didSet {
261 | if isFirstInStack {
262 | headerView?.popButton = nil
263 | } else {
264 | headerView?.popButton = UIButton(type: .custom)
265 | }
266 | }
267 | }
268 |
269 | // MARK: - Initializers -
270 |
271 | required init(contentView: UIView, title: String? = nil, customAppearance: StackItemAppearance? = nil) {
272 | super.init(frame: CGRect.init(x: 0, y: 0, width: contentView.bounds.width, height: contentView.bounds.height + StackItemViewHeader.defaultHeight))
273 |
274 | if let appearance = customAppearance {
275 | self.appearance = appearance
276 | }
277 |
278 | if let title = title {
279 | self.headerView = StackItemViewHeader(title: title)
280 | self.headerView?.frame = CGRect(x: 0, y: 0, width: bounds.width, height: StackItemViewHeader.defaultHeight)
281 | addSubview(headerView!)
282 |
283 | self.headerView?.pushButton = UIButton(type: .custom)
284 | }
285 |
286 | self.contentView = contentView
287 | contentContainer.addSubview(contentView)
288 | addSubview(contentContainer)
289 |
290 | addSubview(tintView)
291 |
292 | setUp()
293 | }
294 |
295 | required init?(coder aDecoder: NSCoder) {
296 | super.init(coder: aDecoder)
297 | }
298 |
299 | deinit {
300 | StackFlowNotification.forget(observer: self)
301 | }
302 |
303 | // MARK: - Life cycle -
304 |
305 | override func layoutSubviews() {
306 | super.layoutSubviews()
307 | }
308 |
309 | // MARK: - Setup -
310 |
311 | private func setUp() {
312 | resetConstraints()
313 | applyAppearance()
314 |
315 | contentContainer.clipsToBounds = true
316 |
317 | tintView.isUserInteractionEnabled = false
318 | tintView.isHidden = true
319 |
320 | StackFlowNotification.observe(name: .itemPopped, by: self, selector: #selector(stackViewPoppedItem(notification:)))
321 | StackFlowNotification.observe(name: .itemPushed, by: self, selector: #selector(stackViewPushedItem(notification:)))
322 | }
323 |
324 | private func applyAppearance() {
325 | backgroundColor = appearance.backgroundColor
326 | headerView?.appearance = appearance.topBarAppearance
327 | }
328 |
329 | private func resetConstraints() {
330 | // Remove previous constraints
331 |
332 | constraints.forEach {
333 | $0.isActive = false
334 | removeConstraint($0)
335 | }
336 |
337 | // Self constraints
338 |
339 | let totalWidth = contentView?.bounds.width ?? 0.0
340 | let totalHeight = (headerView?.bounds.height ?? 0.0) + (contentView?.bounds.height ?? 0.0)
341 |
342 | let sizeConstraints = [
343 | NSLayoutConstraint(item: self, attribute: .width, relatedBy: .equal, toItem: nil, attribute: .notAnAttribute, multiplier: 1.0, constant: totalWidth),
344 | NSLayoutConstraint(item: self, attribute: .height, relatedBy: .equal, toItem: nil, attribute: .notAnAttribute, multiplier: 1.0, constant: totalHeight)
345 | ]
346 |
347 | translatesAutoresizingMaskIntoConstraints = false
348 | sizeConstraints.forEach {
349 | $0.priority = .defaultHigh
350 | $0.isActive = true
351 | }
352 |
353 | // Header constraints
354 |
355 | if let header = headerView {
356 | let headerHeight = header.bounds.height
357 |
358 | let headerConstraints = [
359 | NSLayoutConstraint(item: self, attribute: .top, relatedBy: .equal, toItem: header, attribute: .top, multiplier: 1.0, constant: 0.0),
360 | NSLayoutConstraint(item: self, attribute: .leading, relatedBy: .equal, toItem: header, attribute: .leading, multiplier: 1.0, constant: 0.0),
361 | NSLayoutConstraint(item: self, attribute: .trailing, relatedBy: .equal, toItem: header, attribute: .trailing, multiplier: 1.0, constant: 0.0),
362 | NSLayoutConstraint(item: header, attribute: .height, relatedBy: .equal, toItem: nil, attribute: .notAnAttribute, multiplier: 1.0, constant: headerHeight)
363 | ]
364 |
365 | header.translatesAutoresizingMaskIntoConstraints = false
366 | headerConstraints.forEach { $0.isActive = true }
367 | }
368 |
369 | // Content constraints
370 |
371 | let containerConstraints = [
372 | NSLayoutConstraint(item: contentContainer, attribute: .top, relatedBy: .equal, toItem: headerView ?? self, attribute: headerView != nil ? .bottom : .top, multiplier: 1.0, constant: 0.0),
373 | NSLayoutConstraint(item: contentContainer, attribute: .leading, relatedBy: .equal, toItem: self, attribute: .leading, multiplier: 1.0, constant: 0.0),
374 | NSLayoutConstraint(item: contentContainer, attribute: .trailing, relatedBy: .equal, toItem: self, attribute: .trailing, multiplier: 1.0, constant: 0.0),
375 | NSLayoutConstraint(item: contentContainer, attribute: .bottom, relatedBy: .equal, toItem: self, attribute: .bottom, multiplier: 1.0, constant: 0.0)
376 | ]
377 |
378 | contentContainer.translatesAutoresizingMaskIntoConstraints = false
379 | containerConstraints.forEach { $0.isActive = true }
380 |
381 | let contentConstraints = [
382 | NSLayoutConstraint(item: contentContainer, attribute: .top, relatedBy: .equal, toItem: contentView, attribute: .top, multiplier: 1.0, constant: 0.0),
383 | NSLayoutConstraint(item: contentContainer, attribute: .leading, relatedBy: .equal, toItem: contentView, attribute: .leading, multiplier: 1.0, constant: 0.0),
384 | NSLayoutConstraint(item: contentContainer, attribute: .trailing, relatedBy: .equal, toItem: contentView, attribute: .trailing, multiplier: 1.0, constant: 0.0),
385 | NSLayoutConstraint(item: contentContainer, attribute: .bottom, relatedBy: .equal, toItem: contentView, attribute: .bottom, multiplier: 1.0, constant: 0.0)
386 | ]
387 |
388 | contentView?.translatesAutoresizingMaskIntoConstraints = false
389 | contentConstraints.forEach { $0.isActive = true }
390 |
391 | // Tint view
392 |
393 | let tintConstraints = [
394 | NSLayoutConstraint(item: self, attribute: .top, relatedBy: .equal, toItem: tintView, attribute: .top, multiplier: 1.0, constant: 0.0),
395 | NSLayoutConstraint(item: self, attribute: .leading, relatedBy: .equal, toItem: tintView, attribute: .leading, multiplier: 1.0, constant: 0.0),
396 | NSLayoutConstraint(item: self, attribute: .trailing, relatedBy: .equal, toItem: tintView, attribute: .trailing, multiplier: 1.0, constant: 0.0),
397 | NSLayoutConstraint(item: self, attribute: .bottom, relatedBy: .equal, toItem: tintView, attribute: .bottom, multiplier: 1.0, constant: 0.0)
398 | ]
399 |
400 | tintView.translatesAutoresizingMaskIntoConstraints = false
401 | tintConstraints.forEach { $0.isActive = true }
402 |
403 | // Existing layout invalidation
404 |
405 | setNeedsLayout()
406 | layoutIfNeeded()
407 | }
408 |
409 | // MARK: - Stack events -
410 |
411 | @objc func stackViewPoppedItem(notification: Notification) {
412 | guard let stackNotification = try? StackFlowNotification(plainNotification: notification) else {
413 | return
414 | }
415 |
416 | guard stackNotification.stackFlowView.doesOwn(item: self) else {
417 | return
418 | }
419 |
420 | let isLastInStack = stackNotification.stackFlowView.lastItem == self
421 |
422 | headerView?.popButton = (!isFirstInStack && isLastInStack) ? UIButton(type: .custom) : nil
423 | headerView?.pushButton = isLastInStack ? UIButton(type: .custom) : nil
424 | }
425 |
426 | @objc func stackViewPushedItem(notification: Notification) {
427 | guard let stackNotification = try? StackFlowNotification(plainNotification: notification) else {
428 | return
429 | }
430 |
431 | guard stackNotification.stackFlowView.doesOwn(item: self) else {
432 | return
433 | }
434 |
435 | let isLastInStack = stackNotification.stackFlowView.lastItem == self
436 |
437 | headerView?.popButton = (!isFirstInStack && isLastInStack) ? UIButton(type: .custom) : nil
438 | headerView?.pushButton = isLastInStack ? UIButton(type: .custom) : nil
439 | }
440 |
441 | // MARK: - Control events -
442 |
443 | @objc func popTapped() {
444 | delegate?.stackItemRequestedPop(self)
445 | }
446 |
447 | @objc func pushTapped() {
448 | delegate?.stackItemRequestedPush(self)
449 | }
450 | }
451 |
--------------------------------------------------------------------------------
/StackFlowViewDemo/StackFlowViewDemo.xcodeproj/project.pbxproj:
--------------------------------------------------------------------------------
1 | // !$*UTF8*$!
2 | {
3 | archiveVersion = 1;
4 | classes = {
5 | };
6 | objectVersion = 48;
7 | objects = {
8 |
9 | /* Begin PBXBuildFile section */
10 | FFE891512011428700947221 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFE891502011428700947221 /* AppDelegate.swift */; };
11 | FFE891532011428700947221 /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFE891522011428700947221 /* ViewController.swift */; };
12 | FFE891562011428700947221 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = FFE891542011428700947221 /* Main.storyboard */; };
13 | FFE891582011428700947221 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = FFE891572011428700947221 /* Assets.xcassets */; };
14 | FFE8915B2011428700947221 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = FFE891592011428700947221 /* LaunchScreen.storyboard */; };
15 | FFE89166201142B200947221 /* StackFlow.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFE89163201142B200947221 /* StackFlow.swift */; };
16 | FFE89167201142B200947221 /* StackFlowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFE89164201142B200947221 /* StackFlowView.swift */; };
17 | FFE89168201142B200947221 /* StackItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFE89165201142B200947221 /* StackItemView.swift */; };
18 | FFE8916E20122FAA00947221 /* DemoItemView.xib in Resources */ = {isa = PBXBuildFile; fileRef = FFE8916D20122FAA00947221 /* DemoItemView.xib */; };
19 | FFE8917020122FBB00947221 /* DemoItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFE8916F20122FBB00947221 /* DemoItemView.swift */; };
20 | FFE89173201231F200947221 /* Utils.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFE89172201231F200947221 /* Utils.swift */; };
21 | FFE8917820123E7D00947221 /* CustomStackFlowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFE8917720123E7D00947221 /* CustomStackFlowView.swift */; };
22 | FFF5BA962014AB2100ABAA05 /* bg.jpg in Resources */ = {isa = PBXBuildFile; fileRef = FFF5BA952014AB2100ABAA05 /* bg.jpg */; };
23 | FFF5BA9E2017CA6E00ABAA05 /* Defines.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFF5BA9D2017CA6E00ABAA05 /* Defines.swift */; };
24 | FFF5BAA32017CB2600ABAA05 /* UIKit+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFF5BAA02017CB2600ABAA05 /* UIKit+Extensions.swift */; };
25 | FFF5BAA42017CB2600ABAA05 /* SwiftLibrary+Extentions.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFF5BAA12017CB2600ABAA05 /* SwiftLibrary+Extentions.swift */; };
26 | FFF5BAA52017CB2600ABAA05 /* Foundation+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFF5BAA22017CB2600ABAA05 /* Foundation+Extensions.swift */; };
27 | /* End PBXBuildFile section */
28 |
29 | /* Begin PBXFileReference section */
30 | FFE8914D2011428700947221 /* StackFlowViewDemo.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = StackFlowViewDemo.app; sourceTree = BUILT_PRODUCTS_DIR; };
31 | FFE891502011428700947221 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; };
32 | FFE891522011428700947221 /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; };
33 | FFE891552011428700947221 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; };
34 | FFE891572011428700947221 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; };
35 | FFE8915A2011428700947221 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; };
36 | FFE8915C2011428700947221 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; };
37 | FFE89163201142B200947221 /* StackFlow.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StackFlow.swift; sourceTree = ""; };
38 | FFE89164201142B200947221 /* StackFlowView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StackFlowView.swift; sourceTree = ""; };
39 | FFE89165201142B200947221 /* StackItemView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StackItemView.swift; sourceTree = ""; };
40 | FFE8916D20122FAA00947221 /* DemoItemView.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = DemoItemView.xib; sourceTree = ""; };
41 | FFE8916F20122FBB00947221 /* DemoItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DemoItemView.swift; sourceTree = ""; };
42 | FFE89172201231F200947221 /* Utils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Utils.swift; sourceTree = ""; };
43 | FFE8917720123E7D00947221 /* CustomStackFlowView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomStackFlowView.swift; sourceTree = ""; };
44 | FFF5BA952014AB2100ABAA05 /* bg.jpg */ = {isa = PBXFileReference; lastKnownFileType = image.jpeg; path = bg.jpg; sourceTree = ""; };
45 | FFF5BA9D2017CA6E00ABAA05 /* Defines.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Defines.swift; sourceTree = ""; };
46 | FFF5BAA02017CB2600ABAA05 /* UIKit+Extensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UIKit+Extensions.swift"; sourceTree = ""; };
47 | FFF5BAA12017CB2600ABAA05 /* SwiftLibrary+Extentions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "SwiftLibrary+Extentions.swift"; sourceTree = ""; };
48 | FFF5BAA22017CB2600ABAA05 /* Foundation+Extensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Foundation+Extensions.swift"; sourceTree = ""; };
49 | /* End PBXFileReference section */
50 |
51 | /* Begin PBXFrameworksBuildPhase section */
52 | FFE8914A2011428700947221 /* Frameworks */ = {
53 | isa = PBXFrameworksBuildPhase;
54 | buildActionMask = 2147483647;
55 | files = (
56 | );
57 | runOnlyForDeploymentPostprocessing = 0;
58 | };
59 | /* End PBXFrameworksBuildPhase section */
60 |
61 | /* Begin PBXGroup section */
62 | FFE891442011428700947221 = {
63 | isa = PBXGroup;
64 | children = (
65 | FFE89162201142B200947221 /* StackFlowView */,
66 | FFE8914F2011428700947221 /* StackFlowViewDemo */,
67 | FFE8914E2011428700947221 /* Products */,
68 | );
69 | sourceTree = "";
70 | };
71 | FFE8914E2011428700947221 /* Products */ = {
72 | isa = PBXGroup;
73 | children = (
74 | FFE8914D2011428700947221 /* StackFlowViewDemo.app */,
75 | );
76 | name = Products;
77 | sourceTree = "";
78 | };
79 | FFE8914F2011428700947221 /* StackFlowViewDemo */ = {
80 | isa = PBXGroup;
81 | children = (
82 | FFF5BA9F2017CB2600ABAA05 /* Extensions */,
83 | FFF5BA982014AB3E00ABAA05 /* Misc */,
84 | FFE89171201231E600947221 /* Utils */,
85 | FFE8916C20122F8000947221 /* Controllers */,
86 | FFE8916B20122F7B00947221 /* Views */,
87 | FFF5BA932014AB2100ABAA05 /* Res */,
88 | );
89 | path = StackFlowViewDemo;
90 | sourceTree = "";
91 | };
92 | FFE89162201142B200947221 /* StackFlowView */ = {
93 | isa = PBXGroup;
94 | children = (
95 | FFE89163201142B200947221 /* StackFlow.swift */,
96 | FFE89164201142B200947221 /* StackFlowView.swift */,
97 | FFE89165201142B200947221 /* StackItemView.swift */,
98 | );
99 | name = StackFlowView;
100 | path = ../StackFlowView;
101 | sourceTree = "";
102 | };
103 | FFE8916B20122F7B00947221 /* Views */ = {
104 | isa = PBXGroup;
105 | children = (
106 | FFF5BA972014AB3100ABAA05 /* Storyboards */,
107 | FFE8917620123E5B00947221 /* CustomStackView */,
108 | FFF5BA992014AB9100ABAA05 /* DemoItemView */,
109 | );
110 | path = Views;
111 | sourceTree = "";
112 | };
113 | FFE8916C20122F8000947221 /* Controllers */ = {
114 | isa = PBXGroup;
115 | children = (
116 | FFE891522011428700947221 /* ViewController.swift */,
117 | );
118 | path = Controllers;
119 | sourceTree = "";
120 | };
121 | FFE89171201231E600947221 /* Utils */ = {
122 | isa = PBXGroup;
123 | children = (
124 | FFE89172201231F200947221 /* Utils.swift */,
125 | );
126 | path = Utils;
127 | sourceTree = "";
128 | };
129 | FFE8917620123E5B00947221 /* CustomStackView */ = {
130 | isa = PBXGroup;
131 | children = (
132 | FFE8917720123E7D00947221 /* CustomStackFlowView.swift */,
133 | );
134 | path = CustomStackView;
135 | sourceTree = "";
136 | };
137 | FFF5BA932014AB2100ABAA05 /* Res */ = {
138 | isa = PBXGroup;
139 | children = (
140 | FFE891572011428700947221 /* Assets.xcassets */,
141 | FFF5BA942014AB2100ABAA05 /* UI */,
142 | );
143 | path = Res;
144 | sourceTree = "";
145 | };
146 | FFF5BA942014AB2100ABAA05 /* UI */ = {
147 | isa = PBXGroup;
148 | children = (
149 | FFF5BA952014AB2100ABAA05 /* bg.jpg */,
150 | );
151 | path = UI;
152 | sourceTree = "";
153 | };
154 | FFF5BA972014AB3100ABAA05 /* Storyboards */ = {
155 | isa = PBXGroup;
156 | children = (
157 | FFE891542011428700947221 /* Main.storyboard */,
158 | FFE891592011428700947221 /* LaunchScreen.storyboard */,
159 | );
160 | path = Storyboards;
161 | sourceTree = "";
162 | };
163 | FFF5BA982014AB3E00ABAA05 /* Misc */ = {
164 | isa = PBXGroup;
165 | children = (
166 | FFE891502011428700947221 /* AppDelegate.swift */,
167 | FFE8915C2011428700947221 /* Info.plist */,
168 | FFF5BA9D2017CA6E00ABAA05 /* Defines.swift */,
169 | );
170 | path = Misc;
171 | sourceTree = "";
172 | };
173 | FFF5BA992014AB9100ABAA05 /* DemoItemView */ = {
174 | isa = PBXGroup;
175 | children = (
176 | FFE8916D20122FAA00947221 /* DemoItemView.xib */,
177 | FFE8916F20122FBB00947221 /* DemoItemView.swift */,
178 | );
179 | path = DemoItemView;
180 | sourceTree = "";
181 | };
182 | FFF5BA9F2017CB2600ABAA05 /* Extensions */ = {
183 | isa = PBXGroup;
184 | children = (
185 | FFF5BAA02017CB2600ABAA05 /* UIKit+Extensions.swift */,
186 | FFF5BAA12017CB2600ABAA05 /* SwiftLibrary+Extentions.swift */,
187 | FFF5BAA22017CB2600ABAA05 /* Foundation+Extensions.swift */,
188 | );
189 | path = Extensions;
190 | sourceTree = "";
191 | };
192 | /* End PBXGroup section */
193 |
194 | /* Begin PBXNativeTarget section */
195 | FFE8914C2011428700947221 /* StackFlowViewDemo */ = {
196 | isa = PBXNativeTarget;
197 | buildConfigurationList = FFE8915F2011428700947221 /* Build configuration list for PBXNativeTarget "StackFlowViewDemo" */;
198 | buildPhases = (
199 | FFE891492011428700947221 /* Sources */,
200 | FFE8914A2011428700947221 /* Frameworks */,
201 | FFE8914B2011428700947221 /* Resources */,
202 | );
203 | buildRules = (
204 | );
205 | dependencies = (
206 | );
207 | name = StackFlowViewDemo;
208 | productName = StackFlowViewDemo;
209 | productReference = FFE8914D2011428700947221 /* StackFlowViewDemo.app */;
210 | productType = "com.apple.product-type.application";
211 | };
212 | /* End PBXNativeTarget section */
213 |
214 | /* Begin PBXProject section */
215 | FFE891452011428700947221 /* Project object */ = {
216 | isa = PBXProject;
217 | attributes = {
218 | LastSwiftUpdateCheck = 0910;
219 | LastUpgradeCheck = 0910;
220 | ORGANIZATIONNAME = "0xNSHuman";
221 | TargetAttributes = {
222 | FFE8914C2011428700947221 = {
223 | CreatedOnToolsVersion = 9.1;
224 | ProvisioningStyle = Automatic;
225 | };
226 | };
227 | };
228 | buildConfigurationList = FFE891482011428700947221 /* Build configuration list for PBXProject "StackFlowViewDemo" */;
229 | compatibilityVersion = "Xcode 8.0";
230 | developmentRegion = en;
231 | hasScannedForEncodings = 0;
232 | knownRegions = (
233 | en,
234 | Base,
235 | );
236 | mainGroup = FFE891442011428700947221;
237 | productRefGroup = FFE8914E2011428700947221 /* Products */;
238 | projectDirPath = "";
239 | projectRoot = "";
240 | targets = (
241 | FFE8914C2011428700947221 /* StackFlowViewDemo */,
242 | );
243 | };
244 | /* End PBXProject section */
245 |
246 | /* Begin PBXResourcesBuildPhase section */
247 | FFE8914B2011428700947221 /* Resources */ = {
248 | isa = PBXResourcesBuildPhase;
249 | buildActionMask = 2147483647;
250 | files = (
251 | FFF5BA962014AB2100ABAA05 /* bg.jpg in Resources */,
252 | FFE8916E20122FAA00947221 /* DemoItemView.xib in Resources */,
253 | FFE8915B2011428700947221 /* LaunchScreen.storyboard in Resources */,
254 | FFE891582011428700947221 /* Assets.xcassets in Resources */,
255 | FFE891562011428700947221 /* Main.storyboard in Resources */,
256 | );
257 | runOnlyForDeploymentPostprocessing = 0;
258 | };
259 | /* End PBXResourcesBuildPhase section */
260 |
261 | /* Begin PBXSourcesBuildPhase section */
262 | FFE891492011428700947221 /* Sources */ = {
263 | isa = PBXSourcesBuildPhase;
264 | buildActionMask = 2147483647;
265 | files = (
266 | FFE8917020122FBB00947221 /* DemoItemView.swift in Sources */,
267 | FFE891532011428700947221 /* ViewController.swift in Sources */,
268 | FFE89166201142B200947221 /* StackFlow.swift in Sources */,
269 | FFE89173201231F200947221 /* Utils.swift in Sources */,
270 | FFF5BAA52017CB2600ABAA05 /* Foundation+Extensions.swift in Sources */,
271 | FFE891512011428700947221 /* AppDelegate.swift in Sources */,
272 | FFE89167201142B200947221 /* StackFlowView.swift in Sources */,
273 | FFF5BAA32017CB2600ABAA05 /* UIKit+Extensions.swift in Sources */,
274 | FFE8917820123E7D00947221 /* CustomStackFlowView.swift in Sources */,
275 | FFF5BA9E2017CA6E00ABAA05 /* Defines.swift in Sources */,
276 | FFF5BAA42017CB2600ABAA05 /* SwiftLibrary+Extentions.swift in Sources */,
277 | FFE89168201142B200947221 /* StackItemView.swift in Sources */,
278 | );
279 | runOnlyForDeploymentPostprocessing = 0;
280 | };
281 | /* End PBXSourcesBuildPhase section */
282 |
283 | /* Begin PBXVariantGroup section */
284 | FFE891542011428700947221 /* Main.storyboard */ = {
285 | isa = PBXVariantGroup;
286 | children = (
287 | FFE891552011428700947221 /* Base */,
288 | );
289 | name = Main.storyboard;
290 | sourceTree = "";
291 | };
292 | FFE891592011428700947221 /* LaunchScreen.storyboard */ = {
293 | isa = PBXVariantGroup;
294 | children = (
295 | FFE8915A2011428700947221 /* Base */,
296 | );
297 | name = LaunchScreen.storyboard;
298 | sourceTree = "";
299 | };
300 | /* End PBXVariantGroup section */
301 |
302 | /* Begin XCBuildConfiguration section */
303 | FFE8915D2011428700947221 /* Debug */ = {
304 | isa = XCBuildConfiguration;
305 | buildSettings = {
306 | ALWAYS_SEARCH_USER_PATHS = NO;
307 | CLANG_ANALYZER_NONNULL = YES;
308 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
309 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
310 | CLANG_CXX_LIBRARY = "libc++";
311 | CLANG_ENABLE_MODULES = YES;
312 | CLANG_ENABLE_OBJC_ARC = YES;
313 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
314 | CLANG_WARN_BOOL_CONVERSION = YES;
315 | CLANG_WARN_COMMA = YES;
316 | CLANG_WARN_CONSTANT_CONVERSION = YES;
317 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
318 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
319 | CLANG_WARN_EMPTY_BODY = YES;
320 | CLANG_WARN_ENUM_CONVERSION = YES;
321 | CLANG_WARN_INFINITE_RECURSION = YES;
322 | CLANG_WARN_INT_CONVERSION = YES;
323 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
324 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
325 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
326 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
327 | CLANG_WARN_STRICT_PROTOTYPES = YES;
328 | CLANG_WARN_SUSPICIOUS_MOVE = YES;
329 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
330 | CLANG_WARN_UNREACHABLE_CODE = YES;
331 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
332 | CODE_SIGN_IDENTITY = "iPhone Developer";
333 | COPY_PHASE_STRIP = NO;
334 | DEBUG_INFORMATION_FORMAT = dwarf;
335 | ENABLE_STRICT_OBJC_MSGSEND = YES;
336 | ENABLE_TESTABILITY = YES;
337 | GCC_C_LANGUAGE_STANDARD = gnu11;
338 | GCC_DYNAMIC_NO_PIC = NO;
339 | GCC_NO_COMMON_BLOCKS = YES;
340 | GCC_OPTIMIZATION_LEVEL = 0;
341 | GCC_PREPROCESSOR_DEFINITIONS = (
342 | "DEBUG=1",
343 | "$(inherited)",
344 | );
345 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
346 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
347 | GCC_WARN_UNDECLARED_SELECTOR = YES;
348 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
349 | GCC_WARN_UNUSED_FUNCTION = YES;
350 | GCC_WARN_UNUSED_VARIABLE = YES;
351 | IPHONEOS_DEPLOYMENT_TARGET = 11.1;
352 | MTL_ENABLE_DEBUG_INFO = YES;
353 | ONLY_ACTIVE_ARCH = YES;
354 | SDKROOT = iphoneos;
355 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
356 | SWIFT_OPTIMIZATION_LEVEL = "-Onone";
357 | };
358 | name = Debug;
359 | };
360 | FFE8915E2011428700947221 /* Release */ = {
361 | isa = XCBuildConfiguration;
362 | buildSettings = {
363 | ALWAYS_SEARCH_USER_PATHS = NO;
364 | CLANG_ANALYZER_NONNULL = YES;
365 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
366 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
367 | CLANG_CXX_LIBRARY = "libc++";
368 | CLANG_ENABLE_MODULES = YES;
369 | CLANG_ENABLE_OBJC_ARC = YES;
370 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
371 | CLANG_WARN_BOOL_CONVERSION = YES;
372 | CLANG_WARN_COMMA = YES;
373 | CLANG_WARN_CONSTANT_CONVERSION = YES;
374 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
375 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
376 | CLANG_WARN_EMPTY_BODY = YES;
377 | CLANG_WARN_ENUM_CONVERSION = YES;
378 | CLANG_WARN_INFINITE_RECURSION = YES;
379 | CLANG_WARN_INT_CONVERSION = YES;
380 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
381 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
382 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
383 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
384 | CLANG_WARN_STRICT_PROTOTYPES = YES;
385 | CLANG_WARN_SUSPICIOUS_MOVE = YES;
386 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
387 | CLANG_WARN_UNREACHABLE_CODE = YES;
388 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
389 | CODE_SIGN_IDENTITY = "iPhone Developer";
390 | COPY_PHASE_STRIP = NO;
391 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
392 | ENABLE_NS_ASSERTIONS = NO;
393 | ENABLE_STRICT_OBJC_MSGSEND = YES;
394 | GCC_C_LANGUAGE_STANDARD = gnu11;
395 | GCC_NO_COMMON_BLOCKS = YES;
396 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
397 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
398 | GCC_WARN_UNDECLARED_SELECTOR = YES;
399 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
400 | GCC_WARN_UNUSED_FUNCTION = YES;
401 | GCC_WARN_UNUSED_VARIABLE = YES;
402 | IPHONEOS_DEPLOYMENT_TARGET = 11.1;
403 | MTL_ENABLE_DEBUG_INFO = NO;
404 | SDKROOT = iphoneos;
405 | SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule";
406 | VALIDATE_PRODUCT = YES;
407 | };
408 | name = Release;
409 | };
410 | FFE891602011428700947221 /* Debug */ = {
411 | isa = XCBuildConfiguration;
412 | buildSettings = {
413 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
414 | CODE_SIGN_STYLE = Automatic;
415 | DEVELOPMENT_TEAM = T55G7JLVK5;
416 | INFOPLIST_FILE = StackFlowViewDemo/Misc/Info.plist;
417 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
418 | PRODUCT_BUNDLE_IDENTIFIER = com.vladaverin.StackFlowViewDemo;
419 | PRODUCT_NAME = "$(TARGET_NAME)";
420 | SWIFT_VERSION = 4.0;
421 | TARGETED_DEVICE_FAMILY = "1,2";
422 | };
423 | name = Debug;
424 | };
425 | FFE891612011428700947221 /* Release */ = {
426 | isa = XCBuildConfiguration;
427 | buildSettings = {
428 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
429 | CODE_SIGN_STYLE = Automatic;
430 | DEVELOPMENT_TEAM = T55G7JLVK5;
431 | INFOPLIST_FILE = StackFlowViewDemo/Misc/Info.plist;
432 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
433 | PRODUCT_BUNDLE_IDENTIFIER = com.vladaverin.StackFlowViewDemo;
434 | PRODUCT_NAME = "$(TARGET_NAME)";
435 | SWIFT_VERSION = 4.0;
436 | TARGETED_DEVICE_FAMILY = "1,2";
437 | };
438 | name = Release;
439 | };
440 | /* End XCBuildConfiguration section */
441 |
442 | /* Begin XCConfigurationList section */
443 | FFE891482011428700947221 /* Build configuration list for PBXProject "StackFlowViewDemo" */ = {
444 | isa = XCConfigurationList;
445 | buildConfigurations = (
446 | FFE8915D2011428700947221 /* Debug */,
447 | FFE8915E2011428700947221 /* Release */,
448 | );
449 | defaultConfigurationIsVisible = 0;
450 | defaultConfigurationName = Release;
451 | };
452 | FFE8915F2011428700947221 /* Build configuration list for PBXNativeTarget "StackFlowViewDemo" */ = {
453 | isa = XCConfigurationList;
454 | buildConfigurations = (
455 | FFE891602011428700947221 /* Debug */,
456 | FFE891612011428700947221 /* Release */,
457 | );
458 | defaultConfigurationIsVisible = 0;
459 | defaultConfigurationName = Release;
460 | };
461 | /* End XCConfigurationList section */
462 | };
463 | rootObject = FFE891452011428700947221 /* Project object */;
464 | }
465 |
--------------------------------------------------------------------------------
/StackFlowViewDemo/StackFlowViewDemo.xcodeproj/project.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/StackFlowViewDemo/StackFlowViewDemo/Controllers/ViewController.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ViewController.swift
3 | // StackFlowViewDemo
4 | //
5 | // Created by 0xNSHuman on 19/01/2018.
6 | // Copyright © 2018 0xNSHuman. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | // Helper function to stick StackFlowView's bounds to its container. You can play with values to test different layouts.
12 |
13 | private func pinBounds(of stackView: StackFlowView, to container: UIView, withInsets insets: (CGFloat, CGFloat, CGFloat, CGFloat)) {
14 | container.constraints.forEach {
15 | guard ($0.firstItem as? UIView) == stackView || ($0.secondItem as? UIView) == stackView else {
16 | return
17 | }
18 |
19 | $0.isActive = false
20 | container.removeConstraint($0)
21 | }
22 |
23 | var stackFlowConstraints: [NSLayoutConstraint]
24 | stackView.translatesAutoresizingMaskIntoConstraints = false
25 |
26 | stackFlowConstraints = [
27 | NSLayoutConstraint(item: stackView, attribute: .top, relatedBy: .equal, toItem: container, attribute: .top, multiplier: 1.0, constant: insets.1),
28 | NSLayoutConstraint(item: stackView, attribute: .bottom, relatedBy: .equal, toItem: container, attribute: .bottom, multiplier: 1.0, constant: -(insets.3)),
29 | NSLayoutConstraint(item: stackView, attribute: .leading, relatedBy: .equal, toItem: container, attribute: .leading, multiplier: 1.0, constant: insets.0),
30 | NSLayoutConstraint(item: stackView, attribute: .trailing, relatedBy: .equal, toItem: container, attribute: .trailing, multiplier: 1.0, constant: -(insets.2))
31 | ]
32 |
33 | stackFlowConstraints.forEach { $0.isActive = true }
34 |
35 | container.setNeedsLayout()
36 | container.layoutIfNeeded()
37 | }
38 |
39 | class ViewController: UIViewController {
40 | // MARK: - Outlets -
41 |
42 | @IBOutlet weak var demoLayoutOne: UIView!
43 | @IBOutlet weak var demoLayoutTwo: UIView!
44 |
45 | // MARK: - Properties -
46 |
47 | private let stackView = CustomStackFlowView()
48 |
49 | // MARK: - Life cycle -
50 |
51 | override func viewDidLoad() {
52 | super.viewDidLoad()
53 | setUp()
54 | }
55 |
56 | // MARK: - Setup -
57 |
58 | private func setUp() {
59 | /* — Here is everything you need to present working StackFlowView — */
60 |
61 | view.addSubview(stackView)
62 |
63 | /* — In this demo we prefer StackFlowView subclassing over composition, so delegate is set inside CustomStackFlowView initialization flow — */
64 | //stackView.delegate = self
65 |
66 | /* — It's probably nice to set up some constraints though if you plan to change layout dynamically — */
67 |
68 | pinBounds(of: stackView, to: view, withInsets: (0, 0, 0, 0))
69 |
70 | /* — Now, OPTIONAL customization time! — */
71 |
72 | // How big should padding next to the stack head be?
73 | stackView.headPadding = 0
74 |
75 | // Which direction should new items be pushed in?
76 | stackView.growthDirection = .down
77 |
78 | // Separate by lines or padding?
79 | stackView.separationStyle = .line(thikness: 2.0, color: .black)
80 | // .padding(size: 20.0)
81 | // .none
82 |
83 | // If you want your stack gradually fade away, you can pick any of the styles, or combine them!
84 | stackView.fadingStyle = .combined(styles:
85 | [
86 | .tint(color: .white, preLastAlpha: 0.9, alphaDecrement: 0.1),
87 | .gradientMask(effectDistance: stackView.bounds.height * 0.7)
88 | ]
89 | ) // Or just .none
90 |
91 | // You can swipe up-down or left-right to control your flow, and/or also tap inactive stack area to pop any number of items (depends on where you tap)
92 | stackView.userNavigationOptions = [.swipe, .tap]
93 |
94 | // Fast hops or sloooooow animation?
95 | stackView.transitionDuration = 0.25
96 |
97 | // Set to false if you don't need automatic safe area detection/adoption
98 | stackView.isSeekingSafeArea = true
99 |
100 | // Set to false to turn off stretch-out behaviour for your content items during autolayout updates
101 | stackView.isAutoresizingItems = true
102 |
103 | /* — ——— — */
104 |
105 | /* — Now let's kick off the flow — */
106 |
107 | // This is not necessary, we could just use any available controls (like swipe gesture) on StackFlowView to present the first flow item.
108 | stackView.resetFlow()
109 |
110 | /* — Ok, that was to get you onboard with all the features. Now we're going to discard this stackView, and instead of it try a set of predefined demo cases (if you wish) — */
111 |
112 | let weWantToSeeMoreDemos = false
113 | guard weWantToSeeMoreDemos else {
114 | return
115 | }
116 |
117 | stackView.removeFromSuperview()
118 |
119 | /* — Change to play with different demo cases — */
120 |
121 | let demoLayoutToUse = demoLayoutOne
122 |
123 | for subview in view.subviews {
124 | guard !subview.isKind(of: UIImageView.self) else { continue }
125 |
126 | guard subview != demoLayoutToUse else { subview.isHidden = false; continue }
127 | subview.isHidden = true
128 | }
129 |
130 | for layoutContainer in demoLayoutToUse!.subviews {
131 | let stackFlowView = CustomStackFlowView()
132 |
133 | let caseToTest: DemoCase = {
134 | // Return any case you want. Note, you may get weird behaviour here, since there are lots of parameters to play with in this or other demo files — it's just a playground. Please refer documentation if you want clear instructions.
135 |
136 | return .verticalCardsStack
137 |
138 | return .pageStack(direction: {
139 | return layoutContainer.bounds.width < layoutContainer.bounds.height ? .down : .right
140 | }())
141 |
142 | return .stackOfStacks
143 |
144 | return .feedFallingDown
145 |
146 | return .pageStack(direction: .right)
147 | }()
148 |
149 | layoutContainer.addSubview(stackFlowView)
150 |
151 | configure(stackView: stackFlowView, inside: layoutContainer, as: caseToTest)
152 | //stackFlowView.resetFlow()
153 | }
154 | }
155 | }
156 |
157 | extension ViewController {
158 | override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
159 | super.viewWillTransition(to: size, with: coordinator)
160 | }
161 | }
162 |
163 | // MARK: - Demo cases -
164 |
165 | extension ViewController {
166 | enum DemoCase {
167 | case verticalCardsStack
168 | case pageStack(direction: StackFlowView.Direction)
169 | case feedFallingDown
170 | case stackOfStacks
171 | }
172 |
173 | func configure(stackView: StackFlowView, inside container: UIView, as case: DemoCase) {
174 | switch `case` {
175 | case .verticalCardsStack:
176 | pinBounds(of: stackView, to: container, withInsets: (20, 0, 20, 0))
177 |
178 | stackView.headPadding = 20
179 | stackView.growthDirection = .down
180 | stackView.separationStyle = .padding(size: 20)
181 | stackView.fadingStyle = .combined(styles:
182 | [
183 | .tint(color: .white, preLastAlpha: 0.9, alphaDecrement: 0.1),
184 | .gradientMask(effectDistance: stackView.bounds.height * 0.7)
185 | ]
186 | )
187 |
188 | stackView.userNavigationOptions = [.swipe, .tap]
189 | stackView.transitionDuration = 0.25
190 | stackView.isSeekingSafeArea = true
191 | stackView.isAutoresizingItems = true
192 |
193 | case .pageStack(let direction):
194 | pinBounds(of: stackView, to: container, withInsets: (0, 0, 0, 0))
195 |
196 | stackView.headPadding = 0
197 | stackView.growthDirection = direction
198 | stackView.separationStyle = .line(thikness: 2, color: .white)
199 | stackView.fadingStyle = .combined(styles:
200 | [
201 | .tint(color: .white, preLastAlpha: 0.9, alphaDecrement: 0.1)
202 | ]
203 | )
204 |
205 | stackView.userNavigationOptions = [.swipe, .tap]
206 | stackView.transitionDuration = 0.10
207 | stackView.isSeekingSafeArea = true
208 | stackView.isAutoresizingItems = true
209 |
210 | case .feedFallingDown:
211 | pinBounds(of: stackView, to: container, withInsets: (10, 0, 10, 10))
212 |
213 | stackView.headPadding = 20
214 | stackView.growthDirection = .up
215 | stackView.separationStyle = .line(thikness: 6, color: .white)
216 | stackView.fadingStyle = .tint(color: .white, preLastAlpha: 0.8, alphaDecrement: 0.15)
217 |
218 | stackView.userNavigationOptions = [.swipe, .tap]
219 | stackView.transitionDuration = 0.20
220 | stackView.isSeekingSafeArea = true
221 | stackView.isAutoresizingItems = true
222 |
223 | var timePassed: TimeInterval = 0
224 |
225 | for _ in 0 ..< 24 {
226 | timePassed += 0.05
227 |
228 | Utils.mainQueueTask({
229 | (stackView as? CustomStackFlowView)?.pushNextItem(to: stackView)
230 | }, after: timePassed)
231 | }
232 |
233 | case .stackOfStacks:
234 | pinBounds(of: stackView, to: container, withInsets: (0, 0, 0, 0))
235 |
236 | stackView.headPadding = 0
237 | stackView.growthDirection = .right
238 | stackView.separationStyle = .padding(size: 10)
239 | stackView.fadingStyle = .none
240 |
241 | stackView.userNavigationOptions = [.swipe, .tap]
242 | stackView.transitionDuration = 0.4
243 | stackView.isSeekingSafeArea = false
244 | stackView.isAutoresizingItems = true
245 |
246 | if let customStack = stackView as? CustomStackFlowView {
247 | customStack.customPushFunction = {
248 | customStack.push(
249 | {
250 | let substacksPerScreen: CGFloat = 3
251 | let itemTint = Utils.randomPastelColor()
252 |
253 | let substackWidth: CGFloat = {
254 | if case .padding(let size) = stackView.separationStyle {
255 | return ((customStack.safeSize.width - (size * (substacksPerScreen - 1))) / substacksPerScreen)
256 | }
257 |
258 | return 0
259 | }()
260 |
261 | let itemView = UIView(frame: CGRect(x: 0, y: 0, width: substackWidth, height: customStack.safeSize.height))
262 | itemView.layer.borderWidth = 2.0
263 | itemView.layer.borderColor = itemTint.cgColor
264 |
265 | let subStack = CustomStackFlowView()
266 | itemView.addSubview(subStack)
267 | pinBounds(of: subStack, to: itemView, withInsets: (0, 0, 0, 0))
268 |
269 | subStack.headPadding = 0
270 | subStack.growthDirection = stackView.numberOfItems % 2 > 0 ? .down : .up
271 | subStack.separationStyle = .line(thikness: 2, color: itemTint)
272 | subStack.fadingStyle = .none
273 |
274 | subStack.userNavigationOptions = [.swipe, .tap]
275 | subStack.transitionDuration = 0.20
276 | subStack.isSeekingSafeArea = true
277 | subStack.isAutoresizingItems = true
278 |
279 | return itemView
280 | }()
281 | )
282 | }
283 | }
284 | }
285 | }
286 | }
287 |
--------------------------------------------------------------------------------
/StackFlowViewDemo/StackFlowViewDemo/Extensions/Foundation+Extensions.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Foundation+Extensions.swift
3 | // Created by 0xNSHuman
4 | //
5 |
6 | import Foundation
7 |
8 | extension Date {
9 | static func unixDate(from string: String) -> Date? {
10 | let formatter = DateFormatter()
11 | formatter.locale = Locale(identifier: "en_US_POSIX")
12 | formatter.timeZone = TimeZone(identifier: "UTC")
13 |
14 | formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'"
15 |
16 | if let date = formatter.date(from: string) {
17 | return date
18 | } else {
19 | formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss'Z'"
20 | return formatter.date(from: string)
21 | }
22 | }
23 |
24 | func unixDateString() -> String? {
25 | let formatter = DateFormatter()
26 | formatter.locale = Locale(identifier: "en_US_POSIX")
27 | formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'"
28 | formatter.timeZone = TimeZone(identifier: "UTC")
29 |
30 | return formatter.string(from: self)
31 | }
32 |
33 | func stringRepresentation(format: String) -> String? {
34 | let formatter = DateFormatter()
35 | formatter.dateFormat = format
36 | return formatter.string(from: self)
37 | }
38 | }
39 |
40 | extension Bundle {
41 | func typeFromNib(_ type: T.Type) -> T? {
42 | return Bundle.main.loadNibNamed(String(describing: T.self), owner: nil, options: nil)?.first as? T
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/StackFlowViewDemo/StackFlowViewDemo/Extensions/SwiftLibrary+Extentions.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SwiftLibrary+Extentions.swift
3 | // Created by 0xNSHuman
4 | //
5 |
6 | import Foundation
7 |
8 | private extension String {
9 | var urlEscaped: String {
10 | return self.addingPercentEncoding(withAllowedCharacters: .urlHostAllowed)!
11 | }
12 |
13 | var utf8Encoded: Data {
14 | return self.data(using: .utf8)!
15 | }
16 | }
17 |
18 | extension Dictionary {
19 | static func += (left: inout Dictionary, right: Dictionary) {
20 | // Swift 4 way
21 |
22 | left.merge(right) { (_, new) in new }
23 | return;
24 |
25 | // Swift 3 way
26 |
27 | for (key, value) in right {
28 | left[key] = value
29 | }
30 | }
31 | }
32 |
33 | extension Sequence where Iterator.Element: Hashable {
34 | func unique() -> [Iterator.Element] {
35 | var seen: Set = []
36 | return filter {
37 | if seen.contains($0) {
38 | return false
39 | } else {
40 | seen.insert($0)
41 | return true
42 | }
43 | }
44 | }
45 | }
46 |
47 | // TODO: Not sure about overflow
48 |
49 | extension Int {
50 | static postfix func ++ (_ value: inout Int) {
51 | value = value < Int.max ? value + 1 : Int.max
52 | }
53 |
54 | static postfix func -- (_ value: inout Int) {
55 | value = value > Int.min ? value - 1 : Int.min
56 | }
57 | }
58 |
59 | extension UInt {
60 | static postfix func ++ (_ value: inout UInt) {
61 | value = value < UInt.max ? value + 1 : UInt.max
62 | }
63 |
64 | static postfix func -- (_ value: inout UInt) {
65 | value = value > UInt.min ? value - 1 : UInt.min
66 | }
67 | }
68 |
--------------------------------------------------------------------------------
/StackFlowViewDemo/StackFlowViewDemo/Extensions/UIKit+Extensions.swift:
--------------------------------------------------------------------------------
1 | //
2 | // UIKit+Extensions.swift
3 | // Created by 0xNSHuman
4 | //
5 |
6 | import Foundation
7 | import UIKit
8 |
9 | // MARK: UIImage
10 |
11 | public extension UIImage {
12 | public func applying(tintColor color: UIColor) -> UIImage{
13 | UIGraphicsBeginImageContextWithOptions(self.size, false, self.scale)
14 | let context: CGContext = UIGraphicsGetCurrentContext()!
15 | context.translateBy(x: 0, y: self.size.height)
16 | context.scaleBy(x: 1.0, y: -1.0)
17 | context.setBlendMode(CGBlendMode.normal)
18 | let rect: CGRect = CGRect(x: 0, y: 0, width: self.size.width, height: self.size.height)
19 | context.clip(to: rect, mask: self.cgImage!)
20 | color.setFill()
21 | context.fill(rect);
22 | let newImage: UIImage = UIGraphicsGetImageFromCurrentImageContext()!;
23 | UIGraphicsEndImageContext();
24 | return newImage;
25 | }
26 | }
27 |
28 | public extension UIView {
29 | // Shadow
30 |
31 | func dropShadow(scale: Bool = true, offset: CGFloat = 1.0) {
32 | self.layer.masksToBounds = false
33 | self.layer.shadowColor = UIColor.black.cgColor
34 | self.layer.shadowOpacity = 0.5
35 | self.layer.shadowOffset = CGSize(width: 0, height: offset)
36 | self.layer.shadowRadius = 1
37 |
38 | self.layer.shadowPath = (layer.mask as? CAShapeLayer)?.path ?? UIBezierPath(rect: self.bounds).cgPath
39 | }
40 |
41 | // Mask
42 |
43 | enum GeometryMaskType {
44 | case circle
45 | }
46 |
47 | typealias MaskBorderSettings = (CGFloat, UIColor)
48 |
49 | func applyGeometryMask(type: GeometryMaskType, border: MaskBorderSettings? = nil) {
50 | let maskLayer = CAShapeLayer()
51 | maskLayer.frame = bounds
52 |
53 | maskLayer.path = {
54 | let path = CGMutablePath()
55 |
56 | switch type {
57 | case .circle:
58 | path.addArc(center: CGPoint(x: bounds.width / 2, y: bounds.height / 2), radius: bounds.width / 2, startAngle: -(CGFloat.pi / 2.0), endAngle: 3.0 * CGFloat.pi / 2, clockwise: false)
59 | }
60 |
61 | return path
62 | }()
63 |
64 | layer.mask = maskLayer
65 |
66 | layer.sublayers?.filter({ $0.zPosition == 9999 }).first?.removeFromSuperlayer()
67 |
68 | if let (width, color) = border {
69 | let strokeLayer = CAShapeLayer()
70 | strokeLayer.frame = bounds
71 | strokeLayer.path = maskLayer.path
72 | strokeLayer.lineWidth = width * 2
73 | strokeLayer.strokeColor = color.cgColor
74 | strokeLayer.fillColor = UIColor.clear.cgColor
75 | strokeLayer.zPosition = 9999
76 | layer.addSublayer(strokeLayer)
77 | }
78 | }
79 |
80 | // Constraints
81 |
82 | enum IndentConstraintType {
83 | case left, top, right, bottom
84 | }
85 |
86 | private func removeConstraints(_ cs: [NSLayoutConstraint]) {
87 | cs.forEach {
88 | $0.isActive = false
89 | superview?.removeConstraint($0)
90 | }
91 | }
92 |
93 | func removeAllConstraints() {
94 | removeConstraints(superview?.constraints.filter({ ($0.firstItem as? UIView) == self || ($0.secondItem as? UIView) == self }) ?? [])
95 | removeConstraints(constraints.filter({ ($0.firstItem as? UIView) == self || ($0.secondItem as? UIView) == self }))
96 | }
97 |
98 | var widthConstraint: CGFloat? {
99 | set(value) {
100 | (constraints.flatMap({ return (($0.firstItem as? UIView) == self && $0.firstAttribute == .width) ? $0 : nil }).first)?.constant = value ?? 0.0
101 | }
102 |
103 | get {
104 | return (constraints.flatMap({ return (($0.firstItem as? UIView) == self && $0.firstAttribute == .width) ? $0 : nil }).first)?.constant
105 | }
106 | }
107 |
108 | var heightConstraint: CGFloat? {
109 | set(value) {
110 | let constraint = constraints.flatMap({ return (($0.firstItem as? UIView) == self && $0.firstAttribute == .height) ? $0 : nil }).first
111 | constraint?.constant = value ?? 0.0
112 | }
113 |
114 | get {
115 | let constraint = constraints.flatMap({ return (($0.firstItem as? UIView) == self && $0.firstAttribute == .height) ? $0 : nil }).first
116 | return constraint?.constant
117 | }
118 | }
119 |
120 | var indendationConstraints: (CGFloat?, CGFloat?, CGFloat?, CGFloat?) {
121 | set(value) {
122 | setConstraintConstant(value.0 ?? 0, for: .leading)
123 | setConstraintConstant(value.1 ?? 0, for: .top)
124 | setConstraintConstant(value.2 ?? 0, for: .trailing)
125 | setConstraintConstant(value.3 ?? 0, for: .bottom)
126 | }
127 |
128 | get {
129 | return (
130 | constraintConstant(describing: .leading),
131 | constraintConstant(describing: .top),
132 | constraintConstant(describing: .trailing),
133 | constraintConstant(describing: .bottom)
134 | )
135 | }
136 | }
137 |
138 | private func constraintConstant(describing attribute: NSLayoutAttribute) -> CGFloat? {
139 | return superview?.constraints.filter({ (($0.firstItem as? UIView) == self && $0.firstAttribute == attribute) || (($0.secondItem as? UIView) == self && $0.secondAttribute == attribute) }).first?.constant
140 | }
141 |
142 | private func setConstraintConstant(_ c: CGFloat, for attribute: NSLayoutAttribute){
143 | superview?.constraints.filter({ (($0.firstItem as? UIView) == self && $0.firstAttribute == attribute) || (($0.secondItem as? UIView) == self && $0.secondAttribute == attribute) }).first?.constant = c
144 | }
145 |
146 | // Convenient calculators for dynamic size
147 |
148 | var widthToFit: CGFloat {
149 | return sizeThatFits(CGSize(width: CGFloat.greatestFiniteMagnitude, height: heightConstraint ?? bounds.height)).width
150 | }
151 |
152 | var heightToFit: CGFloat {
153 | return sizeThatFits(CGSize(width: widthConstraint ?? bounds.width, height: CGFloat.greatestFiniteMagnitude)).height
154 | }
155 | }
156 |
157 | public extension UIImageView {
158 | public convenience init(frame: CGRect, image: UIImage?, contentMode: UIViewContentMode = .scaleAspectFit) {
159 |
160 | self.init(frame: frame)
161 | self.image = image
162 | self.contentMode = contentMode
163 | }
164 | }
165 |
166 | public extension UITableView {
167 | func dequeueCell(_ type: CellType.Type) -> CellType {
168 | return dequeueReusableCell(withIdentifier: String(describing: type)) as! CellType
169 | }
170 | }
171 |
172 | public extension UICollectionView {
173 | func dequeueCell(_ type: CellType.Type, for indexPath: IndexPath) -> CellType {
174 | return dequeueReusableCell(withReuseIdentifier: String(describing: type), for: indexPath) as! CellType
175 | }
176 | }
177 |
178 | public extension UIViewController {
179 | enum NavigationBarZone {
180 | case left, right
181 | }
182 |
183 | func setNavigationViews(_ views: [UIView], on zone: NavigationBarZone) {
184 | var barButtonItems = [UIBarButtonItem]()
185 |
186 | for view in views {
187 | let barItem = UIBarButtonItem(customView: view)
188 |
189 | if zone == .left, views.first != view {
190 | barButtonItems.append({
191 | let space = UIBarButtonItem.init(barButtonSystemItem: .fixedSpace, target: nil, action: nil)
192 | space.width = 10.0
193 |
194 | return space
195 | }()
196 | )
197 | }
198 |
199 | barButtonItems.append(barItem)
200 | }
201 |
202 | if zone == .left {
203 | navigationItem.leftBarButtonItems = barButtonItems
204 | } else {
205 | navigationItem.rightBarButtonItems = barButtonItems
206 | }
207 | }
208 |
209 | func makeKeyboardHidableByTapAround() {
210 | let tap: UITapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(UIViewController.dismissKeyboard))
211 | tap.cancelsTouchesInView = false
212 | view.addGestureRecognizer(tap)
213 | }
214 |
215 | @objc func dismissKeyboard() {
216 | view.endEditing(true)
217 | }
218 | }
219 |
220 | public extension UINavigationController {
221 | typealias PopCompletion = () -> ()
222 |
223 | func pushFromRootViewController(_ vc: UIViewController, animated: Bool) {
224 | guard let rootVC = viewControllers.first else {
225 | setViewControllers([vc], animated: animated)
226 | return
227 | }
228 |
229 | setViewControllers([rootVC, vc], animated: animated)
230 | }
231 |
232 | func popToRootViewController(animated: Bool, completion: PopCompletion? = nil) {
233 | CATransaction.begin()
234 | CATransaction.setCompletionBlock(completion)
235 |
236 | _ = popToRootViewController(animated: animated)
237 |
238 | CATransaction.commit()
239 | }
240 | }
241 |
--------------------------------------------------------------------------------
/StackFlowViewDemo/StackFlowViewDemo/Misc/AppDelegate.swift:
--------------------------------------------------------------------------------
1 | //
2 | // AppDelegate.swift
3 | // StackFlowViewDemo
4 | //
5 | // Created by 0xNSHuman on 19/01/2018.
6 | // Copyright © 2018 0xNSHuman. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | @UIApplicationMain
12 | class AppDelegate: UIResponder, UIApplicationDelegate {
13 |
14 | var window: UIWindow?
15 |
16 |
17 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
18 | // Override point for customization after application launch.
19 | return true
20 | }
21 |
22 | func applicationWillResignActive(_ application: UIApplication) {
23 | // Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state.
24 | // Use this method to pause ongoing tasks, disable timers, and invalidate graphics rendering callbacks. Games should use this method to pause the game.
25 | }
26 |
27 | func applicationDidEnterBackground(_ application: UIApplication) {
28 | // Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later.
29 | // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits.
30 | }
31 |
32 | func applicationWillEnterForeground(_ application: UIApplication) {
33 | // Called as part of the transition from the background to the active state; here you can undo many of the changes made on entering the background.
34 | }
35 |
36 | func applicationDidBecomeActive(_ application: UIApplication) {
37 | // Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface.
38 | }
39 |
40 | func applicationWillTerminate(_ application: UIApplication) {
41 | // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:.
42 | }
43 |
44 |
45 | }
46 |
47 |
--------------------------------------------------------------------------------
/StackFlowViewDemo/StackFlowViewDemo/Misc/Defines.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Defines.swift
3 | // StackFlowViewDemo
4 | //
5 | // Created by 0xNSHuman on 23/01/2018.
6 | // Copyright © 2018 0xNSHuman. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import UIKit
11 |
12 | struct Images {
13 | struct Navigation {
14 | static let back = UIImage(named: "back")!.applying(tintColor: .white)
15 | static let forward = UIImage(named: "fwd")!.applying(tintColor: .white)
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/StackFlowViewDemo/StackFlowViewDemo/Misc/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | $(DEVELOPMENT_LANGUAGE)
7 | CFBundleExecutable
8 | $(EXECUTABLE_NAME)
9 | CFBundleIdentifier
10 | $(PRODUCT_BUNDLE_IDENTIFIER)
11 | CFBundleInfoDictionaryVersion
12 | 6.0
13 | CFBundleName
14 | $(PRODUCT_NAME)
15 | CFBundlePackageType
16 | APPL
17 | CFBundleShortVersionString
18 | 1.0
19 | CFBundleVersion
20 | 1
21 | LSRequiresIPhoneOS
22 |
23 | UILaunchStoryboardName
24 | LaunchScreen
25 | UIMainStoryboardFile
26 | Main
27 | UIRequiredDeviceCapabilities
28 |
29 | armv7
30 |
31 | UISupportedInterfaceOrientations
32 |
33 | UIInterfaceOrientationPortrait
34 | UIInterfaceOrientationLandscapeLeft
35 | UIInterfaceOrientationLandscapeRight
36 |
37 | UISupportedInterfaceOrientations~ipad
38 |
39 | UIInterfaceOrientationPortrait
40 | UIInterfaceOrientationPortraitUpsideDown
41 | UIInterfaceOrientationLandscapeLeft
42 | UIInterfaceOrientationLandscapeRight
43 |
44 |
45 |
46 |
--------------------------------------------------------------------------------
/StackFlowViewDemo/StackFlowViewDemo/Res/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 | }
--------------------------------------------------------------------------------
/StackFlowViewDemo/StackFlowViewDemo/Res/Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "version" : 1,
4 | "author" : "xcode"
5 | }
6 | }
--------------------------------------------------------------------------------
/StackFlowViewDemo/StackFlowViewDemo/Res/Assets.xcassets/back.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "universal",
5 | "filename" : "back.png",
6 | "scale" : "1x"
7 | },
8 | {
9 | "idiom" : "universal",
10 | "filename" : "back-1.png",
11 | "scale" : "2x"
12 | },
13 | {
14 | "idiom" : "universal",
15 | "filename" : "back-2.png",
16 | "scale" : "3x"
17 | }
18 | ],
19 | "info" : {
20 | "version" : 1,
21 | "author" : "xcode"
22 | }
23 | }
--------------------------------------------------------------------------------
/StackFlowViewDemo/StackFlowViewDemo/Res/Assets.xcassets/back.imageset/back-1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/0xNSHuman/StackFlowView/60032156606bab7b24dc935348b2afce49902df1/StackFlowViewDemo/StackFlowViewDemo/Res/Assets.xcassets/back.imageset/back-1.png
--------------------------------------------------------------------------------
/StackFlowViewDemo/StackFlowViewDemo/Res/Assets.xcassets/back.imageset/back-2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/0xNSHuman/StackFlowView/60032156606bab7b24dc935348b2afce49902df1/StackFlowViewDemo/StackFlowViewDemo/Res/Assets.xcassets/back.imageset/back-2.png
--------------------------------------------------------------------------------
/StackFlowViewDemo/StackFlowViewDemo/Res/Assets.xcassets/back.imageset/back.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/0xNSHuman/StackFlowView/60032156606bab7b24dc935348b2afce49902df1/StackFlowViewDemo/StackFlowViewDemo/Res/Assets.xcassets/back.imageset/back.png
--------------------------------------------------------------------------------
/StackFlowViewDemo/StackFlowViewDemo/Res/Assets.xcassets/fwd.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "universal",
5 | "filename" : "fwd.png",
6 | "scale" : "1x"
7 | },
8 | {
9 | "idiom" : "universal",
10 | "filename" : "fwd-1.png",
11 | "scale" : "2x"
12 | },
13 | {
14 | "idiom" : "universal",
15 | "filename" : "fwd-2.png",
16 | "scale" : "3x"
17 | }
18 | ],
19 | "info" : {
20 | "version" : 1,
21 | "author" : "xcode"
22 | }
23 | }
--------------------------------------------------------------------------------
/StackFlowViewDemo/StackFlowViewDemo/Res/Assets.xcassets/fwd.imageset/fwd-1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/0xNSHuman/StackFlowView/60032156606bab7b24dc935348b2afce49902df1/StackFlowViewDemo/StackFlowViewDemo/Res/Assets.xcassets/fwd.imageset/fwd-1.png
--------------------------------------------------------------------------------
/StackFlowViewDemo/StackFlowViewDemo/Res/Assets.xcassets/fwd.imageset/fwd-2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/0xNSHuman/StackFlowView/60032156606bab7b24dc935348b2afce49902df1/StackFlowViewDemo/StackFlowViewDemo/Res/Assets.xcassets/fwd.imageset/fwd-2.png
--------------------------------------------------------------------------------
/StackFlowViewDemo/StackFlowViewDemo/Res/Assets.xcassets/fwd.imageset/fwd.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/0xNSHuman/StackFlowView/60032156606bab7b24dc935348b2afce49902df1/StackFlowViewDemo/StackFlowViewDemo/Res/Assets.xcassets/fwd.imageset/fwd.png
--------------------------------------------------------------------------------
/StackFlowViewDemo/StackFlowViewDemo/Res/UI/bg.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/0xNSHuman/StackFlowView/60032156606bab7b24dc935348b2afce49902df1/StackFlowViewDemo/StackFlowViewDemo/Res/UI/bg.jpg
--------------------------------------------------------------------------------
/StackFlowViewDemo/StackFlowViewDemo/Utils/Utils.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Utils.swift
3 | // StackFlowViewDemo
4 | //
5 | // Created by 0xNSHuman on 19/01/2018.
6 | // Copyright © 2018 0xNSHuman. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import UIKit
11 |
12 | // MARK: Shortcuts
13 |
14 | var appWindow: UIWindow? {
15 | guard let window = appDelegate?.window else {
16 | log(message: "[WARNING] Trying to access app window, which doesn't exist!", from: #function)
17 | return nil
18 | }
19 |
20 | return window
21 | }
22 |
23 | var appDelegate: AppDelegate? { return UIApplication.shared.delegate as? AppDelegate }
24 |
25 | struct Utils {
26 |
27 | }
28 |
29 | extension Utils {
30 | static func controllerFromStoryboard(_ type: T.Type) -> T {
31 | return UIStoryboard.init(name: "Main", bundle: nil).instantiateViewController(withIdentifier: String(describing: type)) as! T
32 | }
33 | }
34 |
35 | // MARK: Global heirarchy
36 |
37 | extension Utils {
38 | static func setRootViewController(_ rootVC: UIViewController) {
39 | appDelegate?.window?.subviews.forEach { $0.removeFromSuperview() }
40 | appDelegate?.window = UIWindow(frame: UIScreen.main.bounds)
41 |
42 | appWindow?.backgroundColor = .white
43 | appWindow?.rootViewController = rootVC
44 | appWindow?.makeKeyAndVisible()
45 | }
46 | }
47 |
48 | // MARK: Threads
49 |
50 | extension Utils {
51 | static func mainQueueTask(_ task: @escaping () -> (), after interval: TimeInterval = 0.0) {
52 | DispatchQueue.main.asyncAfter(deadline: .now() + interval, execute: task)
53 | }
54 |
55 | static func bgQueueTask(_ task: @escaping () -> (), after interval: TimeInterval = 0.0) {
56 | DispatchQueue.global(qos: .background).asyncAfter(deadline: .now() + interval, execute: task)
57 | }
58 | }
59 |
60 | // MARK: UIKit Routines
61 |
62 | extension Utils {
63 | static func alert(title: String?, text: String?, handler: (() -> Void)? = nil) -> UIAlertController {
64 | let alert = UIAlertController(title: title, message: text, preferredStyle: .alert)
65 | alert.addAction(UIAlertAction(title: NSLocalizedString("button.ok", value: "Ok", comment: "Alert OK Button"), style: .default, handler: {_ in handler?() }))
66 | return alert
67 | }
68 |
69 | static func animateBlock(_ block: @escaping () -> Void, duration: Double = 0.25, delay: Double = 0.0) {
70 | DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + delay, execute: {
71 | UIView.animate(withDuration: duration, animations: {
72 | block()
73 | })
74 | })
75 | }
76 | }
77 |
78 | // MARK: UI Appearance
79 |
80 | extension Utils {
81 | static func randomPastelColor() -> UIColor {
82 | let randomColorGenerator = { ()-> CGFloat in
83 | CGFloat(arc4random() % 256 ) / 256
84 | }
85 |
86 | let red: CGFloat = randomColorGenerator()
87 | let green: CGFloat = randomColorGenerator()
88 | let blue: CGFloat = randomColorGenerator()
89 |
90 | return UIColor(red: red, green: green, blue: blue, alpha: 1)
91 | }
92 | }
93 |
94 | // MARK: Feedback
95 |
96 | extension Utils {
97 | static func throwAlert(title: String?, text: String?, completion: (() -> ())? = nil) {
98 | appWindow?.rootViewController?.present(alert(title: title, text: text), animated: true, completion: completion)
99 | }
100 | }
101 |
102 | // MARK: Debug
103 |
104 | func log(message: String, from sender: Any) {
105 | print("[\(sender)]: \(message)")
106 | }
107 |
--------------------------------------------------------------------------------
/StackFlowViewDemo/StackFlowViewDemo/Views/CustomStackView/CustomStackFlowView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // CustomStackFlowView.swift
3 | // StackFlowViewDemo
4 | //
5 | // Created by 0xNSHuman on 19/01/2018.
6 | // Copyright © 2018 0xNSHuman. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | /// This is a subclass of StackFlowView, which is one of the possible ways to use it. It lets you build custom behaviour and flow control while enjoying basic navigation features. Another possible way would be making a composition, i.e. using StackFlowView object as a property of another class. It's up tou you and your case which way is better, but for this demo purposes I ended up with subclassing.
12 |
13 | class CustomStackFlowView: StackFlowView {
14 | // MARK: - Types -
15 |
16 | enum FlowStep: Int {
17 | case none = -1
18 | case one = 0, two, three, four, five, six
19 |
20 | static var count: Int { return 6 }
21 |
22 | var title: String {
23 | switch self {
24 | default:
25 | return "Step \(shortSymbol)"
26 | }
27 | }
28 |
29 | var shortSymbol: String {
30 | switch self {
31 | case .one:
32 | return "♦️"
33 |
34 | case .two:
35 | return "♠️"
36 |
37 | case .three:
38 | return "💎"
39 |
40 | case .four:
41 | return "🔮"
42 |
43 | case .five:
44 | return "🎁"
45 |
46 | case .six:
47 | return "📖"
48 |
49 | case .none:
50 | return "❌"
51 | }
52 | }
53 |
54 | var prevStep: FlowStep? {
55 | let prevValue = rawValue - 1
56 | return prevValue >= 0 ? FlowStep(rawValue: prevValue) : nil
57 | }
58 |
59 | var nextStep: FlowStep? {
60 | let nextValue = rawValue + 1
61 | return nextValue < FlowStep.count ? FlowStep(rawValue: nextValue) : nil
62 | }
63 | }
64 |
65 | // MARK: - Properties -
66 |
67 | /* — You can play with finite/infinite stack variations — */
68 | var isEndlessMode = false
69 |
70 | var customPushFunction: (() -> ())? = nil
71 |
72 | // MARK: - State control -
73 |
74 | private var currentStep: FlowStep = .none {
75 | didSet {
76 | let itemTitle = currentStep.title
77 |
78 | let prevItemSymbol = currentStep.prevStep?.shortSymbol
79 | let nextItemSymbol = currentStep.nextStep?.shortSymbol
80 |
81 | let itemView = stepView(for: currentStep)
82 |
83 | // Subfunction to explain possible ways of pushing new item + customization
84 |
85 | func pushStep() {
86 | /* — You can choose how far you want to go in terms of customization of evety item — */
87 |
88 | let chosenWay = 3
89 |
90 | switch chosenWay {
91 | case 1:
92 | // 1. Push your custom view "as is". This way it's just sent to stack flow, and navigaton (push next/pop to previous) is available either through StackFlowView's swipes/taps, or whatever you set yourself in your custom view (the idea is to push/pop after user achieves some goal in your view, such as setting text, picking options, pressing buttons, etc.)
93 | push(itemView)
94 |
95 | case 2:
96 | // 2. Same as above, plus a standard navigation bar on top, showing the title you set, as well as button controls for pop/push. Not much but still nice feature to have in case you don't like gestures, or as part of Accessibility implementation for some users.
97 | push(itemView, title: itemTitle)
98 |
99 | case 3:
100 | // 3. Same as above, but now you have options to customize item's navigation bar appearance properties, such as background and foreground color, title font, pop/push icons text or icons.
101 |
102 |
103 | let topBarAppearance: StackItemAppearance.TopBar = {
104 | /* — Change to see different button appearance example: 0 - icon or 1 - text/emoji. See switch below for details — */
105 |
106 | let buttonsTestOption = 1
107 |
108 | let popButtonAppearance: StackItemAppearance.TopBar.Button
109 | let pushButtonAppearance: StackItemAppearance.TopBar.Button
110 |
111 | if buttonsTestOption == 0 { // Navigation icons playground
112 | popButtonAppearance = StackItemAppearance.TopBar.Button(icon: Images.Navigation.back)
113 | pushButtonAppearance = StackItemAppearance.TopBar.Button(icon: Images.Navigation.forward)
114 | } else { // Navigation text playground
115 | let popButtonTitle = NSAttributedString(string: "\(currentStep.prevStep?.shortSymbol ?? "❌")⬅️", attributes: [.foregroundColor : UIColor.blue])
116 | popButtonAppearance = StackItemAppearance.TopBar.Button(title: popButtonTitle)
117 |
118 | let pushButtonTitle = NSAttributedString(string: "➡️\(currentStep.nextStep?.shortSymbol ?? "❌")", attributes: [.foregroundColor : UIColor.blue])
119 | pushButtonAppearance = StackItemAppearance.TopBar.Button(title: pushButtonTitle)
120 | }
121 |
122 | // Setting StackItemAppearance for the item to insert
123 |
124 | let customBarAppearance = StackItemAppearance.TopBar(backgroundColor: Utils.randomPastelColor(), titleFont: .italicSystemFont(ofSize: 17.0), titleTextColor: .white, popButtonIdentity: popButtonAppearance, pushButtonIdentity: pushButtonAppearance)
125 | return customBarAppearance
126 | }()
127 |
128 | let customAppearance = StackItemAppearance(backgroundColor: Utils.randomPastelColor(), topBarAppearance: topBarAppearance)
129 |
130 | push(itemView, title: itemTitle, customAppearance: customAppearance)
131 |
132 | default:
133 | push(itemView)
134 | }
135 | }
136 |
137 | let delta = currentStep.rawValue - oldValue.rawValue
138 |
139 | if delta < 0 {
140 | for _ in 0 ..< abs(delta) {
141 | pop()
142 | }
143 | } else if delta > 0 {
144 | pushStep()
145 | } else {
146 | pop()
147 | pushStep()
148 | }
149 | }
150 | }
151 |
152 | // MARK: Initializers
153 |
154 | override init(frame: CGRect) {
155 | super.init(frame: frame)
156 | self.delegate = self // Don't forget to set delegate!
157 | }
158 |
159 | required public init?(coder aDecoder: NSCoder) {
160 | fatalError("init(coder:) has not been implemented")
161 | }
162 |
163 | // MARK: Goal creation flow
164 |
165 | func resetFlow() {
166 | guard !isEndlessMode else {
167 | return
168 | }
169 |
170 | setStep(FlowStep(rawValue: 0)!, animated: true)
171 | clean()
172 | setStep(FlowStep(rawValue: 0)!, animated: true)
173 | }
174 |
175 | // MARK: Transitions
176 |
177 | private func goToPrevStep() {
178 | guard let prevStep = currentStep.prevStep else {
179 | return
180 | }
181 |
182 | setStep(prevStep, animated: true)
183 | }
184 |
185 | private func goToNextStep() {
186 | guard let nextStep = currentStep.nextStep else {
187 | return
188 | }
189 |
190 | setStep(nextStep, animated: true)
191 | }
192 |
193 | private func setStep(_ step: FlowStep, animated: Bool) {
194 | self.transitionDuration = animated ? 0.25 : 0.0
195 | self.currentStep = step
196 | }
197 |
198 | // MARK: FlowStep views construction
199 |
200 | private func stepView(for step: FlowStep) -> UIView {
201 | let stepView: DemoItemView
202 |
203 | // Here we don't really care but for a real state control we could create different necessary unique views for evety step
204 |
205 | switch step {
206 | default:
207 | // Please look into DemoItemView.bounds(for:) static method to learn more about defining the best frames for your stack flow item views
208 |
209 | stepView = DemoItemView.stackItem(bounds: DemoItemView.bounds(for: self))
210 | stepView.identityLabel.text = "Any UI"//"Custom UIView #" + String(step.rawValue + 1)
211 |
212 | stepView.popHandler = {
213 | self.goToPrevStep()
214 | }
215 |
216 | stepView.pushHandler = {
217 | self.goToNextStep()
218 | }
219 | }
220 |
221 | return stepView
222 | }
223 | }
224 |
225 | // MARK: - Delegate -
226 |
227 | extension CustomStackFlowView: StackFlowDelegate {
228 | func pushNextItem(to stackView: StackFlowView) {
229 | if let customPush = customPushFunction {
230 | customPush()
231 | return
232 | }
233 |
234 | stackView.push(
235 | {
236 | let item = DemoItemView.stackItem(bounds: DemoItemView.bounds(for: stackView))
237 | item.identityLabel.text = "Custom UI"//"Custom UIView #" + String(stackView.numberOfItems + 1)
238 |
239 | item.popHandler = {
240 | stackView.pop()
241 | }
242 |
243 | item.pushHandler = {
244 | self.pushNextItem(to: stackView)
245 | }
246 |
247 | return item
248 | }()
249 | )
250 | }
251 |
252 | func stackFlowViewDidRequestPop(_ stackView: StackFlowView, numberOfItems: Int) {
253 | log(message: "Requested to go \(numberOfItems) steps back", from: self)
254 |
255 | if isEndlessMode {
256 | stackView.pop(numberOfItems)
257 | } else {
258 | for _ in 0 ..< numberOfItems {
259 | goToPrevStep()
260 | }
261 | }
262 | }
263 |
264 | func stackFlowViewDidRequestPush(_ stackView: StackFlowView) {
265 | log(message: "Requested next item", from: self)
266 |
267 | if isEndlessMode {
268 | pushNextItem(to: stackView)
269 | } else {
270 | goToNextStep()
271 | }
272 | }
273 |
274 | func stackFlowViewWillPop(_ stackView: StackFlowView) {
275 | log(message: "About to go one item back", from: self)
276 | }
277 |
278 | func stackFlowViewDidPop(_ stackView: StackFlowView) {
279 | log(message: "Went one item back", from: self)
280 |
281 | if let demoItem = stackView.lastItemContent as? DemoItemView {
282 | demoItem.becameActive()
283 | }
284 | }
285 |
286 | func stackFlowView(_ stackView: StackFlowView, willPush view: UIView) {
287 | log(message: "About to to go to the next step", from: self)
288 |
289 | if let lastItem = stackView.lastItemContent as? DemoItemView {
290 | lastItem.becameInactive()
291 | }
292 | }
293 |
294 | func stackFlowView(_ stackView: StackFlowView, didPush view: UIView) {
295 | log(message: "Went to next step with view: \(view)", from: self)
296 | }
297 | }
298 |
--------------------------------------------------------------------------------
/StackFlowViewDemo/StackFlowViewDemo/Views/DemoItemView/DemoItemView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // DemoItemView.swift
3 | // StackFlowViewDemo
4 | //
5 | // Created by 0xNSHuman on 19/01/2018.
6 | // Copyright © 2018 0xNSHuman. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | class DemoItemView: UIView {
12 | // MARK: - Constants -
13 |
14 | static let minimumAllowedWidth: CGFloat = 300 //(appDelegate?.window?.bounds.width ?? 0) / 2 //380.0
15 | static let maximumAllowedWidth: CGFloat = minimumAllowedWidth + 1
16 |
17 | static let minimumAllowedHeight: CGFloat = 97.0
18 | static let maximumAllowedHeight: CGFloat = minimumAllowedHeight + 1
19 |
20 | // MARK: - Outlets -
21 |
22 | @IBOutlet weak var identityLabel: UILabel!
23 | @IBOutlet var navigationLabels: [UILabel]!
24 |
25 | // MARK: - Properties -
26 |
27 | var popHandler: (() -> ())? = nil
28 | var pushHandler: (() -> ())? = nil
29 |
30 | // MARK: - Life cycle -
31 |
32 | override func awakeFromNib() {
33 | super.awakeFromNib()
34 | setUp()
35 | }
36 |
37 | // MARK: - Setup -
38 |
39 | private func setUp() {
40 | // Set bg color
41 |
42 | backgroundColor = Utils.randomPastelColor()
43 |
44 | // Attach gestures
45 |
46 | addGestureRecognizer({
47 | let tapGesture = UITapGestureRecognizer()
48 | tapGesture.delegate = self
49 | tapGesture.addTarget(self, action: #selector(tapOccured(_:)))
50 | return tapGesture
51 | }())
52 |
53 | addGestureRecognizer({
54 | let doubleTapGesture = UITapGestureRecognizer()
55 | doubleTapGesture.delegate = self
56 | doubleTapGesture.numberOfTapsRequired = 2
57 | doubleTapGesture.addTarget(self, action: #selector(doubleTapOccured(_:)))
58 | return doubleTapGesture
59 | }())
60 | }
61 |
62 | // MARK: - Tap events -
63 |
64 | @objc private func tapOccured(_ gesture: UITapGestureRecognizer) {
65 | pushHandler?()
66 | }
67 |
68 | @objc private func doubleTapOccured(_ gesture: UITapGestureRecognizer) {
69 | popHandler?()
70 | }
71 | }
72 |
73 | // MARK: - Touch Delegate -
74 |
75 | extension DemoItemView: UIGestureRecognizerDelegate {
76 | func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
77 | return true
78 | }
79 |
80 | func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRequireFailureOf otherGestureRecognizer: UIGestureRecognizer) -> Bool {
81 | guard (gestureRecognizer as? UITapGestureRecognizer)?.numberOfTapsRequired == 1,
82 | (otherGestureRecognizer as? UITapGestureRecognizer)?.numberOfTapsRequired == 2 else {
83 | return false
84 | }
85 |
86 | return true
87 | }
88 | }
89 |
90 | // MARK: - Triggers -
91 |
92 | extension DemoItemView {
93 | func becameActive() {
94 | navigationLabels.forEach { $0.isHidden = false }
95 | }
96 |
97 | func becameInactive() {
98 | navigationLabels.forEach { $0.isHidden = true }
99 | }
100 | }
101 |
102 | // MARK: - Producer for StackFlowView -
103 |
104 | extension DemoItemView {
105 | static func stackItem(bounds: CGRect) -> DemoItemView {
106 | let itemView = Bundle.main.typeFromNib(DemoItemView.self)! // Force unwrap is 100% safe here
107 | itemView.frame = bounds
108 | return itemView
109 | }
110 |
111 | static func bounds(for stackFlowView: StackFlowView) -> CGRect {
112 | // Note this `safeSize` property of StackFlowView. You should use it to get info about its available content area, not blocked by any views outside of safe area
113 |
114 | let safeStackFlowViewWidth = stackFlowView.safeSize.width
115 | let safeStackFlowViewHeight = stackFlowView.safeSize.height
116 |
117 | if stackFlowView.growthDirection.isVertical {
118 | let height = (CGFloat(arc4random()).truncatingRemainder(dividingBy: (maximumAllowedHeight - minimumAllowedHeight))) + minimumAllowedHeight
119 |
120 | return CGRect(x: 0, y: 0, width: safeStackFlowViewWidth, height: {
121 | return min(height, safeStackFlowViewHeight)
122 | }()
123 | )
124 | } else {
125 | let width = (CGFloat(arc4random()).truncatingRemainder(dividingBy: (maximumAllowedWidth - minimumAllowedWidth))) + minimumAllowedWidth
126 |
127 | return CGRect(x: 0, y: 0, width: {
128 | return min(width, safeStackFlowViewWidth)
129 | }(), height: safeStackFlowViewHeight
130 | )
131 | }
132 | }
133 | }
134 |
--------------------------------------------------------------------------------
/StackFlowViewDemo/StackFlowViewDemo/Views/DemoItemView/DemoItemView.xib:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
--------------------------------------------------------------------------------
/StackFlowViewDemo/StackFlowViewDemo/Views/Storyboards/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 |
--------------------------------------------------------------------------------
/StackFlowViewDemo/StackFlowViewDemo/Views/Storyboards/Base.lproj/Main.storyboard:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 |
--------------------------------------------------------------------------------