├── .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 | ![SnackView Header](http://www.lucacasula.it/SVItems/snackview_header_color.png) 2 | 3 | ***Create customizable bottom-half sheets.*** 4 | ![SnackView logo](http://www.lucacasula.it/SVItems/NewSnackViewPreview.jpg) 5 | 6 | [![Test Coverage](https://api.codeclimate.com/v1/badges/9aeb1378d61a9f9a3fe4/test_coverage)](https://codeclimate.com/github/lucacasula91/SnackView/test_coverage) 7 | [![Maintainability](https://api.codeclimate.com/v1/badges/9aeb1378d61a9f9a3fe4/maintainability)](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 | ![SnackView alert](http://www.lucacasula.it/SVItems/SVApplicationItem.svg) 148 | 149 | *** 150 | 151 | **SVDescriptionItem** 152 | 153 | ```swift 154 | SVDescriptionItem(withDescription: "Lorem ipsum dolor sit amet...") 155 | ``` 156 | 157 | ![SnackView alert](http://www.lucacasula.it/SVItems/SVDescriptionItem.svg) 158 | 159 | *** 160 | 161 | **SVTextFieldItem** 162 | 163 | ```swift 164 | SVTextFieldItem(withPlaceholder: "Create Password", 165 |                   isSecureField: true) 166 | ``` 167 | 168 | ![SnackView alert](http://www.lucacasula.it/SVItems/SVTextFieldItem.svg) 169 | 170 | *** 171 | 172 | **SVDetailTextItem** 173 | 174 | ```swift 175 | SVDetailTextItem(withTitle: "Elit amet", 176 | andContent: "Lorem ipsum dolor sit amet...") 177 | ``` 178 | 179 | ![SnackView alert](http://www.lucacasula.it/SVItems/SVDetailTextItem.svg) 180 | 181 | *** 182 | 183 | **SVButtonItem** 184 | 185 | ```swift 186 | SVButtonItem(withTitle: "Continue") { /* Button action here */ } 187 | ``` 188 | 189 | ![SnackView alert](http://www.lucacasula.it/SVItems/SVButtonItem.svg) 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 | ![SnackView alert](http://www.lucacasula.it/SVItems/SVSwitchItem.svg) 201 | 202 | *** 203 | 204 | **SVLoaderItem** 205 | 206 | ```swift 207 | SVLoadingItem(withSize: .large, 208 | andText: "Lorem ipsum dolor sit amet...") 209 | ``` 210 | 211 | ![SnackView alert Item](http://www.lucacasula.it/SVItems/SVLoaderDescriptionItem.svg) 212 | 213 | *** 214 | 215 | **SVImaveViewItem** 216 | 217 | ```swift 218 | SVImageViewItem(with: UIImage(named: "hat_is_new")!, 219 | andContentMode: .scaleAspectFill) 220 | ``` 221 | 222 | ![SnackView alert Item](http://www.lucacasula.it/SVItems/SVImageViewItem.svg) 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 | ![Custom SVItem](http://www.lucacasula.it/SVItems/SnackViewCustomSVItem.jpg) 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 | --------------------------------------------------------------------------------