├── .github └── workflows │ └── deploy_to_cocoapods.yml ├── .gitignore ├── File.swift ├── LICENSE ├── Package.swift ├── README.md ├── Screenshots ├── Airpods-Max-Dark.png ├── Airpods-Max.png ├── Airpods-Pro.png ├── Airtag-Dark.png ├── Apple-Watch-Dark.png ├── AppleTV-Remote.png ├── Car.png ├── Grid.png ├── HomePod.png ├── Song-Downloaded-Dark.png ├── Tests │ └── Queue │ │ └── ToastQueueTest.swift └── Text.png ├── Sources └── Toast │ ├── AnimationType.swift │ ├── Direction.swift │ ├── MulticastDelegate.swift │ ├── Queue │ ├── ToastQueue.swift │ └── ToastQueueDelegate.swift │ ├── Toast.swift │ ├── ToastConfiguration.swift │ ├── ToastDelegate.swift │ ├── ToastHelper.swift │ ├── ToastViewConfiguration.swift │ └── ToastViews │ ├── AppleToastView │ ├── AppleToastView.swift │ ├── IconAppleToastView.swift │ └── TextAppleToastView.swift │ └── ToastView.swift ├── Tests └── ToastTests │ └── Queue │ └── ToastQueueTest.swift └── ToastViewSwift.podspec /.github/workflows/deploy_to_cocoapods.yml: -------------------------------------------------------------------------------- 1 | name: deploy_to_cocoapods 2 | 3 | on: 4 | push: 5 | tags: 6 | - '*' 7 | 8 | jobs: 9 | build: 10 | 11 | runs-on: macOS-latest 12 | 13 | steps: 14 | - uses: actions/checkout@v1 15 | 16 | - name: Install Cocoapods 17 | run: gem install cocoapods 18 | 19 | - name: Deploy to Cocoapods 20 | run: | 21 | set -eo pipefail 22 | export LIB_VERSION=$(git describe --tags `git rev-list --tags --max-count=1`) 23 | pod lib lint --allow-warnings 24 | pod trunk push --allow-warnings 25 | env: 26 | COCOAPODS_TRUNK_TOKEN: ${{ secrets.COCOAPODS_TRUNK_TOKEN }} 27 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.build 3 | /Packages 4 | /*.xcodeproj 5 | xcuserdata/ 6 | DerivedData/ 7 | .swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata 8 | .swiftpm -------------------------------------------------------------------------------- /File.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // Toast 4 | // 5 | // Created by Bas Jansen on 16/09/2023. 6 | // 7 | 8 | import Foundation 9 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Bastiaan Jansen 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.9 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | 4 | import PackageDescription 5 | 6 | let package = Package( 7 | name: "Toast", 8 | platforms: [ 9 | .iOS(.v12), 10 | .tvOS(.v13), 11 | .visionOS(.v1) 12 | ], 13 | products: [ 14 | // Products define the executables and libraries a package produces, and make them visible to other packages. 15 | .library( 16 | name: "Toast", 17 | targets: ["Toast"]) 18 | ], 19 | dependencies: [ 20 | // Dependencies declare other packages that this package depends on. 21 | // .package(url: /* package url */, from: "1.0.0"), 22 | ], 23 | targets: [ 24 | // Targets are the basic building blocks of a package. A target can define a module or a test suite. 25 | // Targets can depend on other targets in this package, and on products in packages this package depends on. 26 | .target( 27 | name: "Toast", 28 | dependencies: []), 29 | .testTarget( 30 | name: "ToastTests", 31 | dependencies: ["Toast"]) 32 | ] 33 | ) 34 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Toast-Swift 2 | 3 | [![Codacy Badge](https://app.codacy.com/project/badge/Grade/6eeb888f65db4c168435e739cb7c84e3)](https://www.codacy.com/gh/BastiaanJansen/Toast-Swift/dashboard?utm_source=github.com&utm_medium=referral&utm_content=BastiaanJansen/Toast-Swift&utm_campaign=Badge_Grade) 4 | ![](https://img.shields.io/github/license/BastiaanJansen/Toast-Swift) 5 | ![](https://img.shields.io/github/issues/BastiaanJansen/Toast-Swift) 6 | 7 | A Swift Toast view - iOS 14 style - built with UIKit. 🍞 8 | 9 | 10 | 11 | ## Installation 12 | 13 | ### Swift Package Manager 14 | You can use The Swift Package Manager to install Toast-Swift by adding the description to your Package.swift file: 15 | ```swift 16 | dependencies: [ 17 | .package(url: "https://github.com/BastiaanJansen/toast-swift", from: "2.1.3") 18 | ] 19 | ``` 20 | 21 | ### CocoaPods 22 | ```swift 23 | pod "ToastViewSwift" 24 | ``` 25 | 26 | ## Usage 27 | To create a simple text based toast: 28 | ```swift 29 | let toast = Toast.text("Safari pasted from Notes") 30 | toast.show() 31 | ``` 32 | 33 | Or add a subtitle: 34 | ```swift 35 | let toast = Toast.text("Safari pasted from Notes", subtitle: "A few seconds ago") 36 | toast.show() 37 | ``` 38 | 39 | And if you want to use your own font(NSAttributedString is supported): 40 | ```swift 41 | let attributes = [ 42 | NSAttributedStringKey.font: UIFont(name: "HelveticaNeue-Bold", size: 17)!, 43 | NSAttributedStringKey.foregroundColor: UIColor.black 44 | ] 45 | let attributedString = NSMutableAttributedString(string: "Safari pasted from Notes" , attributes: attributes) 46 | let toast = Toast.text(attributedString) 47 | toast.show() 48 | ``` 49 | 50 | If you want to add an icon, use the `default` method to construct a toast: 51 | ```swift 52 | let toast = Toast.default( 53 | image: UIImage(systemName: "airpodspro")!, 54 | title: "Airpods Pro", 55 | subtitle: "Connected" 56 | ) 57 | toast.show() 58 | ``` 59 | 60 | Want to use a different layout, but still use the Apple style? Create your own view and inject it into the `AppleToastView` class when creating a custom toast: 61 | ```swift 62 | let customView: UIView = // Custom view 63 | 64 | let appleToastView = AppleToastView(child: customView) 65 | 66 | let toast = Toast.custom(view: appleToastView) 67 | toast.show() 68 | ``` 69 | 70 | The `show` method accepts several optional parameters. `haptic` of type `UINotificationFeedbackGenerator.FeedbackType` to use haptics and `after` of type `TimeInterval` to show the toast after a certain amount of time: 71 | ```swift 72 | toast.show(haptic: .success, after: 1) 73 | ``` 74 | 75 | ### Configuration options 76 | The `text`, `default` and `custom` methods support custom configuration options. The following options are available: 77 | 78 | | Name | Description | Type | Default | 79 | |-----------------|-----------------------------------------------------------------------------------------------------|----------------|---------| 80 | | `direction` | Where the toast will be shown. | `.bottom` or `.up` | `.up` | 81 | | `dismissBy` | Choose when the toast dismisses. | `Dismissable` | [`.time`, `.swipe`] | 82 | | `animationTime` | Duration of the show and close animation in seconds. | `TimeInterval` | `0.2` | 83 | | `enteringAnimation` | The type of animation that will be used when toast is showing | `.slide`, `.fade`, `.scaleAndSlide`, `.scale` and `.custom` | `.default`| 84 | | `exitingAnimation` | The type of animation that will be used when toast is exiting | `.slide`, `.fade`, `.scaleAndSlide`, `.scale` and `.custom` | `.default`| 85 | | `attachTo` | The view which the toast view will be attached to. | `UIView` | `nil` | 86 | 87 | 88 | ```swift 89 | let config = ToastConfiguration( 90 | direction: .top, 91 | dismissBy: [.time(time: 4.0), .swipe(direction: .natural), .longPress], 92 | animationTime: 0.2 93 | ) 94 | 95 | let toast = toast.text("Safari pasted from Notes", config: config) 96 | ``` 97 | 98 | ### Custom entering/exiting animations 99 | ```swift 100 | self.toast = Toast.text( 101 | "Safari pasted from Noted", 102 | config: .init( 103 | direction: .bottom, 104 | enteringAnimation: .fade(alphaValue: 0.5), 105 | exitingAnimation: .slide(x: 0, y: 100)) 106 | ).show() 107 | ``` 108 | The above configuration will show a toast that will appear on screen with an animation of fade-in. And then when exiting will go down and disapear. 109 | 110 | ```swift 111 | self.toast = Toast.text( 112 | "Safari pasted from Noted", 113 | config: .init( 114 | direction: .bottom, 115 | enteringAnimation: .scale(scaleX: 0.6, scaleY: 0.6), 116 | exitingAnimation: .default 117 | ).show() 118 | ``` 119 | The above configuration will show a toast that will appear on screen with scaling up animation from 0.6 to 1.0. And then when exiting will use our default animation (which is scaleAndSlide) 120 | 121 | For more on animation see the `Toast.AnimationType` enum. 122 | 123 | ### Custom toast view 124 | Don't like the default Apple'ish style? No problem, it is also possible to use a custom toast view with the `custom` method. Firstly, create a class that confirms to the `ToastView` protocol: 125 | ```swift 126 | class CustomToastView : UIView, ToastView { 127 | private let text: String 128 | 129 | public init(text: String) { 130 | self.text = text 131 | } 132 | 133 | func createView(for toast: Toast) { 134 | // View is added to superview, create and style layout and add constraints 135 | } 136 | } 137 | ``` 138 | Use your custom view with the `custom` construct method on `Toast`: 139 | ```swift 140 | let customToastView: ToastView = CustomToastView(text: "Safari pasted from Notes") 141 | 142 | let toast = Toast.custom(view: customToastView) 143 | toast.show() 144 | ``` 145 | 146 | ### Queues 147 | To show toasts after each other, use the `ToastQueue` class: 148 | 149 | ```swift 150 | let toast1 = Toast.text("Notification 1") 151 | let toast2 = Toast.text("Notification 2") 152 | let toast3 = Toast.text("Notification 3") 153 | 154 | let queue = ToastQueue([toast1, toast2, toast3]) 155 | 156 | queue.show() 157 | ``` 158 | 159 | ### Delegates 160 | Below delegate functions are optional to implement when implementing `ToastDelegate`. 161 | 162 | ```swift 163 | extension MyViewController: ToastDelegate { 164 | func willShowToast(_ toast: Toast) { 165 | print("Toast will be shown after this") 166 | } 167 | 168 | func didShowToast(_ toast: Toast) { 169 | print("Toast was shown") 170 | } 171 | 172 | func willCloseToast(_ toast: Toast) { 173 | print("Toast will be closed after this") 174 | } 175 | 176 | func didCloseToast(_ toast: Toast) { 177 | print("Toast was closed (either automatically, dismissed by user or programmatically)") 178 | } 179 | } 180 | ``` 181 | ## Licence 182 | Toast-Swift is available under the MIT licence. See the LICENCE for more info. 183 | 184 | [![Stargazers repo roster for @BastiaanJansen/Toast-Swift](https://reporoster.com/stars/BastiaanJansen/Toast-Swift)](https://github.com/BastiaanJansen/Toast-Swift/stargazers) 185 | -------------------------------------------------------------------------------- /Screenshots/Airpods-Max-Dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BastiaanJansen/toast-swift/9201d7e7c84659fa5ab7555dcb4971ec00893580/Screenshots/Airpods-Max-Dark.png -------------------------------------------------------------------------------- /Screenshots/Airpods-Max.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BastiaanJansen/toast-swift/9201d7e7c84659fa5ab7555dcb4971ec00893580/Screenshots/Airpods-Max.png -------------------------------------------------------------------------------- /Screenshots/Airpods-Pro.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BastiaanJansen/toast-swift/9201d7e7c84659fa5ab7555dcb4971ec00893580/Screenshots/Airpods-Pro.png -------------------------------------------------------------------------------- /Screenshots/Airtag-Dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BastiaanJansen/toast-swift/9201d7e7c84659fa5ab7555dcb4971ec00893580/Screenshots/Airtag-Dark.png -------------------------------------------------------------------------------- /Screenshots/Apple-Watch-Dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BastiaanJansen/toast-swift/9201d7e7c84659fa5ab7555dcb4971ec00893580/Screenshots/Apple-Watch-Dark.png -------------------------------------------------------------------------------- /Screenshots/AppleTV-Remote.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BastiaanJansen/toast-swift/9201d7e7c84659fa5ab7555dcb4971ec00893580/Screenshots/AppleTV-Remote.png -------------------------------------------------------------------------------- /Screenshots/Car.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BastiaanJansen/toast-swift/9201d7e7c84659fa5ab7555dcb4971ec00893580/Screenshots/Car.png -------------------------------------------------------------------------------- /Screenshots/Grid.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BastiaanJansen/toast-swift/9201d7e7c84659fa5ab7555dcb4971ec00893580/Screenshots/Grid.png -------------------------------------------------------------------------------- /Screenshots/HomePod.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BastiaanJansen/toast-swift/9201d7e7c84659fa5ab7555dcb4971ec00893580/Screenshots/HomePod.png -------------------------------------------------------------------------------- /Screenshots/Song-Downloaded-Dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BastiaanJansen/toast-swift/9201d7e7c84659fa5ab7555dcb4971ec00893580/Screenshots/Song-Downloaded-Dark.png -------------------------------------------------------------------------------- /Screenshots/Tests/Queue/ToastQueueTest.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ToastQueueTest.swift 3 | // 4 | // 5 | // Created by Bas Jansen on 16/09/2023. 6 | // 7 | 8 | import XCTest 9 | @testable import Toast 10 | 11 | final class ToastQueueTest: XCTestCase { 12 | 13 | override func setUpWithError() throws { 14 | // Put setup code here. This method is called before the invocation of each test method in the class. 15 | } 16 | 17 | override func tearDownWithError() throws { 18 | // Put teardown code here. This method is called after the invocation of each test method in the class. 19 | } 20 | 21 | func testExample() throws { 22 | // This is an example of a functional test case. 23 | // Use XCTAssert and related functions to verify your tests produce the correct results. 24 | // Any test you write for XCTest can be annotated as throws and async. 25 | // Mark your test throws to produce an unexpected failure when your test encounters an uncaught error. 26 | // Mark your test async to allow awaiting for asynchronous code to complete. Check the results with assertions afterwards. 27 | } 28 | 29 | func testPerformanceExample() throws { 30 | // This is an example of a performance test case. 31 | self.measure { 32 | // Put the code you want to measure the time of here. 33 | } 34 | } 35 | 36 | } 37 | -------------------------------------------------------------------------------- /Screenshots/Text.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BastiaanJansen/toast-swift/9201d7e7c84659fa5ab7555dcb4971ec00893580/Screenshots/Text.png -------------------------------------------------------------------------------- /Sources/Toast/AnimationType.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Bas Jansen on 16/09/2023. 6 | // 7 | 8 | import Foundation 9 | import UIKit 10 | 11 | extension Toast { 12 | /// Built-in animations for your toast 13 | public enum AnimationType { 14 | /// Use this type for fading in/out animations. 15 | case slide(x: CGFloat, y: CGFloat) 16 | 17 | /// Use this type for fading in/out animations. 18 | /// 19 | /// alphaValue must be greater or equal to 0 and less or equal to 1. 20 | case fade(alpha: CGFloat) 21 | 22 | /// Use this type for scaling and slide in/out animations. 23 | case scaleAndSlide(scaleX: CGFloat, scaleY: CGFloat, x: CGFloat, y: CGFloat) 24 | 25 | /// Use this type for scaling in/out animations. 26 | case scale(scaleX: CGFloat, scaleY: CGFloat) 27 | 28 | /// Use this type for giving your own affine transformation 29 | case custom(transformation: CGAffineTransform) 30 | 31 | /// Currently the default animation if no explicit one specified. 32 | case `default` 33 | 34 | func apply(to view: UIView) { 35 | switch self { 36 | case .slide(x: let x, y: let y): 37 | view.transform = CGAffineTransform(translationX: x, y: y) 38 | 39 | case .fade(let value): 40 | view.alpha = value 41 | 42 | case .scaleAndSlide(let scaleX, let scaleY, let x, let y): 43 | view.transform = CGAffineTransform(scaleX: scaleX, y: scaleY).translatedBy(x: x, y: y) 44 | 45 | case .scale(let scaleX, let scaleY): 46 | view.transform = CGAffineTransform(scaleX: scaleX, y: scaleY) 47 | 48 | case .custom(let transformation): 49 | view.transform = transformation 50 | 51 | case .`default`: 52 | break 53 | } 54 | } 55 | 56 | /// Undo the effects from the ToastView so that it never happened. 57 | func undo(from view: UIView) { 58 | switch self { 59 | case .slide, .scaleAndSlide, .scale, .custom: 60 | view.transform = .identity 61 | 62 | case .fade: 63 | view.alpha = 1.0 64 | 65 | case .`default`: 66 | break 67 | } 68 | } 69 | } 70 | 71 | } 72 | -------------------------------------------------------------------------------- /Sources/Toast/Direction.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Bas Jansen on 16/09/2023. 6 | // 7 | 8 | import Foundation 9 | 10 | extension Toast { 11 | 12 | /// The direction where the toast will be displayed 13 | public enum Direction { 14 | case top, bottom, center 15 | } 16 | 17 | public enum DismissSwipeDirection: Equatable { 18 | case toTop, 19 | toBottom, 20 | natural 21 | 22 | func shouldApply(_ delta: CGFloat, direction: Direction) -> Bool { 23 | switch self { 24 | case .toTop: 25 | return delta <= 0 26 | case .toBottom: 27 | return delta >= 0 28 | case .natural: 29 | switch direction { 30 | case .top: 31 | return delta <= 0 32 | case .bottom: 33 | return delta >= 0 34 | case .center: 35 | return delta <= 0 36 | } 37 | } 38 | } 39 | } 40 | 41 | } 42 | -------------------------------------------------------------------------------- /Sources/Toast/MulticastDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Bas Jansen on 16/09/2023. 6 | // 7 | 8 | import Foundation 9 | 10 | class MulticastDelegate { 11 | 12 | private let delegates: NSHashTable = NSHashTable.init() 13 | 14 | func add(_ delegate: T) { 15 | delegates.add(delegate as AnyObject) 16 | } 17 | 18 | func remove(_ delegateToRemove: T) { 19 | for delegate in delegates.allObjects.reversed() { 20 | if delegate === delegateToRemove as AnyObject { 21 | delegates.remove(delegate) 22 | } 23 | } 24 | } 25 | 26 | func invoke(_ invocation: (T) -> Void) { 27 | for delegate in delegates.allObjects.reversed() { 28 | invocation(delegate as! T) 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /Sources/Toast/Queue/ToastQueue.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Bas Jansen on 16/09/2023. 6 | // 7 | 8 | import Foundation 9 | 10 | public class ToastQueue { 11 | 12 | private var queue: [Toast] 13 | private var multicast = MulticastDelegate() 14 | private var isShowing = false 15 | 16 | public init(toasts: [Toast] = [], delegates: [ToastQueueDelegate] = []) { 17 | self.queue = toasts 18 | delegates.forEach(multicast.add) 19 | } 20 | 21 | public func enqueue(_ toast: Toast) -> Void { 22 | queue.append(toast) 23 | } 24 | 25 | public func enqueue(_ toasts: [Toast]) -> Void { 26 | let size = queue.count 27 | toasts.forEach({ queue.append($0) }) 28 | 29 | if size == 0 && isShowing { 30 | show() 31 | } 32 | } 33 | 34 | public func dequeue(_ toastToDequeue: Toast) -> Void { 35 | let index: Int? = queue.firstIndex { $0 === toastToDequeue } 36 | 37 | if let index { 38 | queue.remove(at: index) 39 | } 40 | } 41 | 42 | public func size() -> Int { 43 | return queue.count 44 | } 45 | 46 | public func show() -> Void { 47 | show(index: 0) 48 | } 49 | 50 | private func show(index: Int, after: Double = 0.0) -> Void { 51 | isShowing = true 52 | if queue.isEmpty { 53 | return 54 | } 55 | 56 | let toast: Toast = queue.remove(at: index) 57 | let delegate = QueuedToastDelegate(queue: self) 58 | 59 | multicast.invoke { $0.willShowAnyToast(toast, queuedToasts: queue) } 60 | 61 | toast.addDelegate(delegate: delegate) 62 | toast.show(after: after) 63 | } 64 | 65 | 66 | private class QueuedToastDelegate: ToastDelegate { 67 | 68 | private var queue: ToastQueue 69 | 70 | public init(queue: ToastQueue) { 71 | self.queue = queue 72 | } 73 | 74 | public func didCloseToast(_ toast: Toast) { 75 | queue.multicast.invoke { $0.didShowAnyToast(toast, queuedToasts: queue.queue) } 76 | queue.show(index: 0, after: 0.5) 77 | } 78 | 79 | } 80 | 81 | } 82 | -------------------------------------------------------------------------------- /Sources/Toast/Queue/ToastQueueDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Bas Jansen on 16/09/2023. 6 | // 7 | 8 | import Foundation 9 | 10 | public protocol ToastQueueDelegate: AnyObject { 11 | 12 | func willShowAnyToast(_ toast: Toast, queuedToasts: [Toast]) -> Void 13 | 14 | func didShowAnyToast(_ toast: Toast, queuedToasts: [Toast]) -> Void 15 | 16 | } 17 | 18 | extension ToastQueueDelegate { 19 | 20 | public func willShowAnyToast(toast: Toast, queuedToasts: [Toast]) {} 21 | 22 | public func didShowAnyToast(toast: Toast, queuedToasts: [Toast]) {} 23 | 24 | } 25 | -------------------------------------------------------------------------------- /Sources/Toast/Toast.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Toast.swift 3 | // Toast 4 | // 5 | // Created by Bastiaan Jansen on 27/06/2021. 6 | // 7 | 8 | import UIKit 9 | 10 | public class Toast { 11 | private static var activeToasts = [Toast]() 12 | 13 | public let view: ToastView 14 | private var backgroundView: UIView? 15 | 16 | private var closeTimer: Timer? 17 | 18 | /// This is for pan gesture to close. 19 | private var startY: CGFloat = 0 20 | private var startShiftY: CGFloat = 0 21 | 22 | public static var defaultImageTint: UIColor { 23 | if #available(iOS 13.0, *) { 24 | return .label 25 | } else { 26 | return .black 27 | } 28 | } 29 | 30 | private var multicast = MulticastDelegate() 31 | 32 | public private(set) var config: ToastConfiguration 33 | 34 | /// Creates a new Toast with the default Apple style layout with a title and an optional subtitle. 35 | /// - Parameters: 36 | /// - title: Attributed title which is displayed in the toast view 37 | /// - subtitle: Optional attributed subtitle which is displayed in the toast view 38 | /// - config: Configuration options 39 | /// - Returns: A new Toast view with the configured layout 40 | public static func text( 41 | _ title: NSAttributedString, 42 | subtitle: NSAttributedString? = nil, 43 | viewConfig: ToastViewConfiguration = ToastViewConfiguration(), 44 | config: ToastConfiguration = ToastConfiguration() 45 | ) -> Toast { 46 | let view = AppleToastView(child: TextToastView(title, subtitle: subtitle, viewConfig: viewConfig), config: viewConfig) 47 | return self.init(view: view, config: config) 48 | } 49 | 50 | /// Creates a new Toast with the default Apple style layout with a title and an optional subtitle. 51 | /// - Parameters: 52 | /// - title: Title which is displayed in the toast view 53 | /// - subtitle: Optional subtitle which is displayed in the toast view 54 | /// - config: Configuration options 55 | /// - Returns: A new Toast view with the configured layout 56 | public static func text( 57 | _ title: String, 58 | subtitle: String? = nil, 59 | viewConfig: ToastViewConfiguration = ToastViewConfiguration(), 60 | config: ToastConfiguration = ToastConfiguration() 61 | ) -> Toast { 62 | let view = AppleToastView(child: TextToastView(title, subtitle: subtitle, viewConfig: viewConfig), config: viewConfig) 63 | return self.init(view: view, config: config) 64 | } 65 | 66 | /// Creates a new Toast with the default Apple style layout with an icon, title and optional subtitle. 67 | /// - Parameters: 68 | /// - image: Image which is displayed in the toast view 69 | /// - imageTint: Tint of the image 70 | /// - title: Attributed title which is displayed in the toast view 71 | /// - subtitle: Optional attributed subtitle which is displayed in the toast view 72 | /// - config: Configuration options 73 | /// - Returns: A new Toast view with the configured layout 74 | public static func `default`( 75 | image: UIImage, 76 | imageTint: UIColor? = defaultImageTint, 77 | title: NSAttributedString, 78 | subtitle: NSAttributedString? = nil, 79 | viewConfig: ToastViewConfiguration = ToastViewConfiguration(), 80 | config: ToastConfiguration = ToastConfiguration() 81 | ) -> Toast { 82 | let view = AppleToastView( 83 | child: IconAppleToastView(image: image, imageTint: imageTint, title: title, subtitle: subtitle, viewConfig: viewConfig), 84 | config: viewConfig 85 | ) 86 | return self.init(view: view, config: config) 87 | } 88 | 89 | /// Creates a new Toast with the default Apple style layout with an icon, title and optional subtitle. 90 | /// - Parameters: 91 | /// - image: Image which is displayed in the toast view 92 | /// - imageTint: Tint of the image 93 | /// - title: Title which is displayed in the toast view 94 | /// - subtitle: Optional subtitle which is displayed in the toast view 95 | /// - config: Configuration options 96 | /// - Returns: A new Toast view with the configured layout 97 | public static func `default`( 98 | image: UIImage, 99 | imageTint: UIColor? = defaultImageTint, 100 | title: String, 101 | subtitle: String? = nil, 102 | viewConfig: ToastViewConfiguration = ToastViewConfiguration(), 103 | config: ToastConfiguration = ToastConfiguration() 104 | ) -> Toast { 105 | let view = AppleToastView( 106 | child: IconAppleToastView(image: image, imageTint: imageTint, title: title, subtitle: subtitle, viewConfig: viewConfig), 107 | config: viewConfig 108 | ) 109 | return self.init(view: view, config: config) 110 | } 111 | 112 | /// Creates a new Toast with a custom view 113 | /// - Parameters: 114 | /// - view: A view which is displayed when the toast is shown 115 | /// - config: Configuration options 116 | /// - Returns: A new Toast view with the configured layout 117 | public static func custom( 118 | view: ToastView, 119 | config: ToastConfiguration = ToastConfiguration() 120 | ) -> Toast { 121 | return self.init(view: view, config: config) 122 | } 123 | 124 | /// Creates a new Toast with a custom view 125 | /// - Parameters: 126 | /// - view: A view which is displayed when the toast is shown 127 | /// - config: Configuration options 128 | /// - Returns: A new Toast view with the configured layout 129 | public required init(view: ToastView, config: ToastConfiguration) { 130 | self.config = config 131 | self.view = view 132 | 133 | for dismissable in config.dismissables { 134 | switch dismissable { 135 | case .tap: 136 | enableTapToClose() 137 | case .longPress: 138 | enableLongPressToClose() 139 | case .swipe: 140 | enablePanToClose() 141 | default: 142 | break 143 | } 144 | } 145 | } 146 | 147 | #if !os(tvOS) && !os(visionOS) 148 | /// Show the toast with haptic feedback 149 | /// - Parameters: 150 | /// - type: Haptic feedback type 151 | /// - time: Time after which the toast is shown 152 | public func show(haptic type: UINotificationFeedbackGenerator.FeedbackType, after time: TimeInterval = 0) { 153 | UINotificationFeedbackGenerator().notificationOccurred(type) 154 | show(after: time) 155 | } 156 | #endif 157 | 158 | /// Show the toast 159 | /// - Parameter delay: Time after which the toast is shown 160 | public func show(after delay: TimeInterval = 0) { 161 | if let backgroundView = self.createBackgroundView() { 162 | self.backgroundView = backgroundView 163 | config.view?.addSubview(backgroundView) ?? ToastHelper.topController()?.view.addSubview(backgroundView) 164 | } 165 | 166 | UIView.performWithoutAnimation { 167 | config.view?.addSubview(view) ?? ToastHelper.topController()?.view.addSubview(view) 168 | view.createView(for: self) 169 | view.layoutIfNeeded() 170 | } 171 | 172 | multicast.invoke { $0.willShowToast(self) } 173 | 174 | config.enteringAnimation.apply(to: self.view) 175 | let endBackgroundColor = backgroundView?.backgroundColor 176 | backgroundView?.backgroundColor = .clear 177 | UIView.animate(withDuration: config.animationTime, delay: delay, options: [.curveEaseOut, .allowUserInteraction]) { 178 | self.config.enteringAnimation.undo(from: self.view) 179 | self.backgroundView?.backgroundColor = endBackgroundColor 180 | } completion: { [self] _ in 181 | multicast.invoke { $0.didShowToast(self) } 182 | 183 | configureCloseTimer() 184 | if !config.allowToastOverlap { 185 | closeOverlappedToasts() 186 | } 187 | Toast.activeToasts.append(self) 188 | } 189 | } 190 | 191 | private func closeOverlappedToasts() { 192 | Toast.activeToasts.forEach { 193 | $0.closeTimer?.invalidate() 194 | $0.close(animated: false) 195 | } 196 | } 197 | 198 | /// Close the toast 199 | /// - Parameters: 200 | /// - completion: A completion handler which is invoked after the toast is hidden 201 | /// - animated: A Boolean value that determines whether to apply animation. 202 | public func close(animated: Bool = true, completion: (() -> Void)? = nil) { 203 | multicast.invoke { $0.willCloseToast(self) } 204 | 205 | closeTimer?.invalidate() 206 | 207 | UIView.animate(withDuration: config.animationTime, 208 | delay: 0, 209 | options: [.curveEaseIn, .allowUserInteraction], 210 | animations: { 211 | if animated { 212 | self.config.exitingAnimation.apply(to: self.view) 213 | } 214 | self.backgroundView?.backgroundColor = .clear 215 | }, completion: { _ in 216 | self.backgroundView?.removeFromSuperview() 217 | self.view.removeFromSuperview() 218 | if let index = Toast.activeToasts.firstIndex(where: { $0 == self }) { 219 | Toast.activeToasts.remove(at: index) 220 | } 221 | completion?() 222 | self.multicast.invoke { $0.didCloseToast(self) } 223 | }) 224 | } 225 | 226 | public func addDelegate(delegate: ToastDelegate) -> Void { 227 | multicast.add(delegate) 228 | } 229 | 230 | private func createBackgroundView() -> UIView? { 231 | switch (config.background) { 232 | case .none: 233 | return nil 234 | case .color(let color): 235 | let backgroundView = UIView(frame: config.view?.frame ?? ToastHelper.topController()?.view.frame ?? .zero) 236 | backgroundView.backgroundColor = color 237 | backgroundView.layer.zPosition = 998 238 | return backgroundView 239 | } 240 | } 241 | 242 | required init?(coder: NSCoder) { 243 | fatalError("init(coder:) has not been implemented") 244 | } 245 | } 246 | 247 | public extension Toast { 248 | private func enablePanToClose() { 249 | let pan = UIPanGestureRecognizer(target: self, action: #selector(toastOnPan(_:))) 250 | self.view.addGestureRecognizer(pan) 251 | } 252 | 253 | @objc private func toastOnPan(_ gesture: UIPanGestureRecognizer) { 254 | guard let topVc = ToastHelper.topController() else { 255 | return 256 | } 257 | 258 | switch gesture.state { 259 | case .began: 260 | startY = self.view.frame.origin.y 261 | startShiftY = gesture.location(in: topVc.view).y 262 | closeTimer?.invalidate() 263 | case .changed: 264 | let delta = gesture.location(in: topVc.view).y - startShiftY 265 | 266 | for dismissable in config.dismissables { 267 | if case .swipe(let dismissSwipeDirection) = dismissable { 268 | let shouldApply = dismissSwipeDirection.shouldApply(delta, direction: config.direction) 269 | 270 | if shouldApply { 271 | self.view.frame.origin.y = startY + delta 272 | } 273 | } 274 | } 275 | 276 | case .ended: 277 | let threshold = 15.0 // if user drags more than threshold the toast will be dismissed 278 | let ammountOfUserDragged = abs(startY - self.view.frame.origin.y) 279 | let shouldDismissToast = ammountOfUserDragged > threshold 280 | 281 | if shouldDismissToast { 282 | close() 283 | } else { 284 | UIView.animate(withDuration: config.animationTime, delay: 0, options: [.curveEaseOut, .allowUserInteraction]) { 285 | self.view.frame.origin.y = self.startY 286 | } completion: { [self] _ in 287 | configureCloseTimer() 288 | } 289 | } 290 | 291 | case .cancelled, .failed: 292 | configureCloseTimer() 293 | default: 294 | break 295 | } 296 | } 297 | 298 | func enableTapToClose() { 299 | let tap = UITapGestureRecognizer(target: self, action: #selector(toastOnTap)) 300 | self.view.addGestureRecognizer(tap) 301 | } 302 | 303 | func enableLongPressToClose() { 304 | let tap = UILongPressGestureRecognizer(target: self, action: #selector(toastOnTap)) 305 | self.view.addGestureRecognizer(tap) 306 | } 307 | 308 | @objc func toastOnTap(_ gesture: UITapGestureRecognizer) { 309 | closeTimer?.invalidate() 310 | close() 311 | } 312 | 313 | private func configureCloseTimer() { 314 | for dismissable in config.dismissables { 315 | if case .time(let displayTime) = dismissable { 316 | closeTimer = Timer.scheduledTimer(withTimeInterval: .init(displayTime), repeats: false) { [self] _ in 317 | close() 318 | } 319 | } 320 | } 321 | } 322 | } 323 | 324 | extension Toast { 325 | public enum Dismissable: Equatable { 326 | case tap, 327 | longPress, 328 | time(time: TimeInterval), 329 | swipe(direction: DismissSwipeDirection) 330 | } 331 | 332 | public enum Background: Equatable { 333 | case none, 334 | color(color: UIColor = defaultImageTint.withAlphaComponent(0.25)) 335 | } 336 | } 337 | 338 | extension Toast: Equatable { 339 | public static func == (lhs: Toast, rhs: Toast) -> Bool { 340 | return ObjectIdentifier(lhs) == ObjectIdentifier(rhs) 341 | } 342 | } 343 | -------------------------------------------------------------------------------- /Sources/Toast/ToastConfiguration.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ToastConfiguration.swift 3 | // Toast 4 | // 5 | // Created by Bastiaan Jansen on 28/06/2021. 6 | // 7 | 8 | import Foundation 9 | import UIKit 10 | 11 | public struct ToastConfiguration { 12 | public let direction: Toast.Direction 13 | public let dismissables: [Toast.Dismissable] 14 | public let animationTime: TimeInterval 15 | public let enteringAnimation: Toast.AnimationType 16 | public let exitingAnimation: Toast.AnimationType 17 | public let background: Toast.Background 18 | public let allowToastOverlap: Bool 19 | 20 | public let view: UIView? 21 | 22 | /// Creates a new Toast configuration object. 23 | /// - Parameters: 24 | /// - direction: The position the toast will be displayed. 25 | /// - dismissBy: Choose when the toast dismisses. 26 | /// - animationTime:Duration of the animation 27 | /// - enteringAnimation: The entering animation of the toast. 28 | /// - exitingAnimation: The exiting animation of the toast. 29 | /// - attachTo: The view on which the toast view will be attached. 30 | /// - allowToastOverlap: Allows new toasts to appear over existing ones. 31 | public init( 32 | direction: Toast.Direction = .top, 33 | dismissBy: [Toast.Dismissable] = [.time(time: 4.0), .swipe(direction: .natural)], 34 | animationTime: TimeInterval = 0.2, 35 | enteringAnimation: Toast.AnimationType = .default, 36 | exitingAnimation: Toast.AnimationType = .default, 37 | attachTo view: UIView? = nil, 38 | background: Toast.Background = .none, 39 | allowToastOverlap: Bool = true 40 | ) { 41 | self.direction = direction 42 | self.dismissables = dismissBy 43 | self.animationTime = animationTime 44 | self.enteringAnimation = enteringAnimation.isDefault ? Self.defaultEnteringAnimation(with: direction) : enteringAnimation 45 | self.exitingAnimation = exitingAnimation.isDefault ? Self.defaultExitingAnimation(with: direction) : exitingAnimation 46 | self.view = view 47 | self.background = background 48 | self.allowToastOverlap = allowToastOverlap 49 | } 50 | } 51 | 52 | // MARK: Default animations 53 | private extension ToastConfiguration { 54 | private static func defaultEnteringAnimation(with direction: Toast.Direction) -> Toast.AnimationType { 55 | switch direction { 56 | case .top: 57 | return .custom( 58 | transformation: CGAffineTransform(scaleX: 0.9, y: 0.9).translatedBy(x: 0, y: -100) 59 | ) 60 | case .bottom: 61 | return .custom( 62 | transformation: CGAffineTransform(scaleX: 0.9, y: 0.9).translatedBy(x: 0, y: 100) 63 | ) 64 | case .center: 65 | return .custom( 66 | transformation: CGAffineTransform(scaleX: 0.5, y: 0.5) 67 | ) 68 | } 69 | } 70 | 71 | private static func defaultExitingAnimation(with direction: Toast.Direction) -> Toast.AnimationType { 72 | self.defaultEnteringAnimation(with: direction) 73 | } 74 | } 75 | 76 | fileprivate extension Toast.AnimationType { 77 | var isDefault: Bool { 78 | if case .default = self { 79 | return true 80 | } 81 | return false 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /Sources/Toast/ToastDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ToastDelegate.swift 3 | // Toast 4 | // 5 | // Created by Zandor Smith on 01/11/2022. 6 | // 7 | 8 | import Foundation 9 | 10 | public protocol ToastDelegate: AnyObject { 11 | 12 | /// Delegate function that will be called before the Toast is shown. 13 | /// - Parameters: 14 | /// - toast: The toast that will be shown. 15 | func willShowToast(_ toast: Toast) 16 | 17 | /// Delegate function that will be called after the Toast is shown. 18 | /// - Parameters: 19 | /// - toast: The toast that was just shown. 20 | func didShowToast(_ toast: Toast) 21 | 22 | /// Delegate function that will be called before the Toast is closed. 23 | /// - Parameters: 24 | /// - toast: The toast that will be closed. 25 | func willCloseToast(_ toast: Toast) 26 | 27 | /// Delegate function that will be called after the Toast is closed. 28 | /// - Parameters: 29 | /// - toast: The toast that was just closed. 30 | func didCloseToast(_ toast: Toast) 31 | 32 | } 33 | 34 | extension ToastDelegate { 35 | 36 | func willShowToast(_ toast: Toast) {} 37 | 38 | func didShowToast(_ toast: Toast) {} 39 | 40 | func willCloseToast(_ toast: Toast) {} 41 | 42 | func didCloseToast(_ toast: Toast) {} 43 | 44 | } 45 | -------------------------------------------------------------------------------- /Sources/Toast/ToastHelper.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Bas Jansen on 16/09/2023. 6 | // 7 | 8 | import Foundation 9 | import UIKit 10 | 11 | class ToastHelper { 12 | 13 | public static func topController() -> UIViewController? { 14 | if var topController = keyWindow()?.rootViewController { 15 | while let presentedViewController = topController.presentedViewController { 16 | topController = presentedViewController 17 | } 18 | return topController 19 | } 20 | return nil 21 | } 22 | 23 | private static func keyWindow() -> UIWindow? { 24 | if #available(iOS 13.0, *) { 25 | for scene in UIApplication.shared.connectedScenes { 26 | guard let windowScene = scene as? UIWindowScene else { 27 | continue 28 | } 29 | if windowScene.windows.isEmpty { 30 | continue 31 | } 32 | guard let window = windowScene.windows.first(where: { $0.isKeyWindow }) else { 33 | continue 34 | } 35 | return window 36 | } 37 | return nil 38 | } else { 39 | return UIApplication.shared.windows.first(where: { $0.isKeyWindow }) 40 | } 41 | } 42 | 43 | } 44 | -------------------------------------------------------------------------------- /Sources/Toast/ToastViewConfiguration.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ToastViewConfiguration.swift 3 | // Toast 4 | // 5 | // Created by Thomas Maw on 12/9/2023. 6 | // 7 | 8 | import Foundation 9 | import UIKit 10 | 11 | public struct ToastViewConfiguration { 12 | public let minHeight: CGFloat 13 | public let minWidth: CGFloat 14 | 15 | public let darkBackgroundColor: UIColor 16 | public let lightBackgroundColor: UIColor 17 | 18 | public let titleNumberOfLines: Int 19 | public let subtitleNumberOfLines: Int 20 | 21 | public let cornerRadius: CGFloat? 22 | 23 | public let textAlignment: UIStackView.Alignment 24 | 25 | public init( 26 | minHeight: CGFloat = 58, 27 | minWidth: CGFloat = 150, 28 | darkBackgroundColor: UIColor = UIColor(red: 0.13, green: 0.13, blue: 0.13, alpha: 1.00), 29 | lightBackgroundColor: UIColor = UIColor(red: 0.99, green: 0.99, blue: 0.99, alpha: 1.00), 30 | titleNumberOfLines: Int = 1, 31 | subtitleNumberOfLines: Int = 1, 32 | cornerRadius: CGFloat? = nil, 33 | textAlignment: UIStackView.Alignment = .center 34 | ) { 35 | self.minHeight = minHeight 36 | self.minWidth = minWidth 37 | self.darkBackgroundColor = darkBackgroundColor 38 | self.lightBackgroundColor = lightBackgroundColor 39 | self.titleNumberOfLines = titleNumberOfLines 40 | self.subtitleNumberOfLines = subtitleNumberOfLines 41 | self.cornerRadius = cornerRadius 42 | self.textAlignment = textAlignment 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /Sources/Toast/ToastViews/AppleToastView/AppleToastView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ToastView.swift 3 | // Toast 4 | // 5 | // Created by Bastiaan Jansen on 30/06/2021. 6 | // 7 | 8 | import Foundation 9 | import UIKit 10 | 11 | public class AppleToastView : UIView, ToastView { 12 | private let config: ToastViewConfiguration 13 | 14 | private let child: UIView 15 | 16 | private var toast: Toast? 17 | 18 | public init( 19 | child: UIView, 20 | config: ToastViewConfiguration = ToastViewConfiguration() 21 | ) { 22 | self.config = config 23 | self.child = child 24 | super.init(frame: .zero) 25 | 26 | addSubview(child) 27 | } 28 | 29 | public override func removeFromSuperview() { 30 | super.removeFromSuperview() 31 | self.toast = nil 32 | } 33 | 34 | public func createView(for toast: Toast) { 35 | self.toast = toast 36 | guard let superview = superview else { return } 37 | translatesAutoresizingMaskIntoConstraints = false 38 | 39 | NSLayoutConstraint.activate([ 40 | heightAnchor.constraint(greaterThanOrEqualToConstant: config.minHeight), 41 | widthAnchor.constraint(greaterThanOrEqualToConstant: config.minWidth), 42 | leadingAnchor.constraint(greaterThanOrEqualTo: superview.leadingAnchor, constant: 10), 43 | trailingAnchor.constraint(lessThanOrEqualTo: superview.trailingAnchor, constant: -10), 44 | centerXAnchor.constraint(equalTo: superview.centerXAnchor) 45 | ]) 46 | 47 | switch toast.config.direction { 48 | case .bottom: 49 | bottomAnchor.constraint(equalTo: superview.layoutMarginsGuide.bottomAnchor, constant: 0).isActive = true 50 | case .top: 51 | topAnchor.constraint(equalTo: superview.layoutMarginsGuide.topAnchor, constant: 0).isActive = true 52 | case .center: 53 | centerYAnchor.constraint(equalTo: superview.layoutMarginsGuide.centerYAnchor, constant: 0).isActive = true 54 | } 55 | 56 | addSubviewConstraints() 57 | DispatchQueue.main.async { 58 | self.style() 59 | } 60 | } 61 | 62 | public override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { 63 | UIView.animate(withDuration: 0.5) { 64 | self.style() 65 | } 66 | } 67 | 68 | private func style() { 69 | layoutIfNeeded() 70 | clipsToBounds = true 71 | layer.zPosition = 999 72 | layer.cornerRadius = config.cornerRadius ?? frame.height / 2 73 | if #available(iOS 12.0, *) { 74 | backgroundColor = traitCollection.userInterfaceStyle == .light ? config.lightBackgroundColor : config.darkBackgroundColor 75 | } else { 76 | backgroundColor = config.lightBackgroundColor 77 | } 78 | 79 | addShadow() 80 | } 81 | 82 | private func addSubviewConstraints() { 83 | child.translatesAutoresizingMaskIntoConstraints = false 84 | NSLayoutConstraint.activate([ 85 | child.topAnchor.constraint(equalTo: topAnchor, constant: 10), 86 | child.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -10), 87 | child.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 25), 88 | child.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -25) 89 | ]) 90 | } 91 | 92 | private func addShadow() { 93 | layer.masksToBounds = false 94 | layer.shadowOffset = CGSize(width: 0, height: 4) 95 | layer.shadowColor = UIColor.black.withAlphaComponent(0.08).cgColor 96 | layer.shadowOpacity = 1 97 | layer.shadowRadius = 8 98 | } 99 | 100 | required init?(coder: NSCoder) { 101 | fatalError("init(coder:) has not been implemented") 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /Sources/Toast/ToastViews/AppleToastView/IconAppleToastView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DefaultToastView.swift 3 | // Toast 4 | // 5 | // Created by Bastiaan Jansen on 29/06/2021. 6 | // 7 | 8 | import Foundation 9 | import UIKit 10 | 11 | public class IconAppleToastView : UIStackView { 12 | private lazy var vStack: UIStackView = { 13 | let stackView = UIStackView() 14 | stackView.axis = .vertical 15 | stackView.spacing = 2 16 | 17 | return stackView 18 | }() 19 | 20 | private lazy var imageView: UIImageView = { 21 | let imageView = UIImageView() 22 | imageView.contentMode = .scaleAspectFit 23 | NSLayoutConstraint.activate([ 24 | imageView.widthAnchor.constraint(equalToConstant: 28), 25 | imageView.heightAnchor.constraint(equalToConstant: 28) 26 | ]) 27 | 28 | return imageView 29 | }() 30 | 31 | private lazy var titleLabel: UILabel = { 32 | UILabel() 33 | }() 34 | 35 | private lazy var subtitleLabel: UILabel = { 36 | UILabel() 37 | }() 38 | 39 | public static var defaultImageTint: UIColor { 40 | if #available(iOS 13.0, *) { 41 | return .label 42 | } else { 43 | return .black 44 | } 45 | } 46 | 47 | public init( 48 | image: UIImage, 49 | imageTint: UIColor? = defaultImageTint, 50 | title: NSAttributedString, 51 | subtitle: NSAttributedString? = nil, 52 | viewConfig: ToastViewConfiguration 53 | ) { 54 | super.init(frame: CGRect.zero) 55 | vStack.alignment = viewConfig.textAlignment 56 | commonInit() 57 | 58 | self.titleLabel.attributedText = title 59 | self.titleLabel.numberOfLines = viewConfig.titleNumberOfLines 60 | self.vStack.addArrangedSubview(self.titleLabel) 61 | 62 | if let subtitle = subtitle { 63 | self.subtitleLabel.attributedText = subtitle 64 | self.subtitleLabel.numberOfLines = viewConfig.subtitleNumberOfLines 65 | self.vStack.addArrangedSubview(self.subtitleLabel) 66 | } 67 | 68 | self.imageView.image = image 69 | self.imageView.tintColor = imageTint 70 | 71 | addArrangedSubview(self.imageView) 72 | addArrangedSubview(self.vStack) 73 | } 74 | 75 | public init(image: UIImage, imageTint: UIColor? = defaultImageTint, title: String, subtitle: String? = nil, viewConfig: ToastViewConfiguration) { 76 | super.init(frame: CGRect.zero) 77 | vStack.alignment = viewConfig.textAlignment 78 | commonInit() 79 | 80 | self.titleLabel.text = title 81 | self.titleLabel.numberOfLines = viewConfig.titleNumberOfLines 82 | self.titleLabel.font = .systemFont(ofSize: 14, weight: .bold) 83 | self.vStack.addArrangedSubview(self.titleLabel) 84 | 85 | if let subtitle = subtitle { 86 | self.subtitleLabel.textColor = .systemGray 87 | self.subtitleLabel.text = subtitle 88 | self.subtitleLabel.numberOfLines = viewConfig.subtitleNumberOfLines 89 | self.subtitleLabel.font = .systemFont(ofSize: 12, weight: .bold) 90 | self.vStack.addArrangedSubview(self.subtitleLabel) 91 | } 92 | 93 | self.imageView.image = image 94 | self.imageView.tintColor = imageTint 95 | 96 | addArrangedSubview(self.imageView) 97 | addArrangedSubview(self.vStack) 98 | } 99 | 100 | required init(coder: NSCoder) { 101 | fatalError("init(coder:) has not been implemented") 102 | } 103 | 104 | private func commonInit() { 105 | axis = .horizontal 106 | alignment = .center 107 | spacing = 15 108 | distribution = .fill 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /Sources/Toast/ToastViews/AppleToastView/TextAppleToastView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TextToastView.swift 3 | // Toast 4 | // 5 | // Created by Bastiaan Jansen on 29/06/2021. 6 | // 7 | 8 | import Foundation 9 | import UIKit 10 | 11 | public class TextToastView : UIStackView { 12 | private lazy var titleLabel: UILabel = { 13 | UILabel() 14 | }() 15 | 16 | private lazy var subtitleLabel: UILabel = { 17 | UILabel() 18 | }() 19 | 20 | public init(_ title: NSAttributedString, subtitle: NSAttributedString? = nil, viewConfig: ToastViewConfiguration) { 21 | super.init(frame: CGRect.zero) 22 | commonInit() 23 | alignment = viewConfig.textAlignment 24 | 25 | self.titleLabel.attributedText = title 26 | self.titleLabel.numberOfLines = viewConfig.titleNumberOfLines 27 | addArrangedSubview(self.titleLabel) 28 | 29 | if let subtitle = subtitle { 30 | self.subtitleLabel.attributedText = subtitle 31 | self.subtitleLabel.numberOfLines = viewConfig.subtitleNumberOfLines 32 | addArrangedSubview(subtitleLabel) 33 | } 34 | } 35 | 36 | public init(_ title: String, subtitle: String? = nil, viewConfig: ToastViewConfiguration) { 37 | super.init(frame: CGRect.zero) 38 | commonInit() 39 | alignment = viewConfig.textAlignment 40 | 41 | self.titleLabel.text = title 42 | self.titleLabel.numberOfLines = viewConfig.titleNumberOfLines 43 | self.titleLabel.font = .systemFont(ofSize: 14, weight: .bold) 44 | addArrangedSubview(self.titleLabel) 45 | 46 | if let subtitle = subtitle { 47 | self.subtitleLabel.textColor = .systemGray 48 | self.subtitleLabel.text = subtitle 49 | self.subtitleLabel.numberOfLines = viewConfig.subtitleNumberOfLines 50 | self.subtitleLabel.font = .systemFont(ofSize: 12, weight: .bold) 51 | addArrangedSubview(self.subtitleLabel) 52 | } 53 | 54 | } 55 | 56 | required init(coder: NSCoder) { 57 | fatalError("init(coder:) has not been implemented") 58 | } 59 | 60 | private func commonInit() { 61 | axis = .vertical 62 | distribution = .fillEqually 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /Sources/Toast/ToastViews/ToastView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ToastAppearance.swift 3 | // Toast 4 | // 5 | // Created by Bastiaan Jansen on 29/06/2021. 6 | // 7 | 8 | import Foundation 9 | import UIKit 10 | 11 | public protocol ToastView : UIView { 12 | func createView(for toast: Toast) 13 | } 14 | -------------------------------------------------------------------------------- /Tests/ToastTests/Queue/ToastQueueTest.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ToastQueuetest.swift 3 | // 4 | // 5 | // Created by Bas Jansen on 16/09/2023. 6 | // 7 | 8 | import XCTest 9 | @testable import Toast 10 | 11 | final class ToastQueueTest: XCTestCase { 12 | 13 | private var queue: ToastQueue! 14 | 15 | override func setUpWithError() throws { 16 | queue = ToastQueue() 17 | } 18 | 19 | override func tearDownWithError() throws { 20 | 21 | } 22 | 23 | func test_whenEnqueuingToast_sizeIsOne() throws { 24 | let toast = Toast.text("Toast") 25 | 26 | queue.enqueue(toast) 27 | 28 | XCTAssertEqual(queue.size(), 1) 29 | } 30 | 31 | func test_whenEnqueuingMultipleToasts_sizeIsThree() throws { 32 | let toast = Toast.text("Toast") 33 | let toast2 = Toast.text("Toast") 34 | let toast3 = Toast.text("Toast") 35 | 36 | queue.enqueue([toast, toast2, toast3]) 37 | 38 | XCTAssertEqual(queue.size(), 3) 39 | } 40 | 41 | } 42 | -------------------------------------------------------------------------------- /ToastViewSwift.podspec: -------------------------------------------------------------------------------- 1 | # 2 | # Be sure to run `pod spec lint Toast.podspec' to ensure this is a 3 | # valid spec and to remove all comments including this before submitting the spec. 4 | # 5 | # To learn more about Podspec attributes see https://guides.cocoapods.org/syntax/podspec.html 6 | # To see working Podspecs in the CocoaPods repo see https://github.com/CocoaPods/Specs/ 7 | # 8 | 9 | Pod::Spec.new do |spec| 10 | 11 | # ――― Spec Metadata ―――――――――――――――――――――――――――――――――――――――――――――――――――――――――― # 12 | # 13 | # These will help people to find your library, and whilst it 14 | # can feel like a chore to fill in it's definitely to your advantage. The 15 | # summary should be tweet-length, and the description more in depth. 16 | # 17 | 18 | spec.name = "ToastViewSwift" 19 | spec.version = ENV['LIB_VERSION'] || "1.0" 20 | spec.summary = "A Swift Toast view - iOS 14 style and newer - built with UIKit. 🍞" 21 | 22 | # This description is used to generate tags and improve search results. 23 | # * Think: What does it do? Why did you write it? What is the focus? 24 | # * Try to keep it short, snappy and to the point. 25 | # * Write the description between the DESC delimiters below. 26 | # * Finally, don't worry about the indent, CocoaPods strips it! 27 | spec.description = <<-DESC 28 | A Swift Toast view - iOS 14 style and newer - built with UIKit. 🍞 29 | DESC 30 | 31 | spec.homepage = "https://github.com/BastiaanJansen/Toast-Swift" 32 | spec.screenshots = "https://github.com/BastiaanJansen/Toast-Swift/raw/main/Screenshots/Text.png", "https://github.com/BastiaanJansen/Toast-Swift/raw/main/Screenshots/Airpods-Pro.png", "https://github.com/BastiaanJansen/Toast-Swift/raw/main/Screenshots/Text-Dark.png", "https://github.com/BastiaanJansen/Toast-Swift/raw/main/Screenshots/Airpods-Pro-Dark.png" 33 | 34 | 35 | # ――― Spec License ――――――――――――――――――――――――――――――――――――――――――――――――――――――――――― # 36 | # 37 | # Licensing your code is important. See https://choosealicense.com for more info. 38 | # CocoaPods will detect a license file if there is a named LICENSE* 39 | # Popular ones are 'MIT', 'BSD' and 'Apache License, Version 2.0'. 40 | # 41 | 42 | spec.license = "MIT" 43 | # spec.license = { :type => "MIT", :file => "FILE_LICENSE" } 44 | 45 | 46 | # ――― Author Metadata ――――――――――――――――――――――――――――――――――――――――――――――――――――――――― # 47 | # 48 | # Specify the authors of the library, with email addresses. Email addresses 49 | # of the authors are extracted from the SCM log. E.g. $ git log. CocoaPods also 50 | # accepts just a name if you'd rather not provide an email address. 51 | # 52 | # Specify a social_media_url where others can refer to, for example a twitter 53 | # profile URL. 54 | # 55 | 56 | spec.author = { "Bastiaan Jansen" => "Bastiaanj7@outlook.com", "Bas Jansen" => "bastiaan225@icloud.com" } 57 | # Or just: spec.author = "Bastiaan Jansen" 58 | # spec.authors = { "Bastiaan Jansen" => "Bastiaanj7@outlook.com" } 59 | # spec.social_media_url = "https://twitter.com/Bastiaan Jansen" 60 | 61 | # ――― Platform Specifics ――――――――――――――――――――――――――――――――――――――――――――――――――――――― # 62 | # 63 | # If this Pod runs only on iOS or OS X, then specify the platform and 64 | # the deployment target. You can optionally include the target after the platform. 65 | # 66 | 67 | spec.ios.deployment_target = "12.0" 68 | spec.tvos.deployment_target = "13.0" 69 | spec.visionos.deployment_target = "1.0" 70 | 71 | # ――― Source Location ―――――――――――――――――――――――――――――――――――――――――――――――――――――――――― # 72 | # 73 | # Specify the location from where the source should be retrieved. 74 | # Supports git, hg, bzr, svn and HTTP. 75 | # 76 | 77 | spec.source = { :git => "https://github.com/BastiaanJansen/Toast-Swift.git", :tag => "#{spec.version}" } 78 | 79 | 80 | # ――― Source Code ―――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――― # 81 | # 82 | # CocoaPods is smart about how it includes source code. For source files 83 | # giving a folder will include any swift, h, m, mm, c & cpp files. 84 | # For header files it will include any header in the folder. 85 | # Not including the public_header_files will make all headers public. 86 | # 87 | 88 | spec.source_files = "Sources/**/*.{h,m,swift}" 89 | spec.exclude_files = "Classes/Exclude" 90 | 91 | # spec.public_header_files = "Classes/**/*.h" 92 | 93 | 94 | # ――― Resources ―――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――― # 95 | # 96 | # A list of resources included with the Pod. These are copied into the 97 | # target bundle with a build phase script. Anything else will be cleaned. 98 | # You can preserve files from being cleaned, please don't preserve 99 | # non-essential files like tests, examples and documentation. 100 | # 101 | 102 | # spec.resource = "icon.png" 103 | # spec.resources = "Resources/*.png" 104 | 105 | # spec.preserve_paths = "FilesToSave", "MoreFilesToSave" 106 | 107 | 108 | # ――― Project Linking ―――――――――――――――――――――――――――――――――――――――――――――――――――――――――― # 109 | # 110 | # Link your library with frameworks, or libraries. Libraries do not include 111 | # the lib prefix of their name. 112 | # 113 | 114 | # spec.framework = "SomeFramework" 115 | # spec.frameworks = "SomeFramework", "AnotherFramework" 116 | 117 | # spec.library = "iconv" 118 | # spec.libraries = "iconv", "xml2" 119 | 120 | 121 | # ――― Project Settings ――――――――――――――――――――――――――――――――――――――――――――――――――――――――― # 122 | # 123 | # If your library depends on compiler flags you can set them in the xcconfig hash 124 | # where they will only apply to your library. If you depend on other Podspecs 125 | # you can include multiple dependencies to ensure it works. 126 | 127 | # spec.requires_arc = true 128 | 129 | # spec.xcconfig = { "HEADER_SEARCH_PATHS" => "$(SDKROOT)/usr/include/libxml2" } 130 | # spec.dependency "JSONKit", "~> 1.4" 131 | 132 | end 133 | --------------------------------------------------------------------------------