├── .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 | [](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 | 
5 | 
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 | [](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 |
--------------------------------------------------------------------------------