├── .codecov.yml ├── .github └── workflows │ └── CI.yml ├── .gitignore ├── .spi.yml ├── Assets ├── demo.gif └── example.png ├── Drops.podspec ├── Drops.xcodeproj ├── project.pbxproj ├── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist └── xcshareddata │ └── xcschemes │ ├── Drops.xcscheme │ ├── SwiftUIExample.xcscheme │ └── UIKitExample.xcscheme ├── Gemfile ├── Gemfile.lock ├── LICENSE ├── Package.swift ├── README.md ├── Sources ├── AnimationContext.swift ├── Animator.swift ├── Drop.swift ├── DropView.swift ├── Drops.swift ├── Info.plist ├── PassthroughView.swift ├── PassthroughWindow.swift ├── Presenter.swift ├── Weak.swift └── WindowViewController.swift ├── SwiftUIExample ├── App.swift ├── Assets.xcassets │ ├── AccentColor.colorset │ │ └── Contents.json │ ├── AppIcon.appiconset │ │ └── Contents.json │ └── Contents.json ├── ContentView.swift ├── Info.plist └── SwiftUIExample.entitlements ├── Tests ├── AnimatorTests.swift ├── DropTests.swift ├── DropViewTests.swift ├── DropsTests.swift ├── Info.plist ├── PassthroughViewTests.swift ├── PassthroughWindowTests.swift ├── PresenterTests.swift ├── TestAnimatorDelegate.swift ├── WeakTests.swift └── WindowViewControllerTests.swift ├── UIKitExample ├── AppDelegate.swift ├── Assets.xcassets │ ├── AccentColor.colorset │ │ └── Contents.json │ ├── AppIcon.appiconset │ │ └── Contents.json │ └── Contents.json ├── Base.lproj │ └── LaunchScreen.storyboard ├── Info.plist ├── SceneDelegate.swift ├── UIKitExample.entitlements └── ViewController.swift ├── docs ├── Drop │ └── index.html ├── Drop_Accessibility │ └── index.html ├── Drop_Action │ └── index.html ├── Drop_Duration │ └── index.html ├── Drop_Position │ └── index.html ├── Drops │ └── index.html ├── all.css └── index.html └── generate_docs.sh /.codecov.yml: -------------------------------------------------------------------------------- 1 | coverage: 2 | precision: 2 3 | round: down 4 | range: 70...100 5 | 6 | status: 7 | project: true 8 | patch: true 9 | changes: true -------------------------------------------------------------------------------- /.github/workflows/CI.yml: -------------------------------------------------------------------------------- 1 | name: Drops 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | branches: 9 | - main 10 | 11 | jobs: 12 | Darwin: 13 | name: Darwin 14 | runs-on: macos-latest 15 | env: 16 | PROJECT: Drops.xcodeproj 17 | steps: 18 | - uses: actions/checkout@v1 19 | - name: Bundle Install 20 | run: bundle install 21 | - name: Test iOS 22 | run: | 23 | xcodebuild clean build test -project $PROJECT -scheme $SCHEME -destination "$DESTINATION" | XCPRETTY_JSON_FILE_OUTPUT="xcodebuild-ios.json" xcpretty -f `xcpretty-json-formatter` 24 | bash <(curl -s https://codecov.io/bash) -cF ios -J 'Drops' 25 | env: 26 | SCHEME: Drops 27 | DESTINATION: platform=iOS Simulator,name=iPhone 12 28 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Xcode 2 | .DS_Store 3 | 4 | ## Build generated 5 | build/ 6 | DerivedData/ 7 | build.log 8 | 9 | ## Various settings 10 | *.pbxuser 11 | !default.pbxuser 12 | *.mode1v3 13 | !default.mode1v3 14 | *.mode2v3 15 | !default.mode2v3 16 | *.perspectivev3 17 | !default.perspectivev3 18 | xcuserdata/ 19 | 20 | ## Other 21 | *.moved-aside 22 | *.xcuserstate 23 | 24 | ## Obj-C/Swift specific 25 | *.hmap 26 | *.ipa 27 | *.dSYM.zip 28 | *.dSYM 29 | 30 | ## Playgrounds 31 | timeline.xctimeline 32 | playground.xcworkspace 33 | 34 | ## Swift Package Manager 35 | .build/ 36 | .swiftpm/ 37 | 38 | ## CocoaPods 39 | Pods/ 40 | 41 | ## Carthage 42 | Carthage/Build 43 | 44 | ## Fastlane 45 | fastlane/report.xml 46 | fastlane/Preview.html 47 | fastlane/test_output -------------------------------------------------------------------------------- /.spi.yml: -------------------------------------------------------------------------------- 1 | version: 1 2 | builder: 3 | configs: 4 | - documentation_targets: 5 | - Drops 6 | -------------------------------------------------------------------------------- /Assets/demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/omaralbeik/Drops/5824681795286c36bdc4a493081a63e64e2a064e/Assets/demo.gif -------------------------------------------------------------------------------- /Assets/example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/omaralbeik/Drops/5824681795286c36bdc4a493081a63e64e2a064e/Assets/example.png -------------------------------------------------------------------------------- /Drops.podspec: -------------------------------------------------------------------------------- 1 | Pod::Spec.new do |s| 2 | s.name = 'Drops' 3 | s.version = '1.7.0' 4 | s.summary = 'A µFramework for showing iOS 13 like system alerts' 5 | s.description = <<-DESC 6 | A µFramework for showing alerts like the one used when copying from pasteboard or connecting Apple pencil. 7 | DESC 8 | s.homepage = 'https://github.com/omaralbeik/Drops' 9 | s.license = { :type => 'MIT', :file => 'LICENSE' } 10 | s.social_media_url = 'http://twitter.com/omaralbeik' 11 | s.documentation_url = 'https://omaralbeik.github.io/Drops' 12 | s.authors = { 'Omar Albeik' => 'https://twitter.com/omaralbeik' } 13 | s.module_name = 'Drops' 14 | s.source = { :git => 'https://github.com/omaralbeik/Drops.git', :tag => s.version } 15 | s.source_files = 'Sources/**/*.swift' 16 | s.swift_versions = ['5.5', '5.6', '5.7'] 17 | s.requires_arc = true 18 | s.ios.deployment_target = '13.0' 19 | end 20 | -------------------------------------------------------------------------------- /Drops.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Drops.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Drops.xcodeproj/xcshareddata/xcschemes/Drops.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 31 | 32 | 34 | 40 | 41 | 42 | 43 | 44 | 54 | 55 | 61 | 62 | 68 | 69 | 70 | 71 | 73 | 74 | 77 | 78 | 79 | -------------------------------------------------------------------------------- /Drops.xcodeproj/xcshareddata/xcschemes/SwiftUIExample.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 43 | 45 | 51 | 52 | 53 | 54 | 60 | 61 | 63 | 64 | 67 | 68 | 69 | -------------------------------------------------------------------------------- /Drops.xcodeproj/xcshareddata/xcschemes/UIKitExample.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 43 | 45 | 51 | 52 | 53 | 54 | 60 | 62 | 68 | 69 | 70 | 71 | 73 | 74 | 77 | 78 | 79 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | gem 'xcpretty' 4 | gem 'xcpretty-json-formatter' -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: https://rubygems.org/ 3 | specs: 4 | rouge (2.0.7) 5 | xcpretty (0.3.0) 6 | rouge (~> 2.0.7) 7 | xcpretty-json-formatter (0.1.1) 8 | xcpretty (~> 0.2, >= 0.0.7) 9 | 10 | PLATFORMS 11 | ruby 12 | 13 | DEPENDENCIES 14 | xcpretty 15 | xcpretty-json-formatter 16 | 17 | BUNDLED WITH 18 | 2.3.22 19 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Omar Albeik 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.3 2 | // 3 | // Drops 4 | // 5 | // Copyright (c) 2021-Present Omar Albeik - https://github.com/omaralbeik 6 | // 7 | // Permission is hereby granted, free of charge, to any person obtaining a copy 8 | // of this software and associated documentation files (the "Software"), to deal 9 | // in the Software without restriction, including without limitation the rights 10 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | // copies of the Software, and to permit persons to whom the Software is 12 | // furnished to do so, subject to the following conditions: 13 | // 14 | // The above copyright notice and this permission notice shall be included in 15 | // all copies or substantial portions of the Software. 16 | // 17 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 23 | // THE SOFTWARE. 24 | 25 | import PackageDescription 26 | 27 | let package = Package( 28 | name: "Drops", 29 | platforms: [ 30 | .iOS(.v13) 31 | ], 32 | products: [ 33 | .library(name: "Drops", targets: ["Drops"]) 34 | ], 35 | dependencies: [], 36 | targets: [ 37 | .target(name: "Drops", dependencies: [], path: "Sources", exclude: ["Info.plist"]), 38 | .testTarget(name: "DropsTests", dependencies: ["Drops"], path: "Tests", exclude: ["Info.plist"]) 39 | ], 40 | swiftLanguageVersions: [.v5] 41 | ) 42 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Drops 💧 2 | 3 | A µFramework for showing alerts like the one used when copying from pasteboard or connecting Apple pencil. 4 | 5 | ![Demo](https://raw.githubusercontent.com/omaralbeik/Drops/main/Assets/demo.gif) 6 | 7 | --- 8 | 9 | [![CI](https://github.com/omaralbeik/Drops/workflows/Drops/badge.svg)](https://github.com/omaralbeik/Drops/actions) 10 | [![codecov](https://codecov.io/gh/omaralbeik/Drops/branch/main/graph/badge.svg?token=399UQIKSLR)](https://codecov.io/gh/omaralbeik/Drops) 11 | [![](https://img.shields.io/endpoint?url=https%3A%2F%2Fswiftpackageindex.com%2Fapi%2Fpackages%2Fomaralbeik%2FDrops%2Fbadge%3Ftype%3Dswift-versions)](https://swiftpackageindex.com/omaralbeik/Drops) 12 | [![](https://img.shields.io/endpoint?url=https%3A%2F%2Fswiftpackageindex.com%2Fapi%2Fpackages%2Fomaralbeik%2FDrops%2Fbadge%3Ftype%3Dplatforms)](https://swiftpackageindex.com/omaralbeik/Drops) 13 | ## Features 14 | 15 | - iOS 13+ 16 | - Can be used in SwiftUI and UIKit applications 17 | - Light/Dark modes 18 | - Interactive dismissal 19 | - Queue to show consecutive drops 20 | - Support dynamic font sizing 21 | - Support announcing title and subtitle via VoiceOver 22 | - Show from top or bottom of screen 23 | 24 | --- 25 | 26 | ## Usage 27 | 28 | 1. Create a drop: 29 | 30 | ```swift 31 | let drop: Drop = "Title Only" 32 | ``` 33 | 34 | ```swift 35 | let drop = Drop(title: "Title Only") 36 | ``` 37 | 38 | ```swift 39 | let drop = Drop(title: "Title", subtitle: "Subtitle") 40 | ``` 41 | 42 | ```swift 43 | let drop = Drop(title: "Title", subtitle: "Subtitle", duration: 5.0) 44 | ``` 45 | 46 | ```swift 47 | let drop = Drop( 48 | title: "Title", 49 | subtitle: "Subtitle", 50 | icon: UIImage(systemName: "star.fill"), 51 | action: .init { 52 | print("Drop tapped") 53 | Drops.hideCurrent() 54 | }, 55 | position: .bottom, 56 | duration: 5.0, 57 | accessibility: "Alert: Title, Subtitle" 58 | ) 59 | ``` 60 | 61 | 2. Show it: 62 | 63 | ```swift 64 | Drops.show("Title") 65 | ``` 66 | 67 | ```swift 68 | Drops.show(drop) 69 | ``` 70 | 71 | ###### SwiftUI 72 | ```swift 73 | import SwiftUI 74 | import Drops 75 | 76 | struct ContentView: View { 77 | var body: some View { 78 | Button("Show Drop") { 79 | Drops.show(drop) 80 | } 81 | } 82 | } 83 | ``` 84 | 85 | ###### UIKit 86 | ```swift 87 | import UIKit 88 | import Drops 89 | 90 | class ViewController: UIViewController { 91 | let drops = Drops(delayBetweenDrops: 1.0) 92 | 93 | func showDrop() { 94 | drops.show(drop) 95 | } 96 | } 97 | ``` 98 | 99 | Read the [docs](https://omaralbeik.github.io/Drops) for more usage options. 100 | 101 | --- 102 | 103 | ## Example Projects 104 | 105 | - Run the `SwiftUIExample` target to see how Drops works in SwiftUI applications. 106 | - Run the `UIKitExample` target to see how Drops works in UIKit applications. 107 | 108 | ![Example](https://raw.githubusercontent.com/omaralbeik/Drops/main/Assets/example.png) 109 | 110 | --- 111 | 112 | ## Installation 113 | 114 | ### Swift Package Manager 115 | 116 | The [Swift Package Manager](https://swift.org/package-manager/) is a tool for managing the distribution of Swift code. 117 | 118 | 1. Add the following to your `Package.swift` file: 119 | 120 | ```swift 121 | dependencies: [ 122 | .package(url: "https://github.com/omaralbeik/Drops.git", from: "1.7.0") 123 | ] 124 | ``` 125 | 126 | 2. Build your project: 127 | 128 | ```sh 129 | $ swift build 130 | ``` 131 | 132 | ### CocoaPods 133 | 134 | To integrate Drops into your Xcode project using [CocoaPods](https://cocoapods.org), specify it in your Podfile: 135 | 136 | ```rb 137 | pod 'Drops', :git => 'https://github.com/omaralbeik/Drops.git', :tag => '1.7.0' 138 | ``` 139 | 140 | ### Carthage 141 | 142 | To integrate Drops into your Xcode project using [Carthage](https://github.com/Carthage/Carthage), specify it in your Cartfile: 143 | 144 | ``` 145 | github "omaralbeik/Drops" ~> 1.7.0 146 | ``` 147 | 148 | ### Manually 149 | 150 | Add the [Sources](https://github.com/omaralbeik/Drops/tree/main/Sources) folder to your Xcode project. 151 | 152 | --- 153 | 154 | ## Thanks 155 | 156 | Special thanks to [SwiftKickMobile team](https://github.com/SwiftKickMobile) for creating [SwiftMessages](https://github.com/SwiftKickMobile/SwiftMessages), this project was heavily inspired by their work. 157 | 158 | --- 159 | 160 | ## License 161 | 162 | Drops is released under the MIT license. See [LICENSE](https://github.com/omaralbeik/Drops/blob/main/LICENSE) for more information. 163 | -------------------------------------------------------------------------------- /Sources/AnimationContext.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Drops 3 | // 4 | // Copyright (c) 2021-Present Omar Albeik - https://github.com/omaralbeik 5 | // 6 | // Permission is hereby granted, free of charge, to any person obtaining a copy 7 | // of this software and associated documentation files (the "Software"), to deal 8 | // in the Software without restriction, including without limitation the rights 9 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | // copies of the Software, and to permit persons to whom the Software is 11 | // furnished to do so, subject to the following conditions: 12 | // 13 | // The above copyright notice and this permission notice shall be included in 14 | // all copies or substantial portions of the Software. 15 | // 16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | // THE SOFTWARE. 23 | 24 | #if os(iOS) || os(visionOS) 25 | import UIKit 26 | 27 | internal struct AnimationContext { 28 | let view: UIView 29 | let container: UIView 30 | } 31 | #endif 32 | -------------------------------------------------------------------------------- /Sources/Animator.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Drops 3 | // 4 | // Copyright (c) 2021-Present Omar Albeik - https://github.com/omaralbeik 5 | // 6 | // Permission is hereby granted, free of charge, to any person obtaining a copy 7 | // of this software and associated documentation files (the "Software"), to deal 8 | // in the Software without restriction, including without limitation the rights 9 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | // copies of the Software, and to permit persons to whom the Software is 11 | // furnished to do so, subject to the following conditions: 12 | // 13 | // The above copyright notice and this permission notice shall be included in 14 | // all copies or substantial portions of the Software. 15 | // 16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | // THE SOFTWARE. 23 | 24 | #if os(iOS) || os(visionOS) 25 | import UIKit 26 | 27 | internal protocol AnimatorDelegate: AnyObject { 28 | func hide(animator: Animator) 29 | func panStarted(animator: Animator) 30 | func panEnded(animator: Animator) 31 | } 32 | 33 | internal final class Animator { 34 | struct PanState: Equatable { 35 | var closing = false 36 | var closeSpeed: CGFloat = 0.0 37 | var closePercent: CGFloat = 0.0 38 | var panTranslationY: CGFloat = 0.0 39 | } 40 | 41 | init(position: Drop.Position, delegate: AnimatorDelegate) { 42 | self.position = position 43 | self.delegate = delegate 44 | } 45 | 46 | let position: Drop.Position 47 | weak var delegate: AnimatorDelegate? 48 | 49 | var context: AnimationContext? 50 | var panState = PanState() 51 | 52 | let showDuration: TimeInterval = 0.75 53 | let hideDuration: TimeInterval = 0.25 54 | let springDamping: CGFloat = 0.8 55 | let rubberBanding = true 56 | let closeSpeedThreshold: CGFloat = 750.0 57 | let closePercentThreshold: CGFloat = 0.33 58 | let closeAbsoluteThreshold: CGFloat = 75.0 59 | let bounceOffset: CGFloat = 5 60 | 61 | private lazy var panGestureRecognizer: UIPanGestureRecognizer = { 62 | let recognizer = UIPanGestureRecognizer() 63 | recognizer.addTarget(self, action: #selector(handlePan)) 64 | return recognizer 65 | }() 66 | 67 | func install(context: AnimationContext) { 68 | let view = context.view 69 | let container = context.container 70 | 71 | self.context = context 72 | 73 | view.translatesAutoresizingMaskIntoConstraints = false 74 | container.addSubview(view) 75 | 76 | var constraints = [ 77 | view.centerXAnchor.constraint(equalTo: container.safeAreaLayoutGuide.centerXAnchor), 78 | view.leadingAnchor.constraint(greaterThanOrEqualTo: container.safeAreaLayoutGuide.leadingAnchor, constant: 20), 79 | view.trailingAnchor.constraint(lessThanOrEqualTo: container.safeAreaLayoutGuide.trailingAnchor, constant: -20) 80 | ] 81 | 82 | switch position { 83 | case .top: 84 | constraints += [ 85 | view.topAnchor.constraint(equalTo: container.safeAreaLayoutGuide.topAnchor, constant: bounceOffset) 86 | ] 87 | case .bottom: 88 | constraints += [ 89 | view.bottomAnchor.constraint(equalTo: container.safeAreaLayoutGuide.bottomAnchor, constant: -bounceOffset) 90 | ] 91 | } 92 | 93 | NSLayoutConstraint.activate(constraints) 94 | container.layoutIfNeeded() 95 | 96 | let animationDistance = view.frame.height 97 | 98 | switch position { 99 | case .top: 100 | view.transform = CGAffineTransform(translationX: 0, y: -animationDistance) 101 | case .bottom: 102 | view.transform = CGAffineTransform(translationX: 0, y: animationDistance) 103 | } 104 | 105 | view.addGestureRecognizer(panGestureRecognizer) 106 | } 107 | 108 | func show(context: AnimationContext, completion: @escaping AnimationCompletion) { 109 | install(context: context) 110 | show(completion: completion) 111 | } 112 | 113 | func hide(context: AnimationContext, completion: @escaping AnimationCompletion) { 114 | let position = self.position 115 | let view = context.view 116 | UIView.animate( 117 | withDuration: hideDuration, 118 | delay: 0, 119 | options: [.beginFromCurrentState, .curveEaseIn], 120 | animations: { [weak view] in 121 | view?.alpha = 0 122 | let frame = view?.frame ?? .zero 123 | switch position { 124 | case .top: 125 | view?.transform = CGAffineTransform(translationX: 0, y: -frame.height) 126 | case .bottom: 127 | view?.transform = CGAffineTransform(translationX: 0, y: frame.height) 128 | } 129 | }, 130 | completion: completion 131 | ) 132 | } 133 | 134 | func show(completion: @escaping AnimationCompletion) { 135 | guard let view = context?.view else { 136 | completion(false) 137 | return 138 | } 139 | 140 | view.alpha = 0 141 | 142 | let animationDistance = abs(view.transform.ty) 143 | let initialSpringVelocity = animationDistance == 0.0 ? 0.0 : min(0.0, panState.closeSpeed / animationDistance) 144 | 145 | UIView.animate( 146 | withDuration: showDuration, 147 | delay: 0.0, 148 | usingSpringWithDamping: springDamping, 149 | initialSpringVelocity: initialSpringVelocity, 150 | options: [.beginFromCurrentState, .curveLinear, .allowUserInteraction], 151 | animations: { [weak view] in 152 | view?.alpha = 1 153 | view?.transform = .identity 154 | }, 155 | completion: completion 156 | ) 157 | } 158 | 159 | @objc 160 | func handlePan(gestureRecognizer: UIPanGestureRecognizer) { 161 | switch gestureRecognizer.state { 162 | case .changed: 163 | guard let view = context?.view else { return } 164 | let velocity = gestureRecognizer.velocity(in: view) 165 | let translation = gestureRecognizer.translation(in: view) 166 | panState = panChanged(current: panState, view: view, velocity: velocity, translation: translation) 167 | case .ended, .cancelled: 168 | if let initialState = panEnded(current: panState) { 169 | show { [weak self] _ in 170 | guard let self = self else { return } 171 | self.delegate?.panEnded(animator: self) 172 | } 173 | panState = initialState 174 | } 175 | default: 176 | break 177 | } 178 | } 179 | 180 | func panChanged(current: PanState, view: UIView, velocity: CGPoint, translation: CGPoint) -> PanState { 181 | var state = current 182 | var velocity = velocity 183 | var translation = translation 184 | let height = view.bounds.height - bounceOffset 185 | if height <= 0 { return state } 186 | 187 | if case .top = position { 188 | velocity.y *= -1.0 189 | translation.y *= -1.0 190 | } 191 | 192 | var translationAmount = translation.y >= 0 ? translation.y : -pow(abs(translation.y), 0.7) 193 | 194 | if !state.closing { 195 | if !rubberBanding, translationAmount < 0 { return state } 196 | state.closing = true 197 | delegate?.panStarted(animator: self) 198 | } 199 | 200 | if !rubberBanding, translationAmount < 0 { translationAmount = 0 } 201 | 202 | switch position { 203 | case .top: 204 | view.transform = CGAffineTransform(translationX: 0, y: -translationAmount) 205 | case .bottom: 206 | view.transform = CGAffineTransform(translationX: 0, y: translationAmount) 207 | } 208 | 209 | state.closeSpeed = velocity.y 210 | state.closePercent = translation.y / height 211 | state.panTranslationY = translation.y 212 | 213 | return state 214 | } 215 | 216 | func panEnded(current: PanState) -> PanState? { 217 | if current.closeSpeed > closeSpeedThreshold { 218 | delegate?.hide(animator: self) 219 | return nil 220 | } 221 | 222 | if current.closePercent > closePercentThreshold { 223 | delegate?.hide(animator: self) 224 | return nil 225 | } 226 | 227 | if current.panTranslationY > closeAbsoluteThreshold { 228 | delegate?.hide(animator: self) 229 | return nil 230 | } 231 | 232 | return .init() 233 | } 234 | } 235 | #endif 236 | -------------------------------------------------------------------------------- /Sources/Drop.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Drops 3 | // 4 | // Copyright (c) 2021-Present Omar Albeik - https://github.com/omaralbeik 5 | // 6 | // Permission is hereby granted, free of charge, to any person obtaining a copy 7 | // of this software and associated documentation files (the "Software"), to deal 8 | // in the Software without restriction, including without limitation the rights 9 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | // copies of the Software, and to permit persons to whom the Software is 11 | // furnished to do so, subject to the following conditions: 12 | // 13 | // The above copyright notice and this permission notice shall be included in 14 | // all copies or substantial portions of the Software. 15 | // 16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | // THE SOFTWARE. 23 | 24 | #if os(iOS) || os(visionOS) 25 | import UIKit 26 | 27 | /// An object representing a drop. 28 | @available(iOSApplicationExtension, unavailable) 29 | public struct Drop: ExpressibleByStringLiteral { 30 | /// Create a new drop. 31 | /// - Parameters: 32 | /// - title: Title. 33 | /// - titleNumberOfLines: Maximum number of lines that `title` can occupy. Defaults to `1`. 34 | /// A value of 0 means no limit. 35 | /// - subtitle: Optional subtitle. Defaults to `nil`. 36 | /// - subtitleNumberOfLines: Maximum number of lines that `subtitle` can occupy. Defaults to `1`. 37 | /// A value of 0 means no limit. 38 | /// - icon: Optional icon. 39 | /// - action: Optional action. 40 | /// - position: Position. Defaults to `Drop.Position.top`. 41 | /// - duration: Duration. Defaults to `Drop.Duration.recommended`. 42 | /// - accessibility: Accessibility options. Defaults to `nil` which will use "title, subtitle" as its message. 43 | public init( 44 | title: String, 45 | titleNumberOfLines: Int = 1, 46 | subtitle: String? = nil, 47 | subtitleNumberOfLines: Int = 1, 48 | icon: UIImage? = nil, 49 | action: Action? = nil, 50 | position: Position = .top, 51 | duration: Duration = .recommended, 52 | accessibility: Accessibility? = nil 53 | ) { 54 | self.title = title.trimmingCharacters(in: .whitespacesAndNewlines) 55 | self.titleNumberOfLines = titleNumberOfLines 56 | if let subtitle = subtitle?.trimmingCharacters(in: .whitespacesAndNewlines), !subtitle.isEmpty { 57 | self.subtitle = subtitle 58 | } 59 | self.subtitleNumberOfLines = subtitleNumberOfLines 60 | self.icon = icon 61 | self.action = action 62 | self.position = position 63 | self.duration = duration 64 | self.accessibility = accessibility 65 | ?? .init(message: [title, subtitle].compactMap { $0 }.joined(separator: ", ")) 66 | } 67 | 68 | /// Create a new accessibility object. 69 | /// - Parameter message: Message to be announced when the drop is shown. Defaults to drop's "title, subtitle" 70 | public init(stringLiteral title: String) { 71 | self.title = title 72 | titleNumberOfLines = 1 73 | subtitleNumberOfLines = 1 74 | position = .top 75 | duration = .recommended 76 | accessibility = .init(message: title) 77 | } 78 | 79 | /// Title. 80 | public var title: String 81 | 82 | /// Maximum number of lines that `title` can occupy. A value of 0 means no limit. 83 | public var titleNumberOfLines: Int 84 | 85 | /// Subtitle. 86 | public var subtitle: String? 87 | 88 | /// Maximum number of lines that `subtitle` can occupy. A value of 0 means no limit. 89 | public var subtitleNumberOfLines: Int 90 | 91 | /// Icon. 92 | public var icon: UIImage? 93 | 94 | /// Action. 95 | public var action: Action? 96 | 97 | /// Position. 98 | public var position: Position 99 | 100 | /// Duration. 101 | public var duration: Duration 102 | 103 | /// Accessibility. 104 | public var accessibility: Accessibility 105 | } 106 | 107 | public extension Drop { 108 | /// An enum representing drop presentation position. 109 | enum Position: Equatable { 110 | /// Drop is presented from top. 111 | case top 112 | /// Drop is presented from bottom. 113 | case bottom 114 | } 115 | } 116 | 117 | public extension Drop { 118 | /// An enum representing a drop duration on screen. 119 | enum Duration: Equatable, ExpressibleByFloatLiteral { 120 | /// Hides the drop after 2.0 seconds. 121 | case recommended 122 | /// Hides the drop after the specified number of seconds. 123 | case seconds(TimeInterval) 124 | 125 | /// Create a new duration object. 126 | /// - Parameter value: Duration in seconds 127 | public init(floatLiteral value: TimeInterval) { 128 | self = .seconds(value) 129 | } 130 | 131 | internal var value: TimeInterval { 132 | switch self { 133 | case .recommended: 134 | return 2.0 135 | case let .seconds(custom): 136 | return abs(custom) 137 | } 138 | } 139 | } 140 | } 141 | 142 | public extension Drop { 143 | /// An object representing a drop action. 144 | struct Action { 145 | /// Create a new action. 146 | /// - Parameters: 147 | /// - icon: Optional icon image. 148 | /// - handler: Handler to be called when the drop is tapped. 149 | public init(icon: UIImage? = nil, handler: @escaping () -> Void) { 150 | self.icon = icon 151 | self.handler = handler 152 | } 153 | 154 | /// Icon. 155 | public var icon: UIImage? 156 | 157 | /// Handler. 158 | public var handler: () -> Void 159 | } 160 | } 161 | 162 | public extension Drop { 163 | /// An object representing accessibility options. 164 | struct Accessibility: ExpressibleByStringLiteral { 165 | /// Create a new accessibility object. 166 | /// - Parameter message: Message to be announced when the drop is shown. Defaults to drop's "title, subtitle" 167 | public init(message: String) { 168 | self.message = message 169 | } 170 | 171 | /// Create a new accessibility object. 172 | /// - Parameter message: Message to be announced when the drop is shown. Defaults to drop's "title, subtitle" 173 | public init(stringLiteral message: String) { 174 | self.message = message 175 | } 176 | 177 | /// Accessibility message to be announced when the drop is shown. 178 | public let message: String 179 | } 180 | } 181 | #endif 182 | -------------------------------------------------------------------------------- /Sources/DropView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Drops 3 | // 4 | // Copyright (c) 2021-Present Omar Albeik - https://github.com/omaralbeik 5 | // 6 | // Permission is hereby granted, free of charge, to any person obtaining a copy 7 | // of this software and associated documentation files (the "Software"), to deal 8 | // in the Software without restriction, including without limitation the rights 9 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | // copies of the Software, and to permit persons to whom the Software is 11 | // furnished to do so, subject to the following conditions: 12 | // 13 | // The above copyright notice and this permission notice shall be included in 14 | // all copies or substantial portions of the Software. 15 | // 16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | // THE SOFTWARE. 23 | 24 | #if os(iOS) || os(visionOS) 25 | import UIKit 26 | 27 | internal final class DropView: UIView { 28 | required init(drop: Drop) { 29 | self.drop = drop 30 | super.init(frame: .zero) 31 | 32 | backgroundColor = .secondarySystemBackground 33 | 34 | addSubview(stackView) 35 | 36 | let constraints = createLayoutConstraints(for: drop) 37 | NSLayoutConstraint.activate(constraints) 38 | configureViews(for: drop) 39 | } 40 | 41 | required init?(coder _: NSCoder) { 42 | return nil 43 | } 44 | 45 | override var frame: CGRect { 46 | didSet { layer.cornerRadius = frame.cornerRadius } 47 | } 48 | 49 | override var bounds: CGRect { 50 | didSet { layer.cornerRadius = frame.cornerRadius } 51 | } 52 | 53 | let drop: Drop 54 | 55 | func createLayoutConstraints(for drop: Drop) -> [NSLayoutConstraint] { 56 | var constraints: [NSLayoutConstraint] = [] 57 | 58 | constraints += [ 59 | imageView.heightAnchor.constraint(equalToConstant: 25), 60 | imageView.widthAnchor.constraint(equalToConstant: 25) 61 | ] 62 | 63 | constraints += [ 64 | button.heightAnchor.constraint(equalToConstant: 35), 65 | button.widthAnchor.constraint(equalToConstant: 35) 66 | ] 67 | 68 | var insets = UIEdgeInsets(top: 7.5, left: 12.5, bottom: 7.5, right: 12.5) 69 | 70 | if drop.icon == nil { 71 | insets.left = 40 72 | } 73 | 74 | if drop.action?.icon == nil { 75 | insets.right = 40 76 | } 77 | 78 | if drop.subtitle == nil { 79 | insets.top = 15 80 | insets.bottom = 15 81 | if drop.action?.icon != nil { 82 | insets.top = 10 83 | insets.bottom = 10 84 | insets.right = 10 85 | } 86 | } 87 | 88 | if drop.icon == nil, drop.action?.icon == nil { 89 | insets.left = 50 90 | insets.right = 50 91 | } 92 | 93 | constraints += [ 94 | stackView.leadingAnchor.constraint(equalTo: leadingAnchor, constant: insets.left), 95 | stackView.topAnchor.constraint(equalTo: safeAreaLayoutGuide.topAnchor, constant: insets.top), 96 | stackView.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -insets.right), 97 | stackView.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -insets.bottom) 98 | ] 99 | 100 | return constraints 101 | } 102 | 103 | func configureViews(for drop: Drop) { 104 | clipsToBounds = true 105 | 106 | titleLabel.text = drop.title 107 | titleLabel.numberOfLines = drop.titleNumberOfLines 108 | 109 | subtitleLabel.text = drop.subtitle 110 | subtitleLabel.numberOfLines = drop.subtitleNumberOfLines 111 | subtitleLabel.isHidden = drop.subtitle == nil 112 | 113 | imageView.image = drop.icon 114 | imageView.isHidden = drop.icon == nil 115 | 116 | button.setImage(drop.action?.icon, for: .normal) 117 | button.isHidden = drop.action?.icon == nil 118 | 119 | if let action = drop.action, action.icon == nil { 120 | let tap = UITapGestureRecognizer(target: self, action: #selector(didTapButton)) 121 | addGestureRecognizer(tap) 122 | } 123 | 124 | layer.shadowColor = UIColor.black.cgColor 125 | layer.shadowOffset = .zero 126 | layer.shadowRadius = 25 127 | layer.shadowOpacity = 0.15 128 | layer.shouldRasterize = true 129 | #if os(iOS) 130 | layer.rasterizationScale = UIScreen.main.scale 131 | #endif 132 | layer.masksToBounds = false 133 | } 134 | 135 | @objc 136 | func didTapButton() { 137 | drop.action?.handler() 138 | } 139 | 140 | lazy var titleLabel: UILabel = { 141 | let label = UILabel() 142 | label.translatesAutoresizingMaskIntoConstraints = false 143 | label.textAlignment = .center 144 | label.textColor = .label 145 | label.font = UIFont.preferredFont(forTextStyle: .subheadline).bold 146 | label.adjustsFontForContentSizeCategory = true 147 | label.adjustsFontSizeToFitWidth = true 148 | return label 149 | }() 150 | 151 | lazy var subtitleLabel: UILabel = { 152 | let label = UILabel() 153 | label.translatesAutoresizingMaskIntoConstraints = false 154 | label.textAlignment = .center 155 | label.textColor = UIAccessibility.isDarkerSystemColorsEnabled ? .label : .secondaryLabel 156 | label.font = UIFont.preferredFont(forTextStyle: .subheadline) 157 | label.adjustsFontForContentSizeCategory = true 158 | label.adjustsFontSizeToFitWidth = true 159 | return label 160 | }() 161 | 162 | lazy var imageView: UIImageView = { 163 | let view = RoundImageView() 164 | view.translatesAutoresizingMaskIntoConstraints = false 165 | view.contentMode = .scaleAspectFit 166 | view.clipsToBounds = true 167 | view.tintColor = UIAccessibility.isDarkerSystemColorsEnabled ? .label : .secondaryLabel 168 | return view 169 | }() 170 | 171 | lazy var button: UIButton = { 172 | let button = RoundButton(type: .system) 173 | button.translatesAutoresizingMaskIntoConstraints = false 174 | button.addTarget(self, action: #selector(didTapButton), for: .touchUpInside) 175 | button.clipsToBounds = true 176 | button.backgroundColor = .link 177 | button.tintColor = .white 178 | button.imageView?.contentMode = .scaleAspectFit 179 | button.contentEdgeInsets = .init(top: 7.5, left: 7.5, bottom: 7.5, right: 7.5) 180 | return button 181 | }() 182 | 183 | lazy var labelsStackView: UIStackView = { 184 | let view = UIStackView(arrangedSubviews: [titleLabel, subtitleLabel]) 185 | view.translatesAutoresizingMaskIntoConstraints = false 186 | view.axis = .vertical 187 | view.alignment = .fill 188 | view.distribution = .fill 189 | view.spacing = -1 190 | return view 191 | }() 192 | 193 | lazy var stackView: UIStackView = { 194 | let view = UIStackView(arrangedSubviews: [imageView, labelsStackView, button]) 195 | view.translatesAutoresizingMaskIntoConstraints = false 196 | view.axis = .horizontal 197 | view.alignment = .center 198 | view.distribution = .fill 199 | if drop.icon != nil, drop.action?.icon != nil { 200 | view.spacing = 20 201 | } else { 202 | view.spacing = 15 203 | } 204 | return view 205 | }() 206 | } 207 | 208 | final class RoundButton: UIButton { 209 | override var bounds: CGRect { 210 | didSet { layer.cornerRadius = frame.cornerRadius } 211 | } 212 | } 213 | 214 | final class RoundImageView: UIImageView { 215 | override var bounds: CGRect { 216 | didSet { layer.cornerRadius = frame.cornerRadius } 217 | } 218 | } 219 | 220 | extension UIFont { 221 | var bold: UIFont { 222 | guard let descriptor = fontDescriptor.withSymbolicTraits(.traitBold) else { return self } 223 | return UIFont(descriptor: descriptor, size: pointSize) 224 | } 225 | } 226 | 227 | extension CGRect { 228 | var cornerRadius: CGFloat { 229 | return min(width, height) / 2 230 | } 231 | } 232 | #endif 233 | -------------------------------------------------------------------------------- /Sources/Drops.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Drops 3 | // 4 | // Copyright (c) 2021-Present Omar Albeik - https://github.com/omaralbeik 5 | // 6 | // Permission is hereby granted, free of charge, to any person obtaining a copy 7 | // of this software and associated documentation files (the "Software"), to deal 8 | // in the Software without restriction, including without limitation the rights 9 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | // copies of the Software, and to permit persons to whom the Software is 11 | // furnished to do so, subject to the following conditions: 12 | // 13 | // The above copyright notice and this permission notice shall be included in 14 | // all copies or substantial portions of the Software. 15 | // 16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | // THE SOFTWARE. 23 | 24 | #if os(iOS) || os(visionOS) 25 | import UIKit 26 | 27 | internal typealias AnimationCompletion = (_ completed: Bool) -> Void 28 | 29 | /// A shared class used to show and hide drops. 30 | @available(iOSApplicationExtension, unavailable) 31 | public final class Drops { 32 | /// Handler. 33 | public typealias DropHandler = (Drop) -> Void 34 | 35 | // MARK: - Static 36 | 37 | static var shared = Drops() 38 | 39 | /// Show a drop. 40 | /// - Parameter drop: `Drop` to show. 41 | public static func show(_ drop: Drop) { 42 | shared.show(drop) 43 | } 44 | 45 | /// Hide currently shown drop. 46 | public static func hideCurrent() { 47 | shared.hideCurrent() 48 | } 49 | 50 | /// Hide all drops. 51 | public static func hideAll() { 52 | shared.hideAll() 53 | } 54 | 55 | /// A handler to be called before a drop is presented. 56 | public static var willShowDrop: DropHandler? { 57 | get { shared.willShowDrop } 58 | set { shared.willShowDrop = newValue } 59 | } 60 | 61 | /// A handler to be called after a drop is presented. 62 | public static var didShowDrop: DropHandler? { 63 | get { shared.didShowDrop } 64 | set { shared.didShowDrop = newValue } 65 | } 66 | 67 | /// A handler to be called before a drop is dismissed. 68 | public static var willDismissDrop: DropHandler? { 69 | get { shared.willDismissDrop } 70 | set { shared.willDismissDrop = newValue } 71 | } 72 | 73 | /// A handler to be called after a drop is dismissed. 74 | public static var didDismissDrop: DropHandler? { 75 | get { shared.didDismissDrop } 76 | set { shared.didDismissDrop = newValue } 77 | } 78 | 79 | // MARK: - Instance 80 | 81 | /// Create a new instance with a custom delay between drops. 82 | /// - Parameter delayBetweenDrops: Delay between drops in seconds. Defaults to `0.5 seconds`. 83 | public init(delayBetweenDrops: TimeInterval = 0.5) { 84 | self.delayBetweenDrops = delayBetweenDrops 85 | } 86 | 87 | /// Show a drop. 88 | /// - Parameter drop: `Drop` to show. 89 | public func show(_ drop: Drop) { 90 | DispatchQueue.main.async { 91 | let presenter = Presenter(drop: drop, delegate: self) 92 | self.enqueue(presenter: presenter) 93 | } 94 | } 95 | 96 | /// Hide currently shown drop. 97 | public func hideCurrent() { 98 | guard let current = current, !current.isHiding else { return } 99 | willDismissDrop?(current.drop) 100 | DispatchQueue.main.async { 101 | current.hide(animated: true) { [weak self] completed in 102 | guard completed, let self = self else { return } 103 | self.dispatchQueue.sync { 104 | self.didDismissDrop?(current.drop) 105 | guard self.current === current else { return } 106 | self.current = nil 107 | } 108 | } 109 | } 110 | } 111 | 112 | /// Hide all drops. 113 | public func hideAll() { 114 | dispatchQueue.sync { 115 | queue.removeAll() 116 | hideCurrent() 117 | } 118 | } 119 | 120 | /// A handler to be called before a drop is presented. 121 | public var willShowDrop: DropHandler? 122 | 123 | /// A handler to be called after a drop is presented. 124 | public var didShowDrop: DropHandler? 125 | 126 | /// A handler to be called before a drop is dismissed. 127 | public var willDismissDrop: DropHandler? 128 | 129 | /// A handler to be called after a drop is dismissed. 130 | public var didDismissDrop: DropHandler? 131 | 132 | // MARK: - Helpers 133 | 134 | let delayBetweenDrops: TimeInterval 135 | 136 | let dispatchQueue = DispatchQueue(label: "com.omaralbeik.drops") 137 | var queue: [Presenter] = [] 138 | 139 | var current: Presenter? { 140 | didSet { 141 | guard oldValue != nil else { return } 142 | let delayTime = DispatchTime.now() + delayBetweenDrops 143 | dispatchQueue.asyncAfter(deadline: delayTime) { [weak self] in 144 | self?.dequeueNext() 145 | } 146 | } 147 | } 148 | 149 | weak var autohideToken: Presenter? 150 | 151 | func enqueue(presenter: Presenter) { 152 | queue.append(presenter) 153 | dequeueNext() 154 | } 155 | 156 | func hide(presenter: Presenter) { 157 | if presenter == current { 158 | hideCurrent() 159 | } else { 160 | queue = queue.filter { $0 != presenter } 161 | } 162 | } 163 | 164 | func dequeueNext() { 165 | guard current == nil, !queue.isEmpty else { return } 166 | current = queue.removeFirst() 167 | autohideToken = current 168 | 169 | DispatchQueue.main.async { [weak self] in 170 | guard let self = self else { return } 171 | guard let current = self.current else { return } 172 | self.willShowDrop?(current.drop) 173 | current.show { completed in 174 | self.didShowDrop?(current.drop) 175 | guard completed else { 176 | self.dispatchQueue.sync { 177 | self.hide(presenter: current) 178 | } 179 | return 180 | } 181 | if current === self.autohideToken { 182 | self.queueAutoHide() 183 | } 184 | } 185 | } 186 | } 187 | 188 | func queueAutoHide() { 189 | guard let current = current else { return } 190 | autohideToken = current 191 | let delayTime = DispatchTime.now() + current.drop.duration.value 192 | dispatchQueue.asyncAfter(deadline: delayTime) { [weak self] in 193 | if self?.autohideToken !== current { return } 194 | self?.hide(presenter: current) 195 | } 196 | } 197 | } 198 | 199 | extension Drops: AnimatorDelegate { 200 | func hide(animator: Animator) { 201 | dispatchQueue.sync { [weak self] in 202 | guard let presenter = self?.presenter(forAnimator: animator) else { return } 203 | self?.hide(presenter: presenter) 204 | } 205 | } 206 | 207 | func panStarted(animator _: Animator) { 208 | autohideToken = nil 209 | } 210 | 211 | func panEnded(animator _: Animator) { 212 | queueAutoHide() 213 | } 214 | 215 | private func presenter(forAnimator animator: Animator) -> Presenter? { 216 | if let current = current, animator === current.animator { 217 | return current 218 | } 219 | return queue.first { $0.animator === animator } 220 | } 221 | } 222 | #endif 223 | -------------------------------------------------------------------------------- /Sources/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 17 | CFBundleShortVersionString 18 | $(MARKETING_VERSION) 19 | CFBundleVersion 20 | $(CURRENT_PROJECT_VERSION) 21 | 22 | 23 | -------------------------------------------------------------------------------- /Sources/PassthroughView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Drops 3 | // 4 | // Copyright (c) 2021-Present Omar Albeik - https://github.com/omaralbeik 5 | // 6 | // Permission is hereby granted, free of charge, to any person obtaining a copy 7 | // of this software and associated documentation files (the "Software"), to deal 8 | // in the Software without restriction, including without limitation the rights 9 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | // copies of the Software, and to permit persons to whom the Software is 11 | // furnished to do so, subject to the following conditions: 12 | // 13 | // The above copyright notice and this permission notice shall be included in 14 | // all copies or substantial portions of the Software. 15 | // 16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | // THE SOFTWARE. 23 | 24 | #if os(iOS) || os(visionOS) 25 | import UIKit 26 | 27 | internal final class PassthroughView: UIView { 28 | override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { 29 | let view = super.hitTest(point, with: event) 30 | return view == self ? nil : view 31 | } 32 | } 33 | #endif 34 | -------------------------------------------------------------------------------- /Sources/PassthroughWindow.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Drops 3 | // 4 | // Copyright (c) 2021-Present Omar Albeik - https://github.com/omaralbeik 5 | // 6 | // Permission is hereby granted, free of charge, to any person obtaining a copy 7 | // of this software and associated documentation files (the "Software"), to deal 8 | // in the Software without restriction, including without limitation the rights 9 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | // copies of the Software, and to permit persons to whom the Software is 11 | // furnished to do so, subject to the following conditions: 12 | // 13 | // The above copyright notice and this permission notice shall be included in 14 | // all copies or substantial portions of the Software. 15 | // 16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | // THE SOFTWARE. 23 | 24 | #if os(iOS) || os(visionOS) 25 | import UIKit 26 | 27 | internal final class PassthroughWindow: UIWindow { 28 | init(hitTestView: UIView) { 29 | self.hitTestView = hitTestView 30 | super.init(frame: .zero) 31 | } 32 | 33 | required init?(coder _: NSCoder) { 34 | return nil 35 | } 36 | 37 | override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { 38 | let view = super.hitTest(point, with: event) 39 | if let view = view, let hitTestView = hitTestView, hitTestView.isDescendant(of: view), hitTestView != view { 40 | return nil 41 | } 42 | return view 43 | } 44 | 45 | private weak var hitTestView: UIView? 46 | } 47 | #endif 48 | -------------------------------------------------------------------------------- /Sources/Presenter.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Drops 3 | // 4 | // Copyright (c) 2021-Present Omar Albeik - https://github.com/omaralbeik 5 | // 6 | // Permission is hereby granted, free of charge, to any person obtaining a copy 7 | // of this software and associated documentation files (the "Software"), to deal 8 | // in the Software without restriction, including without limitation the rights 9 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | // copies of the Software, and to permit persons to whom the Software is 11 | // furnished to do so, subject to the following conditions: 12 | // 13 | // The above copyright notice and this permission notice shall be included in 14 | // all copies or substantial portions of the Software. 15 | // 16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | // THE SOFTWARE. 23 | 24 | #if os(iOS) || os(visionOS) 25 | import UIKit 26 | 27 | internal final class Presenter: NSObject { 28 | init(drop: Drop, delegate: AnimatorDelegate) { 29 | self.drop = drop 30 | view = DropView(drop: drop) 31 | viewController = .init(value: WindowViewController()) 32 | animator = Animator(position: drop.position, delegate: delegate) 33 | context = AnimationContext(view: view, container: maskingView) 34 | } 35 | 36 | let drop: Drop 37 | let animator: Animator 38 | var isHiding = false 39 | 40 | func show(completion: @escaping AnimationCompletion) { 41 | install() 42 | animator.show(context: context) { [weak self] completed in 43 | if let drop = self?.drop { 44 | self?.announcementAccessibilityMessage(for: drop) 45 | } 46 | completion(completed) 47 | } 48 | } 49 | 50 | func hide(animated: Bool, completion: @escaping AnimationCompletion) { 51 | isHiding = true 52 | let action = { [weak self] in 53 | self?.viewController.value?.uninstall() 54 | self?.maskingView.removeFromSuperview() 55 | completion(true) 56 | } 57 | guard animated else { 58 | action() 59 | return 60 | } 61 | animator.hide(context: context) { _ in 62 | action() 63 | } 64 | } 65 | 66 | let maskingView = PassthroughView() 67 | let view: UIView 68 | let viewController: Weak 69 | let context: AnimationContext 70 | 71 | func install() { 72 | guard let container = viewController.value else { return } 73 | guard let containerView = container.view else { return } 74 | 75 | container.install() 76 | 77 | maskingView.translatesAutoresizingMaskIntoConstraints = false 78 | containerView.addSubview(maskingView) 79 | 80 | NSLayoutConstraint.activate([ 81 | maskingView.leadingAnchor.constraint(equalTo: containerView.leadingAnchor), 82 | maskingView.trailingAnchor.constraint(equalTo: containerView.trailingAnchor), 83 | maskingView.topAnchor.constraint(equalTo: containerView.topAnchor), 84 | maskingView.bottomAnchor.constraint(equalTo: containerView.bottomAnchor) 85 | ]) 86 | 87 | containerView.layoutIfNeeded() 88 | } 89 | 90 | func announcementAccessibilityMessage(for drop: Drop) { 91 | UIAccessibility.post( 92 | notification: UIAccessibility.Notification.announcement, 93 | argument: drop.accessibility.message 94 | ) 95 | } 96 | } 97 | #endif 98 | -------------------------------------------------------------------------------- /Sources/Weak.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Drops 3 | // 4 | // Copyright (c) 2021-Present Omar Albeik - https://github.com/omaralbeik 5 | // 6 | // Permission is hereby granted, free of charge, to any person obtaining a copy 7 | // of this software and associated documentation files (the "Software"), to deal 8 | // in the Software without restriction, including without limitation the rights 9 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | // copies of the Software, and to permit persons to whom the Software is 11 | // furnished to do so, subject to the following conditions: 12 | // 13 | // The above copyright notice and this permission notice shall be included in 14 | // all copies or substantial portions of the Software. 15 | // 16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | // THE SOFTWARE. 23 | 24 | #if os(iOS) || os(visionOS) 25 | internal struct Weak { 26 | init(value: T?) { 27 | self.value = value 28 | } 29 | 30 | weak var value: T? 31 | } 32 | #endif 33 | -------------------------------------------------------------------------------- /Sources/WindowViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Drops 3 | // 4 | // Copyright (c) 2021-Present Omar Albeik - https://github.com/omaralbeik 5 | // 6 | // Permission is hereby granted, free of charge, to any person obtaining a copy 7 | // of this software and associated documentation files (the "Software"), to deal 8 | // in the Software without restriction, including without limitation the rights 9 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | // copies of the Software, and to permit persons to whom the Software is 11 | // furnished to do so, subject to the following conditions: 12 | // 13 | // The above copyright notice and this permission notice shall be included in 14 | // all copies or substantial portions of the Software. 15 | // 16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | // THE SOFTWARE. 23 | 24 | #if os(iOS) || os(visionOS) 25 | import UIKit 26 | 27 | internal final class WindowViewController: UIViewController { 28 | init() { 29 | let view = PassthroughView() 30 | let window = PassthroughWindow(hitTestView: view) 31 | self.window = window 32 | super.init(nibName: nil, bundle: nil) 33 | self.view = view 34 | window.rootViewController = self 35 | } 36 | 37 | required init?(coder _: NSCoder) { 38 | return nil 39 | } 40 | 41 | override var preferredStatusBarStyle: UIStatusBarStyle { 42 | // Workaround for https://github.com/omaralbeik/Drops/pull/22 43 | let app = UIApplication.shared 44 | let windowScene = app.activeWindowScene 45 | let topViewController = windowScene?.windows.first(where: \.isKeyWindow)?.rootViewController?.top 46 | if let controller = topViewController, controller === self { 47 | return .default 48 | } 49 | return topViewController?.preferredStatusBarStyle 50 | ?? windowScene?.statusBarManager?.statusBarStyle 51 | ?? .default 52 | } 53 | 54 | func install() { 55 | #if os(iOS) 56 | window?.frame = UIScreen.main.bounds 57 | #endif 58 | window?.isHidden = false 59 | if let window = window, let activeScene = UIApplication.shared.activeWindowScene { 60 | window.windowScene = activeScene 61 | window.frame = activeScene.coordinateSpace.bounds 62 | } 63 | } 64 | 65 | func uninstall() { 66 | window?.isHidden = true 67 | window?.windowScene = nil 68 | window = nil 69 | } 70 | 71 | var window: UIWindow? 72 | } 73 | 74 | internal extension UIApplication { 75 | var activeWindowScene: UIWindowScene? { 76 | return connectedScenes 77 | .compactMap { $0 as? UIWindowScene } 78 | .first { $0.activationState == .foregroundActive } 79 | } 80 | } 81 | 82 | internal extension UIViewController { 83 | var top: UIViewController? { 84 | if let controller = self as? UINavigationController { 85 | return controller.topViewController?.top 86 | } 87 | if let controller = self as? UISplitViewController { 88 | return controller.viewControllers.last?.top 89 | } 90 | if let controller = self as? UITabBarController { 91 | return controller.selectedViewController?.top 92 | } 93 | if let controller = presentedViewController { 94 | return controller.top 95 | } 96 | return self 97 | } 98 | } 99 | #endif 100 | -------------------------------------------------------------------------------- /SwiftUIExample/App.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Drops 3 | // 4 | // Copyright (c) 2021-Present Omar Albeik - https://github.com/omaralbeik 5 | // 6 | // Permission is hereby granted, free of charge, to any person obtaining a copy 7 | // of this software and associated documentation files (the "Software"), to deal 8 | // in the Software without restriction, including without limitation the rights 9 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | // copies of the Software, and to permit persons to whom the Software is 11 | // furnished to do so, subject to the following conditions: 12 | // 13 | // The above copyright notice and this permission notice shall be included in 14 | // all copies or substantial portions of the Software. 15 | // 16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | // THE SOFTWARE. 23 | 24 | import SwiftUI 25 | 26 | @main 27 | struct SwiftUIExampleApp: App { 28 | var body: some Scene { 29 | WindowGroup { 30 | ContentView() 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /SwiftUIExample/Assets.xcassets/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "idiom" : "universal" 5 | } 6 | ], 7 | "info" : { 8 | "author" : "xcode", 9 | "version" : 1 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /SwiftUIExample/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "scale" : "2x", 6 | "size" : "20x20" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "scale" : "3x", 11 | "size" : "20x20" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "scale" : "2x", 16 | "size" : "29x29" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "scale" : "3x", 21 | "size" : "29x29" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "scale" : "2x", 26 | "size" : "40x40" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "scale" : "3x", 31 | "size" : "40x40" 32 | }, 33 | { 34 | "idiom" : "iphone", 35 | "scale" : "2x", 36 | "size" : "60x60" 37 | }, 38 | { 39 | "idiom" : "iphone", 40 | "scale" : "3x", 41 | "size" : "60x60" 42 | }, 43 | { 44 | "idiom" : "ipad", 45 | "scale" : "1x", 46 | "size" : "20x20" 47 | }, 48 | { 49 | "idiom" : "ipad", 50 | "scale" : "2x", 51 | "size" : "20x20" 52 | }, 53 | { 54 | "idiom" : "ipad", 55 | "scale" : "1x", 56 | "size" : "29x29" 57 | }, 58 | { 59 | "idiom" : "ipad", 60 | "scale" : "2x", 61 | "size" : "29x29" 62 | }, 63 | { 64 | "idiom" : "ipad", 65 | "scale" : "1x", 66 | "size" : "40x40" 67 | }, 68 | { 69 | "idiom" : "ipad", 70 | "scale" : "2x", 71 | "size" : "40x40" 72 | }, 73 | { 74 | "idiom" : "ipad", 75 | "scale" : "1x", 76 | "size" : "76x76" 77 | }, 78 | { 79 | "idiom" : "ipad", 80 | "scale" : "2x", 81 | "size" : "76x76" 82 | }, 83 | { 84 | "idiom" : "ipad", 85 | "scale" : "2x", 86 | "size" : "83.5x83.5" 87 | }, 88 | { 89 | "idiom" : "ios-marketing", 90 | "scale" : "1x", 91 | "size" : "1024x1024" 92 | } 93 | ], 94 | "info" : { 95 | "author" : "xcode", 96 | "version" : 1 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /SwiftUIExample/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /SwiftUIExample/ContentView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Drops 3 | // 4 | // Copyright (c) 2021-Present Omar Albeik - https://github.com/omaralbeik 5 | // 6 | // Permission is hereby granted, free of charge, to any person obtaining a copy 7 | // of this software and associated documentation files (the "Software"), to deal 8 | // in the Software without restriction, including without limitation the rights 9 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | // copies of the Software, and to permit persons to whom the Software is 11 | // furnished to do so, subject to the following conditions: 12 | // 13 | // The above copyright notice and this permission notice shall be included in 14 | // all copies or substantial portions of the Software. 15 | // 16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | // THE SOFTWARE. 23 | 24 | import SwiftUI 25 | import Drops 26 | 27 | struct ContentView: View { 28 | 29 | @State var title: String = "Hello There!" 30 | @State var subtitle: String = "Use Drops to show alerts" 31 | @State var positionIndex: Int = 0 32 | @State var duration: TimeInterval = 2.0 33 | @State var hasIcon: Bool = false 34 | @State var hasActionIcon: Bool = false 35 | 36 | var body: some View { 37 | ZStack { 38 | Color(.secondarySystemBackground).ignoresSafeArea(.all) 39 | VStack(alignment: .center, spacing: 20) { 40 | VStack { 41 | HStack { 42 | Text("Title").font(.caption) 43 | Spacer() 44 | } 45 | TextField("Title", text: $title).textFieldStyle(RoundedBorderTextFieldStyle()) 46 | } 47 | VStack { 48 | HStack { 49 | Text("Optional Subtitle").font(.caption) 50 | Spacer() 51 | } 52 | TextField("Subtitle", text: $subtitle).textFieldStyle(RoundedBorderTextFieldStyle()) 53 | } 54 | VStack { 55 | HStack { 56 | Text("Position").font(.caption) 57 | Spacer() 58 | } 59 | Picker(selection: $positionIndex, label: Text("Position")) { 60 | Text("Top").tag(0) 61 | Text("Bottom").tag(1) 62 | } 63 | .pickerStyle(SegmentedPickerStyle()) 64 | } 65 | VStack { 66 | HStack { 67 | Text("Duration (\(String(format: "%.1f", duration)) s)").font(.caption) 68 | Spacer() 69 | } 70 | Slider(value: $duration, in: 0.1...10) 71 | } 72 | Toggle("Icon", isOn: $hasIcon) 73 | Toggle("Button", isOn: $hasActionIcon) 74 | Spacer() 75 | Button(action: { 76 | showDrop() 77 | }, label: { 78 | Text("Show Drop") 79 | .foregroundColor(.white) 80 | .frame(maxWidth: .infinity) 81 | .padding(10) 82 | }) 83 | .frame(maxWidth: .infinity) 84 | .background(Color.blue) 85 | .cornerRadius(7.5) 86 | } 87 | .padding() 88 | .padding(.top, 80) 89 | } 90 | .ignoresSafeArea(.keyboard) 91 | .onTapGesture { 92 | UIApplication.shared.endEditing() 93 | } 94 | } 95 | 96 | private func showDrop() { 97 | UIApplication.shared.endEditing() 98 | 99 | let aTitle = title.trimmingCharacters(in: .whitespacesAndNewlines) 100 | let aSubtitle = subtitle.trimmingCharacters(in: .whitespacesAndNewlines) 101 | let position: Drop.Position = positionIndex == 0 ? .top : .bottom 102 | 103 | let icon = hasIcon ? UIImage(systemName: "star.fill") : nil 104 | let buttonIcon = hasActionIcon ? UIImage(systemName: "arrowshape.turn.up.left") : nil 105 | 106 | let drop = Drop( 107 | title: aTitle, 108 | subtitle: aSubtitle, 109 | icon: icon, 110 | action: .init(icon: buttonIcon, handler: { 111 | print("Drop tapped") 112 | Drops.hideCurrent() 113 | }), 114 | position: position, 115 | duration: .seconds(duration) 116 | ) 117 | Drops.show(drop) 118 | } 119 | } 120 | 121 | private extension UIApplication { 122 | func endEditing() { 123 | sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil) 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /SwiftUIExample/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | LSRequiresIPhoneOS 22 | 23 | UIApplicationSceneManifest 24 | 25 | UIApplicationSupportsMultipleScenes 26 | 27 | 28 | UIApplicationSupportsIndirectInputEvents 29 | 30 | UILaunchScreen 31 | 32 | UIRequiredDeviceCapabilities 33 | 34 | armv7 35 | 36 | UISupportedInterfaceOrientations 37 | 38 | UIInterfaceOrientationPortrait 39 | 40 | UISupportedInterfaceOrientations~ipad 41 | 42 | UIInterfaceOrientationPortrait 43 | UIInterfaceOrientationPortraitUpsideDown 44 | UIInterfaceOrientationLandscapeLeft 45 | UIInterfaceOrientationLandscapeRight 46 | 47 | 48 | 49 | -------------------------------------------------------------------------------- /SwiftUIExample/SwiftUIExample.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.app-sandbox 6 | 7 | com.apple.security.network.client 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /Tests/AnimatorTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Drops 3 | // 4 | // Copyright (c) 2021-Present Omar Albeik - https://github.com/omaralbeik 5 | // 6 | // Permission is hereby granted, free of charge, to any person obtaining a copy 7 | // of this software and associated documentation files (the "Software"), to deal 8 | // in the Software without restriction, including without limitation the rights 9 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | // copies of the Software, and to permit persons to whom the Software is 11 | // furnished to do so, subject to the following conditions: 12 | // 13 | // The above copyright notice and this permission notice shall be included in 14 | // all copies or substantial portions of the Software. 15 | // 16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | // THE SOFTWARE. 23 | 24 | #if os(iOS) 25 | import XCTest 26 | @testable import Drops 27 | 28 | final class AnimatorTests: XCTestCase { 29 | func testInitializer() { 30 | let position = Drop.Position.bottom 31 | let delegate = TestAnimatorDelegate() 32 | let animator = Animator(position: position, delegate: delegate) 33 | 34 | XCTAssertEqual(animator.position, position) 35 | XCTAssert(animator.delegate === delegate) 36 | } 37 | 38 | func testInstall() { 39 | func install(position: Drop.Position) { 40 | let delegate = TestAnimatorDelegate() 41 | let animator = Animator(position: position, delegate: delegate) 42 | 43 | let view = UIView() 44 | let container = UIView() 45 | let context = AnimationContext(view: view, container: container) 46 | 47 | animator.install(context: context) 48 | 49 | XCTAssertEqual(animator.context?.view, view) 50 | XCTAssertEqual(animator.context?.container, container) 51 | 52 | XCTAssertFalse(view.translatesAutoresizingMaskIntoConstraints) 53 | XCTAssertEqual(container.subviews[0], view) 54 | XCTAssertEqual(view.superview, container) 55 | 56 | var expectedConstraints: [NSLayoutConstraint] = [ 57 | view.centerXAnchor.constraint(equalTo: container.safeAreaLayoutGuide.centerXAnchor), 58 | view.leadingAnchor.constraint(greaterThanOrEqualTo: container.safeAreaLayoutGuide.leadingAnchor, constant: 20), 59 | view.trailingAnchor.constraint(lessThanOrEqualTo: container.safeAreaLayoutGuide.trailingAnchor, constant: -20), 60 | view.topAnchor.constraint(equalTo: container.safeAreaLayoutGuide.topAnchor, constant: animator.bounceOffset) 61 | ] 62 | 63 | switch position { 64 | case .top: 65 | expectedConstraints += [ 66 | ] 67 | case .bottom: 68 | expectedConstraints += [ 69 | view.bottomAnchor.constraint( 70 | equalTo: container.safeAreaLayoutGuide.bottomAnchor, 71 | constant: -animator.bounceOffset 72 | ) 73 | ] 74 | } 75 | 76 | for (actual, expected) in zip(view.constraints, expectedConstraints) { 77 | XCTAssertEqual(actual, expected) 78 | } 79 | 80 | let animationDistance = view.frame.height 81 | 82 | switch position { 83 | case .top: 84 | XCTAssertEqual(view.transform, CGAffineTransform(translationX: 0, y: -animationDistance)) 85 | case .bottom: 86 | XCTAssertEqual(view.transform, CGAffineTransform(translationX: 0, y: animationDistance)) 87 | } 88 | } 89 | 90 | install(position: .top) 91 | install(position: .bottom) 92 | } 93 | 94 | func testShowWithCompletionBeforeCallingInstall() { 95 | let delegate = TestAnimatorDelegate() 96 | let animator = Animator(position: .top, delegate: delegate) 97 | 98 | let exp = expectation(description: "Completion called with false") 99 | animator.show { completed in 100 | if !completed { 101 | exp.fulfill() 102 | } 103 | } 104 | waitForExpectations(timeout: 2) 105 | } 106 | 107 | func testShowWithCompletion() { 108 | let delegate = TestAnimatorDelegate() 109 | let animator = Animator(position: .top, delegate: delegate) 110 | 111 | let view = UIView() 112 | let container = UIView() 113 | let context = AnimationContext(view: view, container: container) 114 | 115 | animator.install(context: context) 116 | 117 | let exp = expectation(description: "Completion called") 118 | animator.show { _ in 119 | if view.alpha == 1 && view.transform == .identity { 120 | exp.fulfill() 121 | } 122 | } 123 | waitForExpectations(timeout: 2) 124 | } 125 | 126 | func testShow() { 127 | let delegate = TestAnimatorDelegate() 128 | let animator = Animator(position: .top, delegate: delegate) 129 | 130 | let view = UIView() 131 | let container = UIView() 132 | let context = AnimationContext(view: view, container: container) 133 | 134 | let exp = expectation(description: "Completion called") 135 | animator.show(context: context) { _ in 136 | if view.alpha == 1 && view.transform == .identity { 137 | exp.fulfill() 138 | } 139 | } 140 | waitForExpectations(timeout: 2) 141 | } 142 | 143 | func testHide() { 144 | func hide(position: Drop.Position) { 145 | let delegate = TestAnimatorDelegate() 146 | let animator = Animator(position: position, delegate: delegate) 147 | 148 | let view = UIView() 149 | let container = UIView() 150 | let context = AnimationContext(view: view, container: container) 151 | 152 | let exp = expectation(description: "Completion called") 153 | animator.hide(context: context) { _ in 154 | if view.alpha == 0 { 155 | exp.fulfill() 156 | } 157 | } 158 | waitForExpectations(timeout: 2) 159 | } 160 | 161 | hide(position: .top) 162 | hide(position: .bottom) 163 | } 164 | 165 | func testPanChangedWhenHeightIsZero() { 166 | let delegate = TestAnimatorDelegate() 167 | let animator = Animator(position: .top, delegate: delegate) 168 | let state = Animator.PanState() 169 | XCTAssertEqual(state, animator.panChanged(current: state, view: .init(), velocity: .zero, translation: .zero)) 170 | } 171 | 172 | func testPanChanged() { 173 | let delegate = TestAnimatorDelegate() 174 | let animator = Animator(position: .top, delegate: delegate) 175 | let state = Animator.PanState() 176 | let view = UIView(frame: .init(origin: .zero, size: .init(width: 100, height: 100))) 177 | let changedState = animator.panChanged( 178 | current: state, 179 | view: view, 180 | velocity: .init(x: 5, y: 5), 181 | translation: .init(x: 25, y: 25) 182 | ) 183 | 184 | XCTAssertEqual(changedState.closing, true) 185 | XCTAssertEqual(changedState.closeSpeed, -5) 186 | XCTAssertEqual(changedState.closePercent, -0.26, accuracy: 0.99) 187 | XCTAssertEqual(changedState.panTranslationY, -25) 188 | } 189 | 190 | func testPanEnded() { 191 | let delegate = TestAnimatorDelegate() 192 | let animator = Animator(position: .top, delegate: delegate) 193 | 194 | var state = Animator.PanState() 195 | var endedState = animator.panEnded(current: state) 196 | XCTAssertEqual(endedState, state) 197 | 198 | state.closeSpeed = 800 199 | endedState = animator.panEnded(current: state) 200 | XCTAssertNil(endedState) 201 | 202 | state = .init() 203 | state.closePercent = 0.5 204 | endedState = animator.panEnded(current: state) 205 | XCTAssertNil(endedState) 206 | 207 | state = .init() 208 | state.panTranslationY = 80 209 | endedState = animator.panEnded(current: state) 210 | XCTAssertNil(endedState) 211 | } 212 | } 213 | #endif 214 | -------------------------------------------------------------------------------- /Tests/DropTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Drops 3 | // 4 | // Copyright (c) 2021-Present Omar Albeik - https://github.com/omaralbeik 5 | // 6 | // Permission is hereby granted, free of charge, to any person obtaining a copy 7 | // of this software and associated documentation files (the "Software"), to deal 8 | // in the Software without restriction, including without limitation the rights 9 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | // copies of the Software, and to permit persons to whom the Software is 11 | // furnished to do so, subject to the following conditions: 12 | // 13 | // The above copyright notice and this permission notice shall be included in 14 | // all copies or substantial portions of the Software. 15 | // 16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | // THE SOFTWARE. 23 | 24 | #if os(iOS) 25 | import XCTest 26 | @testable import Drops 27 | 28 | final class DropTests: XCTestCase { 29 | func testDefaultInitializer() { 30 | let drop = Drop(title: "Hello world") 31 | XCTAssertEqual(drop.title, "Hello world") 32 | XCTAssertEqual(drop.titleNumberOfLines, 1) 33 | XCTAssertNil(drop.subtitle) 34 | XCTAssertEqual(drop.subtitleNumberOfLines, 1) 35 | XCTAssertNil(drop.icon) 36 | XCTAssertNil(drop.action) 37 | XCTAssertEqual(drop.position, .top) 38 | XCTAssertEqual(drop.duration, .recommended) 39 | XCTAssertEqual(drop.accessibility.message, "Hello world") 40 | } 41 | 42 | func testExpressiblesInitializer() { 43 | let drop1: Drop = "Hello world" 44 | XCTAssertEqual(drop1.title, "Hello world") 45 | XCTAssertEqual(drop1.position, .top) 46 | XCTAssertEqual(drop1.duration, .recommended) 47 | XCTAssertEqual(drop1.accessibility.message, "Hello world") 48 | 49 | let drop2 = Drop(title: "Hello world", duration: 5.0, accessibility: "Alert: Hello world") 50 | XCTAssertEqual(drop2.title, "Hello world") 51 | XCTAssertEqual(drop2.duration.value, 5.0) 52 | XCTAssertEqual(drop2.accessibility.message, "Alert: Hello world") 53 | } 54 | 55 | func testInitializer() { 56 | let icon = UIImage(systemName: "drop") 57 | let dismissIcon = UIImage(systemName: "xmark") 58 | let action = Drop.Action(icon: dismissIcon) { } 59 | let drop = Drop( 60 | title: "Hello world", 61 | titleNumberOfLines: 3, 62 | subtitle: "I'm a drop!", 63 | subtitleNumberOfLines: 0, 64 | icon: icon, 65 | action: action, 66 | position: .bottom, 67 | duration: .seconds(1) 68 | ) 69 | XCTAssertEqual(drop.title, "Hello world") 70 | XCTAssertEqual(drop.titleNumberOfLines, 3) 71 | XCTAssertEqual(drop.subtitle, "I'm a drop!") 72 | XCTAssertEqual(drop.subtitleNumberOfLines, 0) 73 | XCTAssertEqual(drop.icon, icon) 74 | XCTAssertEqual(drop.action?.icon, dismissIcon) 75 | XCTAssertEqual(drop.position, .bottom) 76 | XCTAssertEqual(drop.duration, .seconds(1)) 77 | XCTAssertEqual(drop.accessibility.message, "Hello world, I'm a drop!") 78 | } 79 | 80 | func testDurationValue() { 81 | XCTAssertEqual(Drop.Duration.recommended.value, 2) 82 | XCTAssertEqual(Drop.Duration.seconds(1).value, 1) 83 | } 84 | } 85 | #endif 86 | -------------------------------------------------------------------------------- /Tests/DropViewTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Drops 3 | // 4 | // Copyright (c) 2021-Present Omar Albeik - https://github.com/omaralbeik 5 | // 6 | // Permission is hereby granted, free of charge, to any person obtaining a copy 7 | // of this software and associated documentation files (the "Software"), to deal 8 | // in the Software without restriction, including without limitation the rights 9 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | // copies of the Software, and to permit persons to whom the Software is 11 | // furnished to do so, subject to the following conditions: 12 | // 13 | // The above copyright notice and this permission notice shall be included in 14 | // all copies or substantial portions of the Software. 15 | // 16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | // THE SOFTWARE. 23 | 24 | #if os(iOS) 25 | import XCTest 26 | @testable import Drops 27 | 28 | final class DropViewTests: XCTestCase { 29 | func testInitializer() { 30 | let drop = Drop(title: "Test") 31 | let view = DropView(drop: drop) 32 | XCTAssertEqual(view.drop, drop) 33 | XCTAssertEqual(view.backgroundColor, .secondarySystemBackground) 34 | XCTAssertFalse(view.constraints.isEmpty) 35 | XCTAssertFalse(view.subviews.isEmpty) 36 | 37 | XCTAssertNil(DropView(coder: NSCoder())) 38 | } 39 | 40 | func testLayoutConstraintsForTitle() { 41 | let drop = Drop(title: "Title") 42 | let view = DropView(drop: drop) 43 | 44 | let created = view.createLayoutConstraints(for: drop) 45 | let expected: [NSLayoutConstraint] = [ 46 | view.imageView.heightAnchor.constraint(equalToConstant: 25), 47 | view.imageView.widthAnchor.constraint(equalToConstant: 25), 48 | view.button.heightAnchor.constraint(equalToConstant: 35), 49 | view.button.widthAnchor.constraint(equalToConstant: 35), 50 | view.stackView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 50), 51 | view.stackView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 15), 52 | view.stackView.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -50), 53 | view.stackView.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: -15) 54 | ] 55 | 56 | XCTAssertEqual(created.count, expected.count) 57 | 58 | zip(created, expected) 59 | .forEach { created, expected in 60 | XCTAssertEqual(created.constant, expected.constant) 61 | XCTAssertEqual(created.multiplier, expected.multiplier) 62 | } 63 | } 64 | 65 | func testLayoutConstraintsForTitleAndSubtitle() { 66 | let drop = Drop(title: "Title", subtitle: "Subtitle") 67 | let view = DropView(drop: drop) 68 | 69 | let created = view.createLayoutConstraints(for: drop) 70 | let expected: [NSLayoutConstraint] = [ 71 | view.imageView.heightAnchor.constraint(equalToConstant: 25), 72 | view.imageView.widthAnchor.constraint(equalToConstant: 25), 73 | view.button.heightAnchor.constraint(equalToConstant: 35), 74 | view.button.widthAnchor.constraint(equalToConstant: 35), 75 | view.stackView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 50), 76 | view.stackView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 7.5), 77 | view.stackView.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -50), 78 | view.stackView.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: -7.5) 79 | ] 80 | 81 | XCTAssertEqual(created.count, expected.count) 82 | 83 | zip(created, expected) 84 | .forEach { created, expected in 85 | XCTAssertEqual(created.constant, expected.constant) 86 | XCTAssertEqual(created.multiplier, expected.multiplier) 87 | } 88 | } 89 | 90 | func testLayoutConstraintsForTitleAndAction() { 91 | let drop = Drop(title: "Title", action: .init(icon: UIImage(), handler: {})) 92 | let view = DropView(drop: drop) 93 | 94 | let created = view.createLayoutConstraints(for: drop) 95 | let expected: [NSLayoutConstraint] = [ 96 | view.imageView.heightAnchor.constraint(equalToConstant: 25), 97 | view.imageView.widthAnchor.constraint(equalToConstant: 25), 98 | view.button.heightAnchor.constraint(equalToConstant: 35), 99 | view.button.widthAnchor.constraint(equalToConstant: 35), 100 | view.stackView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 40), 101 | view.stackView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 10), 102 | view.stackView.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -10), 103 | view.stackView.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: -10) 104 | ] 105 | 106 | XCTAssertEqual(created.count, expected.count) 107 | 108 | zip(created, expected) 109 | .forEach { created, expected in 110 | XCTAssertEqual(created.constant, expected.constant) 111 | XCTAssertEqual(created.multiplier, expected.multiplier) 112 | } 113 | } 114 | 115 | func testTapGestureAddedWhenActionWithNoIcon() { 116 | let drop = Drop(title: "Title", action: .init(handler: {})) 117 | let view = DropView(drop: drop) 118 | XCTAssertEqual(view.gestureRecognizers?.count, 1) 119 | XCTAssert(view.gestureRecognizers?.first is UITapGestureRecognizer) 120 | } 121 | 122 | func testViewCornerRadius() { 123 | let drop = Drop(title: "Title") 124 | let view = DropView(drop: drop) 125 | view.bounds = .init(x: 0, y: 0, width: 300, height: 100) 126 | 127 | let imageView = RoundImageView() 128 | imageView.bounds = .init(x: 0, y: 0, width: 40, height: 40) 129 | XCTAssertEqual(imageView.layer.cornerRadius, 20) 130 | 131 | let button = RoundButton() 132 | button.bounds = .init(x: 0, y: 0, width: 40, height: 40) 133 | XCTAssertEqual(button.layer.cornerRadius, 20) 134 | } 135 | 136 | func testStackViewSpacing() { 137 | let drop1 = Drop(title: "Title") 138 | let view1 = DropView(drop: drop1) 139 | XCTAssertEqual(view1.stackView.spacing, 15) 140 | 141 | let drop2 = Drop(title: "Title", icon: UIImage(), action: .init(icon: UIImage(), handler: {})) 142 | let view2 = DropView(drop: drop2) 143 | XCTAssertEqual(view2.stackView.spacing, 20) 144 | 145 | } 146 | 147 | func testActionIsCalledWhenButtonIsTapped() { 148 | let exp = expectation(description: "Completion called with true") 149 | 150 | let drop = Drop(title: "Title", action: .init(icon: UIImage(), handler: { 151 | exp.fulfill() 152 | })) 153 | 154 | let view = DropView(drop: drop) 155 | view.didTapButton() 156 | 157 | waitForExpectations(timeout: 1) 158 | } 159 | 160 | func testLabelNumberOfLines() { 161 | let drop1 = Drop(title: "Title") 162 | let view1 = DropView(drop: drop1) 163 | XCTAssertEqual(view1.titleLabel.numberOfLines, 1) 164 | XCTAssertEqual(view1.subtitleLabel.numberOfLines, 1) 165 | 166 | let drop2 = Drop( 167 | title: "Title", 168 | titleNumberOfLines: 3, 169 | subtitle: "Subtitle", 170 | subtitleNumberOfLines: 0 171 | ) 172 | let view2 = DropView(drop: drop2) 173 | XCTAssertEqual(view2.titleLabel.numberOfLines, 3) 174 | XCTAssertEqual(view2.subtitleLabel.numberOfLines, 0) 175 | } 176 | } 177 | 178 | extension Drop: Equatable { 179 | public static func == (lhs: Drop, rhs: Drop) -> Bool { 180 | return lhs.title == rhs.title 181 | } 182 | } 183 | #endif 184 | -------------------------------------------------------------------------------- /Tests/DropsTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Drops 3 | // 4 | // Copyright (c) 2021-Present Omar Albeik - https://github.com/omaralbeik 5 | // 6 | // Permission is hereby granted, free of charge, to any person obtaining a copy 7 | // of this software and associated documentation files (the "Software"), to deal 8 | // in the Software without restriction, including without limitation the rights 9 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | // copies of the Software, and to permit persons to whom the Software is 11 | // furnished to do so, subject to the following conditions: 12 | // 13 | // The above copyright notice and this permission notice shall be included in 14 | // all copies or substantial portions of the Software. 15 | // 16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | // THE SOFTWARE. 23 | 24 | #if os(iOS) 25 | import XCTest 26 | @testable import Drops 27 | 28 | final class DropsTests: XCTestCase { 29 | func testShow() async { 30 | let drops = Drops() 31 | 32 | let drop1 = Drop(title: "Test 1", duration: .seconds(1)) 33 | drops.show(drop1) 34 | await Task.sleep(seconds: 0.1) 35 | if drops.current == nil { 36 | XCTFail("First drop is not presented") 37 | } 38 | 39 | await Task.sleep(seconds: 2) 40 | if drops.current != nil { 41 | XCTFail("First drop is not hidden") 42 | } 43 | } 44 | 45 | func testStaticShow() async { 46 | Drops.shared = .init(delayBetweenDrops: 0) 47 | 48 | let drop1 = Drop(title: "Test 1", duration: .seconds(1)) 49 | Drops.show(drop1) 50 | 51 | await Task.sleep(seconds: 0.1) 52 | if Drops.shared.current == nil { 53 | XCTFail("First drop is not presented") 54 | } 55 | 56 | await Task.sleep(seconds: 2) 57 | if Drops.shared.current != nil { 58 | XCTFail("First drop is not hidden") 59 | } 60 | } 61 | 62 | func testDropsAreQueued() async throws { 63 | let drops = Drops(delayBetweenDrops: 0) 64 | (0..<5) 65 | .map { Drop(title: "\($0)", duration: .seconds(0.1)) } 66 | .forEach(drops.show) 67 | 68 | await Task.sleep(seconds: 0.1) 69 | if drops.queue.count != 4 { 70 | XCTFail("First drop is not hidden") 71 | } 72 | 73 | await Task.sleep(seconds: 5) 74 | if drops.queue.isEmpty == false { 75 | XCTFail("All drops are not hidden") 76 | } 77 | } 78 | 79 | func testStaticDropsAreQueued() async { 80 | Drops.shared = .init(delayBetweenDrops: 0) 81 | 82 | (0..<5) 83 | .map { Drop(title: "\($0)", duration: .seconds(0.1)) } 84 | .forEach(Drops.show) 85 | 86 | await Task.sleep(seconds: 0.1) 87 | if Drops.shared.queue.count != 4 { 88 | XCTFail("First drop is not hidden") 89 | } 90 | 91 | await Task.sleep(seconds: 5) 92 | if Drops.shared.queue.isEmpty == false { 93 | XCTFail("All drops are not hidden") 94 | } 95 | } 96 | 97 | func testHideAll() { 98 | let drops = Drops(delayBetweenDrops: 0) 99 | 100 | (0..<10) 101 | .map { Drop(title: "\($0)", duration: .seconds(0.1)) } 102 | .forEach(drops.show) 103 | 104 | drops.hideAll() 105 | 106 | XCTAssert(drops.queue.isEmpty) 107 | } 108 | 109 | func testStaticHideAll() { 110 | Drops.shared = .init(delayBetweenDrops: 0) 111 | 112 | (0..<10) 113 | .map { Drop(title: "\($0)", duration: .seconds(0.1)) } 114 | .forEach(Drops.show) 115 | 116 | Drops.hideAll() 117 | 118 | XCTAssert(Drops.shared.queue.isEmpty) 119 | } 120 | 121 | func testHandlers() { 122 | let drops = Drops(delayBetweenDrops: 0) 123 | let expectedDrop = Drop(title: "Hello world!", duration: .seconds(0.1)) 124 | 125 | let willShowDropExp = expectation(description: "willShowDrop is called") 126 | let didShowDropExp = expectation(description: "didShowDrop is called") 127 | let willDismissDropExp = expectation(description: "willDismissDrop is called") 128 | let didDismissDropExp = expectation(description: "didDismissDrop is called") 129 | 130 | drops.willShowDrop = { drop in 131 | XCTAssertEqual(drop, expectedDrop) 132 | willShowDropExp.fulfill() 133 | } 134 | 135 | drops.didShowDrop = { drop in 136 | XCTAssertEqual(drop, expectedDrop) 137 | didShowDropExp.fulfill() 138 | } 139 | 140 | drops.willDismissDrop = { drop in 141 | XCTAssertEqual(drop, expectedDrop) 142 | willDismissDropExp.fulfill() 143 | } 144 | 145 | drops.didDismissDrop = { drop in 146 | XCTAssertEqual(drop, expectedDrop) 147 | didDismissDropExp.fulfill() 148 | } 149 | 150 | drops.show(expectedDrop) 151 | 152 | waitForExpectations(timeout: 2) 153 | } 154 | 155 | func testStaticHandlers() { 156 | Drops.shared = .init(delayBetweenDrops: 0) 157 | 158 | let expectedDrop = Drop(title: "Hello world!", duration: .seconds(0.1)) 159 | 160 | let willShowDropExp = expectation(description: "willShowDrop is called") 161 | let didShowDropExp = expectation(description: "didShowDrop is called") 162 | let willDismissDropExp = expectation(description: "willDismissDrop is called") 163 | let didDismissDropExp = expectation(description: "didDismissDrop is called") 164 | 165 | Drops.willShowDrop = { drop in 166 | XCTAssertEqual(drop, expectedDrop) 167 | willShowDropExp.fulfill() 168 | } 169 | 170 | Drops.didShowDrop = { drop in 171 | XCTAssertEqual(drop, expectedDrop) 172 | didShowDropExp.fulfill() 173 | } 174 | 175 | Drops.willDismissDrop = { drop in 176 | XCTAssertEqual(drop, expectedDrop) 177 | willDismissDropExp.fulfill() 178 | } 179 | 180 | Drops.didDismissDrop = { drop in 181 | XCTAssertEqual(drop, expectedDrop) 182 | didDismissDropExp.fulfill() 183 | } 184 | 185 | Drops.show(expectedDrop) 186 | 187 | waitForExpectations(timeout: 2) 188 | } 189 | 190 | func testStaticHandlersSettersAndGetters() { 191 | Drops.willShowDrop = { _ in } 192 | XCTAssertNotNil(Drops.shared.willShowDrop) 193 | Drops.willShowDrop = nil 194 | XCTAssertNil(Drops.shared.willShowDrop) 195 | 196 | Drops.didShowDrop = { _ in } 197 | XCTAssertNotNil(Drops.shared.didShowDrop) 198 | Drops.didShowDrop = nil 199 | XCTAssertNil(Drops.shared.didShowDrop) 200 | 201 | Drops.willDismissDrop = { _ in } 202 | XCTAssertNotNil(Drops.shared.willDismissDrop) 203 | Drops.willDismissDrop = nil 204 | XCTAssertNil(Drops.shared.willDismissDrop) 205 | 206 | Drops.didDismissDrop = { _ in } 207 | XCTAssertNotNil(Drops.shared.didDismissDrop) 208 | Drops.didDismissDrop = nil 209 | XCTAssertNil(Drops.shared.didDismissDrop) 210 | 211 | Drops.shared.willShowDrop = { _ in } 212 | XCTAssertNotNil(Drops.willShowDrop) 213 | Drops.shared.willShowDrop = nil 214 | XCTAssertNil(Drops.willShowDrop) 215 | 216 | Drops.shared.didShowDrop = { _ in } 217 | XCTAssertNotNil(Drops.didShowDrop) 218 | Drops.shared.didShowDrop = nil 219 | XCTAssertNil(Drops.didShowDrop) 220 | 221 | Drops.shared.willDismissDrop = { _ in } 222 | XCTAssertNotNil(Drops.willDismissDrop) 223 | Drops.shared.willDismissDrop = nil 224 | XCTAssertNil(Drops.willDismissDrop) 225 | 226 | Drops.shared.didDismissDrop = { _ in } 227 | XCTAssertNotNil(Drops.didDismissDrop) 228 | Drops.shared.didDismissDrop = nil 229 | XCTAssertNil(Drops.didDismissDrop) 230 | } 231 | } 232 | 233 | private extension Task where Success == Never, Failure == Never { 234 | static func sleep(seconds: Double) async { 235 | let duration = UInt64(seconds * 1_000_000_000) 236 | do { 237 | try await Task.sleep(nanoseconds: duration) 238 | } catch { 239 | XCTFail(error.localizedDescription) 240 | } 241 | } 242 | } 243 | #endif 244 | -------------------------------------------------------------------------------- /Tests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | 22 | 23 | -------------------------------------------------------------------------------- /Tests/PassthroughViewTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Drops 3 | // 4 | // Copyright (c) 2021-Present Omar Albeik - https://github.com/omaralbeik 5 | // 6 | // Permission is hereby granted, free of charge, to any person obtaining a copy 7 | // of this software and associated documentation files (the "Software"), to deal 8 | // in the Software without restriction, including without limitation the rights 9 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | // copies of the Software, and to permit persons to whom the Software is 11 | // furnished to do so, subject to the following conditions: 12 | // 13 | // The above copyright notice and this permission notice shall be included in 14 | // all copies or substantial portions of the Software. 15 | // 16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | // THE SOFTWARE. 23 | 24 | #if os(iOS) 25 | import XCTest 26 | @testable import Drops 27 | 28 | final class PassthroughViewTests: XCTestCase { 29 | func testHitTest() { 30 | let frame = CGRect(x: 0, y: 0, width: 100, height: 100) 31 | let view = PassthroughView(frame: frame) 32 | 33 | let result = view.hitTest(.init(x: 50, y: 50), with: .init()) 34 | XCTAssertNil(result) 35 | } 36 | } 37 | #endif 38 | -------------------------------------------------------------------------------- /Tests/PassthroughWindowTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Drops 3 | // 4 | // Copyright (c) 2021-Present Omar Albeik - https://github.com/omaralbeik 5 | // 6 | // Permission is hereby granted, free of charge, to any person obtaining a copy 7 | // of this software and associated documentation files (the "Software"), to deal 8 | // in the Software without restriction, including without limitation the rights 9 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | // copies of the Software, and to permit persons to whom the Software is 11 | // furnished to do so, subject to the following conditions: 12 | // 13 | // The above copyright notice and this permission notice shall be included in 14 | // all copies or substantial portions of the Software. 15 | // 16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | // THE SOFTWARE. 23 | 24 | #if os(iOS) 25 | import XCTest 26 | @testable import Drops 27 | 28 | final class PassthroughWindowTests: XCTestCase { 29 | func testInitializer() { 30 | XCTAssertNil(PassthroughWindow(coder: NSCoder())) 31 | } 32 | 33 | func testHitTest() { 34 | let frame = CGRect(x: 0, y: 0, width: 100, height: 100) 35 | let view = UIView(frame: frame) 36 | let window = PassthroughWindow(hitTestView: view) 37 | 38 | let result = window.hitTest(.init(x: 50, y: 50), with: .init()) 39 | XCTAssertNil(result) 40 | } 41 | } 42 | #endif 43 | -------------------------------------------------------------------------------- /Tests/PresenterTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Drops 3 | // 4 | // Copyright (c) 2021-Present Omar Albeik - https://github.com/omaralbeik 5 | // 6 | // Permission is hereby granted, free of charge, to any person obtaining a copy 7 | // of this software and associated documentation files (the "Software"), to deal 8 | // in the Software without restriction, including without limitation the rights 9 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | // copies of the Software, and to permit persons to whom the Software is 11 | // furnished to do so, subject to the following conditions: 12 | // 13 | // The above copyright notice and this permission notice shall be included in 14 | // all copies or substantial portions of the Software. 15 | // 16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | // THE SOFTWARE. 23 | 24 | #if os(iOS) 25 | import XCTest 26 | @testable import Drops 27 | 28 | final class PresenterTests: XCTestCase { 29 | func testInitializer() { 30 | let drop = Drop(title: "Hello world!") 31 | let delegate = TestAnimatorDelegate() 32 | let presenter = Presenter(drop: drop, delegate: delegate) 33 | 34 | XCTAssertEqual(presenter.drop, drop) 35 | XCTAssert(presenter.view is DropView) 36 | XCTAssertEqual((presenter.view as? DropView)?.drop, drop) 37 | XCTAssertNotNil(presenter.viewController.value) 38 | XCTAssertEqual(presenter.animator.position, drop.position) 39 | XCTAssert(presenter.animator.delegate === delegate) 40 | XCTAssertEqual(presenter.context.view, presenter.view) 41 | XCTAssertEqual(presenter.context.container, presenter.maskingView) 42 | } 43 | 44 | func testInstall() { 45 | let drop = Drop(title: "Hello world!") 46 | let delegate = TestAnimatorDelegate() 47 | let presenter = Presenter(drop: drop, delegate: delegate) 48 | presenter.install() 49 | 50 | guard let containerView = presenter.viewController.value?.view else { 51 | XCTFail("Unable to get view") 52 | return 53 | } 54 | 55 | XCTAssertFalse(presenter.maskingView.translatesAutoresizingMaskIntoConstraints) 56 | XCTAssertEqual(containerView.subviews[0], presenter.maskingView) 57 | XCTAssertEqual(presenter.maskingView.superview, containerView) 58 | 59 | let expectedConstraints: [NSLayoutConstraint] = [ 60 | presenter.maskingView.leadingAnchor.constraint(equalTo: containerView.leadingAnchor), 61 | presenter.maskingView.trailingAnchor.constraint(equalTo: containerView.trailingAnchor), 62 | presenter.maskingView.topAnchor.constraint(equalTo: containerView.topAnchor), 63 | presenter.maskingView.bottomAnchor.constraint(equalTo: containerView.bottomAnchor) 64 | ] 65 | 66 | for (actual, expected) in zip(presenter.maskingView.constraints, expectedConstraints) { 67 | XCTAssertEqual(actual, expected) 68 | } 69 | } 70 | 71 | func testShow() { 72 | let drop = Drop(title: "Hello world!") 73 | let delegate = TestAnimatorDelegate() 74 | let presenter = Presenter(drop: drop, delegate: delegate) 75 | 76 | let exp = expectation(description: "Completion called") 77 | presenter.show { _ in 78 | exp.fulfill() 79 | } 80 | waitForExpectations(timeout: 2) 81 | } 82 | 83 | func testHide() { 84 | func hide(animated: Bool) { 85 | let drop = Drop(title: "Hello world!") 86 | let delegate = TestAnimatorDelegate() 87 | let presenter = Presenter(drop: drop, delegate: delegate) 88 | 89 | let exp = expectation(description: "Completion called") 90 | presenter.hide(animated: false) { _ in 91 | if presenter.maskingView.superview == nil && presenter.viewController.value?.window == nil { 92 | exp.fulfill() 93 | } 94 | } 95 | waitForExpectations(timeout: animated ? 2.0 : 0.1) 96 | } 97 | 98 | hide(animated: true) 99 | hide(animated: false) 100 | } 101 | } 102 | #endif 103 | -------------------------------------------------------------------------------- /Tests/TestAnimatorDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Drops 3 | // 4 | // Copyright (c) 2021-Present Omar Albeik - https://github.com/omaralbeik 5 | // 6 | // Permission is hereby granted, free of charge, to any person obtaining a copy 7 | // of this software and associated documentation files (the "Software"), to deal 8 | // in the Software without restriction, including without limitation the rights 9 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | // copies of the Software, and to permit persons to whom the Software is 11 | // furnished to do so, subject to the following conditions: 12 | // 13 | // The above copyright notice and this permission notice shall be included in 14 | // all copies or substantial portions of the Software. 15 | // 16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | // THE SOFTWARE. 23 | 24 | #if os(iOS) 25 | @testable import Drops 26 | 27 | final class TestAnimatorDelegate: AnimatorDelegate { 28 | var didCallHide = false 29 | var didCallPanStarted = false 30 | var didCallPanEnded = false 31 | 32 | func hide(animator: Animator) { 33 | didCallHide = true 34 | } 35 | 36 | func panStarted(animator: Animator) { 37 | didCallPanStarted = true 38 | } 39 | 40 | func panEnded(animator: Animator) { 41 | didCallPanEnded = true 42 | } 43 | } 44 | #endif 45 | -------------------------------------------------------------------------------- /Tests/WeakTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Drops 3 | // 4 | // Copyright (c) 2021-Present Omar Albeik - https://github.com/omaralbeik 5 | // 6 | // Permission is hereby granted, free of charge, to any person obtaining a copy 7 | // of this software and associated documentation files (the "Software"), to deal 8 | // in the Software without restriction, including without limitation the rights 9 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | // copies of the Software, and to permit persons to whom the Software is 11 | // furnished to do so, subject to the following conditions: 12 | // 13 | // The above copyright notice and this permission notice shall be included in 14 | // all copies or substantial portions of the Software. 15 | // 16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | // THE SOFTWARE. 23 | 24 | #if os(iOS) 25 | import XCTest 26 | @testable import Drops 27 | 28 | final class WeakTests: XCTestCase { 29 | func testWeak() { 30 | var instance: TestClass? = TestClass() 31 | let weak = Weak(value: instance) 32 | 33 | XCTAssertEqual(weak.value, instance) 34 | 35 | instance = nil 36 | XCTAssertNil(weak.value) 37 | } 38 | 39 | private final class TestClass: NSObject {} 40 | } 41 | #endif 42 | -------------------------------------------------------------------------------- /Tests/WindowViewControllerTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Drops 3 | // 4 | // Copyright (c) 2021-Present Omar Albeik - https://github.com/omaralbeik 5 | // 6 | // Permission is hereby granted, free of charge, to any person obtaining a copy 7 | // of this software and associated documentation files (the "Software"), to deal 8 | // in the Software without restriction, including without limitation the rights 9 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | // copies of the Software, and to permit persons to whom the Software is 11 | // furnished to do so, subject to the following conditions: 12 | // 13 | // The above copyright notice and this permission notice shall be included in 14 | // all copies or substantial portions of the Software. 15 | // 16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | // THE SOFTWARE. 23 | 24 | #if os(iOS) 25 | import XCTest 26 | @testable import Drops 27 | 28 | final class WindowViewControllerTests: XCTestCase { 29 | func testInitializer() { 30 | let viewController = WindowViewController() 31 | 32 | XCTAssert(viewController.view is PassthroughView) 33 | XCTAssert(viewController.window is PassthroughWindow) 34 | 35 | XCTAssertNil(WindowViewController(coder: NSCoder())) 36 | } 37 | 38 | func testPreferredStatusBarStyle() { 39 | let viewController = WindowViewController() 40 | XCTAssertEqual(viewController.preferredStatusBarStyle, .default) 41 | } 42 | 43 | func testInstall() { 44 | let viewController = WindowViewController() 45 | viewController.install() 46 | 47 | XCTAssertEqual(viewController.window?.frame, UIScreen.main.bounds) 48 | XCTAssertEqual(viewController.window?.isHidden, false) 49 | } 50 | 51 | func testUninstall() { 52 | let viewController = WindowViewController() 53 | viewController.uninstall() 54 | 55 | XCTAssertNil(viewController.window) 56 | } 57 | 58 | func testTopViewController() { 59 | let navController = UINavigationController() 60 | XCTAssertEqual(navController.top, navController.topViewController) 61 | 62 | let splitController = UISplitViewController() 63 | let controller = UIViewController() 64 | splitController.viewControllers = [controller] 65 | XCTAssertEqual(splitController.top, controller) 66 | 67 | let tabBarController = UITabBarController() 68 | tabBarController.viewControllers = [controller] 69 | tabBarController.selectedIndex = 0 70 | XCTAssertEqual(tabBarController.top, controller) 71 | 72 | XCTAssertEqual(controller.top, controller) 73 | } 74 | } 75 | #endif 76 | -------------------------------------------------------------------------------- /UIKitExample/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Drops 3 | // 4 | // Copyright (c) 2021-Present Omar Albeik - https://github.com/omaralbeik 5 | // 6 | // Permission is hereby granted, free of charge, to any person obtaining a copy 7 | // of this software and associated documentation files (the "Software"), to deal 8 | // in the Software without restriction, including without limitation the rights 9 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | // copies of the Software, and to permit persons to whom the Software is 11 | // furnished to do so, subject to the following conditions: 12 | // 13 | // The above copyright notice and this permission notice shall be included in 14 | // all copies or substantial portions of the Software. 15 | // 16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | // THE SOFTWARE. 23 | 24 | import UIKit 25 | 26 | @main 27 | final class AppDelegate: UIResponder, UIApplicationDelegate { 28 | func application( 29 | _ application: UIApplication, 30 | configurationForConnecting connectingSceneSession: UISceneSession, 31 | options: UIScene.ConnectionOptions 32 | ) -> UISceneConfiguration { 33 | return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role) 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /UIKitExample/Assets.xcassets/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "idiom" : "universal" 5 | } 6 | ], 7 | "info" : { 8 | "author" : "xcode", 9 | "version" : 1 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /UIKitExample/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "scale" : "2x", 6 | "size" : "20x20" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "scale" : "3x", 11 | "size" : "20x20" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "scale" : "2x", 16 | "size" : "29x29" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "scale" : "3x", 21 | "size" : "29x29" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "scale" : "2x", 26 | "size" : "40x40" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "scale" : "3x", 31 | "size" : "40x40" 32 | }, 33 | { 34 | "idiom" : "iphone", 35 | "scale" : "2x", 36 | "size" : "60x60" 37 | }, 38 | { 39 | "idiom" : "iphone", 40 | "scale" : "3x", 41 | "size" : "60x60" 42 | }, 43 | { 44 | "idiom" : "ipad", 45 | "scale" : "1x", 46 | "size" : "20x20" 47 | }, 48 | { 49 | "idiom" : "ipad", 50 | "scale" : "2x", 51 | "size" : "20x20" 52 | }, 53 | { 54 | "idiom" : "ipad", 55 | "scale" : "1x", 56 | "size" : "29x29" 57 | }, 58 | { 59 | "idiom" : "ipad", 60 | "scale" : "2x", 61 | "size" : "29x29" 62 | }, 63 | { 64 | "idiom" : "ipad", 65 | "scale" : "1x", 66 | "size" : "40x40" 67 | }, 68 | { 69 | "idiom" : "ipad", 70 | "scale" : "2x", 71 | "size" : "40x40" 72 | }, 73 | { 74 | "idiom" : "ipad", 75 | "scale" : "1x", 76 | "size" : "76x76" 77 | }, 78 | { 79 | "idiom" : "ipad", 80 | "scale" : "2x", 81 | "size" : "76x76" 82 | }, 83 | { 84 | "idiom" : "ipad", 85 | "scale" : "2x", 86 | "size" : "83.5x83.5" 87 | }, 88 | { 89 | "idiom" : "ios-marketing", 90 | "scale" : "1x", 91 | "size" : "1024x1024" 92 | } 93 | ], 94 | "info" : { 95 | "author" : "xcode", 96 | "version" : 1 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /UIKitExample/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /UIKitExample/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /UIKitExample/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | LSRequiresIPhoneOS 22 | 23 | UIApplicationSceneManifest 24 | 25 | UIApplicationSupportsMultipleScenes 26 | 27 | UISceneConfigurations 28 | 29 | UIWindowSceneSessionRoleApplication 30 | 31 | 32 | UISceneConfigurationName 33 | Default Configuration 34 | UISceneDelegateClassName 35 | $(PRODUCT_MODULE_NAME).SceneDelegate 36 | 37 | 38 | 39 | 40 | UIApplicationSupportsIndirectInputEvents 41 | 42 | UILaunchStoryboardName 43 | LaunchScreen 44 | UIRequiredDeviceCapabilities 45 | 46 | armv7 47 | 48 | UISupportedInterfaceOrientations 49 | 50 | UIInterfaceOrientationPortrait 51 | 52 | UISupportedInterfaceOrientations~ipad 53 | 54 | UIInterfaceOrientationPortrait 55 | UIInterfaceOrientationPortraitUpsideDown 56 | UIInterfaceOrientationLandscapeLeft 57 | UIInterfaceOrientationLandscapeRight 58 | 59 | 60 | 61 | -------------------------------------------------------------------------------- /UIKitExample/SceneDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Drops 3 | // 4 | // Copyright (c) 2021-Present Omar Albeik - https://github.com/omaralbeik 5 | // 6 | // Permission is hereby granted, free of charge, to any person obtaining a copy 7 | // of this software and associated documentation files (the "Software"), to deal 8 | // in the Software without restriction, including without limitation the rights 9 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | // copies of the Software, and to permit persons to whom the Software is 11 | // furnished to do so, subject to the following conditions: 12 | // 13 | // The above copyright notice and this permission notice shall be included in 14 | // all copies or substantial portions of the Software. 15 | // 16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | // THE SOFTWARE. 23 | 24 | import UIKit 25 | 26 | final class SceneDelegate: UIResponder, UIWindowSceneDelegate { 27 | var window: UIWindow? 28 | 29 | func scene( 30 | _ scene: UIScene, 31 | willConnectTo session: UISceneSession, 32 | options connectionOptions: UIScene.ConnectionOptions 33 | ) { 34 | guard let scene = (scene as? UIWindowScene) else { return } 35 | let window = UIWindow(windowScene: scene) 36 | window.rootViewController = ViewController() 37 | window.makeKeyAndVisible() 38 | self.window = window 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /UIKitExample/UIKitExample.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.app-sandbox 6 | 7 | com.apple.security.network.client 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /UIKitExample/ViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Drops 3 | // 4 | // Copyright (c) 2021-Present Omar Albeik - https://github.com/omaralbeik 5 | // 6 | // Permission is hereby granted, free of charge, to any person obtaining a copy 7 | // of this software and associated documentation files (the "Software"), to deal 8 | // in the Software without restriction, including without limitation the rights 9 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | // copies of the Software, and to permit persons to whom the Software is 11 | // furnished to do so, subject to the following conditions: 12 | // 13 | // The above copyright notice and this permission notice shall be included in 14 | // all copies or substantial portions of the Software. 15 | // 16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | // THE SOFTWARE. 23 | 24 | import UIKit 25 | import Drops 26 | 27 | final class ViewController: UIViewController { 28 | override func viewDidLoad() { 29 | super.viewDidLoad() 30 | 31 | view.backgroundColor = .secondarySystemBackground 32 | let safeArea = view.safeAreaLayoutGuide 33 | 34 | view.addSubview(fieldsStackView) 35 | NSLayoutConstraint.activate([ 36 | fieldsStackView.leadingAnchor.constraint(equalTo: safeArea.leadingAnchor, constant: 20), 37 | fieldsStackView.topAnchor.constraint(equalTo: safeArea.topAnchor, constant: 80), 38 | fieldsStackView.trailingAnchor.constraint(equalTo: safeArea.trailingAnchor, constant: -20) 39 | ]) 40 | 41 | view.addSubview(showButton) 42 | NSLayoutConstraint.activate([ 43 | showButton.leadingAnchor.constraint(equalTo: safeArea.leadingAnchor, constant: 20), 44 | showButton.trailingAnchor.constraint(equalTo: safeArea.trailingAnchor, constant: -20), 45 | showButton.bottomAnchor.constraint(equalTo: safeArea.bottomAnchor, constant: -20) 46 | ]) 47 | 48 | let tap = UITapGestureRecognizer(target: self, action: #selector(didTap)) 49 | view.addGestureRecognizer(tap) 50 | } 51 | 52 | private lazy var titleLabel: UILabel = { 53 | let label = UILabel() 54 | label.translatesAutoresizingMaskIntoConstraints = false 55 | label.text = "Title" 56 | label.font = .preferredFont(forTextStyle: .caption1) 57 | return label 58 | }() 59 | 60 | private lazy var titleTextField: UITextField = { 61 | let field = UITextField() 62 | field.translatesAutoresizingMaskIntoConstraints = false 63 | field.borderStyle = .roundedRect 64 | field.placeholder = "Title" 65 | field.text = "Hello There!" 66 | return field 67 | }() 68 | 69 | private lazy var subtitleLabel: UILabel = { 70 | let label = UILabel() 71 | label.translatesAutoresizingMaskIntoConstraints = false 72 | label.text = "Optional Subtitle" 73 | label.font = .preferredFont(forTextStyle: .caption1) 74 | return label 75 | }() 76 | 77 | private lazy var subtitleTextField: UITextField = { 78 | let field = UITextField() 79 | field.translatesAutoresizingMaskIntoConstraints = false 80 | field.borderStyle = .roundedRect 81 | field.placeholder = "Subtitle" 82 | field.text = "Use Drops to show alerts" 83 | return field 84 | }() 85 | 86 | private lazy var positionLabel: UILabel = { 87 | let label = UILabel() 88 | label.translatesAutoresizingMaskIntoConstraints = false 89 | label.text = "Position" 90 | label.font = .preferredFont(forTextStyle: .caption1) 91 | return label 92 | }() 93 | 94 | private lazy var positionSegmentedControl: UISegmentedControl = { 95 | let control = UISegmentedControl(items: ["Top", "Bottom"]) 96 | control.translatesAutoresizingMaskIntoConstraints = false 97 | control.selectedSegmentIndex = 0 98 | return control 99 | }() 100 | 101 | private lazy var durationLabel: UILabel = { 102 | let label = UILabel() 103 | label.translatesAutoresizingMaskIntoConstraints = false 104 | label.text = "Duration" 105 | label.font = .preferredFont(forTextStyle: .caption1) 106 | return label 107 | }() 108 | 109 | private lazy var durationSlider: UISlider = { 110 | let slider = UISlider() 111 | slider.translatesAutoresizingMaskIntoConstraints = false 112 | slider.minimumValue = 0.1 113 | slider.maximumValue = 10.0 114 | slider.value = 2.0 115 | slider.addTarget(self, action: #selector(didChangeDuration), for: .valueChanged) 116 | return slider 117 | }() 118 | 119 | private lazy var iconLabel: UILabel = { 120 | let label = UILabel() 121 | label.translatesAutoresizingMaskIntoConstraints = false 122 | label.text = "Icon" 123 | label.font = .preferredFont(forTextStyle: .caption1) 124 | return label 125 | }() 126 | 127 | private lazy var iconSwitch: UISwitch = { 128 | let control = UISwitch() 129 | control.translatesAutoresizingMaskIntoConstraints = false 130 | return control 131 | }() 132 | 133 | private lazy var buttonLabel: UILabel = { 134 | let label = UILabel() 135 | label.translatesAutoresizingMaskIntoConstraints = false 136 | label.text = "Button" 137 | label.font = .preferredFont(forTextStyle: .caption1) 138 | return label 139 | }() 140 | 141 | private lazy var buttonSwitch: UISwitch = { 142 | let control = UISwitch() 143 | control.translatesAutoresizingMaskIntoConstraints = false 144 | return control 145 | }() 146 | 147 | private lazy var fieldsStackView: UIStackView = { 148 | let views: [UIView] = [ 149 | titleLabel, titleTextField, 150 | subtitleLabel, subtitleTextField, 151 | iconLabel, iconSwitch, 152 | buttonLabel, buttonSwitch, 153 | positionLabel, positionSegmentedControl, 154 | durationLabel, durationSlider 155 | ] 156 | 157 | let view = UIStackView(arrangedSubviews: views) 158 | view.translatesAutoresizingMaskIntoConstraints = false 159 | view.axis = .vertical 160 | view.alignment = .fill 161 | view.distribution = .fill 162 | view.spacing = 5 163 | view.setCustomSpacing(20, after: titleTextField) 164 | view.setCustomSpacing(20, after: subtitleTextField) 165 | view.setCustomSpacing(20, after: iconSwitch) 166 | view.setCustomSpacing(20, after: buttonSwitch) 167 | view.setCustomSpacing(20, after: positionSegmentedControl) 168 | return view 169 | }() 170 | 171 | private lazy var showButton: UIButton = { 172 | let button = UIButton(type: .system) 173 | button.translatesAutoresizingMaskIntoConstraints = false 174 | button.setTitle("Show Drop", for: .normal) 175 | button.backgroundColor = .systemBlue 176 | button.tintColor = .white 177 | button.contentEdgeInsets = .init(top: 10, left: 20, bottom: 10, right: 20) 178 | button.layer.cornerRadius = 7.5 179 | button.addTarget(self, action: #selector(didTapShowButton), for: .touchUpInside) 180 | return button 181 | }() 182 | 183 | @objc 184 | private func didTapShowButton() { 185 | view.endEditing(true) 186 | 187 | let title = titleTextField.text?.trimmingCharacters(in: .whitespacesAndNewlines) ?? "" 188 | let subtitle = subtitleTextField.text?.trimmingCharacters(in: .whitespacesAndNewlines) 189 | let position: Drop.Position = positionSegmentedControl.selectedSegmentIndex == 0 ? .top : .bottom 190 | let duration = TimeInterval(durationSlider.value) 191 | 192 | let icon = iconSwitch.isOn ? UIImage(systemName: "star.fill") : nil 193 | let buttonIcon = buttonSwitch.isOn ? UIImage(systemName: "arrowshape.turn.up.left") : nil 194 | 195 | let drop = Drop( 196 | title: title, 197 | subtitle: subtitle, 198 | icon: icon, 199 | action: .init(icon: buttonIcon, handler: { 200 | print("Drop tapped") 201 | Drops.hideCurrent() 202 | }), 203 | position: position, 204 | duration: .seconds(duration) 205 | ) 206 | Drops.show(drop) 207 | } 208 | 209 | @objc 210 | private func didTap() { 211 | view.endEditing(true) 212 | } 213 | 214 | @objc func didChangeDuration(slider: UISlider) { 215 | let duration = slider.value 216 | durationLabel.text = "Duration (\(String(format: "%.1f", duration)) s)" 217 | } 218 | } 219 | -------------------------------------------------------------------------------- /docs/Drop/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Drops - Drop 7 | 8 | 9 | 10 |
11 | 12 | 13 | Drops 14 | 15 | Documentation 16 | 17 |
18 | 19 | 24 | 25 | 31 | 32 |
33 |
34 |

35 | Structure 36 | Drop 37 |

38 | 39 |
40 |
@available(iOSApplicationExtension, unavailable)
 41 | public struct Drop: ExpressibleByStringLiteral  
42 |
43 |
44 |

An object representing a drop.

45 | 46 |
47 |
48 | 49 |
50 | 51 | 53 | 55 | 56 | 58 | 59 | 60 | 61 | 62 | Drop 63 | 64 | 65 | Drop 66 | 67 | 68 | 69 | 70 | 71 | ExpressibleByStringLiteral 72 | 73 | ExpressibleByStringLiteral 74 | 75 | 76 | 77 | Drop->ExpressibleByStringLiteral 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 |
87 |

Nested Types

88 |
89 |
Drop.Position
90 |

An enum representing drop presentation position.

91 |
92 |
Drop.Duration
93 |

An enum representing a drop duration on screen.

94 |
95 |
Drop.Action
96 |

An object representing a drop action.

97 |
98 |
Drop.Accessibility
99 |

An object representing accessibility options.

100 |
101 |
102 |

Conforms To

103 |
104 |
ExpressibleByStringLiteral
105 |
106 |
107 |
108 |

Initializers

109 | 110 |
111 |

112 | init(title:​title​Number​OfLines:​subtitle:​subtitle​Number​OfLines:​icon:​action:​position:​duration:​accessibility:​) 113 |

114 |
115 |
public init(
116 |         title: String,
117 |         titleNumberOfLines: Int = 1,
118 |         subtitle: String? = nil,
119 |         subtitleNumberOfLines: Int = 1,
120 |         icon: UIImage? = nil,
121 |         action: Action? = nil,
122 |         position: Position = .top,
123 |         duration: Duration = .recommended,
124 |         accessibility: Accessibility? = nil
125 |     )  
126 |
127 |
128 |

Create a new drop.

129 | 130 |
131 |

Parameters

132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 147 | 148 | 149 | 150 | 151 | 153 | 154 | 155 | 156 | 157 | 159 | 160 | 161 | 162 | 163 | 165 | 166 | 167 | 168 | 169 | 171 | 172 | 173 | 174 | 175 | 177 | 178 | 179 | 180 | 181 | 183 | 184 | 185 | 186 | 187 | 189 | 190 | 191 | 192 | 193 | 195 | 196 | 197 |
titleString

Title.

146 |
title​Number​OfLinesInt

Maximum number of lines that title can occupy. Defaults to 1. A value of 0 means no limit.

152 |
subtitleString?

Optional subtitle. Defaults to nil.

158 |
subtitle​Number​OfLinesInt

Maximum number of lines that subtitle can occupy. Defaults to 1. A value of 0 means no limit.

164 |
iconUIImage?

Optional icon.

170 |
actionAction?

Optional action.

176 |
positionPosition

Position. Defaults to Drop.Position.top.

182 |
durationDuration

Duration. Defaults to Drop.Duration.recommended.

188 |
accessibilityAccessibility?

Accessibility options. Defaults to nil which will use "title, subtitle" as its message.

194 |
198 |
199 |
200 |

201 | init(string​Literal:​) 202 |

203 |
204 |
public init(stringLiteral title: String)  
205 |
206 |
207 |

Create a new accessibility object.

208 | 209 |
210 |

Parameters

211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | 226 | 227 | 228 |
message

Message to be announced when the drop is shown. Defaults to drop's "title, subtitle"

225 |
229 |
230 |
231 |
232 |

Properties

233 | 234 |
235 |

236 | title 237 |

238 |
239 |
public var title: String
240 |
241 |
242 |

Title.

243 | 244 |
245 |
246 |
247 |

248 | title​Number​OfLines 249 |

250 |
251 |
public var titleNumberOfLines: Int
252 |
253 |
254 |

Maximum number of lines that title can occupy. Defaults to 1. A value of 0 means no limit.

255 | 256 |
257 |
258 |
259 |

260 | subtitle 261 |

262 |
263 |
public var subtitle: String? 
264 |
265 |
266 |

Subtitle.

267 | 268 |
269 |
270 |
271 |

272 | subtitle​Number​OfLines 273 |

274 |
275 |
public var subtitleNumberOfLines: Int
276 |
277 |
278 |

Maximum number of lines that subtitle can occupy. Defaults to 1. A value of 0 means no limit.

279 | 280 |
281 |
282 |
283 |

284 | icon 285 |

286 |
287 |
public var icon: UIImage? 
288 |
289 |
290 |

Icon.

291 | 292 |
293 |
294 |
295 |

296 | action 297 |

298 |
299 |
public var action: Action? 
300 |
301 |
302 |

Action.

303 | 304 |
305 |
306 |
307 |

308 | position 309 |

310 |
311 |
public var position: Position
312 |
313 |
314 |

Position.

315 | 316 |
317 |
318 |
319 |

320 | duration 321 |

322 |
323 |
public var duration: Duration
324 |
325 |
326 |

Duration.

327 | 328 |
329 |
330 |
331 |

332 | accessibility 333 |

334 |
335 |
public var accessibility: Accessibility
336 |
337 |
338 |

Accessibility.

339 | 340 |
341 |
342 |
343 | 344 | 345 | 346 |
347 |
348 | 349 |
350 |

351 | Generated on using swift-doc 1.0.0-rc.1. 352 |

353 |
354 | 355 | 356 | -------------------------------------------------------------------------------- /docs/Drop_Accessibility/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Drops - Drop.Accessibility 7 | 8 | 9 | 10 |
11 | 12 | 13 | Drops 14 | 15 | Documentation 16 | 17 |
18 | 19 | 24 | 25 | 31 | 32 |
33 |
34 |

35 | Structure 36 | Drop.​Accessibility 37 |

38 | 39 |
40 |
public struct Accessibility: ExpressibleByStringLiteral  
41 |
42 |
43 |

An object representing accessibility options.

44 | 45 |
46 |
47 | 48 |
49 | 50 | 52 | 54 | 55 | 57 | 58 | 59 | 60 | 61 | Drop.Accessibility 62 | 63 | 64 | Drop.Accessibility 65 | 66 | 67 | 68 | 69 | 70 | ExpressibleByStringLiteral 71 | 72 | ExpressibleByStringLiteral 73 | 74 | 75 | 76 | Drop.Accessibility->ExpressibleByStringLiteral 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 |
86 |

Member Of

87 |
88 |
Drop
89 |

An object representing a drop.

90 |
91 |
92 |

Conforms To

93 |
94 |
ExpressibleByStringLiteral
95 |
96 |
97 |
98 |

Initializers

99 | 100 |
101 |

102 | init(message:​) 103 |

104 |
105 |
public init(message: String)  
106 |
107 |
108 |

Create a new accessibility object.

109 | 110 |
111 |

Parameters

112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 127 | 128 | 129 |
messageString

Message to be announced when the drop is shown. Defaults to drop's "title, subtitle"

126 |
130 |
131 |
132 |

133 | init(string​Literal:​) 134 |

135 |
136 |
public init(stringLiteral message: String)  
137 |
138 |
139 |

Create a new accessibility object.

140 | 141 |
142 |

Parameters

143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 158 | 159 | 160 |
messageString

Message to be announced when the drop is shown. Defaults to drop's "title, subtitle"

157 |
161 |
162 |
163 |
164 |

Properties

165 | 166 |
167 |

168 | message 169 |

170 |
171 |
public let message: String
172 |
173 |
174 |

Accessibility message to be announced when the drop is shown.

175 | 176 |
177 |
178 |
179 | 180 | 181 | 182 |
183 |
184 | 185 |
186 |

187 | Generated on using swift-doc 1.0.0-rc.1. 188 |

189 |
190 | 191 | 192 | -------------------------------------------------------------------------------- /docs/Drop_Action/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Drops - Drop.Action 7 | 8 | 9 | 10 |
11 | 12 | 13 | Drops 14 | 15 | Documentation 16 | 17 |
18 | 19 | 24 | 25 | 31 | 32 |
33 |
34 |

35 | Structure 36 | Drop.​Action 37 |

38 | 39 |
40 |
public struct Action  
41 |
42 |
43 |

An object representing a drop action.

44 | 45 |
46 |
47 | 48 | 49 |

Member Of

50 |
51 |
Drop
52 |

An object representing a drop.

53 |
54 |
55 |
56 |
57 |

Initializers

58 | 59 |
60 |

61 | init(icon:​handler:​) 62 |

63 |
64 |
public init(icon: UIImage? = nil, handler: @escaping () -> Void)  
65 |
66 |
67 |

Create a new action.

68 | 69 |
70 |

Parameters

71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 86 | 87 | 88 | 89 | 90 | 92 | 93 | 94 |
iconUIImage?

Optional icon image.

85 |
handler@escaping () -> Void

Handler to be called when the drop is tapped.

91 |
95 |
96 |
97 |
98 |

Properties

99 | 100 |
101 |

102 | icon 103 |

104 |
105 |
public var icon: UIImage? 
106 |
107 |
108 |

Icon.

109 | 110 |
111 |
112 |
113 |

114 | handler 115 |

116 |
117 |
public var handler: () -> Void
118 |
119 |
120 |

Handler.

121 | 122 |
123 |
124 |
125 | 126 | 127 | 128 |
129 |
130 | 131 |
132 |

133 | Generated on using swift-doc 1.0.0-rc.1. 134 |

135 |
136 | 137 | 138 | -------------------------------------------------------------------------------- /docs/Drop_Duration/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Drops - Drop.Duration 7 | 8 | 9 | 10 |
11 | 12 | 13 | Drops 14 | 15 | Documentation 16 | 17 |
18 | 19 | 24 | 25 | 31 | 32 |
33 |
34 |

35 | Enumeration 36 | Drop.​Duration 37 |

38 | 39 |
40 |
public enum Duration: Equatable, ExpressibleByFloatLiteral  
41 |
42 |
43 |

An enum representing a drop duration on screen.

44 | 45 |
46 |
47 | 48 |
49 | 50 | 52 | 54 | 55 | 57 | 58 | 59 | 60 | 61 | Drop.Duration 62 | 63 | 64 | Drop.Duration 65 | 66 | 67 | 68 | 69 | 70 | Equatable 71 | 72 | Equatable 73 | 74 | 75 | 76 | Drop.Duration->Equatable 77 | 78 | 79 | 80 | 81 | 82 | ExpressibleByFloatLiteral 83 | 84 | ExpressibleByFloatLiteral 85 | 86 | 87 | 88 | Drop.Duration->ExpressibleByFloatLiteral 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 |
98 |

Member Of

99 |
100 |
Drop
101 |

An object representing a drop.

102 |
103 |
104 |

Conforms To

105 |
106 |
Equatable
107 |
ExpressibleByFloatLiteral
108 |
109 |
110 |
111 |

Initializers

112 | 113 |
114 |

115 | init(float​Literal:​) 116 |

117 |
118 |
public init(floatLiteral value: TimeInterval)  
119 |
120 |
121 |

Create a new duration object.

122 | 123 |
124 |

Parameters

125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 140 | 141 | 142 |
valueTime​Interval

Duration in seconds

139 |
143 |
144 |
145 |
146 |

Enumeration Cases

147 | 148 |
149 |

150 | recommended 151 |

152 |
153 |
case recommended
154 |
155 |
156 |

Hides the drop after 2.0 seconds.

157 | 158 |
159 |
160 |
161 |

162 | seconds 163 |

164 |
165 |
case seconds(TimeInterval) 
166 |
167 |
168 |

Hides the drop after the specified number of seconds.

169 | 170 |
171 |
172 |
173 | 174 | 175 | 176 |
177 |
178 | 179 |
180 |

181 | Generated on using swift-doc 1.0.0-rc.1. 182 |

183 |
184 | 185 | 186 | -------------------------------------------------------------------------------- /docs/Drop_Position/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Drops - Drop.Position 7 | 8 | 9 | 10 |
11 | 12 | 13 | Drops 14 | 15 | Documentation 16 | 17 |
18 | 19 | 24 | 25 | 31 | 32 |
33 |
34 |

35 | Enumeration 36 | Drop.​Position 37 |

38 | 39 |
40 |
public enum Position: Equatable  
41 |
42 |
43 |

An enum representing drop presentation position.

44 | 45 |
46 |
47 | 48 |
49 | 50 | 52 | 54 | 55 | 57 | 58 | 59 | 60 | 61 | Drop.Position 62 | 63 | 64 | Drop.Position 65 | 66 | 67 | 68 | 69 | 70 | Equatable 71 | 72 | Equatable 73 | 74 | 75 | 76 | Drop.Position->Equatable 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 |
86 |

Member Of

87 |
88 |
Drop
89 |

An object representing a drop.

90 |
91 |
92 |

Conforms To

93 |
94 |
Equatable
95 |
96 |
97 |
98 |

Enumeration Cases

99 | 100 |
101 |

102 | top 103 |

104 |
105 |
case top
106 |
107 |
108 |

Drop is presented from top.

109 | 110 |
111 |
112 |
113 |

114 | bottom 115 |

116 |
117 |
case bottom
118 |
119 |
120 |

Drop is presented from bottom.

121 | 122 |
123 |
124 |
125 | 126 | 127 | 128 |
129 |
130 | 131 |
132 |

133 | Generated on using swift-doc 1.0.0-rc.1. 134 |

135 |
136 | 137 | 138 | -------------------------------------------------------------------------------- /docs/Drops/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Drops - Drops 7 | 8 | 9 | 10 |
11 | 12 | 13 | Drops 14 | 15 | Documentation 16 | 17 |
18 | 19 | 24 | 25 | 31 | 32 |
33 |
34 |

35 | Class 36 | Drops 37 |

38 | 39 |
40 |
@available(iOSApplicationExtension, unavailable)
 41 | public final class Drops  
42 |
43 |
44 |

A shared class used to show and hide drops.

45 | 46 |
47 | 48 |
49 |

Nested Type Aliases

50 | 51 |
52 |

53 | Drop​Handler 54 |

55 |
56 |
public typealias DropHandler = (Drop) -> Void
57 |
58 |
59 |

Handler.

60 | 61 |
62 |
63 |
64 |
65 |

Initializers

66 | 67 |
68 |

69 | init(delay​Between​Drops:​) 70 |

71 |
72 |
public init(delayBetweenDrops: TimeInterval = 0.5)  
73 |
74 |
75 |

Create a new instance with a custom delay between drops.

76 | 77 |
78 |

Parameters

79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 94 | 95 | 96 |
delay​Between​DropsTime​Interval

Delay between drops in seconds. Defaults to 0.5 seconds.

93 |
97 |
98 |
99 |
100 |

Properties

101 | 102 |
103 |

104 | will​Show​Drop 105 |

106 |
107 |
public static var willShowDrop: DropHandler?  
108 |
109 |
110 |

A handler to be called before a drop is presented.

111 | 112 |
113 |
114 |
115 |

116 | did​Show​Drop 117 |

118 |
119 |
public static var didShowDrop: DropHandler?  
120 |
121 |
122 |

A handler to be called after a drop is presented.

123 | 124 |
125 |
126 |
127 |

128 | will​Dismiss​Drop 129 |

130 |
131 |
public static var willDismissDrop: DropHandler?  
132 |
133 |
134 |

A handler to be called before a drop is dismissed.

135 | 136 |
137 |
138 |
139 |

140 | did​Dismiss​Drop 141 |

142 |
143 |
public static var didDismissDrop: DropHandler?  
144 |
145 |
146 |

A handler to be called after a drop is dismissed.

147 | 148 |
149 |
150 |
151 |

152 | will​Show​Drop 153 |

154 |
155 |
public var willShowDrop: DropHandler? 
156 |
157 |
158 |

A handler to be called before a drop is presented.

159 | 160 |
161 |
162 |
163 |

164 | did​Show​Drop 165 |

166 |
167 |
public var didShowDrop: DropHandler? 
168 |
169 |
170 |

A handler to be called after a drop is presented.

171 | 172 |
173 |
174 |
175 |

176 | will​Dismiss​Drop 177 |

178 |
179 |
public var willDismissDrop: DropHandler? 
180 |
181 |
182 |

A handler to be called before a drop is dismissed.

183 | 184 |
185 |
186 |
187 |

188 | did​Dismiss​Drop 189 |

190 |
191 |
public var didDismissDrop: DropHandler? 
192 |
193 |
194 |

A handler to be called after a drop is dismissed.

195 | 196 |
197 |
198 |
199 |
200 |

Methods

201 | 202 |
203 |

204 | show(_:​) 205 |

206 |
207 |
public static func show(_ drop: Drop)  
208 |
209 |
210 |

Show a drop.

211 | 212 |
213 |

Parameters

214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | 229 | 230 | 231 |
dropDrop

Drop to show.

228 |
232 |
233 |
234 |

235 | hide​Current() 236 |

237 |
238 |
public static func hideCurrent()  
239 |
240 |
241 |

Hide currently shown drop.

242 | 243 |
244 |
245 |
246 |

247 | hide​All() 248 |

249 |
250 |
public static func hideAll()  
251 |
252 |
253 |

Hide all drops.

254 | 255 |
256 |
257 |
258 |

259 | show(_:​) 260 |

261 |
262 |
public func show(_ drop: Drop)  
263 |
264 |
265 |

Show a drop.

266 | 267 |
268 |

Parameters

269 | 270 | 271 | 272 | 273 | 274 | 275 | 276 | 277 | 278 | 279 | 280 | 281 | 282 | 284 | 285 | 286 |
dropDrop

Drop to show.

283 |
287 |
288 |
289 |

290 | hide​Current() 291 |

292 |
293 |
public func hideCurrent()  
294 |
295 |
296 |

Hide currently shown drop.

297 | 298 |
299 |
300 |
301 |

302 | hide​All() 303 |

304 |
305 |
public func hideAll()  
306 |
307 |
308 |

Hide all drops.

309 | 310 |
311 |
312 |
313 | 314 | 315 | 316 |
317 |
318 | 319 |
320 |

321 | Generated on using swift-doc 1.0.0-rc.1. 322 |

323 |
324 | 325 | 326 | -------------------------------------------------------------------------------- /docs/all.css: -------------------------------------------------------------------------------- 1 | :root{--system-red:#ff3b30;--system-orange:#ff9500;--system-yellow:#fc0;--system-green:#34c759;--system-teal:#5ac8fa;--system-blue:#007aff;--system-indigo:#5856d6;--system-purple:#af52de;--system-pink:#ff2d55;--system-gray:#8e8e93;--system-gray2:#aeaeb2;--system-gray3:#c7c7cc;--system-gray4:#d1d1d6;--system-gray5:#e5e5ea;--system-gray6:#f2f2f7;--label:#000;--secondary-label:#3c3c43;--tertiary-label:#48484a;--quaternary-label:#636366;--placeholder-text:#8e8e93;--link:#007aff;--separator:#e5e5ea;--opaque-separator:#c6c6c8;--system-fill:#787880;--secondary-system-fill:#787880;--tertiary-system-fill:#767680;--quaternary-system-fill:#747480;--system-background:#fff;--secondary-system-background:#f2f2f7;--tertiary-system-background:#fff;--system-grouped-background:#f2f2f7;--secondary-system-grouped-background:#fff;--tertiary-system-grouped-background:#f2f2f7}@supports (color:color(display-p3 1 1 1)){:root{--system-red:color(display-p3 1 0.2314 0.1882);--system-orange:color(display-p3 1 0.5843 0);--system-yellow:color(display-p3 1 0.8 0);--system-green:color(display-p3 0.2039 0.7804 0.349);--system-teal:color(display-p3 0.3529 0.7843 0.9804);--system-blue:color(display-p3 0 0.4784 1);--system-indigo:color(display-p3 0.3451 0.3373 0.8392);--system-purple:color(display-p3 0.6863 0.3216 0.8706);--system-pink:color(display-p3 1 0.1765 0.3333);--system-gray:color(display-p3 0.5569 0.5569 0.5765);--system-gray2:color(display-p3 0.6824 0.6824 0.698);--system-gray3:color(display-p3 0.7804 0.7804 0.8);--system-gray4:color(display-p3 0.8196 0.8196 0.8392);--system-gray5:color(display-p3 0.898 0.898 0.9176);--system-gray6:color(display-p3 0.949 0.949 0.9686);--label:color(display-p3 0 0 0);--secondary-label:color(display-p3 0.2353 0.2353 0.2627);--tertiary-label:color(display-p3 0.2823 0.2823 0.2901);--quaternary-label:color(display-p3 0.4627 0.4627 0.5019);--placeholder-text:color(display-p3 0.5568 0.5568 0.5764);--link:color(display-p3 0 0.4784 1);--separator:color(display-p3 0.898 0.898 0.9176);--opaque-separator:color(display-p3 0.7765 0.7765 0.7843);--system-fill:color(display-p3 0.4706 0.4706 0.502);--secondary-system-fill:color(display-p3 0.4706 0.4706 0.502);--tertiary-system-fill:color(display-p3 0.4627 0.4627 0.502);--quaternary-system-fill:color(display-p3 0.4549 0.4549 0.502);--system-background:color(display-p3 1 1 1);--secondary-system-background:color(display-p3 0.949 0.949 0.9686);--tertiary-system-background:color(display-p3 1 1 1);--system-grouped-background:color(display-p3 0.949 0.949 0.9686);--secondary-system-grouped-background:color(display-p3 1 1 1);--tertiary-system-grouped-background:color(display-p3 0.949 0.949 0.9686)}}:root{--large-title:600 32pt/39pt sans-serif;--title-1:600 26pt/32pt sans-serif;--title-2:600 20pt/25pt sans-serif;--title-3:500 18pt/23pt sans-serif;--headline:500 15pt/20pt sans-serif;--body:300 15pt/20pt sans-serif;--callout:300 14pt/19pt sans-serif;--subhead:300 13pt/18pt sans-serif;--footnote:300 12pt/16pt sans-serif;--caption-1:300 11pt/13pt sans-serif;--caption-2:300 11pt/13pt sans-serif;--icon-associatedtype:url("data:image/svg+xml;charset=utf-8,%3Csvg viewBox='0 0 100 100' xmlns='http://www.w3.org/2000/svg'%3E%3Crect fill='%23ff6682' height='90' rx='8' stroke='%23ff2d55' stroke-miterlimit='10' stroke-width='4' width='90' x='5' y='5'/%3E%3Cpath d='M42 81.71V31.3H24.47v-13h51.06v13H58v50.41z' fill='%23fff'/%3E%3C/svg%3E");--icon-case:url("data:image/svg+xml;charset=utf-8,%3Csvg viewBox='0 0 100 100' xmlns='http://www.w3.org/2000/svg'%3E%3Crect fill='%2389c5e6' height='90' rx='8' stroke='%236bb7e1' stroke-miterlimit='10' stroke-width='4' width='90' x='5' y='5'/%3E%3Cpath d='M20.21 50c0-20.7 11.9-32.79 30.8-32.79 16 0 28.21 10.33 28.7 25.32H64.19C63.4 35 58.09 30.11 51 30.11c-8.79 0-14.37 7.52-14.37 19.82s5.54 20 14.41 20c7.08 0 12.22-4.66 13.23-12.09h15.52c-.74 15.07-12.43 25-28.78 25C32 82.81 20.21 70.72 20.21 50z' fill='%23fff'/%3E%3C/svg%3E");--icon-class:url("data:image/svg+xml;utf8,%3Csvg viewBox='0 0 100 100' xmlns='http://www.w3.org/2000/svg'%3E%3Crect fill='%239b98e6' height='90' rx='8' stroke='%235856d6' stroke-miterlimit='10' stroke-width='4' width='90' x='5' y='5'/%3E%3Cpath d='m20.21 50c0-20.7 11.9-32.79 30.8-32.79 16 0 28.21 10.33 28.7 25.32h-15.52c-.79-7.53-6.1-12.42-13.19-12.42-8.79 0-14.37 7.52-14.37 19.82s5.54 20 14.41 20c7.08 0 12.22-4.66 13.23-12.09h15.52c-.74 15.07-12.43 25-28.78 25-19.01-.03-30.8-12.12-30.8-32.84z' fill='%23fff'/%3E%3C/svg%3E");--icon-enumeration:url("data:image/svg+xml;charset=utf-8,%3Csvg viewBox='0 0 100 100' xmlns='http://www.w3.org/2000/svg'%3E%3Crect fill='%23eca95b' height='90' rx='8' stroke='%23e89234' stroke-miterlimit='10' stroke-width='4' width='90' x='5.17' y='5'/%3E%3Cpath d='M71.9 81.71H28.43V18.29H71.9v13H44.56v12.62h25.71v11.87H44.56V68.7H71.9z' fill='%23fff'/%3E%3C/svg%3E");--icon-extension:url("data:image/svg+xml;charset=utf-8,%3Csvg viewBox='0 0 100 100' xmlns='http://www.w3.org/2000/svg'%3E%3Crect fill='%23eca95b' height='90' rx='8' stroke='%23e89234' stroke-miterlimit='10' stroke-width='4' width='90' x='5' y='5'/%3E%3Cg fill='%23fff'%3E%3Cpath d='M54.43 81.93H20.51V18.07h33.92v12.26H32.61v13.8h20.45v11.32H32.61v14.22h21.82zM68.74 74.58h-.27l-2.78 7.35h-7.28L64 69.32l-6-12.54h8l2.74 7.3h.27l2.76-7.3h7.64l-6.14 12.54 5.89 12.61h-7.64z'/%3E%3C/g%3E%3C/svg%3E");--icon-function:url("data:image/svg+xml;charset=utf-8,%3Csvg viewBox='0 0 100 100' xmlns='http://www.w3.org/2000/svg'%3E%3Crect fill='%237ac673' height='90' rx='8' stroke='%235bb74f' stroke-miterlimit='10' stroke-width='4' width='90' x='5' y='5'/%3E%3Cpath d='M24.25 75.66A5.47 5.47 0 0 1 30 69.93c1.55 0 3.55.41 6.46.41 3.19 0 4.78-1.55 5.46-6.65l1.5-10.14h-9.34a6 6 0 1 1 0-12h11.1l1.09-7.27C47.82 23.39 54.28 17.7 64 17.7c6.69 0 11.74 1.77 11.74 6.64A5.47 5.47 0 0 1 70 30.07c-1.55 0-3.55-.41-6.46-.41-3.14 0-4.73 1.51-5.46 6.65l-.78 5.27h11.44a6 6 0 1 1 .05 12H55.6l-1.78 12.11C52.23 76.61 45.72 82.3 36 82.3c-6.7 0-11.75-1.77-11.75-6.64z' fill='%23fff'/%3E%3C/svg%3E");--icon-method:url("data:image/svg+xml;charset=utf-8,%3Csvg viewBox='0 0 100 100' xmlns='http://www.w3.org/2000/svg'%3E%3Crect fill='%235a98f8' height='90' rx='8' stroke='%232974ed' stroke-miterlimit='10' stroke-width='4' width='90' x='5' y='5'/%3E%3Cpath d='M70.61 81.71v-39.6h-.31l-15.69 39.6h-9.22l-15.65-39.6h-.35v39.6H15.2V18.29h18.63l16 41.44h.36l16-41.44H84.8v63.42z' fill='%23fff'/%3E%3C/svg%3E");--icon-operator:url("data:image/svg+xml;charset=utf-8,%3Csvg viewBox='0 0 100 100' xmlns='http://www.w3.org/2000/svg'%3E%3Crect fill='%237ac673' height='90' rx='8' stroke='%235bb74f' stroke-miterlimit='10' stroke-width='4' width='90' x='5' y='5'/%3E%3Ccircle fill='%23fff' cx='50' cy='50' r='16'/%3E%3C/svg%3E");--icon-property:url("data:image/svg+xml;charset=utf-8,%3Csvg viewBox='0 0 100 100' xmlns='http://www.w3.org/2000/svg'%3E%3Crect fill='%2389c5e6' height='90' rx='8' stroke='%236bb7e1' stroke-miterlimit='10' stroke-width='4' width='90' x='5' y='5'/%3E%3Cpath d='M52.31 18.29c13.62 0 22.85 8.84 22.85 22.46s-9.71 22.37-23.82 22.37H41v18.59H24.84V18.29zM41 51h7c6.85 0 10.89-3.56 10.89-10.2S54.81 30.64 48 30.64h-7z' fill='%23fff'/%3E%3C/svg%3E");--icon-protocol:url("data:image/svg+xml;charset=utf-8,%3Csvg viewBox='0 0 100 100' xmlns='http://www.w3.org/2000/svg'%3E%3Crect fill='%23ff6682' height='90' rx='8' stroke='%23ff2d55' stroke-miterlimit='10' stroke-width='4' width='90' x='5' y='5'/%3E%3Cg fill='%23fff'%3E%3Cpath d='M46.28 18.29c11.84 0 20 8.66 20 21.71s-8.44 21.71-20.6 21.71H34.87v20H22.78V18.29zM34.87 51.34H43c6.93 0 11-4 11-11.29S50 28.8 43.07 28.8h-8.2zM62 57.45h8v4.77h.16c.84-3.45 2.54-5.12 5.17-5.12a5.06 5.06 0 0 1 1.92.35V65a5.69 5.69 0 0 0-2.39-.51c-3.08 0-4.66 1.74-4.66 5.12v12.1H62z'/%3E%3C/g%3E%3C/svg%3E");--icon-structure:url("data:image/svg+xml;charset=utf-8,%3Csvg viewBox='0 0 100 100' xmlns='http://www.w3.org/2000/svg'%3E%3Crect fill='%23b57edf' height='90' rx='8' stroke='%239454c2' stroke-miterlimit='10' stroke-width='4' width='90' x='5' y='5'/%3E%3Cpath d='M38.38 63c.74 4.53 5.62 7.16 11.82 7.16s10.37-2.81 10.37-6.68c0-3.51-2.73-5.31-10.24-6.76l-6.5-1.23C31.17 53.14 24.62 47 24.62 37.28c0-12.22 10.59-20.09 25.18-20.09 16 0 25.36 7.83 25.53 19.91h-15c-.26-4.57-4.57-7.29-10.42-7.29s-9.31 2.63-9.31 6.37c0 3.34 2.9 5.18 9.8 6.5l6.5 1.23C70.46 46.51 76.61 52 76.61 62c0 12.74-10 20.83-26.72 20.83-15.82 0-26.28-7.3-26.5-19.78z' fill='%23fff'/%3E%3C/svg%3E");--icon-typealias:url("data:image/svg+xml;charset=utf-8,%3Csvg viewBox='0 0 100 100' xmlns='http://www.w3.org/2000/svg'%3E%3Crect fill='%237ac673' height='90' rx='8' stroke='%235bb74f' stroke-miterlimit='10' stroke-width='4' width='90' x='5' y='5'/%3E%3Cpath d='M42 81.71V31.3H24.47v-13h51.06v13H58v50.41z' fill='%23fff'/%3E%3C/svg%3E");--icon-variable:url("data:image/svg+xml;charset=utf-8,%3Csvg viewBox='0 0 100 100' xmlns='http://www.w3.org/2000/svg'%3E%3Crect fill='%237ac673' height='90' rx='8' stroke='%235bb74f' stroke-miterlimit='10' stroke-width='4' width='90' x='5' y='5'/%3E%3Cpath d='M39.85 81.71 19.63 18.29H38l12.18 47.64h.35L62.7 18.29h17.67L60.15 81.71z' fill='%23fff'/%3E%3C/svg%3E")}body,button,input,select,textarea{-moz-font-feature-settings:"kern";-moz-osx-font-smoothing:grayscale;-webkit-font-smoothing:antialiased;direction:ltr;font-synthesis:none;text-align:left}h1:first-of-type,h2:first-of-type,h3:first-of-type,h4:first-of-type,h5:first-of-type,h6:first-of-type{margin-top:0}h1 code,h2 code,h3 code,h4 code,h5 code,h6 code{font-family:inherit;font-weight:inherit}h1 img,h2 img,h3 img,h4 img,h5 img,h6 img{display:inline-block;margin:0 .5em .2em 0;vertical-align:middle}h1+*,h2+*,h3+*,h4+*,h5+*,h6+*{margin-top:.8em}img+h1{margin-top:.5em}img+h1,img+h2,img+h3,img+h4,img+h5,img+h6{margin-top:.3em}:is(h1)+:is(h1),:is(h1)+:is(h2),:is(h1)+:is(h3),:is(h1)+:is(h4),:is(h1)+:is(h5),:is(h1)+:is(h6),:is(h2)+:is(h1),:is(h2)+:is(h2),:is(h2)+:is(h3),:is(h2)+:is(h4),:is(h2)+:is(h5),:is(h2)+:is(h6),:is(h3)+:is(h1),:is(h3)+:is(h2),:is(h3)+:is(h3),:is(h3)+:is(h4),:is(h3)+:is(h5),:is(h3)+:is(h6),:is(h4)+:is(h1),:is(h4)+:is(h2),:is(h4)+:is(h3),:is(h4)+:is(h4),:is(h4)+:is(h5),:is(h4)+:is(h6),:is(h5)+:is(h1),:is(h5)+:is(h2),:is(h5)+:is(h3),:is(h5)+:is(h4),:is(h5)+:is(h5),:is(h5)+:is(h6),:is(h6)+:is(h1),:is(h6)+:is(h2),:is(h6)+:is(h3),:is(h6)+:is(h4),:is(h6)+:is(h5),:is(h6)+:is(h6){margin-top:.4em}h1+h1,h1+h2,h1+h3,h1+h4,h1+h5,h1+h6,h2+h1,h2+h2,h2+h3,h2+h4,h2+h5,h2+h6,h3+h1,h3+h2,h3+h3,h3+h4,h3+h5,h3+h6,h4+h1,h4+h2,h4+h3,h4+h4,h4+h5,h4+h6,h5+h1,h5+h2,h5+h3,h5+h4,h5+h5,h5+h6,h6+h1,h6+h2,h6+h3,h6+h4,h6+h5,h6+h6{margin-top:.4em}:is(p,ul,ol)+:is(h1),:is(p,ul,ol)+:is(h2),:is(p,ul,ol)+:is(h3),:is(p,ul,ol)+:is(h4),:is(p,ul,ol)+:is(h5),:is(p,ul,ol)+:is(h6){margin-top:1.6em}ol+h1,ol+h2,ol+h3,ol+h4,ol+h5,ol+h6,p+h1,p+h2,p+h3,p+h4,p+h5,p+h6,ul+h1,ul+h2,ul+h3,ul+h4,ul+h5,ul+h6{margin-top:1.6em}:is(p,ul,ol)+*{margin-top:.8em}:matches(p,ul,ol)+*{margin-top:.8em}ol,ul{margin-left:1.17647em}:matches(ul,ol) :matches(ul,ol){margin-bottom:0;margin-top:0}nav h2{-webkit-font-feature-settings:"c2sc";font-feature-settings:"c2sc";color:#3c3c43;color:var(--secondary-label);font-size:1rem;font-variant:small-caps;font-weight:600;text-transform:uppercase}nav ol,nav ul{list-style:none;margin:0}nav li li{font-size:smaller}a:link,a:visited{text-decoration:none}a:hover{text-decoration:underline}a:active{text-decoration:none}b,strong{font-weight:600}.discussion,.summary{font:300 14pt/19pt sans-serif;font:var(--callout)}article>.discussion{margin-bottom:2em}.discussion .highlight{background:transparent;border:1px solid #e5e5ea;border:1px solid var(--separator);font:300 11pt/13pt sans-serif;font:var(--caption-1);padding:1em;text-indent:0}cite,dfn,em,i{font-style:italic}:matches(h1,h2,h3) sup{font-size:.4em}sup a{color:inherit;vertical-align:inherit}sup a:hover{color:#007aff;color:var(--link);text-decoration:none}sub{line-height:1}abbr{border:0}:lang(ja),:lang(ko),:lang(th),:lang(zh){font-style:normal}:lang(ko){word-break:keep-all}form fieldset{margin:1em auto;max-width:450px;width:95%}form label{display:block;font-size:1em;font-weight:400;line-height:1.5em;margin-bottom:14px;position:relative;width:100%}input[type=email],input[type=number],input[type=password],input[type=tel],input[type=text],input[type=url],textarea{border:1px solid #e5e5ea;border:1px solid var(--separator);border-radius:4px;color:#333;font-family:inherit;font-size:100%;font-weight:400;height:34px;margin:0;padding:0 1em;position:relative;vertical-align:top;width:100%;z-index:1}input[type=email],input[type=email]:focus,input[type=number],input[type=number]:focus,input[type=password],input[type=password]:focus,input[type=tel],input[type=tel]:focus,input[type=text],input[type=text]:focus,input[type=url],input[type=url]:focus,textarea,textarea:focus{-webkit-appearance:none;-moz-appearance:none;appearance:none}input[type=email]:focus,input[type=number]:focus,input[type=password]:focus,input[type=tel]:focus,input[type=text]:focus,input[type=url]:focus,textarea:focus{border-color:#08c;box-shadow:0 0 0 3px rgba(0,136,204,.3);outline:0;z-index:9}input[type=email]:-moz-read-only,input[type=number]:-moz-read-only,input[type=password]:-moz-read-only,input[type=tel]:-moz-read-only,input[type=text]:-moz-read-only,input[type=url]:-moz-read-only,textarea:-moz-read-only{background:none;border:none;box-shadow:none;padding-left:0}input[type=email]:read-only,input[type=number]:read-only,input[type=password]:read-only,input[type=tel]:read-only,input[type=text]:read-only,input[type=url]:read-only,textarea:read-only{background:none;border:none;box-shadow:none;padding-left:0}::-webkit-input-placeholder{color:#8e8e93;color:var(--placeholder-text)}::-moz-placeholder{color:#8e8e93;color:var(--placeholder-text)}::-ms-input-placeholder{color:#8e8e93;color:var(--placeholder-text)}::placeholder{color:#8e8e93;color:var(--placeholder-text)}textarea{-webkit-overflow-scrolling:touch;line-height:1.4737;min-height:134px;overflow-y:auto;resize:vertical;-webkit-transform:translateZ(0);transform:translateZ(0)}textarea,textarea:focus{-webkit-appearance:none;-moz-appearance:none;appearance:none}select{background:transparent;border:none;border-radius:4px;cursor:pointer;font-family:inherit;font-size:1em;height:34px;margin:0;padding:0 1em;width:100%}select,select:focus{-webkit-appearance:none;-moz-appearance:none;appearance:none}select:focus{border-color:#08c;box-shadow:0 0 0 3px rgba(0,136,204,.3);outline:0;z-index:9}input[type=file]{background:#fafafa;border-radius:4px;color:#333;cursor:pointer;font-family:inherit;font-size:100%;height:34px;margin:0;padding:6px 1em;position:relative;vertical-align:top;width:100%;z-index:1}input[type=file]:focus{border-color:#08c;box-shadow:0 0 0 3px rgba(0,136,204,.3);outline:0;z-index:9}button,button:focus,input[type=file]:focus,input[type=file]:focus:focus,input[type=reset],input[type=reset]:focus,input[type=submit],input[type=submit]:focus{-webkit-appearance:none;-moz-appearance:none;appearance:none}:matches(button,input[type=reset],input[type=submit]){background-color:#e3e3e3;background:linear-gradient(#fff,#e3e3e3);border-color:#d6d6d6;color:#0070c9}:matches(button,input[type=reset],input[type=submit]):hover{background-color:#eee;background:linear-gradient(#fff,#eee);border-color:#d9d9d9}:matches(button,input[type=reset],input[type=submit]):active{background-color:#dcdcdc;background:linear-gradient(#f7f7f7,#dcdcdc);border-color:#d0d0d0}:matches(button,input[type=reset],input[type=submit]):disabled{background-color:#e3e3e3;background:linear-gradient(#fff,#e3e3e3);border-color:#d6d6d6;color:#0070c9}body{background:#f2f2f7;background:var(--system-grouped-background);color:#000;color:var(--label);font-family:ui-system,-apple-system,BlinkMacSystemFont,sans-serif;font:300 15pt/20pt sans-serif;font:var(--body)}h1{font:600 32pt/39pt sans-serif;font:var(--large-title)}h2{font:600 20pt/25pt sans-serif;font:var(--title-2)}h3{font:500 18pt/23pt sans-serif;font:var(--title-3)}h4,h5,h6{font:500 15pt/20pt sans-serif;font:var(--headline)}a{color:#007aff;color:var(--link)}label{font:300 14pt/19pt sans-serif;font:var(--callout)}input,label{display:block}input{margin-bottom:1em}hr{border:none;border-top:1px solid #e5e5ea;border-top:1px solid var(--separator);margin:1em 0}table{caption-side:bottom;font:300 11pt/13pt sans-serif;font:var(--caption-1);margin-bottom:2em;width:100%}td,th{padding:0 1em}th{font-weight:600;text-align:left}thead th{border-bottom:1px solid #e5e5ea;border-bottom:1px solid var(--separator)}tr:last-of-type td,tr:last-of-type th{border-bottom:none}td,th{border-bottom:1px solid #e5e5ea;border-bottom:1px solid var(--separator);color:#3c3c43;color:var(--secondary-label)}caption{color:#48484a;color:var(--tertiary-label);font:300 11pt/13pt sans-serif;font:var(--caption-2);margin-top:2em;text-align:left}.graph text,code{font-family:SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace;font-weight:300}.graph>polygon{display:none}.graph text{fill:currentColor!important}.graph ellipse,.graph path,.graph polygon,.graph rect{stroke:currentColor!important}body{margin:1em auto;max-width:1280px;width:90vw}body>header{font:600 26pt/32pt sans-serif;font:var(--title-1);padding:.5em 0}body>header a{color:#000;color:var(--label)}body>header span{font-weight:400}body>header sup{font-size:small;font-weight:300;letter-spacing:.1ch;text-transform:uppercase}body>footer,body>header sup{color:#3c3c43;color:var(--secondary-label)}body>footer{clear:both;font:300 11pt/13pt sans-serif;font:var(--caption-1);padding:1em 0}@media screen and (max-width:768px){body{max-width:100%;width:96vw}body>header{font:500 18pt/23pt sans-serif;font:var(--title-3);padding:1em 0;text-align:left}body>nav{display:none}body>main{padding:0 1em}#relationships figure{display:none}section>[role=article][class] pre{margin-left:-1em;margin-right:-1em}section>[role=article][class] div{margin-left:-2em}}main,nav{overflow-x:auto}main{background:#fff;background:var(--system-background);border-radius:8px;padding:0 2em}main section{border-bottom:1px solid #e5e5ea;border-bottom:1px solid var(--separator);margin-bottom:2em;padding-bottom:1em}main section:last-of-type{border-bottom:none;margin-bottom:0}nav{float:right;margin-left:1em;max-height:100vh;max-width:14em;overflow:auto;padding:0 1em 3em;position:sticky;top:1em;width:20vw}nav a{color:#3c3c43;color:var(--secondary-label)}nav ul a{color:#48484a;color:var(--tertiary-label)}nav ol,nav ul{padding:0}nav ul{font:300 14pt/19pt sans-serif;font:var(--callout);margin-bottom:1em}nav ol>li>a{display:block;font-size:smaller;font:500 15pt/20pt sans-serif;font:var(--headline);margin:.5em 0}nav li{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}blockquote{--link:#3c3c43;--link:var(--secondary-label);border-left:4px solid #e5e5ea;border-left:4px solid var(--separator);color:#3c3c43;color:var(--secondary-label);font-size:smaller;margin-left:0;padding-left:2em}blockquote a{text-decoration:underline}.discussion aside{--link:var(--accent-color);border:.25px solid #e5e5ea;border-left:6px solid #e5e5ea;border:.25px solid var(--separator);border-bottom-left-radius:8px;border-bottom-right-radius:8px;border-left-width:6px;border-top-left-radius:8px;border-top-right-radius:8px;color:var(--accent-color);margin-bottom:1em;padding:.125em 1em}.discussion aside:before{-webkit-font-feature-settings:"c2sc";font-feature-settings:"c2sc";color:var(--accent-color);content:attr(title);font-variant:small-caps;text-transform:lowercase}.discussion aside>p{margin-bottom:.25em;margin-top:.25em}.discussion aside{--accent-color:#000049;--separator:#007aff;--separator:var(--system-blue);background:rgba(0,122,255,.01)}.discussion aside.author,.discussion aside.authors,.discussion aside.copyright,.discussion aside.date{--accent-color:#333;--separator:#787880;--separator:var(--system-fill);background:#fff;background:var(--system-background)}.discussion aside.attention,.discussion aside.important,.discussion aside.warning{--accent-color:#4c2502;--separator:#ff9500;--separator:var(--system-orange);background:rgba(255,149,0,.01)}.discussion aside.bug{--accent-color:#4e0029;--separator:#ff2d55;--separator:var(--system-pink);background:rgba(255,59,48,.01)}article{padding:2em 0 1em}article>.summary{border-bottom:1px solid #e5e5ea;border-bottom:1px solid var(--separator);margin-bottom:2em;padding-bottom:1em}article>.summary:last-child{border-bottom:none}.parameters th{text-align:right}.parameters td{color:#3c3c43;color:var(--secondary-label)}.parameters th+td{text-align:center}dl{padding-top:1em}dt{font:500 15pt/20pt sans-serif;font:var(--headline)}dd{margin-bottom:1em;margin-left:2em}dd p{margin-top:0}.highlight{background:#f2f2f7;background:var(--secondary-system-background);border-radius:8px;font-size:.75em;margin-bottom:2em;overflow-x:auto;padding:1em 1em 1em 3em;text-indent:-2em;white-space:pre-wrap}.highlight .p{white-space:nowrap}.highlight .placeholder{color:#000;color:var(--label)}.highlight a{color:#8e8e93;color:var(--placeholder-text);text-decoration:underline}.highlight .attribute,.highlight .keyword,.highlight .literal{color:#af52de;color:var(--system-purple)}.highlight .number{color:#007aff;color:var(--system-blue)}.highlight .declaration{color:#5ac8fa;color:var(--system-teal)}.highlight .type{color:#5856d6;color:var(--system-indigo)}.highlight .directive{color:#ff9500;color:var(--system-orange)}.highlight .comment{color:#8e8e93;color:var(--system-gray)}main summary:hover{text-decoration:underline}figure{margin:2em 0;padding:1em 0}figure svg{display:block;height:auto!important;margin:0 auto;max-width:100%}h1 small{color:#636366;color:var(--quaternary-label);display:block;font-size:.5em;font-weight:400;line-height:1.5}h3 small{color:#48484a;color:var(--tertiary-label)}dd code,li code,p code{font-size:smaller}a code{text-decoration:underline}dl dt[class],nav li[class],section>[role=article][class]{background-image:var(--background-image);background-position:left .25em;background-repeat:no-repeat;background-size:1em;padding-left:3em}dl dt[class]{background-position-y:.125em}section>[role=article]{border-bottom:1px solid #e5e5ea;border-bottom:1px solid var(--separator);margin-bottom:1em;padding-bottom:1em;padding-left:2em!important}section>[role=article]:last-of-type{border-bottom:none;margin-bottom:0;padding-bottom:0}dl dt[class],nav li[class]{list-style:none;margin-bottom:.5em;text-indent:-1em}nav li[class]{padding-left:2.5em}.associatedtype{--background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg viewBox='0 0 100 100' xmlns='http://www.w3.org/2000/svg'%3E%3Crect fill='%23ff6682' height='90' rx='8' stroke='%23ff2d55' stroke-miterlimit='10' stroke-width='4' width='90' x='5' y='5'/%3E%3Cpath d='M42 81.71V31.3H24.47v-13h51.06v13H58v50.41z' fill='%23fff'/%3E%3C/svg%3E");--background-image:var(--icon-associatedtype);--link:#ff2d55;--link:var(--system-pink)}.case,.enumeration_case{--background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg viewBox='0 0 100 100' xmlns='http://www.w3.org/2000/svg'%3E%3Crect fill='%2389c5e6' height='90' rx='8' stroke='%236bb7e1' stroke-miterlimit='10' stroke-width='4' width='90' x='5' y='5'/%3E%3Cpath d='M20.21 50c0-20.7 11.9-32.79 30.8-32.79 16 0 28.21 10.33 28.7 25.32H64.19C63.4 35 58.09 30.11 51 30.11c-8.79 0-14.37 7.52-14.37 19.82s5.54 20 14.41 20c7.08 0 12.22-4.66 13.23-12.09h15.52c-.74 15.07-12.43 25-28.78 25C32 82.81 20.21 70.72 20.21 50z' fill='%23fff'/%3E%3C/svg%3E");--background-image:var(--icon-case);--link:#5ac8fa;--link:var(--system-teal)}.class{--background-image:url("data:image/svg+xml;utf8,%3Csvg viewBox='0 0 100 100' xmlns='http://www.w3.org/2000/svg'%3E%3Crect fill='%239b98e6' height='90' rx='8' stroke='%235856d6' stroke-miterlimit='10' stroke-width='4' width='90' x='5' y='5'/%3E%3Cpath d='m20.21 50c0-20.7 11.9-32.79 30.8-32.79 16 0 28.21 10.33 28.7 25.32h-15.52c-.79-7.53-6.1-12.42-13.19-12.42-8.79 0-14.37 7.52-14.37 19.82s5.54 20 14.41 20c7.08 0 12.22-4.66 13.23-12.09h15.52c-.74 15.07-12.43 25-28.78 25-19.01-.03-30.8-12.12-30.8-32.84z' fill='%23fff'/%3E%3C/svg%3E");--background-image:var(--icon-class);--link:#5856d6;--link:var(--system-indigo)}.enumeration{--background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg viewBox='0 0 100 100' xmlns='http://www.w3.org/2000/svg'%3E%3Crect fill='%23eca95b' height='90' rx='8' stroke='%23e89234' stroke-miterlimit='10' stroke-width='4' width='90' x='5.17' y='5'/%3E%3Cpath d='M71.9 81.71H28.43V18.29H71.9v13H44.56v12.62h25.71v11.87H44.56V68.7H71.9z' fill='%23fff'/%3E%3C/svg%3E");--background-image:var(--icon-enumeration);--link:#ff9500;--link:var(--system-orange)}.extension{--background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg viewBox='0 0 100 100' xmlns='http://www.w3.org/2000/svg'%3E%3Crect fill='%23eca95b' height='90' rx='8' stroke='%23e89234' stroke-miterlimit='10' stroke-width='4' width='90' x='5' y='5'/%3E%3Cg fill='%23fff'%3E%3Cpath d='M54.43 81.93H20.51V18.07h33.92v12.26H32.61v13.8h20.45v11.32H32.61v14.22h21.82zM68.74 74.58h-.27l-2.78 7.35h-7.28L64 69.32l-6-12.54h8l2.74 7.3h.27l2.76-7.3h7.64l-6.14 12.54 5.89 12.61h-7.64z'/%3E%3C/g%3E%3C/svg%3E");--background-image:var(--icon-extension);--link:#ff9500;--link:var(--system-orange)}.function{--background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg viewBox='0 0 100 100' xmlns='http://www.w3.org/2000/svg'%3E%3Crect fill='%237ac673' height='90' rx='8' stroke='%235bb74f' stroke-miterlimit='10' stroke-width='4' width='90' x='5' y='5'/%3E%3Cpath d='M24.25 75.66A5.47 5.47 0 0 1 30 69.93c1.55 0 3.55.41 6.46.41 3.19 0 4.78-1.55 5.46-6.65l1.5-10.14h-9.34a6 6 0 1 1 0-12h11.1l1.09-7.27C47.82 23.39 54.28 17.7 64 17.7c6.69 0 11.74 1.77 11.74 6.64A5.47 5.47 0 0 1 70 30.07c-1.55 0-3.55-.41-6.46-.41-3.14 0-4.73 1.51-5.46 6.65l-.78 5.27h11.44a6 6 0 1 1 .05 12H55.6l-1.78 12.11C52.23 76.61 45.72 82.3 36 82.3c-6.7 0-11.75-1.77-11.75-6.64z' fill='%23fff'/%3E%3C/svg%3E");--background-image:var(--icon-function);--link:#34c759;--link:var(--system-green)}.initializer,.method{--background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg viewBox='0 0 100 100' xmlns='http://www.w3.org/2000/svg'%3E%3Crect fill='%235a98f8' height='90' rx='8' stroke='%232974ed' stroke-miterlimit='10' stroke-width='4' width='90' x='5' y='5'/%3E%3Cpath d='M70.61 81.71v-39.6h-.31l-15.69 39.6h-9.22l-15.65-39.6h-.35v39.6H15.2V18.29h18.63l16 41.44h.36l16-41.44H84.8v63.42z' fill='%23fff'/%3E%3C/svg%3E");--background-image:var(--icon-method);--link:#007aff;--link:var(--system-blue)}.operator{--background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg viewBox='0 0 100 100' xmlns='http://www.w3.org/2000/svg'%3E%3Crect fill='%237ac673' height='90' rx='8' stroke='%235bb74f' stroke-miterlimit='10' stroke-width='4' width='90' x='5' y='5'/%3E%3Ccircle fill='%23fff' cx='50' cy='50' r='16'/%3E%3C/svg%3E");--background-image:var(--icon-operator);--link:#34c759;--link:var(--system-green)}.property{--background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg viewBox='0 0 100 100' xmlns='http://www.w3.org/2000/svg'%3E%3Crect fill='%2389c5e6' height='90' rx='8' stroke='%236bb7e1' stroke-miterlimit='10' stroke-width='4' width='90' x='5' y='5'/%3E%3Cpath d='M52.31 18.29c13.62 0 22.85 8.84 22.85 22.46s-9.71 22.37-23.82 22.37H41v18.59H24.84V18.29zM41 51h7c6.85 0 10.89-3.56 10.89-10.2S54.81 30.64 48 30.64h-7z' fill='%23fff'/%3E%3C/svg%3E");--background-image:var(--icon-property);--link:#5ac8fa;--link:var(--system-teal)}.protocol{--background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg viewBox='0 0 100 100' xmlns='http://www.w3.org/2000/svg'%3E%3Crect fill='%23ff6682' height='90' rx='8' stroke='%23ff2d55' stroke-miterlimit='10' stroke-width='4' width='90' x='5' y='5'/%3E%3Cg fill='%23fff'%3E%3Cpath d='M46.28 18.29c11.84 0 20 8.66 20 21.71s-8.44 21.71-20.6 21.71H34.87v20H22.78V18.29zM34.87 51.34H43c6.93 0 11-4 11-11.29S50 28.8 43.07 28.8h-8.2zM62 57.45h8v4.77h.16c.84-3.45 2.54-5.12 5.17-5.12a5.06 5.06 0 0 1 1.92.35V65a5.69 5.69 0 0 0-2.39-.51c-3.08 0-4.66 1.74-4.66 5.12v12.1H62z'/%3E%3C/g%3E%3C/svg%3E");--background-image:var(--icon-protocol);--link:#ff2d55;--link:var(--system-pink)}.structure{--background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg viewBox='0 0 100 100' xmlns='http://www.w3.org/2000/svg'%3E%3Crect fill='%23b57edf' height='90' rx='8' stroke='%239454c2' stroke-miterlimit='10' stroke-width='4' width='90' x='5' y='5'/%3E%3Cpath d='M38.38 63c.74 4.53 5.62 7.16 11.82 7.16s10.37-2.81 10.37-6.68c0-3.51-2.73-5.31-10.24-6.76l-6.5-1.23C31.17 53.14 24.62 47 24.62 37.28c0-12.22 10.59-20.09 25.18-20.09 16 0 25.36 7.83 25.53 19.91h-15c-.26-4.57-4.57-7.29-10.42-7.29s-9.31 2.63-9.31 6.37c0 3.34 2.9 5.18 9.8 6.5l6.5 1.23C70.46 46.51 76.61 52 76.61 62c0 12.74-10 20.83-26.72 20.83-15.82 0-26.28-7.3-26.5-19.78z' fill='%23fff'/%3E%3C/svg%3E");--background-image:var(--icon-structure);--link:#af52de;--link:var(--system-purple)}.typealias{--background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg viewBox='0 0 100 100' xmlns='http://www.w3.org/2000/svg'%3E%3Crect fill='%237ac673' height='90' rx='8' stroke='%235bb74f' stroke-miterlimit='10' stroke-width='4' width='90' x='5' y='5'/%3E%3Cpath d='M42 81.71V31.3H24.47v-13h51.06v13H58v50.41z' fill='%23fff'/%3E%3C/svg%3E");--background-image:var(--icon-typealias);--link:#34c759;--link:var(--system-green)}.variable{--background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg viewBox='0 0 100 100' xmlns='http://www.w3.org/2000/svg'%3E%3Crect fill='%237ac673' height='90' rx='8' stroke='%235bb74f' stroke-miterlimit='10' stroke-width='4' width='90' x='5' y='5'/%3E%3Cpath d='M39.85 81.71 19.63 18.29H38l12.18 47.64h.35L62.7 18.29h17.67L60.15 81.71z' fill='%23fff'/%3E%3C/svg%3E");--background-image:var(--icon-variable);--link:#34c759;--link:var(--system-green)}.unknown{--link:#636366;--link:var(--quaternary-label);color:#007aff;color:var(--link)} -------------------------------------------------------------------------------- /docs/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Drops - Drops 7 | 8 | 9 | 10 |
11 | 12 | 13 | Drops 14 | 15 | Documentation 16 | 17 |
18 | 19 | 24 | 25 | 31 | 32 |
33 |
34 |
35 |

Classes

36 |
37 |
38 | 39 | Drops 40 | 41 |
42 |
43 |

A shared class used to show and hide drops.

44 | 45 |
46 |
47 |
48 |
49 |

Structures

50 |
51 |
52 | 53 | Drop 54 | 55 |
56 |
57 |

An object representing a drop.

58 | 59 |
60 |
61 | 62 | Drop.​Action 63 | 64 |
65 |
66 |

An object representing a drop action.

67 | 68 |
69 |
70 | 71 | Drop.​Accessibility 72 | 73 |
74 |
75 |

An object representing accessibility options.

76 | 77 |
78 |
79 |
80 |
81 |

Enumerations

82 |
83 |
84 | 85 | Drop.​Position 86 | 87 |
88 |
89 |

An enum representing drop presentation position.

90 | 91 |
92 |
93 | 94 | Drop.​Duration 95 | 96 |
97 |
98 |

An enum representing a drop duration on screen.

99 | 100 |
101 |
102 |
103 | 104 |
105 |
106 | 107 | 112 | 113 | 114 | -------------------------------------------------------------------------------- /generate_docs.sh: -------------------------------------------------------------------------------- 1 | rm -rf docs 2 | swift doc generate Sources --module-name Drops --output docs --format html --base-url https://omaralbeik.github.io/Drops/ --------------------------------------------------------------------------------