├── .codeclimate.yml
├── .gitignore
├── .swift-version
├── .swiftlint.yml
├── .swiftpm
├── SnackView.xctestplan
└── xcode
│ ├── package.xcworkspace
│ ├── contents.xcworkspacedata
│ └── xcshareddata
│ │ └── IDEWorkspaceChecks.plist
│ └── xcshareddata
│ └── xcschemes
│ └── SnackView.xcscheme
├── .travis.yml
├── Icon.jpg
├── Package.resolved
├── Package.swift
├── README.md
├── SnackView.xctestplan
├── Sources
└── SnackView
│ ├── SVScrollView.swift
│ ├── SVSkeletonView.swift
│ ├── SnackView Items
│ ├── SVApplicationItem.swift
│ ├── SVButtonItem.swift
│ ├── SVDescriptionItem.swift
│ ├── SVDetailTextItem.swift
│ ├── SVImageViewItem.swift
│ ├── SVItem.swift
│ ├── SVLoaderItem.swift
│ ├── SVPriceRowItem.swift
│ ├── SVSegmentedControllerItem.swift
│ ├── SVSliderItem.swift
│ ├── SVSpacerItem.swift
│ ├── SVStepperItem.swift
│ ├── SVSwitchItem.swift
│ ├── SVTextFieldItem.swift
│ └── SVTitleItem.swift
│ ├── SnackView Utilities
│ ├── CustomInputAccessoryView.swift
│ └── UIView-Extensions.swift
│ ├── SnackView.swift
│ ├── SnackViewDataSource.swift
│ ├── SnackViewInternalMethods.swift
│ ├── SnackViewKeyboardObserver.swift
│ └── SnackViewPublicMethods.swift
└── Tests
├── SnackView copy.xctestplan
└── SnackViewTests
├── Extensions.swift
├── MockSnackViewDataSource.swift
├── SnackViewItemsTests.swift
└── SnackViewTests.swift
/.codeclimate.yml:
--------------------------------------------------------------------------------
1 | ---
2 | version: "2"
3 | plugins:
4 | swiftlint:
5 | enabled: true
6 | exclude_patterns:
7 | - "Carthage/"
8 | - "fastlane/"
9 | - "SnackViewTests/"
10 | checks:
11 | identical-code:
12 | config:
13 | threshold: 25
14 | similar-code:
15 | config:
16 | threshold: 50
17 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # OS X Finder
2 | .DS_Store
3 |
4 | ### Swift.gitignore
5 | # Xcode
6 | #
7 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore
8 |
9 | ## Build generated
10 | build/
11 | DerivedData/
12 |
13 | ## Various settings
14 | *.pbxuser
15 | !default.pbxuser
16 | *.mode1v3
17 | !default.mode1v3
18 | *.mode2v3
19 | !default.mode2v3
20 | *.perspectivev3
21 | !default.perspectivev3
22 | xcuserdata/
23 |
24 | # Xcode per-user config
25 | *.mode1
26 | *.perspective
27 |
28 | ## Other
29 | *.moved-aside
30 | *.xccheckout
31 | *.xcscmblueprint
32 | *.xcuserstate
33 |
34 | # Build products
35 | build/
36 | *.o
37 | *.LinkFileList
38 |
39 | ## Obj-C/Swift specific
40 | *.hmap
41 | *.ipa
42 | *.dSYM.zip
43 | *.dSYM
44 |
45 | ## Playgrounds
46 | timeline.xctimeline
47 | playground.xcworkspace
48 |
49 | # Swift Package Manager
50 | #
51 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies.
52 | # Packages/
53 | # Package.pins
54 | # Package.resolved
55 | .build/
56 |
57 | # CocoaPods
58 | #
59 | # We recommend against adding the Pods directory to your .gitignore. However
60 | # you should judge for yourself, the pros and cons are mentioned at:
61 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control
62 | #
63 | # Pods/
64 | #
65 | # Add this line if you want to avoid checking in source code from the Xcode workspace
66 | # *.xcworkspace
67 |
68 | # Carthage
69 | #
70 | # Add this line if you want to avoid checking in source code from Carthage dependencies.
71 | # Carthage/Checkouts
72 |
73 | Carthage
74 |
75 | # fastlane
76 | #
77 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the
78 | # screenshots whenever they are needed.
79 | # For more information about the recommended setup visit:
80 | # https://docs.fastlane.tools/best-practices/source-control/#source-control
81 |
82 | fastlane/report.xml
83 | fastlane/Preview.html
84 | fastlane/screenshots/**/*.png
85 | fastlane/test_output
86 |
87 |
88 | cobertura.xml
89 |
--------------------------------------------------------------------------------
/.swift-version:
--------------------------------------------------------------------------------
1 | 4.2
2 |
--------------------------------------------------------------------------------
/.swiftlint.yml:
--------------------------------------------------------------------------------
1 | disabled_rules:
2 | - line_length
3 | - type_name
4 | - function_body_length
5 | - identifier_name
6 | - trailing_whitespace
7 | - todo
8 |
9 | opt_in_rules: # some rules are only opt-in
10 | - force_unwrapping
11 | - empty_count
12 | - conditional_binding_cascade
13 |
14 | included:
15 | - SnackView/Classes
16 |
17 | # paths to ignore during linting. Takes precedence over `included`.
18 | excluded:
19 | - Example
20 | - Carthage
21 | - Pods
22 |
23 | vertical_whitespace:
24 | max_empty_lines: 2
25 |
26 | function_body_length:
27 | - 125 #warning
28 | - 200 #error
29 |
30 | cyclomatic_complexity:
31 | warning: 2 # two nested ifs are acceptable
32 | error: 4 # 4 nested ifs shows warning, 5 causes compile error
33 |
34 | identifier_name:
35 | min_length: # only min_length
36 | error: 4 # only error
37 |
38 | type_body_length:
39 | - 300 # warning
40 | - 400 # error
41 |
42 | file_length:
43 | - 1000 #warning
44 |
45 | reporter: "json"
46 |
--------------------------------------------------------------------------------
/.swiftpm/SnackView.xctestplan:
--------------------------------------------------------------------------------
1 | {
2 | "configurations" : [
3 | {
4 | "id" : "274E9F6C-DEBB-44B7-81C7-8B3EE4826C25",
5 | "name" : "Test Scheme Action",
6 | "options" : {
7 |
8 | }
9 | }
10 | ],
11 | "defaultOptions" : {
12 |
13 | },
14 | "testTargets" : [
15 | {
16 | "target" : {
17 | "containerPath" : "container:",
18 | "identifier" : "SnackViewTests",
19 | "name" : "SnackViewTests"
20 | }
21 | }
22 | ],
23 | "version" : 1
24 | }
25 |
--------------------------------------------------------------------------------
/.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/.swiftpm/xcode/package.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/.swiftpm/xcode/xcshareddata/xcschemes/SnackView.xcscheme:
--------------------------------------------------------------------------------
1 |
2 |
5 |
8 |
9 |
15 |
21 |
22 |
23 |
29 |
35 |
36 |
37 |
43 |
49 |
50 |
51 |
52 |
53 |
58 |
59 |
62 |
63 |
64 |
65 |
67 |
73 |
74 |
75 |
76 |
77 |
87 |
88 |
94 |
95 |
101 |
102 |
103 |
104 |
106 |
107 |
110 |
111 |
112 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: objective-c
2 | os: osx
3 | osx_image: xcode12.4
4 | before_install:
5 | - rvm install 2.5.1
6 | - bundle install --without=documentation
7 | - bundle update fastlane
8 | #- carthage update --use-xcframeworks
9 | env:
10 | global:
11 | - APPNAME="SnackView"
12 | - FASTLANE_FOLDER="fastlane"
13 | - BUILD_FOLDER="Build"
14 | - TRAVIS_FASTLANE_BASE_DIR=$TRAVIS_BUILD_DIR/$FASTLANE_FOLDER
15 | - CC_TEST_REPORTER_ID="f835b1f14aec8efeacfcf7b2a670dc854326da4af496dc1cc21d225436b5451c"
16 | - LC_CTYPE=en_US.UTF-8 LANG=en_US.UTF-8
17 | jobs:
18 | include:
19 | - stage: test
20 | name: Fastlane scan
21 | before_script:
22 | - curl -L https://codeclimate.com/downloads/test-reporter/test-reporter-latest-darwin-amd64
23 | > ./cc-test-reporter
24 | - chmod +x ./cc-test-reporter
25 | - "./cc-test-reporter before-build"
26 | script:
27 | - bundle exec fastlane test_with_coverage
28 | after_success:
29 | - "./cc-test-reporter after-build --exit-code $TRAVIS_TEST_RESULT"
30 | - stage: release
31 | name: Tag Version
32 | if: "(branch = master)"
33 | script:
34 | - bundle exec create_release
35 | stages:
36 | - test
37 | - name: release
38 | if: (branch = master) AND (type != pull_request) AND (tag =~ /v\d\.\d{1,3}\.\d{1,3}$/)
39 | branches:
40 | only:
41 | - master
42 | - development
43 | - develop
44 | - "/^(hotfix)$*/"
45 | except:
46 | - "/^[^\\/]+\\/\\d+(?:\\.\\d+)+\\/\\d+$/"
47 | language: ruby
48 | cache:
49 | bundler: true
50 | directories:
51 | - Carthage
52 |
--------------------------------------------------------------------------------
/Icon.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lucacasula91/SnackView/4185534bc1352a8cb5143a47dcc9d70c21b224fa/Icon.jpg
--------------------------------------------------------------------------------
/Package.resolved:
--------------------------------------------------------------------------------
1 | {
2 | "object": {
3 | "pins": [
4 | {
5 | "package": "CwlCatchException",
6 | "repositoryURL": "https://github.com/mattgallagher/CwlCatchException.git",
7 | "state": {
8 | "branch": null,
9 | "revision": "3b123999de19bf04905bc1dfdb76f817b0f2cc00",
10 | "version": "2.1.2"
11 | }
12 | },
13 | {
14 | "package": "CwlPreconditionTesting",
15 | "repositoryURL": "https://github.com/mattgallagher/CwlPreconditionTesting.git",
16 | "state": {
17 | "branch": null,
18 | "revision": "a23ded2c91df9156628a6996ab4f347526f17b6b",
19 | "version": "2.1.2"
20 | }
21 | },
22 | {
23 | "package": "Nimble",
24 | "repositoryURL": "https://github.com/Quick/Nimble.git",
25 | "state": {
26 | "branch": null,
27 | "revision": "1f3bde57bde12f5e7b07909848c071e9b73d6edc",
28 | "version": "10.0.0"
29 | }
30 | },
31 | {
32 | "package": "Quick",
33 | "repositoryURL": "https://github.com/Quick/Quick.git",
34 | "state": {
35 | "branch": null,
36 | "revision": "f9d519828bb03dfc8125467d8f7b93131951124c",
37 | "version": "5.0.1"
38 | }
39 | }
40 | ]
41 | },
42 | "version": 1
43 | }
44 |
--------------------------------------------------------------------------------
/Package.swift:
--------------------------------------------------------------------------------
1 | // swift-tools-version: 5.4
2 | // The swift-tools-version declares the minimum version of Swift required to build this package.
3 |
4 | import PackageDescription
5 |
6 | let package = Package(
7 | name: "SnackView",
8 | platforms: [
9 | .iOS(.v13),
10 | ],
11 | products: [
12 | .library(
13 | name: "SnackView",
14 | targets: ["SnackView"]),
15 | ],
16 | dependencies: [
17 | .package(url: "https://github.com/Quick/Nimble.git", .upToNextMajor(from: "10.0.0")),
18 | .package(url: "https://github.com/Quick/Quick.git", .upToNextMajor(from: "5.0.0"))
19 |
20 | ],
21 | targets: [
22 | .target(
23 | name: "SnackView",
24 | dependencies: []),
25 | .testTarget(
26 | name: "SnackViewTests",
27 | dependencies: ["SnackView", "Quick", "Nimble"]),
28 | ]
29 | )
30 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | 
2 |
3 | ***Create customizable bottom-half sheets.***
4 | 
5 |
6 | [](https://codeclimate.com/github/lucacasula91/SnackView/test_coverage)
7 | [](https://codeclimate.com/github/lucacasula91/SnackView/maintainability)
8 |
9 |
10 | - [Roadmap](#roadmap)
11 | - [What's new](#whats-new)
12 | - [What's new in 1.2.0](#whats-new-in-120)
13 | - [Installation](#installation)
14 | - [Swift Package Manager](#swift-package-manager)
15 | - ~~[CocoaPods](#cocoapods)~~ Not available yet for 1.2.0
16 | - ~~[Carthage](#carthage)~~ Not available yet for 1.2.0
17 | - [Manual installation](#manual-installation)
18 | - [Usage](#usage)
19 | - [Create your custom SnackView alert](#create-your-custom-snackview-alert)
20 | - [SVItems included](#svitems-included)
21 | - [Create custom SVItems](#create-custom-svitems)
22 | - [Contributing](#contributing)
23 |
24 | ## Roadmap
25 | - Create DocC documentation
26 | - Allow UIFont override
27 | - Add support for Xib hosted SVItems
28 | - Add new SVItems
29 |
30 |
31 | ## What's new
32 |
33 | ### What's new in 1.2.0
34 | - **Dynamic type support**
35 | - **SVSegmentedControlItem**
36 | - **SVStepperItem**
37 | - **SVSpacerItem**
38 |
39 | ## Installation
40 |
41 | ### Swift Package Manager
42 | To install SnackView library using Swift Package Manager from Xcode select **File** > **Add Package** and enter:
43 | ```
44 | https://github.com/lucacasula91/SnackView
45 | ```
46 |
47 | ### Manual installation
48 |
49 | If you want to install SnackView manually, just drag **Sources** folder into your project.
50 |
51 | ## Usage
52 |
53 | ### Create your custom SnackView sheet
54 | To create a custom SnackView your UIViewController or you NSObject class should conform to to **SnackViewDataSource** protocol.
55 | SnackViewDataSource allows you to specify title bar elements and UI elements that sheet should present.
56 |
57 | The first step is to specify the title of you SnackView sheet:
58 | ```swift
59 | func titleFor(snackView: SnackView) -> String {
60 | return "My Title"
61 | }
62 | ```
63 |
64 | Then you can specify the dismission button title. This method can return an Optional string value to hide the dismission button.
65 | - Note: If you pass nil it is up to you to handle manually the SnackView dismission logic.
66 | ```swift
67 | func cancelTitleFor(snackView: SnackView) -> String? {
68 | return "Cancel"
69 | }
70 | ```
71 |
72 | The last part required is the method with which specify the items to show.
73 | ```swift
74 | func itemsFor(snackView: SnackView) -> [SVItem] {
75 | let descriptionItem = SVDescriptionItem(withDescription: "In this last release of SnackView we...")
76 | let imageItem = SVImageViewItem(withImage: UIImage(named: "what_is_new")!,
77 | andContentMode: .scaleAspectFill)
78 |
79 | return [descriptionItem, imageItem]
80 | }
81 | ```
82 |
83 | Once conformed to SnackViewDataSource you are ready to present you SnackView sheet:
84 | ```swift
85 | let snackView = SnackView(with: dataSource)
86 | snackView.show()
87 | ```
88 |
89 | ## Example
90 | Here below an example of SnackView implementation:
91 |
92 | ```swift
93 | class MyCustomClass: UIViewController {
94 |
95 | override func viewDidLoad() { }
96 |
97 | override func viewDidAppear(_ animated: Bool) {
98 | super.viewDidAppear(animated)
99 |
100 | // Present the SnackView
101 | let dataSource = self
102 | SnackView(with: dataSource).show()
103 | }
104 | }
105 |
106 | // MARK: - SnackViewDataSource Section
107 | extension MyCustomClass: SnackViewDataSource {
108 |
109 | func titleFor(snackView: SnackView) -> String {
110 | return "What's New"
111 | }
112 |
113 | func cancelTitleFor(snackView: SnackView) -> String? {
114 | return "Dismiss"
115 | }
116 |
117 | func itemsFor(snackView: SnackView) -> [SVItem] {
118 | let descriptionItem = SVDescriptionItem(withDescription: "In this last release of SnackView we...")
119 | let imageItem = SVImageViewItem(withImage: UIImage(named: "what_is_new")!,
120 | andContentMode: .scaleAspectFill)
121 |
122 | return [descriptionItem, imageItem]
123 | }
124 | }
125 |
126 | ```
127 |
128 |
129 |
130 | ------
131 |
132 | ## SVItems included
133 | SnackView provides some SVItems ready to use, here below the list of some SVItems available:
134 | - **SVStepperItem**
135 | - **SVSliderItem**
136 | - **SVSegmentedControllerItem**
137 | - **SVSpacerItem**
138 |
139 | **SVApplicationItem**
140 |
141 | ```swift
142 | SVApplicationItem(withIcon: UIImage(named: "AppIcon"),
143 | withTitle: "Ipsum lorem",
144 | andDescription: "Lorem ipsum dolor sit amet...")
145 | ```
146 |
147 | 
148 |
149 | ***
150 |
151 | **SVDescriptionItem**
152 |
153 | ```swift
154 | SVDescriptionItem(withDescription: "Lorem ipsum dolor sit amet...")
155 | ```
156 |
157 | 
158 |
159 | ***
160 |
161 | **SVTextFieldItem**
162 |
163 | ```swift
164 | SVTextFieldItem(withPlaceholder: "Create Password",
165 | isSecureField: true)
166 | ```
167 |
168 | 
169 |
170 | ***
171 |
172 | **SVDetailTextItem**
173 |
174 | ```swift
175 | SVDetailTextItem(withTitle: "Elit amet",
176 | andContent: "Lorem ipsum dolor sit amet...")
177 | ```
178 |
179 | 
180 |
181 | ***
182 |
183 | **SVButtonItem**
184 |
185 | ```swift
186 | SVButtonItem(withTitle: "Continue") { /* Button action here */ }
187 | ```
188 |
189 | 
190 |
191 | ***
192 |
193 | **SVSwitchItem**
194 |
195 | ```swift
196 | SVSwitchItem(withTitle: "Push Notifications",
197 | andContent: "Activate to stay up to date...") { (isOn) in /* Switch action here */ }
198 | ```
199 |
200 | 
201 |
202 | ***
203 |
204 | **SVLoaderItem**
205 |
206 | ```swift
207 | SVLoadingItem(withSize: .large,
208 | andText: "Lorem ipsum dolor sit amet...")
209 | ```
210 |
211 | 
212 |
213 | ***
214 |
215 | **SVImaveViewItem**
216 |
217 | ```swift
218 | SVImageViewItem(with: UIImage(named: "hat_is_new")!,
219 | andContentMode: .scaleAspectFill)
220 | ```
221 |
222 | 
223 |
224 | ***
225 |
226 |
227 | # Create custom SVItems
228 | #### You can create custom items subclassing SVItem class.
229 | Here below an example.
230 | ```swift
231 | import UIKit
232 | import SnackView
233 |
234 | //Create a subclass of SVItem
235 | class SVCustomItem: SVItem {
236 |
237 | //Pass all parameters in init method to customize your SVItem
238 | init(withColor color: UIColor) {
239 | super.init()
240 |
241 | //Add an UIView
242 | let customView = UIView()
243 | customView.translatesAutoresizingMaskIntoConstraints = false
244 | customView.backgroundColor = color
245 | self.addSubview(customView)
246 |
247 | //Add UIView contraints
248 | let vConstraints = NSLayoutConstraint.constraints(withVisualFormat: "V:|-[customView(70)]-|", options: [], metrics: nil, views: ["customView":customView])
249 |
250 | let hConstraints = NSLayoutConstraint.constraints(withVisualFormat: "H:|-[customView]-|", options: [], metrics: nil, views: ["customView": customView])
251 |
252 | self.addConstraints(vConstraints + hConstraints)
253 | }
254 |
255 | required public convenience init?(coder aDecoder: NSCoder) {
256 | self.init(coder: aDecoder)
257 | }
258 | }
259 | ```
260 |
261 | 
262 |
263 | ***
264 |
265 | ## Contributing
266 | If you want to contribute to make SnackView a better framework, **submit a pull request**.
267 |
268 | Please consider to **open an issue** for the following reasons:
269 | * If you have questions or if you need help using SnackView
270 | * If you found a bug
271 | * If you have some feature request
272 |
273 |
274 |
--------------------------------------------------------------------------------
/SnackView.xctestplan:
--------------------------------------------------------------------------------
1 | {
2 | "configurations" : [
3 | {
4 | "id" : "274E9F6C-DEBB-44B7-81C7-8B3EE4826C25",
5 | "name" : "Test Scheme Action",
6 | "options" : {
7 |
8 | }
9 | }
10 | ],
11 | "defaultOptions" : {
12 |
13 | },
14 | "testTargets" : [
15 | {
16 | "target" : {
17 | "containerPath" : "container:",
18 | "identifier" : "SnackViewTests",
19 | "name" : "SnackViewTests"
20 | }
21 | }
22 | ],
23 | "version" : 1
24 | }
25 |
--------------------------------------------------------------------------------
/Sources/SnackView/SVScrollView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SVScrollView.swift
3 | // SnackView
4 | //
5 | // Created by Luca Casula on 11/06/21.
6 | // Copyright © 2021 LucaCasula. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | class SVScrollView: UIView {
12 |
13 | // MARK: - Public Properties
14 | public var scrollView = UIScrollView()
15 |
16 | // MARK: - Internal Properties
17 | internal var stackView = UIStackView(arrangedSubviews: [])
18 |
19 | // MARK: - Initialization Methods
20 | init() {
21 | super.init(frame: CGRect.zero)
22 | self.setupUI()
23 | }
24 |
25 | required init?(coder: NSCoder) {
26 | super.init(coder: coder)
27 | self.setupUI()
28 | }
29 |
30 | // MARK: - Public Methods
31 | public func reload(with items: [SVItem]) {
32 | self.addItemsInsideStackView(items)
33 | }
34 |
35 | // MARK: - Private Method
36 | internal func setupUI() {
37 | self.addScrollView()
38 | self.addStackViewInsideScrollViewWithConstraints()
39 | }
40 |
41 | internal func addScrollView() {
42 | self.scrollView.delegate = self
43 | self.scrollView.translatesAutoresizingMaskIntoConstraints = false
44 | self.scrollView.keyboardDismissMode = .interactive
45 | self.scrollView.bounces = true
46 | self.scrollView.alwaysBounceVertical = false
47 | self.scrollView.backgroundColor = UIColor.clear
48 | self.addSubview(self.scrollView)
49 |
50 | let views: [String: Any] = ["scrollView": self.scrollView]
51 | self.addVisualConstraint("H:|[scrollView]|", for: views)
52 | self.addVisualConstraint("V:|[scrollView]|", for: views)
53 | }
54 |
55 | internal func addStackViewInsideScrollViewWithConstraints() {
56 | self.stackView.axis = .vertical
57 | self.stackView.distribution = .fill
58 | self.stackView.translatesAutoresizingMaskIntoConstraints = false
59 | self.scrollView.addSubview(self.stackView)
60 |
61 | let views: [String: Any] = ["stackView": self.stackView, "scrollView": self.scrollView]
62 | self.addVisualConstraint("H:|[stackView(==scrollView)]|", for: views)
63 | self.addVisualConstraint("V:|[stackView]|", for: views)
64 |
65 | // Set scrollView height constraint with low priority
66 | let scrollViewHeight = self.scrollView.heightAnchor.constraint(equalTo: self.stackView.heightAnchor, multiplier: 1, constant: 0)
67 | scrollViewHeight.priority = .defaultLow
68 | scrollViewHeight.isActive = true
69 | }
70 |
71 | /// This method add all SVItems to scrollView content view.
72 | internal func addItemsInsideStackView(_ items: [SVItem]) {
73 | self.stackView.arrangedSubviews.forEach { $0.removeFromSuperview() }
74 | items.forEach { self.stackView.addArrangedSubview($0)}
75 | self.stackView.layoutIfNeeded()
76 | }
77 |
78 | }
79 |
80 | extension SVScrollView: UIScrollViewDelegate {
81 |
82 | // This is a workaround to fix the paralax effect during an interactive dismiss of the keyboard.
83 | func scrollViewDidScroll(_ scrollView: UIScrollView) {
84 | scrollView.contentInset.top = scrollView.contentOffset.y
85 | }
86 |
87 | func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) {
88 | scrollView.contentInset.top = 0
89 | }
90 | }
91 |
--------------------------------------------------------------------------------
/Sources/SnackView/SVSkeletonView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SVSkeletonView.swift
3 | // SnackView
4 | //
5 | // Created by Luca Casula on 11/06/21.
6 | // Copyright © 2021 LucaCasula. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | class SVSkeletonView: UIView {
12 |
13 | // MARK: - Public Properties
14 | public var titleBar = SVTitleItem()
15 |
16 | // MARK: - Private Properties
17 | internal var scrollView = SVScrollView()
18 | internal var safeAreaView = UIView()
19 |
20 | // MARK: - Initialization Method
21 | init() {
22 | super.init(frame: CGRect.zero)
23 | self.setupUI()
24 | }
25 |
26 | required init?(coder: NSCoder) {
27 | super.init(coder: coder)
28 | }
29 |
30 | // MARK: - Public Methods
31 | public func setTitle(_ title: String, andCancelTitle cancelTitle: String?) {
32 | self.titleBar.setTitle(title)
33 | self.titleBar.setCancelTitle(cancelTitle)
34 | }
35 |
36 | public func reload(with items: [SVItem]) {
37 | self.scrollView.reload(with: items)
38 | }
39 |
40 | public func getSafeAreaHeight() -> CGFloat {
41 | return safeAreaView.frame.height
42 | }
43 |
44 | public func injectCancelButton(from snackView: SnackView) {
45 | self.titleBar.cancelButton.addTarget(snackView, action: #selector(snackView.closeActionSelector), for: UIControl.Event.touchUpInside)
46 | }
47 | // MARK: - Private Methods
48 |
49 | internal func setupUI() {
50 | self.isHidden = true
51 | self.backgroundColor = UIColor.white.withAlphaComponent(0.75)
52 | if #available(iOS 13.0, *) {
53 | self.backgroundColor = UIColor.tertiarySystemBackground
54 | }
55 | self.addVisualEffectViewToContentView()
56 | self.addMainSkeletonView()
57 | self.addMainConstraintsToContentView()
58 | }
59 |
60 | /// This method adds a UIVisualEffectView under ContentView to reproduce blur effect.
61 | internal func addVisualEffectViewToContentView() {
62 | var effect: UIBlurEffect = UIBlurEffect(style: .light)
63 |
64 | if #available(iOS 13.0, *) {
65 | effect = UIBlurEffect(style: .systemMaterial)
66 | }
67 |
68 | let visualEffectView = UIVisualEffectView(effect: effect)
69 | visualEffectView.frame = self.bounds
70 | visualEffectView.autoresizingMask = [.flexibleHeight, .flexibleWidth]
71 |
72 | self.addSubview(visualEffectView)
73 | }
74 |
75 | /// Adds the three main views of SnackView, TitleBar, ScrollView and SafeArea View
76 | internal func addMainSkeletonView() {
77 | // Add TitleBar
78 | self.titleBar.translatesAutoresizingMaskIntoConstraints = false
79 | self.addSubview(self.titleBar)
80 |
81 | // Add ScrollView
82 | self.scrollView = SVScrollView()
83 | self.scrollView.translatesAutoresizingMaskIntoConstraints = false
84 | self.addSubview(self.scrollView)
85 |
86 | // Safe Area View
87 | self.safeAreaView = UIView()
88 | self.safeAreaView.translatesAutoresizingMaskIntoConstraints = false
89 | self.safeAreaView.backgroundColor = UIColor.clear
90 | self.addSubview(self.safeAreaView)
91 | }
92 |
93 | /// Adds the constraints for the three main views. Here is managed also the safeArea view constraints.
94 | internal func addMainConstraintsToContentView() {
95 | // Add vertical constraints
96 | let views: [String: Any] = ["title": titleBar, "scrollView": scrollView]
97 | self.addVisualConstraint("V:|[title(>=44)][scrollView]-|", for: views)
98 |
99 | self.safeAreaView.topAnchor.constraint(equalTo: scrollView.bottomAnchor).isActive = true
100 | self.safeAreaView.bottomAnchor.constraint(equalTo: self.bottomAnchor).isActive = true
101 |
102 | // Add horizontal constraints
103 | let items = [self.titleBar, self.scrollView, self.safeAreaView] as [Any]
104 | items.forEach { self.addVisualConstraint("H:|[item]|", for: ["item": $0])}
105 | }
106 | }
107 |
--------------------------------------------------------------------------------
/Sources/SnackView/SnackView Items/SVApplicationItem.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SVApplicationItem.swift
3 | // SnackView
4 | //
5 | // Created by Luca Casula on 08/11/17.
6 | // Copyright © 2017 Luca Casula. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | /** SVApplicationItem is an SVItem with which to show an image, a title and a description text.
12 |
13 | It can be useful to show the icon with title and description of an application, of an in-app purchase or to show a list of steps.
14 | */
15 | public class SVApplicationItem: SVItem {
16 |
17 | // MARK: - Private Properties
18 | private var titleLabel: UILabel = UILabel()
19 | private var descriptionLabel: UILabel = UILabel()
20 | private var imageContainer: UIView = UIView()
21 |
22 | // MARK: - Public Properties
23 | private(set) var icon: UIImage
24 | private(set) var title: String
25 | private(set) var descriptionText: String
26 |
27 | // MARK: - Initialization Method
28 | /**
29 | Initialization method for SVApplicationItem view. You can customize this item with image, title and description text.
30 | - parameter icon: The image you want to show at left of title and description text
31 | - parameter title: The title of application or in-app purchase
32 | - parameter description: The description text of application or in-app purchase
33 | */
34 | public init(withIcon icon: UIImage, withTitle title: String, andDescription description: String) {
35 | self.icon = icon
36 | self.title = title
37 | self.descriptionText = description
38 |
39 | super.init()
40 | self.setupUI()
41 | }
42 |
43 | required public convenience init?(coder aDecoder: NSCoder) {
44 | return nil
45 | }
46 |
47 | // MARK: - Private Methods
48 | private func setupUI() {
49 | [imageContainer, titleLabel, descriptionLabel].forEach {
50 | $0.translatesAutoresizingMaskIntoConstraints = false
51 | self.addSubview($0)
52 | }
53 |
54 | self.addImageView()
55 | self.addTitleLabel()
56 | self.addDescriptionLabel()
57 | }
58 |
59 | private func addImageView() {
60 | self.addVisualConstraint("H:|-[imageContainer(==\(self.leftContentWidth))]", for: ["imageContainer": imageContainer])
61 | self.addVisualConstraint("V:|-[imageContainer]-|", for: ["imageContainer": imageContainer])
62 |
63 | let imageView = UIImageView()
64 | imageView.translatesAutoresizingMaskIntoConstraints = false
65 | imageView.image = icon
66 | imageContainer.addSubview(imageView)
67 |
68 | self.addVisualConstraint("H:[imageView(40)]|", for: ["imageView": imageView])
69 | self.addVisualConstraint("V:|[imageView(40)]-(>=0)-|", for: ["imageView": imageView])
70 |
71 | //Customize the UI of imageView
72 | imageView.layer.cornerRadius = 9
73 | imageView.layer.masksToBounds = true
74 | }
75 |
76 | private func addTitleLabel() {
77 | titleLabel.text = title
78 | titleLabel.textColor = self.primaryTextColor
79 | titleLabel.font = UIFont.preferredFont(forTextStyle: .subheadline)
80 | titleLabel.adjustsFontForContentSizeCategory = true
81 | titleLabel.numberOfLines = 0
82 |
83 | self.addVisualConstraint("H:[imageContainer]-[titleLabel]-|", for: ["titleLabel": titleLabel, "imageContainer": imageContainer])
84 | self.addVisualConstraint("V:|-[titleLabel]", for: ["titleLabel": titleLabel])
85 | }
86 |
87 | private func addDescriptionLabel() {
88 | descriptionLabel.text = self.descriptionText
89 | descriptionLabel.textColor = secondaryTextColor
90 | descriptionLabel.font = UIFont.preferredFont(forTextStyle: .body)
91 | descriptionLabel.adjustsFontForContentSizeCategory = true
92 | descriptionLabel.numberOfLines = 0
93 |
94 | self.addVisualConstraint("H:[imageContainer]-[descriptionLabel]-|", for: ["imageContainer": imageContainer, "descriptionLabel": descriptionLabel])
95 | self.addVisualConstraint("V:[titleLabel][descriptionLabel]-|", for: ["titleLabel": titleLabel, "descriptionLabel": descriptionLabel])
96 | }
97 |
98 | }
99 |
--------------------------------------------------------------------------------
/Sources/SnackView/SnackView Items/SVButtonItem.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SVButtonItem.swift
3 | // SnackView
4 | //
5 | // Created by Luca Casula on 09/11/17.
6 | // Copyright © 2017 Luca Casula. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | /** SVButtonItem is an SVItem consisting of a simple button that can perform the action that you want. */
12 | public class SVButtonItem: SVItem {
13 |
14 | // MARK: - Private Properties
15 | private var buttonItem: UIButton
16 | private var buttonTintColor: UIColor?
17 |
18 | // MARK: - Public Properties
19 | private(set) var title: String
20 |
21 | // MARK: - Initialization Method
22 | /**
23 | Initialization method for SVButtonItem view. You can customize this item with title, tint color and action.
24 | - parameter title: The title of the button
25 | - parameter color: The button text color
26 | - parameter buttonAction: A closure in which to write the action that the button must perform
27 | */
28 | public init(withTitle title: String, tintColor color: UIColor? = nil, withButtonAction buttonAction: @escaping () -> Void) {
29 | self.title = title
30 | self.buttonTintColor = color
31 | self.buttonItem = UIButton()
32 | super.init()
33 |
34 | self.addButtonItem()
35 |
36 | //Assign the action block to tmpAction variable
37 | self.tmpAction = buttonAction
38 | }
39 |
40 | required public convenience init?(coder aDecoder: NSCoder) {
41 | return nil
42 | }
43 |
44 |
45 | // MARK: - Private Method
46 | private func addButtonItem() {
47 | self.buttonItem.translatesAutoresizingMaskIntoConstraints = false
48 | self.buttonItem.setTitle(title, for: UIControl.State())
49 | self.buttonItem.setTitleColor((buttonTintColor ?? blueButtonColor), for: UIControl.State.normal)
50 | self.buttonItem.setTitleColor((buttonTintColor ?? blueButtonColor).withAlphaComponent(0.5), for: UIControl.State.highlighted)
51 | self.buttonItem.addTarget(self, action: #selector(buttonSelector), for: .touchUpInside)
52 | self.buttonItem.titleLabel?.font = UIFont.preferredFont(forTextStyle: .headline)
53 | self.buttonItem.titleLabel?.adjustsFontForContentSizeCategory = true
54 | self.addSubview(self.buttonItem)
55 |
56 | //Add constraints to buttonItem
57 | let views: [String: Any] = ["buttonItem": buttonItem]
58 | self.addVisualConstraint("H:|-[buttonItem]-|", for: views)
59 | self.addVisualConstraint("V:|-[buttonItem]-|", for: views)
60 | }
61 |
62 | // MARK: - Custom Stuff
63 | private var tmpAction:() -> Void = {}
64 | @objc public func buttonSelector() {
65 | self.tmpAction()
66 | }
67 | }
68 |
--------------------------------------------------------------------------------
/Sources/SnackView/SnackView Items/SVDescriptionItem.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SVDescriptionItem.swift
3 | // SnackView
4 | //
5 | // Created by Luca Casula on 08/11/17.
6 | // Copyright © 2017 Luca Casula. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | /** SVDescriptionItem is an SVItem with which to show a multi-line description text. */
12 | public class SVDescriptionItem: SVItem {
13 |
14 | // MARK: - Properties
15 | private var descriptionLabel: UILabel
16 | private(set) var descriptionText: String
17 |
18 | // MARK: - Initialization Method
19 | /**
20 | Initialization method for SVDescriptionItem view. You can customize this item with a description text.
21 | - parameter description: The text you want to show
22 | */
23 | public init(withDescription description: String) {
24 | self.descriptionLabel = UILabel()
25 | self.descriptionText = description
26 | super.init()
27 |
28 | self.addDescriptionLabel()
29 | }
30 |
31 | required public convenience init?(coder aDecoder: NSCoder) {
32 | return nil
33 | }
34 |
35 | // MARK: - Private Methods
36 | private func addDescriptionLabel() {
37 | self.descriptionLabel.translatesAutoresizingMaskIntoConstraints = false
38 | self.descriptionLabel.text = self.descriptionText
39 | self.descriptionLabel.textColor = secondaryTextColor
40 | self.descriptionLabel.font = UIFont.preferredFont(forTextStyle: .body)
41 | self.descriptionLabel.adjustsFontForContentSizeCategory = true
42 | self.descriptionLabel.numberOfLines = 0
43 | self.addSubview(descriptionLabel)
44 |
45 | //Add constraints to descriptionLabel
46 | let views: [String: Any] = ["descriptionLabel": descriptionLabel]
47 | self.addVisualConstraint("H:|-[descriptionLabel]-|", for: views)
48 | self.addVisualConstraint("V:|-[descriptionLabel]-|", for: views)
49 | }
50 |
51 | }
52 |
--------------------------------------------------------------------------------
/Sources/SnackView/SnackView Items/SVDetailTextItem.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SVDetailText.swift
3 | // SnackView
4 | //
5 | // Created by Luca Casula on 10/11/17.
6 | // Copyright © 2017 Luca Casula. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | /** SVDetailTextItem is an SVItem with which to show a title and a multi-line description text. */
12 | public class SVDetailTextItem: SVItem {
13 |
14 | // MARK: - Properties
15 | private var titleLabel: UILabel
16 | private var descriptionLabel: UILabel
17 | private(set) var title: String
18 | private(set) var descriptionText: String
19 |
20 | // MARK: - Initialization Method
21 | /**
22 | Initialization method for SVDetailTextItem view. You can customize this item with a title and a description text.
23 | - parameter title: The title to show
24 | - parameter description: The description text to show
25 |
26 | **Note that label text on the left will be rendered as uppercased text**.
27 |
28 | To force the placeholder text to be rendered in multi-line please enter **\n** where you want the text to wrap.
29 |
30 |
31 | **Here an example of wrapped text**:
32 | ```
33 | SVDetailTextItem(withTitle: "Terms and\nConditions",
34 | andDescription: "Ipsum lorem sit...")
35 | ```
36 | */
37 | public init(withTitle title: String, andDescription description: String) {
38 | self.titleLabel = UILabel()
39 | self.descriptionLabel = UILabel()
40 | self.title = title
41 | self.descriptionText = description
42 | super.init()
43 |
44 | self.addTitleLabel()
45 | self.addDescriptionLabel()
46 | }
47 |
48 | required public convenience init?(coder aDecoder: NSCoder) {
49 | return nil
50 | }
51 |
52 | // MARK: - Private Methods
53 | private func addTitleLabel() {
54 | self.titleLabel.translatesAutoresizingMaskIntoConstraints = false
55 | self.titleLabel.text = self.title.uppercased()
56 | self.titleLabel.textAlignment = .right
57 | self.titleLabel.textColor = secondaryTextColor
58 | self.titleLabel.font = UIFont.preferredFont(forTextStyle: .subheadline)
59 | self.titleLabel.adjustsFontForContentSizeCategory = true
60 | self.titleLabel.numberOfLines = 0
61 | self.addSubview(self.titleLabel)
62 |
63 | //Add constraints to titleLabel
64 | let titleHContraints = NSLayoutConstraint.constraints(withVisualFormat: "H:|-[titleLabel(==\(self.leftContentWidth))]", options: [], metrics: nil, views: ["titleLabel": titleLabel])
65 | self.addConstraints(titleHContraints)
66 |
67 | let titleVContraints = NSLayoutConstraint.constraints(withVisualFormat: "V:|-[titleLabel(>=28)]-|", options: [], metrics: nil, views: ["titleLabel": titleLabel])
68 | self.addConstraints(titleVContraints)
69 | }
70 |
71 | private func addDescriptionLabel() {
72 | self.descriptionLabel.translatesAutoresizingMaskIntoConstraints = false
73 | self.descriptionLabel.text = self.descriptionText
74 | self.descriptionLabel.textAlignment = .left
75 | self.descriptionLabel.textColor = self.primaryTextColor
76 | self.descriptionLabel.font = UIFont.preferredFont(forTextStyle: .body)
77 | self.descriptionLabel.adjustsFontForContentSizeCategory = true
78 | self.descriptionLabel.numberOfLines = 0
79 | self.addSubview(descriptionLabel)
80 |
81 | //Add constraints to descriptionLabel
82 | let descriptionHContraints = NSLayoutConstraint.constraints(withVisualFormat: "H:|-[titleLabel]-[descriptionLabel]-|", options: [], metrics: nil, views: ["titleLabel": titleLabel, "descriptionLabel": descriptionLabel])
83 | self.addConstraints(descriptionHContraints)
84 |
85 | let descriptionVContraints = NSLayoutConstraint.constraints(withVisualFormat: "V:|-[descriptionLabel]-|", options: [], metrics: nil, views: ["descriptionLabel": descriptionLabel])
86 | self.addConstraints(descriptionVContraints)
87 | }
88 | }
89 |
--------------------------------------------------------------------------------
/Sources/SnackView/SnackView Items/SVImageViewItem.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SVImageViewItem.swift
3 | // SnackView
4 | //
5 | // Created by Luca Casula on 02/05/18.
6 | // Copyright © 2018 LucaCasula. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | public class SVImageViewItem: SVItem {
12 |
13 | // MARK: - Properties
14 | private(set) var image: UIImage
15 | private(set) var currentContentMode: UIView.ContentMode
16 | private(set) var currentHeight: CGFloat?
17 |
18 | /// An item in which present a UIImage with contentMode and height.
19 | /// - Parameters:
20 | /// - image: The image to show in the SVImageViewItem item.
21 | /// - contentMode: Options to specify how a view adjusts its content when its size changes.
22 | /// - height: CGFloat value with which specify the height of the imageView.
23 | public init(with image: UIImage, andContentMode contentMode: UIView.ContentMode, andHeight height: CGFloat? = nil) {
24 | self.image = image
25 | self.currentContentMode = contentMode
26 | self.currentHeight = height
27 | super.init()
28 |
29 | self.setMinimumHeightActive(active: false)
30 |
31 | let imageView = UIImageView(image: image)
32 | imageView.translatesAutoresizingMaskIntoConstraints = false
33 | imageView.contentMode = contentMode
34 | self.addSubview(imageView)
35 |
36 | //Add constraints to descriptionLabel
37 | let views = ["imageView": imageView] as [String: Any]
38 | let imageViewHContraints = NSLayoutConstraint.constraints(withVisualFormat: "H:|[imageView]|",
39 | options: [],
40 | metrics: nil,
41 | views: views)
42 |
43 | let imageViewVContraints = NSLayoutConstraint.constraints(withVisualFormat: "V:|[imageView]|",
44 | options: [],
45 | metrics: nil,
46 | views: views)
47 | self.addConstraints(imageViewHContraints)
48 | self.addConstraints(imageViewVContraints)
49 |
50 | if let customHeight = height {
51 | imageView.heightAnchor.constraint(equalToConstant: customHeight).isActive = true
52 | }
53 | }
54 |
55 | required public convenience init?(coder aDecoder: NSCoder) {
56 | return nil
57 | }
58 |
59 | }
60 |
--------------------------------------------------------------------------------
/Sources/SnackView/SnackView Items/SVItem.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SVItem.swift
3 | // SnackView
4 | //
5 | // Created by Luca Casula on 08/11/17.
6 | // Copyright © 2017 Luca Casula. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | /**
12 | SVItem is the main class with which all the items that can be used with SnackView have been created.
13 |
14 | SVItem provides by default some properties common to all elements such as the separator line that appears between the items or the minimum height of the item that is active by default.
15 |
16 | If you need to create a custom item, subclass the SVItem class first.
17 | */
18 | open class SVItem: UIView {
19 |
20 | // MARK: - Private variables
21 | private var bottomLine: UIView!
22 |
23 | // MARK: - Public Variables
24 | private(set) var heightConstraint: NSLayoutConstraint?
25 | public let leftContentWidth: CGFloat = 111
26 |
27 |
28 | /// The color for text labels that contain primary content.
29 | public var primaryTextColor: UIColor {
30 | if #available(iOS 13.0, *) {
31 | return UIColor.label
32 | } else {
33 | return #colorLiteral(red: 0, green: 0, blue: 0, alpha: 1)
34 | }
35 | }
36 |
37 | /// The color for text labels that contain secondary content.
38 | public var secondaryTextColor: UIColor {
39 | if #available(iOS 13.0, *) {
40 | return UIColor.secondaryLabel
41 | } else {
42 | return #colorLiteral(red: 0.5553562641, green: 0.5649003983, blue: 0.5733956099, alpha: 1)
43 | }
44 | }
45 |
46 | /// A blue color that automatically adapts to the current trait environment.
47 | public var blueButtonColor: UIColor {
48 | if #available(iOS 13.0, *) {
49 | return UIColor.systemBlue
50 | } else {
51 | return #colorLiteral(red: 0, green: 0.4779834747, blue: 0.9985283017, alpha: 1)
52 | }
53 | }
54 |
55 | /// The color for thin borders or divider lines that allows some underlying content to be visible.
56 | public var separatorColor: UIColor {
57 | if #available(iOS 13.0, *) {
58 | return UIColor.separator
59 | } else {
60 | return #colorLiteral(red: 0.8010786772, green: 0.8010975718, blue: 0.8010874391, alpha: 1)
61 | }
62 | }
63 |
64 |
65 | // MARK: - Init Method
66 | public init() {
67 | super.init(frame: CGRect.zero)
68 | self.backgroundColor = UIColor.clear
69 |
70 | // Add separator line
71 | self.addBottomLine()
72 |
73 | self.clipsToBounds = true
74 | }
75 |
76 | required public convenience init?(coder aDecoder: NSCoder) {
77 | return nil
78 | }
79 |
80 | /**
81 | Use this method to add or remove the automatic height constraint. SVItem has a minimum height value equal or greater than 50 pixels.
82 | - parameter active: Bool value
83 | */
84 | public func setMinimumHeightActive(active: Bool) {
85 |
86 | if active {
87 | self.setDefaultHeightConstraint()
88 | }
89 | else {
90 | if let tmpConstraint = self.heightConstraint {
91 | self.removeConstraint(tmpConstraint)
92 | self.heightConstraint = nil
93 | }
94 | }
95 | }
96 |
97 | // MARK: - Internal methods
98 | internal func addBottomLine() {
99 | if bottomLine == nil {
100 | bottomLine = UIView()
101 | bottomLine.backgroundColor = self.separatorColor
102 | bottomLine.translatesAutoresizingMaskIntoConstraints = false
103 | self.addSubview(bottomLine)
104 |
105 | //Add constraints to bottomLine
106 | if let _bottomLine = bottomLine {
107 | let bottomLineHConstraints = NSLayoutConstraint.constraints(withVisualFormat: "H:|[bottomLine]|", options: [], metrics: nil, views: ["bottomLine": _bottomLine])
108 | let bottomLineVConstraints = NSLayoutConstraint.constraints(withVisualFormat: "V:[bottomLine(0.5)]|", options: [], metrics: nil, views: ["bottomLine": _bottomLine])
109 | self.addConstraints(bottomLineHConstraints + bottomLineVConstraints)
110 | }
111 |
112 | //Add minimum view height
113 | self.setDefaultHeightConstraint()
114 | }
115 | }
116 |
117 | internal func setDefaultHeightConstraint() {
118 | self.heightConstraint = NSLayoutConstraint(item: self, attribute: .height, relatedBy: .greaterThanOrEqual, toItem: nil, attribute: .notAnAttribute, multiplier: 1, constant: 50)
119 |
120 | if let tmpConstraint = self.heightConstraint {
121 | self.addConstraint(tmpConstraint)
122 | }
123 | }
124 | }
125 |
--------------------------------------------------------------------------------
/Sources/SnackView/SnackView Items/SVLoaderItem.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SVLoaderItem.swift
3 | // SnackView
4 | //
5 | // Created by Luca Casula on 22/03/18.
6 | // Copyright © 2018 Luca Casula. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | /** SVLoaderItem is an item that show an animated activity indicator view. The item can be more specific by showing a text message. */
12 | public class SVLoaderItem: SVItem {
13 |
14 | /** Enumerator describing the size of activity indicator item */
15 | public enum ActivityIndicatorSize {
16 | /** The default size of UIActivityIndicatorView */
17 | case little
18 |
19 | /** The large style of UIActivityIndicatorView */
20 | case large
21 | }
22 |
23 | // MARK: - Properties
24 | private var messageLabel: UILabel
25 | private var activityIndicator: UIActivityIndicatorView
26 | private(set) var size: ActivityIndicatorSize
27 | private(set) var text: String?
28 |
29 | // MARK: - Initialization Method
30 | /**
31 | Initialization method for SVLoaderItem view. You can customize this item with size of the activity indicator view and a custom text message.
32 | - parameter size: The size of activity indicator view
33 | - parameter text: A text that can appear on top of activity indicator view
34 | */
35 | public init(withSize size: ActivityIndicatorSize, andText text: String?) {
36 | self.messageLabel = UILabel()
37 | self.activityIndicator = UIActivityIndicatorView(style: size == .little ? .white : .whiteLarge)
38 |
39 | self.size = size
40 | self.text = text
41 | super.init()
42 |
43 | self.addActivityIndicator()
44 | self.addMessageLabelFor(text: text)
45 | }
46 |
47 | required public init?(coder aDecoder: NSCoder) {
48 | return nil
49 | }
50 |
51 | // MARK: - Private Method
52 |
53 | private func addActivityIndicator() {
54 | self.setMinimumHeightActive(active: false)
55 | self.heightAnchor.constraint(greaterThanOrEqualToConstant: size == .little ? 50 : 70).isActive = true
56 |
57 | self.activityIndicator.color = UIColor.gray
58 | self.activityIndicator.startAnimating()
59 | self.activityIndicator.translatesAutoresizingMaskIntoConstraints = false
60 | self.addSubview(self.activityIndicator)
61 | }
62 |
63 | private func addMessageLabelFor(text: String?) {
64 | if let unwrappedText = text {
65 | self.messageLabel.translatesAutoresizingMaskIntoConstraints = false
66 | self.messageLabel.textColor = self.secondaryTextColor
67 | self.messageLabel.numberOfLines = 0
68 | self.messageLabel.font = UIFont.preferredFont(forTextStyle: .body)
69 | self.messageLabel.adjustsFontForContentSizeCategory = true
70 | self.messageLabel.textAlignment = .center
71 | self.messageLabel.text = unwrappedText
72 | self.addSubview(self.messageLabel)
73 |
74 | //Add constraints to message label
75 | let hConstraints = NSLayoutConstraint.constraints(withVisualFormat: "H:|-[messageLabel]-|",
76 | options: [],
77 | metrics: nil,
78 | views: ["messageLabel": messageLabel])
79 |
80 | let vConstraints = NSLayoutConstraint.constraints(withVisualFormat: "V:|-[messageLabel]-[activityIndicator]-|",
81 | options: [],
82 | metrics: nil,
83 | views: ["messageLabel": messageLabel, "activityIndicator": activityIndicator])
84 | self.addConstraints(hConstraints + vConstraints)
85 |
86 | activityIndicator.centerXAnchor.constraint(equalTo: self.centerXAnchor).isActive = true
87 | } else {
88 | activityIndicator.centerXAnchor.constraint(equalTo: self.centerXAnchor).isActive = true
89 | activityIndicator.centerYAnchor.constraint(equalTo: self.centerYAnchor).isActive = true
90 | }
91 | }
92 | }
93 |
--------------------------------------------------------------------------------
/Sources/SnackView/SnackView Items/SVPriceRowItem.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SVPriceRowItem.swift
3 | //
4 | //
5 | // Created by Luca Casula on 27/04/23.
6 | //
7 |
8 | import UIKit
9 |
10 | /** SVPriceRowItem is an SVItem with which to show a title, a multi-line description text and a price label. */
11 | public class SVPriceRowItem: SVItem {
12 |
13 | public enum Size: CGFloat {
14 | case large = 22
15 | case medium = 17
16 | case small = 12
17 | }
18 |
19 | // MARK: - Properties
20 | private var titleLabel = UILabel()
21 | private var descriptionLabel = UILabel()
22 | private var priceLabel = UILabel()
23 | private(set) var title: String?
24 | private(set) var descriptionText: String?
25 | private(set) var priceText: String
26 | private(set) var size: Size
27 |
28 | // MARK: - Initialization Method
29 | /**
30 | Initialization method for SVPriceRowItem view. You can customize this item with a title and a description text.
31 |
32 | **Note that label text on the left will be rendered as uppercased text**.
33 | To force the placeholder text to be rendered in multi-line please enter **\n** where you want the text to wrap.
34 |
35 |
36 | **Here an example of wrapped text**:
37 | ```
38 | SVPriceRowItem(withTitle: "Price\nOrder", andDescription: "Nike Shoes", andPrice: "€ 79.50")
39 | ```
40 |
41 | - parameter title: The title to show
42 | - parameter description: The description text to show
43 |
44 | */
45 | public init(withTitle title: String?, andDescription description: String?, andPrice priceText: String, size: Size = .large) {
46 | self.title = title
47 | self.descriptionText = description
48 | self.priceText = priceText
49 | self.size = size
50 | super.init()
51 |
52 | [titleLabel, descriptionLabel, priceLabel].forEach { $0.translatesAutoresizingMaskIntoConstraints = false }
53 | self.addTitleLabel()
54 | self.addDescriptionAndPriceLabels()
55 | }
56 |
57 | required public convenience init?(coder aDecoder: NSCoder) {
58 | return nil
59 | }
60 |
61 | // MARK: - Private Methods
62 | private func addTitleLabel() {
63 | self.titleLabel.text = self.title?.uppercased()
64 | self.titleLabel.textAlignment = .right
65 | self.titleLabel.textColor = secondaryTextColor
66 | self.titleLabel.font = UIFont.preferredFont(forTextStyle: .subheadline)
67 | self.titleLabel.adjustsFontForContentSizeCategory = true
68 | self.titleLabel.numberOfLines = 0
69 | self.addSubview(self.titleLabel)
70 |
71 | let views = ["titleLabel": titleLabel]
72 | self.addVisualConstraint("H:|-[titleLabel(==\(self.leftContentWidth))]", for: views)
73 | self.addVisualConstraint("V:|-[titleLabel(>=28)]-|", for: views)
74 | }
75 |
76 | private func addDescriptionAndPriceLabels() {
77 | self.descriptionLabel.text = self.descriptionText
78 | self.descriptionLabel.textAlignment = .left
79 | self.descriptionLabel.textColor = self.primaryTextColor
80 | self.descriptionLabel.font = UIFont.preferredFont(forTextStyle: .body)
81 | self.descriptionLabel.adjustsFontForContentSizeCategory = true
82 | self.descriptionLabel.numberOfLines = 0
83 | self.descriptionLabel.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
84 | self.addSubview(descriptionLabel)
85 |
86 | self.priceLabel.text = self.priceText
87 | self.priceLabel.textAlignment = .right
88 | self.priceLabel.textColor = self.primaryTextColor
89 | self.priceLabel.font = UIFont.preferredFont(forTextStyle: .headline).withSize(size.rawValue)
90 | self.priceLabel.adjustsFontForContentSizeCategory = true
91 | self.priceLabel.numberOfLines = 1
92 | self.priceLabel.setContentCompressionResistancePriority(.defaultHigh, for: .horizontal)
93 |
94 | self.addSubview(priceLabel)
95 |
96 | let views = ["titleLabel": titleLabel, "descriptionLabel": descriptionLabel, "priceLabel": priceLabel]
97 | self.addVisualConstraint("V:|-[descriptionLabel]-|", for: views)
98 | self.addVisualConstraint("H:|-[titleLabel]-[descriptionLabel]-[priceLabel]-|", for: views)
99 | self.addVisualConstraint("V:|-[priceLabel]-|", for: views)
100 | }
101 |
102 | }
103 |
104 |
--------------------------------------------------------------------------------
/Sources/SnackView/SnackView Items/SVSegmentedControllerItem.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SVSegmentedControllerItem.swift
3 | // SnackView
4 | //
5 | // Created by Luca Casula on 18/05/2021.
6 | // Copyright © 2021 LucaCasula. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | /** SVSegmentedControllerItem is an SVItem with which to show a title and a UISegmentedController item. */
12 | public class SVSegmentedControllerItem: SVItem {
13 |
14 | // MARK: - Properties
15 | private var titleLabel: UILabel = UILabel()
16 | private var segmentedController: UISegmentedControl = UISegmentedControl()
17 |
18 | // Title property of the SVSegmentedControllerItem
19 | private(set) var title: String
20 |
21 | // Segments to show within the SVSegmentedControllerItem
22 | private(set) var segments: [String]
23 |
24 | // The index of the selected segment
25 | public var selectedSegment: Int {
26 | set {
27 | self.segmentedController.selectedSegmentIndex = newValue
28 | self.segmentedController.sendActions(for: .valueChanged)
29 | }
30 |
31 | get { return self.segmentedController.selectedSegmentIndex }
32 | }
33 |
34 | // MARK: - Initialization Method
35 |
36 | /**
37 | Initialization method for SVSegmentedControllerItem view. You can customize this item with a title and a segment array.
38 |
39 | - parameter title: Title of the SVSegmentedControllerItem
40 | - parameter segments: String array of the segment to show
41 | - parameter selectionDidChange: Completion handler invoked at the change selection event
42 |
43 | **Note that label text on the left will be rendered as uppercased text**.
44 |
45 | To force the placeholder text to be rendered in multi-line please enter **\n** where you want the text to wrap.
46 |
47 | **Here an example of wrapped text**:
48 | ```
49 | SVSwitchItem(withTitle: "App\nTheme",
50 | segments: ["Dark", "Light"]) { selectedIndex in
51 | print(selectedIndex)
52 | }
53 | ```
54 | */
55 | public init(withTitle title: String, segments: [String], selectionDidChange: @escaping (Int) -> Void) {
56 | self.title = title
57 | self.segments = segments
58 | super.init()
59 |
60 | [titleLabel, segmentedController].forEach {
61 | $0.translatesAutoresizingMaskIntoConstraints = false
62 | self.addSubview($0)
63 | }
64 | self.tmpAction = selectionDidChange
65 | self.setupUI()
66 | }
67 |
68 | required public convenience init?(coder aDecoder: NSCoder) {
69 | return nil
70 | }
71 |
72 | // MARK: - Private Methods
73 | private func setupUI() {
74 | self.addTitleLabel()
75 | self.addSegmentedController()
76 | }
77 |
78 | private func addTitleLabel() {
79 | self.titleLabel.text = self.title.uppercased()
80 | self.titleLabel.textAlignment = .right
81 | self.titleLabel.textColor = secondaryTextColor
82 | self.titleLabel.font = UIFont.preferredFont(forTextStyle: .subheadline)
83 | self.titleLabel.adjustsFontForContentSizeCategory = true
84 | self.titleLabel.numberOfLines = 0
85 |
86 | let views: [String: Any] = ["titleLabel": self.titleLabel]
87 | self.addVisualConstraint("H:|-[titleLabel(==\(self.leftContentWidth))]", for: views)
88 | self.addVisualConstraint("V:|-[titleLabel(>=28)]-|", for: views)
89 | }
90 |
91 | private func addSegmentedController() {
92 | self.segmentedController.addTarget(self, action: #selector(segmentedControllerSelectionDidChange(_:)), for: .valueChanged)
93 | for (index, title) in segments.enumerated() {
94 | self.segmentedController.insertSegment(withTitle: title, at: index, animated: false)
95 | }
96 |
97 | let views: [String: Any] = ["titleLabel": titleLabel as Any, "segment": segmentedController]
98 | self.addVisualConstraint("H:|-[titleLabel]-[segment]-|", for: views)
99 | self.addVisualConstraint("V:|-[segment]-|", for: views)
100 | }
101 |
102 | // MARK: - Custom Stuff
103 | private var tmpAction:(Int) -> Void = {_ in }
104 | @objc public func segmentedControllerSelectionDidChange(_ sender: UISegmentedControl) {
105 | self.tmpAction(sender.selectedSegmentIndex)
106 | }
107 | }
108 |
--------------------------------------------------------------------------------
/Sources/SnackView/SnackView Items/SVSliderItem.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SVSliderItem.swift
3 | // SnackView
4 | //
5 | // Created by Luca Casula on 13/05/2021.
6 | // Copyright © 2021 LucaCasula. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | /** SVSliderItem is an SVItem with which to show a title and a UISlider item. */
12 | public class SVSliderItem: SVItem {
13 |
14 | // MARK: - Properties
15 | private var titleLabel: UILabel = UILabel()
16 | private var slider: UISlider = UISlider()
17 | private(set) var title: String
18 | public var currentValue: Float {
19 | set {
20 | self.slider.value = newValue
21 | self.sliderValueDidChanged(self.slider)
22 | }
23 | get {return self.slider.value}
24 | }
25 |
26 | // MARK: - Initialization Method
27 |
28 | /**
29 | Initialization method for SVSliderItem view. You can customize this item with a title and a description text.
30 | - parameter title: The title to show
31 | - parameter minimum: The minimum value of the slider.
32 | - parameter maximum: The maximum value of the slider.
33 | - parameter current: The slider’s current value.
34 |
35 | **Note that label text on the left will be rendered as uppercased text**.
36 |
37 | To force the placeholder text to be rendered in multi-line please enter **\n** where you want the text to wrap.
38 |
39 |
40 | **Here an example of wrapped text**:
41 | ```
42 | SVSliderItem(withTitle: "Photo\nSaturation",
43 | minimum: 5,
44 | maximum: 20,
45 | current: 12)
46 | ```
47 | */
48 | public init(withTitle title: String, minimum: Float, maximum: Float, current: Float) {
49 | self.title = title
50 | super.init()
51 |
52 | [titleLabel, slider].forEach {
53 | $0.translatesAutoresizingMaskIntoConstraints = false
54 | self.addSubview($0)
55 | }
56 |
57 | self.addTitleLabel()
58 | self.addSliderItem(withMinimumValue: minimum, andMaximumValue: maximum, andCurrentValue: current)
59 | self.setTitle(for: current)
60 | }
61 |
62 | required public convenience init?(coder aDecoder: NSCoder) {
63 | return nil
64 | }
65 |
66 | // MARK: - Private Methods
67 |
68 | private func addTitleLabel() {
69 | self.titleLabel.text = self.title.uppercased()
70 | self.titleLabel.textAlignment = .right
71 | self.titleLabel.textColor = secondaryTextColor
72 | self.titleLabel.font = UIFont.systemFont(ofSize: 14)
73 | self.titleLabel.font = UIFont.preferredFont(forTextStyle: .subheadline)
74 | self.titleLabel.adjustsFontForContentSizeCategory = true
75 | self.titleLabel.numberOfLines = 0
76 |
77 | let views: [String: Any] = ["titleLabel": self.titleLabel]
78 | self.addVisualConstraint("H:|-[titleLabel(==\(self.leftContentWidth))]", for: views)
79 | self.addVisualConstraint("V:|-[titleLabel(>=28)]-|", for: views)
80 | }
81 |
82 | private func addSliderItem(withMinimumValue minimum: Float, andMaximumValue maximum: Float, andCurrentValue current: Float) {
83 | self.slider.minimumValue = minimum
84 | self.slider.maximumValue = maximum
85 | self.slider.value = current
86 | self.slider.addTarget(self, action: #selector(sliderValueDidChanged(_:)), for: .valueChanged)
87 |
88 | let views: [String: Any] = ["titleLabel": titleLabel, "slider": self.slider]
89 | self.addVisualConstraint("[titleLabel]-[slider]-|", for: views)
90 | self.slider.centerYAnchor.constraint(equalTo: self.centerYAnchor).isActive = true
91 | }
92 |
93 | @objc private func sliderValueDidChanged(_ sender: UISlider) {
94 | self.setTitle(for: sender.value)
95 | }
96 |
97 | private func setTitle(for value: Float) {
98 | let formattedValue = String(format: "%.2f", value)
99 | let title = self.title.uppercased()
100 | self.titleLabel.text = "\(title)\n\(formattedValue)"
101 | }
102 |
103 | }
104 |
--------------------------------------------------------------------------------
/Sources/SnackView/SnackView Items/SVSpacerItem.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SVSpacerItem.swift
3 | // SnackView
4 | //
5 | // Created by Luca Casula on 04/06/2021.
6 | // Copyright © 2021 LucaCasula. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | /** SVSpacerItem is an item that show */
12 | public class SVSpacerItem: SVItem {
13 |
14 | /** Enumerator describing the size of activity indicator item */
15 | public enum SpacerSize {
16 | /** The height of the spacer is of 12 pixels */
17 | case little
18 | /** The height of the spacer is of 36 pixels */
19 | case medium
20 | /** The height of the spacer is of 60 pixels */
21 | case large
22 | /** Allow to set a custom height value for the spacer*/
23 | case custom(height: CGFloat)
24 | }
25 |
26 | // MARK: - Initialization Method
27 | /**
28 | Initialization method for SVLoaderItem view. You can customize this item with size of the activity indicator view and a custom text message.
29 | - parameter size: The size of activity indicator view
30 | - parameter text: A text that can appear on top of activity indicator view
31 | */
32 | public init(withSize size: SpacerSize) {
33 | super.init()
34 | self.setMinimumHeightActive(active: false)
35 |
36 | let height: CGFloat = self.getHeight(for: size)
37 | self.heightAnchor.constraint(equalToConstant: height).isActive = true
38 | }
39 |
40 | required public init?(coder aDecoder: NSCoder) {
41 | return nil
42 | }
43 |
44 | // MARK: - Private Methods
45 | private func getHeight(for spacerSize: SpacerSize) -> CGFloat {
46 | switch spacerSize {
47 | case .little: return 12
48 | case .medium: return 36
49 | case .large: return 60
50 | case .custom(let height): return height
51 | }
52 | }
53 | }
54 |
55 |
--------------------------------------------------------------------------------
/Sources/SnackView/SnackView Items/SVStepperItem.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SVStepperItem.swift
3 | // SnackView
4 | //
5 | // Created by Luca Casula on 20/05/2021.
6 | // Copyright © 2021 LucaCasula. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | /** SVStepperItem is an SVItem with which to show a title and a UIStepper item. */
12 | public class SVStepperItem: SVItem {
13 |
14 | // MARK: - Properties
15 | private var titleLabel: UILabel
16 | private var countLabel: UILabel
17 | private var stepper: UIStepper
18 |
19 | // Title property of the SVStepperItem
20 | private(set) var title: String
21 |
22 | // The amount selected from the stepper
23 | public var count: Double {
24 | set {
25 | self.stepper.value = newValue
26 | self.countLabel.text = "\(Int(count))"
27 | self.tmpAction(count)
28 | }
29 | get { stepper.value }
30 | }
31 |
32 | // MARK: - Initialization Method
33 |
34 | /// Initialization method for SVStepperItem view.
35 | /// - Parameters:
36 | /// - title: Title of the SVStepperItem
37 | /// - minimum: Minimum value of the UIStepper controller
38 | /// - maximum: Maximum value of the UIStepper controller
39 | /// - current: Current value of the UIStepper controller
40 | /// - amountDidChange: Completion handler invoked at the change of amount
41 | public init(withTitle title: String, minimum: Double, maximum: Double, current: Double, amountDidChange: @escaping (Double) -> Void) {
42 | self.title = title
43 | self.titleLabel = UILabel()
44 | self.countLabel = UILabel()
45 | self.stepper = UIStepper()
46 |
47 | //Assign the action to tmpAction
48 | self.tmpAction = amountDidChange
49 | super.init()
50 |
51 | self.addTitleLabel()
52 | self.addCountLabel(with: current)
53 | self.addStepperController(withMinimum: minimum, maximum: maximum, andCurrent: current)
54 | }
55 |
56 | required public convenience init?(coder aDecoder: NSCoder) {
57 | return nil
58 | }
59 |
60 | // MARK: - Private Methods
61 | private func addTitleLabel() {
62 | self.titleLabel.translatesAutoresizingMaskIntoConstraints = false
63 | self.titleLabel.adjustsFontForContentSizeCategory = true
64 | self.titleLabel.text = self.title.uppercased()
65 | self.titleLabel.textAlignment = .right
66 | self.titleLabel.textColor = secondaryTextColor
67 | self.titleLabel.font = UIFont.preferredFont(forTextStyle: .subheadline)
68 | self.titleLabel.numberOfLines = 0
69 | self.addSubview(self.titleLabel)
70 |
71 | //Add constraints to titleLabel
72 | let titleHContraints = NSLayoutConstraint.constraints(withVisualFormat: "H:|-[titleLabel(==\(self.leftContentWidth))]",
73 | options: [],
74 | metrics: nil,
75 | views: ["titleLabel": self.titleLabel])
76 | self.addConstraints(titleHContraints)
77 |
78 | let titleVContraints = NSLayoutConstraint.constraints(withVisualFormat: "V:|-[titleLabel(>=28)]-|",
79 | options: [],
80 | metrics: nil,
81 | views: ["titleLabel": self.titleLabel])
82 | self.addConstraints(titleVContraints)
83 | }
84 |
85 | private func addCountLabel(with currentValue: Double) {
86 | self.countLabel.translatesAutoresizingMaskIntoConstraints = false
87 | self.countLabel.text = "\(Int(currentValue))"
88 | self.countLabel.textAlignment = .left
89 | self.countLabel.textColor = primaryTextColor
90 | self.countLabel.font = UIFont.preferredFont(forTextStyle: .headline)
91 | self.countLabel.adjustsFontForContentSizeCategory = true
92 | self.countLabel.numberOfLines = 1
93 | self.addSubview(self.countLabel)
94 |
95 | //Add constraints to titleLabel
96 | let countLabelVContraints = NSLayoutConstraint.constraints(withVisualFormat: "V:|-[countLabel(>=28)]-|",
97 | options: [],
98 | metrics: nil,
99 | views: ["countLabel": self.countLabel])
100 | self.addConstraints(countLabelVContraints)
101 | }
102 |
103 | private func addStepperController(withMinimum minimum: Double, maximum: Double, andCurrent current: Double) {
104 | self.stepper.addTarget(self, action: #selector(stepperDidChangeAmount(_:)), for: .valueChanged)
105 | self.stepper.translatesAutoresizingMaskIntoConstraints = false
106 | self.addSubview(self.stepper)
107 | self.stepper.minimumValue = minimum
108 | self.stepper.maximumValue = maximum
109 | self.stepper.value = current
110 |
111 | //Add constraints to slider item
112 | let segmentControllerHContraints = NSLayoutConstraint.constraints(withVisualFormat:
113 | "H:|-[titleLabel]-[countLabel]-(>=8)-[stepper]-|",
114 | options: [],
115 | metrics: nil,
116 | views: ["titleLabel": titleLabel as Any,
117 | "countLabel": self.countLabel,
118 | "stepper": stepper])
119 | self.addConstraints(segmentControllerHContraints)
120 |
121 | self.stepper.centerYAnchor.constraint(equalTo: self.centerYAnchor).isActive = true
122 |
123 | }
124 |
125 | // MARK: - Custom Stuff
126 | private var tmpAction:(Double) -> Void = {_ in }
127 | @objc public func stepperDidChangeAmount(_ sender: UIStepper) {
128 | self.tmpAction(sender.value)
129 | self.countLabel.text = "\(Int(sender.value))"
130 | }
131 | }
132 |
--------------------------------------------------------------------------------
/Sources/SnackView/SnackView Items/SVSwitchItem.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SVSwitchItem.swift
3 | // SnackView
4 | //
5 | // Created by Luca Casula on 09/04/18.
6 | // Copyright © 2018 LucaCasula. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | /** SVSwitchItem is an SVItem with which to show a title, a description and a UISwitch */
12 | public class SVSwitchItem: SVItem {
13 |
14 | // MARK: - Private Properties
15 | private var titleLabel: UILabel = UILabel()
16 | private var switchItem: UISwitch = UISwitch()
17 | private var descriptionLabel: UILabel = UILabel()
18 |
19 | // MARK: - Properties
20 | private(set) var title: String?
21 | private(set) var descriptionText: String?
22 | private(set) var currentState: Bool
23 |
24 | // MARK: - Initialization Method
25 | /**
26 | Initialization method for SVSwitchItem view. You can customize this item with a title, a description text and an action to assign when UISwitch value change.
27 | - parameter title: The title you want to show
28 | - parameter description: The description text you want to show. This parameter is nullable
29 | - parameter state: The initial state of UISwitch
30 | - parameter switchAction: The action to perform when UISwitch value change
31 |
32 | **Note that label text on the left will be rendered as uppercased text**.
33 |
34 | To force the placeholder text to be rendered in multi-line please enter **\n** where you want the text to wrap.
35 |
36 |
37 | **Here an example of wrapped text**:
38 | ```
39 | SVSwitchItem(withTitle: "Push\nNotifications",
40 | andDescription: "Ipsum lorem sit...",
41 | withState: false) { isSwitchOn in
42 | print(isSwitchOn)
43 | }
44 | ```
45 | */
46 | public init(withTitle title: String?, andDescription description: String?, withState state: Bool, withSwitchAction switchAction: @escaping (_ switchValue: Bool) -> Void) {
47 | self.title = title
48 | self.descriptionText = description
49 | self.currentState = state
50 |
51 | super.init()
52 | [titleLabel, switchItem, descriptionLabel].forEach {
53 | $0.translatesAutoresizingMaskIntoConstraints = false
54 | self.addSubview($0)
55 | }
56 | self.addTitleLabel()
57 | self.addSwitch()
58 | self.addDescriptionLabel()
59 |
60 | //Assign the UISwitch action to tmpAction
61 | self.tmpAction = switchAction
62 | }
63 |
64 | required public convenience init?(coder aDecoder: NSCoder) {
65 | return nil
66 | }
67 |
68 | // MARK: - Private Methods
69 | private func addTitleLabel() {
70 | self.titleLabel.text = title?.uppercased()
71 | self.titleLabel.textAlignment = .right
72 | self.titleLabel.textColor = secondaryTextColor
73 | self.titleLabel.font = UIFont.preferredFont(forTextStyle: .subheadline)
74 | self.titleLabel.adjustsFontForContentSizeCategory = true
75 | self.titleLabel.numberOfLines = 0
76 |
77 | let views: [String: Any] = ["titleLabel": titleLabel]
78 | self.addVisualConstraint("H:|-[titleLabel(==\(self.leftContentWidth))]", for: views)
79 | self.addVisualConstraint("V:|-[titleLabel(>=28)]-|", for: views)
80 | }
81 |
82 | private func addSwitch() {
83 | self.switchItem.addTarget(self, action: #selector(switchSelector(switchItem:)), for: .valueChanged)
84 | self.switchItem.tintColor = self.secondaryTextColor
85 |
86 | let views: [String: Any] = ["switch": switchItem]
87 | self.addVisualConstraint("H:[switch]-|", for: views)
88 | self.switchItem.centerYAnchor.constraint(equalTo: self.centerYAnchor).isActive = true
89 |
90 | }
91 |
92 | private func addDescriptionLabel() {
93 | if let text = descriptionText {
94 | descriptionLabel.text = text
95 | }
96 | self.descriptionLabel.textAlignment = .left
97 | self.descriptionLabel.textColor = self.primaryTextColor
98 | self.descriptionLabel.textColor = self.primaryTextColor
99 | self.descriptionLabel.font = UIFont.preferredFont(forTextStyle: .body)
100 | self.descriptionLabel.adjustsFontForContentSizeCategory = true
101 | self.descriptionLabel.numberOfLines = 0
102 |
103 | descriptionLabel.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
104 | let views: [String: Any] = ["titleLabel": titleLabel, "switch": switchItem, "descriptionLabel": descriptionLabel]
105 | self.addVisualConstraint("V:|-[descriptionLabel]-|", for: views)
106 | self.addVisualConstraint("H:[titleLabel]-[descriptionLabel]-[switch]", for: views)
107 | }
108 |
109 | // MARK: - Custom stuff
110 | var tmpAction:(_ switchValue: Bool) -> Void = {_ in }
111 | @objc func switchSelector(switchItem: UISwitch) {
112 | let currentState = switchItem.isOn
113 |
114 | self.tmpAction(currentState)
115 | self.currentState = currentState
116 | }
117 |
118 | }
119 |
--------------------------------------------------------------------------------
/Sources/SnackView/SnackView Items/SVTextFieldItem.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SVTextFieldItem.swift
3 | // SnackView
4 | //
5 | // Created by Luca Casula on 15/11/17.
6 | // Copyright © 2017 Luca Casula. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | /** SVTextFieldItem is an SVItem with which to show a placeholder text and a UITextField.
12 |
13 | You can access to text field content using the 'text' property of SVTextFieldItem.
14 | */
15 | public class SVTextFieldItem: SVItem {
16 |
17 | // MARK: - Private Properties
18 | private var titleLabel: UILabel = UILabel()
19 | private var textField: UITextField = UITextField()
20 |
21 | // MARK: - Public Properties
22 | private(set) var placeholder: String
23 | private(set) var isSecure: Bool
24 |
25 |
26 | /// The current text that is displayed by the label.
27 | public var text: String? {
28 | get { return self.textField.text }
29 | set { self.textField.text = newValue }
30 | }
31 |
32 | // MARK: - Initialization Method
33 | /**
34 | Initialization method for SVTextFieldItem view. You can customize this item with a placeholder text and with a boolean value to set UITextField as secure text entry.
35 | - parameter placeholder: The placeholder text to show on left item and as placeholder for UITextField
36 | - parameter isSecureField: A boolean value to indicate if UITextField is secure text entry
37 |
38 |
39 | **Note that label text on the left will be rendered as uppercased text**.
40 |
41 | To force the placeholder text to be rendered in multi-line please enter **\n** where you want the text to wrap.
42 |
43 | **Here an example of wrapped text**:
44 | ```
45 | SVTextFieldItem(withPlaceholder: "Repeat\nPassword",
46 | isSecureField: true)
47 | ```
48 | */
49 | public init(withPlaceholder placeholder: String, isSecureField isSecure: Bool) {
50 | self.placeholder = placeholder
51 | self.isSecure = isSecure
52 | super.init()
53 |
54 | [titleLabel, textField].forEach {
55 | $0.translatesAutoresizingMaskIntoConstraints = false
56 | self.addSubview($0)
57 | }
58 |
59 | self.addTitleLabel()
60 | self.addTextField()
61 | }
62 |
63 | required public convenience init?(coder aDecoder: NSCoder) {
64 | return nil
65 | }
66 |
67 | // MARK: - Private Methods
68 | private func addTitleLabel() {
69 | self.titleLabel.text = placeholder.uppercased()
70 | self.titleLabel.textAlignment = .right
71 | self.titleLabel.textColor = secondaryTextColor
72 | self.titleLabel.font = UIFont.preferredFont(forTextStyle: .subheadline)
73 | self.titleLabel.adjustsFontForContentSizeCategory = true
74 | self.titleLabel.numberOfLines = 0
75 |
76 | let views: [String: Any] = ["titleLabel": titleLabel]
77 | self.addVisualConstraint("H:|-[titleLabel(==\(self.leftContentWidth))]", for: views)
78 | self.addVisualConstraint("V:|-[titleLabel(>=28)]-|", for: views)
79 | }
80 |
81 | private func addTextField() {
82 | self.textField.isSecureTextEntry = isSecure
83 | self.textField.borderStyle = .none
84 | self.textField.font = UIFont.preferredFont(forTextStyle: .body)
85 | self.textField.adjustsFontForContentSizeCategory = true
86 | self.textField.placeholder = self.removeNewLine(fromString: placeholder)
87 |
88 | let views: [String: Any] = ["titleLabel": titleLabel, "textfield": textField]
89 | self.addVisualConstraint("H:[titleLabel]-[textfield]-|", for: views)
90 | self.addVisualConstraint("V:|-[textfield]-|", for: views)
91 | }
92 |
93 | // MARK: - Custom stuff
94 |
95 | /** This is a private function that remove the occurence of strings **\n** and **\r** and replace it with a white space. */
96 | private func removeNewLine(fromString string: String) -> String {
97 | var result = string.replacingOccurrences(of: "\r", with: " ")
98 | result = result.replacingOccurrences(of: "\n", with: " ")
99 | return result
100 | }
101 | }
102 |
--------------------------------------------------------------------------------
/Sources/SnackView/SnackView Items/SVTitleItem.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SVTitleItem.swift
3 | // SnackView
4 | //
5 | // Created by Luca Casula on 08/11/17.
6 | // Copyright © 2017 Luca Casula. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | class SVTitleItem: SVItem {
12 |
13 | // MARK: - Private Properties
14 | private var titleLabel: UILabel
15 | private(set) var title: String!
16 | private(set) var cancelButtonTitle: String!
17 |
18 | // MARK: - Public Properties
19 | public var cancelButton: UIButton
20 |
21 | // MARK: - Initialization Method
22 | public override init() {
23 | self.titleLabel = UILabel()
24 | self.cancelButton = UIButton()
25 |
26 | super.init()
27 | self.addTitleLabel()
28 | self.addCancelButton()
29 | }
30 |
31 | // MARK: - Public Methods
32 | public func setTitle(_ title: String) {
33 | self.title = title
34 | self.titleLabel.text = title
35 | }
36 |
37 | public func setCancelTitle(_ cancelTitle: String?) {
38 | guard let cancelTitle = cancelTitle else {
39 | self.cancelButton.isHidden = true
40 | return
41 | }
42 | self.cancelButtonTitle = cancelTitle
43 | self.cancelButton.isHidden = false
44 | self.cancelButton.setTitle(cancelTitle, for: UIControl.State())
45 | }
46 |
47 | // MARK: - Private Methods
48 | private func addTitleLabel() {
49 | //Disable minimum height value
50 | self.setMinimumHeightActive(active: false)
51 |
52 | //Add title label
53 | self.titleLabel.translatesAutoresizingMaskIntoConstraints = false
54 | self.titleLabel.text = "SnackView"
55 |
56 | self.titleLabel.font = UIFont.preferredFont(forTextStyle: .headline)
57 | self.titleLabel.adjustsFontForContentSizeCategory = true
58 | self.titleLabel.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
59 |
60 | self.titleLabel.textColor = self.primaryTextColor
61 | self.addSubview(self.titleLabel)
62 |
63 | let titleVContraints = NSLayoutConstraint.constraints(withVisualFormat: "V:|-[titleLabel]-|", options: [], metrics: nil, views: ["titleLabel": titleLabel])
64 | self.addConstraints(titleVContraints)
65 | }
66 |
67 | private func addCancelButton() {
68 | self.cancelButton.translatesAutoresizingMaskIntoConstraints = false
69 | self.cancelButton.setTitle("Cancel", for: UIControl.State())
70 | self.cancelButton.titleLabel?.font = UIFont.preferredFont(forTextStyle: .headline)
71 | self.cancelButton.titleLabel?.adjustsFontForContentSizeCategory = true
72 | self.cancelButton.setContentCompressionResistancePriority(.defaultHigh, for: .horizontal)
73 | self.cancelButton.setTitleColor(blueButtonColor, for: UIControl.State.normal)
74 | self.cancelButton.setTitleColor(blueButtonColor.withAlphaComponent(0.5), for: UIControl.State.highlighted)
75 | self.addSubview(self.cancelButton)
76 |
77 | self.cancelButton.centerYAnchor.constraint(equalTo: self.centerYAnchor).isActive = true
78 | let cancelButtonHContraints = NSLayoutConstraint.constraints(withVisualFormat: "H:|-[titleLabel]-(>=8)-[cancelButton]-|",
79 | options: [],
80 | metrics: nil,
81 | views: ["titleLabel": titleLabel, "cancelButton": cancelButton])
82 | self.addConstraints(cancelButtonHContraints)
83 |
84 | }
85 | }
86 |
--------------------------------------------------------------------------------
/Sources/SnackView/SnackView Utilities/CustomInputAccessoryView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // CustomInputAccessoryView.swift
3 | // SnackView
4 | //
5 | // Created by Luca Casula on 15/11/17.
6 | // Copyright © 2017 Luca Casula. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | class CustomInputAccessoryView: UIView {
12 |
13 | override func willMove(toSuperview newSuperview: UIView?) {
14 | if let oldSuperView = self.superview {
15 | oldSuperView.removeObserver(self, forKeyPath: "center")
16 | }
17 | newSuperview?.addObserver(self, forKeyPath: "center", options: NSKeyValueObservingOptions.new, context: nil)
18 | super.willMove(toSuperview: newSuperview)
19 | }
20 |
21 | override public func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey: Any]?, context: UnsafeMutableRawPointer?) {
22 | if change?[NSKeyValueChangeKey.newKey] != nil {
23 | if let originY = self.superview?.frame.origin.y {
24 | let screenHeight = UIScreen.main.bounds.height
25 | let constant = screenHeight - originY
26 | let notificationName = NSNotification.Name(rawValue: "KeyboardFrameDidChange")
27 | NotificationCenter.default.post(name: notificationName,
28 | object: nil,
29 | userInfo: ["constant": constant])
30 | }
31 | }
32 | }
33 | }
34 |
35 |
--------------------------------------------------------------------------------
/Sources/SnackView/SnackView Utilities/UIView-Extensions.swift:
--------------------------------------------------------------------------------
1 | //
2 | // UIView-Extensions.swift
3 | // SnackView
4 | //
5 | // Created by Luca Casula on 11/06/21.
6 | // Copyright © 2021 LucaCasula. All rights reserved.
7 | //
8 |
9 |
10 | import UIKit
11 |
12 | extension UIView {
13 | public func addVisualConstraint(_ constrantFormat: String, for views: [String : Any]) {
14 |
15 | let constraint = NSLayoutConstraint.constraints(withVisualFormat: constrantFormat,
16 | options: [],
17 | metrics: nil,
18 | views: views)
19 | self.addConstraints(constraint)
20 | }
21 | }
22 |
23 |
24 |
--------------------------------------------------------------------------------
/Sources/SnackView/SnackView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SnackView.swift
3 | // SnackView
4 | //
5 | // Created by Luca Casula on 08/11/17.
6 | // Copyright © 2017 Luca Casula. All rights reserved.
7 | //
8 |
9 | import UIKit
10 | import os.log
11 |
12 | public class SnackView: UIViewController {
13 |
14 | // MARK: - Outlets and Variables
15 | internal weak var dataSource: SnackViewDataSource?
16 |
17 | public internal(set) var items: [SVItem]? = []
18 | internal var window: UIWindow?
19 | internal var skeletonView: SVSkeletonView
20 | internal var bottomContentViewConstant = NSLayoutConstraint()
21 | internal var keyboardObserver: SnackViewKeyboardObserver?
22 | override public var inputAccessoryView: UIView? {
23 | let customInput = CustomInputAccessoryView()
24 | customInput.frame.size.height = 0.1
25 | return customInput
26 | }
27 |
28 | // MARK: - Initialization Methods
29 | /// Initialization method for SnackView object
30 | ///
31 | /// - Parameter dataSource: Class conformed to SnackViewProtocol
32 | public init(with dataSource: SnackViewDataSource) {
33 | self.dataSource = dataSource
34 | self.skeletonView = SVSkeletonView()
35 |
36 | super.init(nibName: nil, bundle: nil)
37 | }
38 |
39 | required public init?(coder aDecoder: NSCoder) {
40 | return nil
41 | }
42 |
43 | deinit {
44 | NSLog("SnackView has been deinitialized.")
45 | }
46 |
47 | // MARK: - UIViewController Methods
48 | override public func viewDidLoad() {
49 | super.viewDidLoad()
50 |
51 | /// Prepare the SnackView view controller with modalPresentationStyle and contentView hidden.
52 | self.modalPresentationStyle = .overCurrentContext
53 | self.addContentViewWithConstraints()
54 | self.skeletonView.injectCancelButton(from: self)
55 |
56 | let scrollView = self.skeletonView.scrollView.scrollView
57 | let snackView: SnackView = self
58 | self.keyboardObserver = SnackViewKeyboardObserver(with: self.bottomContentViewConstant,
59 | from: snackView,
60 | and: scrollView)
61 |
62 | }
63 |
64 | override public func viewWillAppear(_ animated: Bool) {
65 | super.viewWillAppear(animated)
66 | self.setBackgroundForWillAppear()
67 | self.getDataFromDataSource()
68 | }
69 |
70 | override public func viewDidAppear(_ animated: Bool) {
71 | super.viewDidAppear(animated)
72 |
73 | self.showSnackViewWithAnimation()
74 | }
75 |
76 | }
77 |
--------------------------------------------------------------------------------
/Sources/SnackView/SnackViewDataSource.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SnackViewDataSource.swift
3 | // SnackView
4 | //
5 | // Created by Luca Casula on 09/01/19.
6 | // Copyright © 2019 LucaCasula. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | /**
12 | The methods adopted by the object you use to manage data and provide SVItem array for a SnackView
13 |
14 | SnackView manage only the presentation of their data; they do not manage the data itself. To manage the data, you provide the SnackView with a data source object—that is, an object that implements the SnackViewDataSource protocol. A data source object responds to data-related requests from the SnackView. It also manages the SnackView's data directly, or coordinates with other parts of your app to manage that data. Other responsibilities of the data source object include:
15 | - Reporting the title for a specific SnackView.
16 | - Providing the title for the cancel / dismiss button for a specific SnackView.
17 | - Providing the SVItem array containing the items to display for a specific SnackView.
18 |
19 | */
20 | public protocol SnackViewDataSource: AnyObject {
21 |
22 | /// Tells the data source to return the title for a specific SnackView.
23 | ///
24 | /// - Parameter snackView: Instance of SnackView for which get title string
25 | /// - Returns: String value rapresenting SnackView title
26 | func titleFor(snackView: SnackView) -> String
27 |
28 | /// Tells the data source to return the title of **cancel** or **dismiss** button for a specific SnackView.
29 | ///
30 | /// Dismission button can be obmitted by returning a `nil` value. In case it is responsability of the developer allow the user to dismiss the `SnackView` sheet.
31 | ///
32 | /// - Parameter snackView: Instance of SnackView for which get cancel button title string
33 | /// - Returns: Optional string value rapresenting SnackView cancel button title or nil if you want to hide Cancel button.
34 | func cancelTitleFor(snackView: SnackView) -> String?
35 |
36 | /// Tells the data source to return an SVItem array for a specific SnackView.
37 | ///
38 | /// - Parameter snackView: Instance of SnackView for which get items array
39 | /// - Returns: Array containing SVItem's to display in SnackView
40 | func itemsFor(snackView: SnackView) -> [SVItem]
41 | }
42 |
--------------------------------------------------------------------------------
/Sources/SnackView/SnackViewInternalMethods.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SnackViewExtension.swift
3 | // SnackView
4 | //
5 | // Created by Luca Casula on 27/12/18.
6 | // Copyright © 2018 LucaCasula. All rights reserved.
7 | //
8 | import UIKit
9 |
10 | extension SnackView {
11 |
12 | // MARK: - SnackView Setup
13 |
14 | /// Prepare SnackView for will appear state, set background color to clear and translate view off screen.
15 | internal func setBackgroundForWillAppear() {
16 | DispatchQueue.main.async {
17 | // Set SnackView visible
18 | self.skeletonView.isHidden = false
19 | self.view.backgroundColor = UIColor.clear
20 |
21 | // Hide the SnackView out the screen bounds and set visible
22 | let contentViewHeight = self.skeletonView.frame.size.height + self.skeletonView.getSafeAreaHeight()
23 | self.skeletonView.transform = CGAffineTransform(translationX: 0, y: contentViewHeight)
24 | }
25 | }
26 |
27 | /// Animate the SnackView presentation, set a background color with alpha 0.5 and then translate SnackView to original position.
28 | internal func showSnackViewWithAnimation() {
29 |
30 | func animateBackgroundColor() {
31 | let backgroundColor: UIColor = UIColor.black
32 | UIView.animate(withDuration: 0.25, animations: {
33 | self.view.backgroundColor = backgroundColor.withAlphaComponent(0.4)
34 | }) { (_) in
35 | showSnackViewAnimation()
36 | }
37 | }
38 |
39 | func showSnackViewAnimation() {
40 | UIView.animate(withDuration: 0.25, animations: {
41 | self.skeletonView.transform = CGAffineTransform.identity
42 | })
43 | }
44 |
45 | DispatchQueue.main.async { animateBackgroundColor() }
46 | }
47 |
48 | // MARK: - SnackView skeleton
49 |
50 | /// ContentView is a view that contains all the items of SnackView such as TitleBar, ScrollView with all the items inside and the safeArea view.
51 | internal func addContentViewWithConstraints() {
52 | self.skeletonView = SVSkeletonView()
53 | self.skeletonView.translatesAutoresizingMaskIntoConstraints = false
54 | self.view.addSubview(skeletonView)
55 |
56 | /// Default top constraint anchor
57 | var topConstraint = self.view.topAnchor
58 |
59 | // Use safe area layout guide if possible
60 | if #available(iOS 11.0, *) {
61 | topConstraint = self.view.safeAreaLayoutGuide.topAnchor
62 | }
63 |
64 | self.skeletonView.topAnchor.constraint(greaterThanOrEqualTo: topConstraint, constant: 0).isActive = true
65 | self.skeletonView.leftAnchor.constraint(equalTo: self.view.leftAnchor).isActive = true
66 | self.skeletonView.rightAnchor.constraint(equalTo: self.view.rightAnchor).isActive = true
67 |
68 | self.bottomContentViewConstant = self.skeletonView.bottomAnchor.constraint(equalTo: self.view.bottomAnchor)
69 | self.view.addConstraint(bottomContentViewConstant)
70 | }
71 |
72 | internal func getDataFromDataSource() {
73 | var title: String = ""
74 | if let _title = self.dataSource?.titleFor(snackView: self) { title = _title }
75 | let cancelTitle = self.dataSource?.cancelTitleFor(snackView: self)
76 | self.skeletonView.setTitle(title, andCancelTitle: cancelTitle)
77 |
78 | self.reloadData()
79 | }
80 |
81 | internal func checkIfItemArrayIsEmpty(_ items: [SVItem]) -> [SVItem] {
82 | if items.isEmpty {
83 | self.skeletonView.setTitle("Invalid configuration", andCancelTitle: "Close")
84 |
85 | let description = SVDescriptionItem(withDescription: "It seems thet SnackView isn't properly configured.\nHere's what could have gone wrong.")
86 | let firstCause = SVDetailTextItem(withTitle: "Empty items array", andDescription: "Maybe you are trying to show an empty items array.")
87 | let secondCause = SVDetailTextItem(withTitle: "DataSource with weak reference", andDescription: "If you have a standalone datasource class, you need to keep the reference from the UIViewController that want to present the SnackView.")
88 |
89 | return [description, firstCause, secondCause]
90 | }
91 | return items
92 | }
93 |
94 | // MARK: - Helper methods
95 |
96 | /// This method creates a UIViewController which is the root view controller of UIWindow.
97 | ///
98 | /// - Returns: UIViewController to use to present the SnackView
99 | internal func getPresenterViewController() -> UIViewController {
100 | let containerViewController = UIViewController()
101 | containerViewController.title = "SnackView Container"
102 | containerViewController.modalPresentationStyle = .overFullScreen
103 | containerViewController.view.backgroundColor = UIColor.clear
104 | containerViewController.view.isUserInteractionEnabled = true
105 | self.setupWindow(for: containerViewController)
106 |
107 | return containerViewController
108 | }
109 |
110 | internal func setupWindow(for viewController: UIViewController) {
111 | window = nil
112 | window = UIWindow(frame: UIScreen.main.bounds)
113 |
114 | if #available(iOS 13.0, *),
115 | let scene = UIApplication.shared.connectedScenes.filter({ $0.activationState == .foregroundActive }).first as? UIWindowScene {
116 | self.window = UIWindow(windowScene: scene)
117 | }
118 |
119 | window?.rootViewController = viewController
120 | window?.backgroundColor = UIColor.clear
121 | window?.windowLevel = UIWindow.Level.alert+1
122 | window?.makeKeyAndVisible()
123 | window?.resignFirstResponder()
124 | }
125 | // MARK: - Private custom selector
126 |
127 | /// Animate the SnackView dismiss, translate SnackView off screen and set background color to clear.
128 | @objc internal func closeActionSelector() {
129 | DispatchQueue.main.async {
130 | // Hide the SnackView out the screen bounds and set visible
131 | self.animateContentView()
132 | }
133 | }
134 |
135 | internal func dismissAndClean() {
136 | self.dismiss(animated: false) {
137 | self.window?.rootViewController = nil
138 | self.window?.resignFirstResponder()
139 | self.window?.removeFromSuperview()
140 | self.window = nil
141 | }
142 | }
143 |
144 | internal func animateBackgroundColor() {
145 | UIView.animate(withDuration: 0.25, animations: {
146 | self.view.backgroundColor = UIColor.clear
147 | }) { (_) in self.dismissAndClean() }
148 | }
149 |
150 | internal func animateContentView() {
151 | let contentViewHeight = self.skeletonView.frame.size.height + self.skeletonView.getSafeAreaHeight()
152 |
153 | // Background Color Animation
154 | UIView.animate(withDuration: 0.25, animations: {
155 | self.skeletonView.transform = CGAffineTransform(translationX: 0, y: contentViewHeight)
156 | }) { (_) in self.animateBackgroundColor() }
157 | }
158 | }
159 |
--------------------------------------------------------------------------------
/Sources/SnackView/SnackViewKeyboardObserver.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SnackViewKeyboardObserver.swift
3 | // SnackView
4 | //
5 | // Created by Luca Casula on 10/06/2021.
6 | // Copyright © 2021 LucaCasula. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | class SnackViewConstant {
12 | static public let animationSpeed: TimeInterval = 0.25
13 | }
14 |
15 | class SnackViewKeyboardObserver {
16 |
17 | // MARK: - Private Properties
18 | private var bottomContentViewConstant: NSLayoutConstraint
19 | private var snackView: SnackView
20 | private var scrollView: UIScrollView
21 |
22 | // MARK: - Init Method
23 | init(with constraint: NSLayoutConstraint, from snackView: SnackView, and scrollView: UIScrollView) {
24 | self.bottomContentViewConstant = constraint
25 | self.snackView = snackView
26 | self.scrollView = scrollView
27 |
28 | self.addNotificationsObserver()
29 | }
30 |
31 | @objc func keyboardWillShow(notification: Notification) {
32 | scrollView.alwaysBounceVertical = true
33 |
34 | let keyboardSize = self.getKeyboardSizeFrom(notification: notification)
35 | let animationSpeed = self.getAnimationDurationFrom(notification: notification)
36 |
37 | bottomContentViewConstant.constant = -keyboardSize.height
38 |
39 | UIView.animate(withDuration: animationSpeed.doubleValue) {
40 | self.snackView.view.layoutIfNeeded()
41 | }
42 | }
43 |
44 | @objc func keyboardWillHide(notification: Notification) {
45 | scrollView.alwaysBounceVertical = false
46 | let animationSpeed = self.getAnimationDurationFrom(notification: notification)
47 |
48 | self.bottomContentViewConstant.constant = 0
49 |
50 | UIView.animate(withDuration: animationSpeed.doubleValue) {
51 | self.snackView.view.layoutIfNeeded()
52 | }
53 | }
54 |
55 | @objc func keyboardFrameDidChange(notification: Notification) {
56 | if let constant = notification.userInfo?["constant"] as? CGFloat {
57 | bottomContentViewConstant.constant = -constant
58 | }
59 | }
60 |
61 | // MARK: - Private Methods
62 | private func addNotificationsObserver() {
63 | let notificationCenter = NotificationCenter.default
64 | let keyboardWillShow = UIResponder.keyboardWillShowNotification
65 | notificationCenter.addObserver(self,
66 | selector: #selector(keyboardWillShow(notification:)),
67 | name: keyboardWillShow,
68 | object: nil)
69 |
70 | let keyboardWillHide = UIResponder.keyboardWillHideNotification
71 | notificationCenter.addObserver(self,
72 | selector: #selector(keyboardWillHide(notification:)),
73 | name: keyboardWillHide,
74 | object: nil)
75 |
76 | let keyboardFrameDidChange = NSNotification.Name(rawValue: "KeyboardFrameDidChange")
77 | notificationCenter.addObserver(self,
78 | selector: #selector(keyboardFrameDidChange(notification:)),
79 | name: keyboardFrameDidChange,
80 | object: nil)
81 | }
82 |
83 | private func getKeyboardSizeFrom(notification: Notification) -> CGRect {
84 | if let keyboardSize = notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? CGRect {
85 | return keyboardSize
86 | }
87 | return CGRect.zero
88 | }
89 |
90 | private func getAnimationDurationFrom(notification: Notification) -> NSNumber {
91 | if let animationDuration = notification.userInfo?[UIResponder.keyboardAnimationDurationUserInfoKey] as? NSNumber {
92 | return animationDuration
93 | }
94 | return NSNumber(value: SnackViewConstant.animationSpeed)
95 | }
96 |
97 | }
98 |
--------------------------------------------------------------------------------
/Sources/SnackView/SnackViewPublicMethods.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SnackViewPublicMethods.swift
3 | // SnackView
4 | //
5 | // Created by Luca Casula on 28/12/18.
6 | // Copyright © 2018 LucaCasula. All rights reserved.
7 | //
8 |
9 | extension SnackView {
10 |
11 | // MARK: - Presentation and Dismission methods
12 |
13 | /// Present SnackView with custom animation.
14 | public func show() {
15 | self.modalPresentationStyle = .overFullScreen
16 |
17 | let presenter = self.getPresenterViewController()
18 | presenter.present(self, animated: false, completion: nil)
19 | }
20 |
21 | /// Dismiss SnackView with custom animation.
22 | public func close() {
23 | self.closeActionSelector()
24 | }
25 |
26 | /// Reload the content of SnackView in case of update.
27 | public func reloadData() {
28 | if let _items = self.dataSource?.itemsFor(snackView: self) {
29 | let checkedItems = checkIfItemArrayIsEmpty(_items)
30 | self.items = checkedItems
31 | self.skeletonView.reload(with: checkedItems)
32 | }
33 |
34 | }
35 |
36 |
37 | }
38 |
--------------------------------------------------------------------------------
/Tests/SnackView copy.xctestplan:
--------------------------------------------------------------------------------
1 | {
2 | "configurations" : [
3 |
4 | ],
5 | "defaultOptions" : {
6 | "codeCoverage" : {
7 | "targets" : [
8 | {
9 | "containerPath" : "container:",
10 | "identifier" : "SnackView",
11 | "name" : "SnackView"
12 | }
13 | ]
14 | },
15 | "uiTestingScreenshotsLifetime" : "keepNever",
16 | "userAttachmentLifetime" : "keepNever"
17 | },
18 | "testTargets" : [
19 | {
20 | "target" : {
21 | "containerPath" : "container:",
22 | "identifier" : "SnackViewTests",
23 | "name" : "SnackViewTests"
24 | }
25 | }
26 | ],
27 | "version" : 1
28 | }
29 |
--------------------------------------------------------------------------------
/Tests/SnackViewTests/Extensions.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Extensions.swift
3 | //
4 | //
5 | // Created by Luca Casula on 21/04/23.
6 | //
7 |
8 | import UIKit
9 |
10 | extension UIImage {
11 | class func generateBlackSquare() -> UIImage {
12 | let size = CGSize(width: 100, height: 100)
13 | let rect = CGRect(x: 0, y: 0, width: size.width, height: size.height)
14 | UIGraphicsBeginImageContextWithOptions(size, false, 0)
15 | UIColor.black.setFill()
16 | UIRectFill(rect)
17 | let image = UIGraphicsGetImageFromCurrentImageContext()
18 | UIGraphicsEndImageContext()
19 | return image!
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/Tests/SnackViewTests/MockSnackViewDataSource.swift:
--------------------------------------------------------------------------------
1 | //
2 | // MockSnackViewDataSource.swift
3 | // SnackViewTests
4 | //
5 | // Created by Luca Casula on 14/05/2021.
6 | // Copyright © 2021 LucaCasula. All rights reserved.
7 | //
8 |
9 | import UIKit
10 | import SnackView
11 |
12 | class MockSnackViewDataSource: SnackViewDataSource {
13 |
14 | private var items: [SVItem] = []
15 |
16 | func titleFor(snackView: SnackView) -> String {
17 | return "Mock SnackView"
18 | }
19 |
20 | func cancelTitleFor(snackView: SnackView) -> String? {
21 | "Cancel"
22 | }
23 |
24 | func itemsFor(snackView: SnackView) -> [SVItem] {
25 | return self.items
26 | }
27 |
28 | public func set(items: [SVItem]) {
29 | self.items = items
30 | }
31 |
32 | }
33 |
--------------------------------------------------------------------------------
/Tests/SnackViewTests/SnackViewItemsTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SnackViewItemsTests.swift
3 | // SnackViewTests
4 | //
5 | // Created by Luca Casula on 14/05/2021.
6 | // Copyright © 2021 LucaCasula. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import UIKit
11 | import Nimble
12 | import Quick
13 |
14 | @testable import SnackView
15 |
16 | class SnackViewItemsTests: QuickSpec {
17 | override func spec() {
18 |
19 | var snackView: SnackView?
20 | var snackViewSpy: MockSnackViewDataSource?
21 |
22 | beforeEach {
23 | snackViewSpy = MockSnackViewDataSource()
24 | snackView = SnackView(with: snackViewSpy!)
25 |
26 | _ = snackView?.view
27 | }
28 |
29 | describe("SVDetailTextItem") {
30 | let detailedTextItem = SVDetailTextItem(withTitle: "Details", andDescription: "Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book.")
31 |
32 | it("with long text had to have at least 50 px of height.") {
33 | snackViewSpy?.set(items: [detailedTextItem])
34 | snackView?.reloadData()
35 |
36 | expect(detailedTextItem.frame.height).to(beGreaterThan(50))
37 | }
38 | }
39 |
40 | describe("SVDetailTextItem from init with coder") {
41 | let archiver = NSKeyedArchiver(requiringSecureCoding: false)
42 | let detailedTextItem = SVDetailTextItem(coder: archiver)
43 |
44 | it("had to return nil.") {
45 | expect(detailedTextItem).to(beNil())
46 | }
47 | }
48 |
49 | describe("SVDescriptionItem") {
50 | let descriptionItem = SVDescriptionItem(withDescription: "Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book.")
51 |
52 | it("with long text had to have at least 50 px of height.") {
53 | snackViewSpy?.set(items: [descriptionItem])
54 | snackView?.reloadData()
55 |
56 | expect(descriptionItem.frame.height).to(beGreaterThan(50))
57 | }
58 | }
59 |
60 | describe("SVDescriptionItem from init with coder") {
61 | let archiver = NSKeyedArchiver(requiringSecureCoding: false)
62 | let descriptionItem = SVDescriptionItem(coder: archiver)
63 |
64 | it("had to return nil.") {
65 | expect(descriptionItem).to(beNil())
66 | }
67 | }
68 |
69 |
70 | describe("SVTextFieldItem") {
71 | let textFieldItem = SVTextFieldItem(withPlaceholder: "First name", isSecureField: false)
72 | textFieldItem.text = "John"
73 |
74 | it("had to have 'First name' as placeholder") {
75 | snackViewSpy?.set(items: [textFieldItem])
76 | snackView?.reloadData()
77 |
78 | expect(textFieldItem.text).to(equal("John"))
79 | expect(textFieldItem.placeholder).to(equal("First name"))
80 | expect(textFieldItem.isSecure).to(beFalse())
81 | }
82 | }
83 |
84 | describe("SVTextFieldItem from init with coder") {
85 | let archiver = NSKeyedArchiver(requiringSecureCoding: false)
86 | let textFieldItem = SVTextFieldItem(coder: archiver)
87 |
88 | it("had to return nil.") {
89 | expect(textFieldItem).to(beNil())
90 | }
91 | }
92 |
93 | describe("SVTitleItem") {
94 | let titleItem = SVTitleItem()
95 | titleItem.setTitle("My custom title")
96 | titleItem.setCancelTitle("Close")
97 |
98 | it("had to have 'My custom title' as title") {
99 | expect(titleItem.title).to(equal("My custom title"))
100 | }
101 |
102 | it("had to have close button visible.") {
103 | expect(titleItem.cancelButton.isHidden).to(beFalse())
104 | }
105 |
106 | it("had to have close button hidden.") {
107 | titleItem.setCancelTitle(nil)
108 | expect(titleItem.cancelButton.isHidden).to(beTrue())
109 | }
110 |
111 | it("had to have 'My second title' as title with setTitle method.") {
112 | titleItem.setTitle("My second title")
113 | expect(titleItem.title).to(equal("My second title"))
114 | }
115 | }
116 |
117 | describe("SVTitleItem from init with coder") {
118 | let archiver = NSKeyedArchiver(requiringSecureCoding: false)
119 | let titleItem = SVTitleItem(coder: archiver)
120 |
121 | it("had to return nil.") {
122 | expect(titleItem).to(beNil())
123 | }
124 | }
125 |
126 | describe("SVImageView") {
127 | let image: UIImage = UIImage.generateBlackSquare()
128 | let imageView = SVImageViewItem(with: image, andContentMode: UIView.ContentMode.center, andHeight: 123.0)
129 |
130 | it("had to have 'image' property non nil.") {
131 | snackViewSpy?.set(items: [imageView])
132 | snackView?.reloadData()
133 |
134 | expect(imageView.image).toNot(beNil())
135 | }
136 |
137 | it("had to have 'height' constraint setted to 123.") {
138 | snackViewSpy?.set(items: [imageView])
139 | snackView?.reloadData()
140 |
141 | expect(imageView.currentHeight).to(equal(123))
142 | }
143 |
144 | describe("SVTitleItem from init with coder") {
145 | let archiver = NSKeyedArchiver(requiringSecureCoding: false)
146 | let imageView = SVImageViewItem(coder: archiver)
147 |
148 | it("had to return nil.") {
149 | expect(imageView).to(beNil())
150 | }
151 | }
152 |
153 | }
154 |
155 | describe("SVSwitchItem") {
156 | var switchValueChanged: Bool = false
157 | let switchItem = SVSwitchItem(withTitle: "My Title", andDescription: "My Description", withState: false, withSwitchAction: { _ in switchValueChanged = true })
158 |
159 | it("had to have 'title', 'description' and 'currentState' property non nil.") {
160 | snackViewSpy?.set(items: [switchItem])
161 | snackView?.reloadData()
162 |
163 | expect(switchItem.title).to(equal("My Title"))
164 | expect(switchItem.descriptionText).to(equal("My Description"))
165 | expect(switchItem.currentState).to(beFalse())
166 |
167 | switchItem.switchSelector(switchItem: UISwitch())
168 | expect(switchValueChanged).to(beTrue())
169 | }
170 |
171 | describe("SVSwitchItem from init with coder") {
172 | let archiver = NSKeyedArchiver(requiringSecureCoding: false)
173 | let switchItem = SVSwitchItem(coder: archiver)
174 |
175 | it("had to return nil.") {
176 | expect(switchItem).to(beNil())
177 | }
178 | }
179 | }
180 |
181 | describe("SVApplicationItem") {
182 | let applicationItem = SVApplicationItem(withIcon: UIImage.generateBlackSquare(), withTitle: "My Application", andDescription: "My Description")
183 |
184 | it("had to have 'title', 'description' and 'icon' property non nil.") {
185 | snackViewSpy?.set(items: [applicationItem])
186 | snackView?.reloadData()
187 |
188 | expect(applicationItem.title).to(equal("My Application"))
189 | expect(applicationItem.descriptionText).to(equal("My Description"))
190 | expect(applicationItem.icon).toNot(beNil())
191 | }
192 |
193 | describe("SVApplicationItem from init with coder") {
194 | let archiver = NSKeyedArchiver(requiringSecureCoding: false)
195 | let applicationItem = SVApplicationItem(coder: archiver)
196 |
197 | it("had to return nil.") {
198 | expect(applicationItem).to(beNil())
199 | }
200 | }
201 | }
202 |
203 | describe("SVButtonItem") {
204 | var buttonClicked: Bool = false
205 | let buttonItem = SVButtonItem(withTitle: "My Button", withButtonAction: { buttonClicked = true })
206 |
207 | it("had to have 'title' property non nil.") {
208 | snackViewSpy?.set(items: [buttonItem])
209 | snackView?.reloadData()
210 |
211 | expect(buttonItem.title).to(equal("My Button"))
212 |
213 | buttonItem.buttonSelector()
214 | expect(buttonClicked).to(beTrue())
215 | }
216 |
217 | describe("SVButton from init with coder") {
218 | let archiver = NSKeyedArchiver(requiringSecureCoding: false)
219 | let buttonItem = SVButtonItem(coder: archiver)
220 |
221 | it("had to return nil.") {
222 | expect(buttonItem).to(beNil())
223 | }
224 | }
225 | }
226 |
227 | describe("SVLoaderItem") {
228 | context("when contains text") {
229 | let loaderItem = SVLoaderItem(withSize: .little, andText: "Loading content")
230 |
231 | it("had to have 'text' property non nil.") {
232 | snackViewSpy?.set(items: [loaderItem])
233 | snackView?.reloadData()
234 |
235 | expect(loaderItem.text).to(equal("Loading content"))
236 | expect(loaderItem.size).to(equal(.little))
237 | }
238 | }
239 |
240 | context("when initialized without text") {
241 | let loaderItem = SVLoaderItem(withSize: .large, andText: nil)
242 |
243 | it("had to have 'text' property nil.") {
244 | snackViewSpy?.set(items: [loaderItem])
245 | snackView?.reloadData()
246 |
247 | expect(loaderItem.size).to(equal(.large))
248 | }
249 | }
250 |
251 | describe("SVLoaderItem from init with coder") {
252 | let archiver = NSKeyedArchiver(requiringSecureCoding: false)
253 | let loaderItem = SVLoaderItem(coder: archiver)
254 |
255 | it("had to return nil.") {
256 | expect(loaderItem).to(beNil())
257 | }
258 | }
259 | }
260 |
261 | describe("SVSlider") {
262 | let sliderItem = SVSliderItem(withTitle: "My Slider", minimum: 10, maximum: 20, current: 13)
263 |
264 | it("had to have 'currentValue' property non nil.") {
265 | snackViewSpy?.set(items: [sliderItem])
266 | snackView?.reloadData()
267 |
268 | expect(sliderItem.currentValue).to(equal(13))
269 | }
270 |
271 | describe("SVSliderItem from init with coder") {
272 | let archiver = NSKeyedArchiver(requiringSecureCoding: false)
273 | let sliderItem = SVSliderItem(coder: archiver)
274 |
275 | it("had to return nil.") {
276 | expect(sliderItem).to(beNil())
277 | }
278 | }
279 | }
280 |
281 | describe("SVPriceRowItem") {
282 | let priceItem = SVPriceRowItem(withTitle: "Total", andDescription: nil, andPrice: "€ 8,99")
283 |
284 | it("had to have 'descriptionText' property non nil.") {
285 | snackViewSpy?.set(items: [priceItem])
286 | snackView?.reloadData()
287 |
288 | expect(priceItem.descriptionText).to(beNil())
289 | }
290 |
291 | it("had to have 'title' property setted to 'Total'") {
292 | snackViewSpy?.set(items: [priceItem])
293 | snackView?.reloadData()
294 |
295 | expect(priceItem.title).to(equal("Total"))
296 | }
297 |
298 | it("had to have 'price' property setted to '€ 8,99'") {
299 | snackViewSpy?.set(items: [priceItem])
300 | snackView?.reloadData()
301 |
302 | expect(priceItem.priceText).to(equal("€ 8,99"))
303 | }
304 |
305 | describe("SVPriceRowItem from init with coder") {
306 | let archiver = NSKeyedArchiver(requiringSecureCoding: false)
307 | let priceRowItem = SVPriceRowItem(coder: archiver)
308 |
309 | it("had to return nil.") {
310 | expect(priceRowItem).to(beNil())
311 | }
312 | }
313 |
314 | }
315 | }
316 | }
317 |
318 |
--------------------------------------------------------------------------------
/Tests/SnackViewTests/SnackViewTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SnackViewTests.swift
3 | // SnackViewTests
4 | //
5 | // Created by Luca Casula on 14/05/2021.
6 | // Copyright © 2021 LucaCasula. All rights reserved.
7 | //
8 | //
9 | import Foundation
10 | import Nimble
11 | import Quick
12 |
13 | @testable import SnackView
14 |
15 | class SVItemsTests: QuickSpec {
16 | override func spec() {
17 |
18 | var snackView: SnackView?
19 | var snackViewSpy: MockSnackViewDataSource?
20 |
21 | beforeEach {
22 | snackViewSpy = MockSnackViewDataSource()
23 | snackView = SnackView(with: snackViewSpy!)
24 |
25 | _ = snackView?.view
26 | snackView?.reloadData()
27 | }
28 |
29 | describe("SnackView with invalid configuration") {
30 | sleep(2)
31 |
32 | context(".viewWillAppear") {
33 | it("had to be titled 'Invalid configuration'") {
34 | expect(snackView?.skeletonView.titleBar.title).to(equal("Invalid configuration"))
35 | }
36 |
37 | it("had to contains 'Close' button.") {
38 | expect(snackView?.skeletonView.titleBar.cancelButtonTitle).to(equal("Close"))
39 | }
40 |
41 | it("had to contains three elements.") {
42 | expect(snackView?.items?.count).to(equal(3))
43 | }
44 | }
45 |
46 | }
47 |
48 | describe("SnackView reloadData method") {
49 | let detailedTextItem = SVDetailTextItem(withTitle: "Details", andDescription: "Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book.")
50 |
51 | it("had to update the items.") {
52 | snackViewSpy?.set(items: [detailedTextItem])
53 | snackView?.reloadData()
54 |
55 | expect(snackView?.items?.count).to(equal(1))
56 | expect(detailedTextItem.frame.height).to(beGreaterThan(50))
57 | }
58 | }
59 |
60 | describe("SnackView from init with coder") {
61 | let archiver = NSKeyedArchiver(forWritingWith: NSMutableData())
62 | let mySnackView = SnackView(coder: archiver)
63 |
64 | it("had to return nil.") {
65 | expect(mySnackView).to(beNil())
66 | }
67 | }
68 |
69 | describe("SnackView") {
70 | it("had to have not nil inputAccessoryView property.") {
71 | expect(snackView?.inputAccessoryView).toNot(beNil())
72 | }
73 | }
74 |
75 | describe("SnackView") {
76 | context("when presented") {
77 | it("had to have a parent called 'SnackView Container'.") {
78 | snackView?.show()
79 |
80 | waitUntil(timeout: DispatchTimeInterval.seconds(3)) { (done) in
81 | DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 2) {
82 | done()
83 | }
84 | }
85 |
86 | expect(snackView?.presentingViewController?.title).to(equal("SnackView Container"))
87 | }
88 | }
89 |
90 | context("when dismissed") {
91 | it("had to have window property nil.") {
92 | snackView?.close()
93 |
94 | waitUntil(timeout: DispatchTimeInterval.seconds(3)) { (done) in
95 | DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 2) {
96 | done()
97 | }
98 | }
99 |
100 | expect(snackView?.window).to(beNil())
101 | }
102 | }
103 |
104 | }
105 | }
106 | }
107 |
108 |
--------------------------------------------------------------------------------