├── .swiftpm └── xcode │ ├── package.xcworkspace │ ├── contents.xcworkspacedata │ └── xcuserdata │ │ └── macintoshi.xcuserdatad │ │ └── UserInterfaceState.xcuserstate │ └── xcuserdata │ └── macintoshi.xcuserdatad │ └── xcschemes │ └── xcschememanagement.plist ├── LICENSE ├── Package.swift ├── README.md ├── Sources └── ToastKit │ ├── Enums │ └── ToastEnums.swift │ ├── Extensions │ └── Extension+View.swift │ ├── ToastKit.swift │ ├── ToastModifier.swift │ └── ToastStack │ ├── ToastItemModel.swift │ ├── ToastStackManager.swift │ └── ToastStackView.swift └── Tests └── ToastKitTests ├── EnumsTests.swift └── ToastStackTests.swift /.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.swiftpm/xcode/package.xcworkspace/xcuserdata/macintoshi.xcuserdatad/UserInterfaceState.xcuserstate: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Desp0o/ToastKit/27b97664873141e04766bb0e527d76e0f12bc0d6/.swiftpm/xcode/package.xcworkspace/xcuserdata/macintoshi.xcuserdatad/UserInterfaceState.xcuserstate -------------------------------------------------------------------------------- /.swiftpm/xcode/xcuserdata/macintoshi.xcuserdatad/xcschemes/xcschememanagement.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | SchemeUserState 6 | 7 | ToastKit.xcscheme_^#shared#^_ 8 | 9 | orderHint 10 | 0 11 | 12 | 13 | SuppressBuildableAutocreation 14 | 15 | ToastKit 16 | 17 | primary 18 | 19 | 20 | ToastKitTests 21 | 22 | primary 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 Tornike Despotashvili 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: 6.0 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: "ToastKit", 8 | products: [ 9 | // Products define the executables and libraries a package produces, making them visible to other packages. 10 | .library( 11 | name: "ToastKit", 12 | targets: ["ToastKit"]), 13 | ], 14 | targets: [ 15 | // Targets are the basic building blocks of a package, defining a module or a test suite. 16 | // Targets can depend on other targets in this package and products from dependencies. 17 | .target( 18 | name: "ToastKit"), 19 | .testTarget( 20 | name: "ToastKitTests", 21 | dependencies: ["ToastKit"] 22 | ), 23 | ] 24 | ) 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | ![ToastKit Logo Design-2](https://github.com/user-attachments/assets/5935dd17-029d-488b-b7e3-2e767cc2b9e1) 3 | 4 | # ToastKit 5 | ToastKit is a lightweight and fully customizable Swift package that helps you display informative toast messages in your app. It’s easy to use, supports 6 | various built-in toast styles like success, warning, info, error, with icons.... and also allows full customization for your specific needs. 7 | 8 | You can quickly use ready-made toasts or create your own custom toast view with complete control over layout, colors, animations, icons, and more. 9 | 10 | 11 | ![Static Badge](https://img.shields.io/badge/Swit-6.0-orange) ![Static Badge](https://img.shields.io/badge/iOS-17.0%2B-white) ![Static Badge](https://img.shields.io/badge/Version%20-%2026.0.0-skyblue) ![Static Badge](https://img.shields.io/badge/LICENSE-MIT-yellow) ![Static Badge](https://img.shields.io/badge/SPM-SUCCESS-green) 12 | 13 | 14 | 15 | ## Features 🚀 16 | - Full Customization 17 | - Glass Effect 18 | - Max Width Support 19 | - Custom Icons & SF Symbols 20 | - Auto Dismiss 21 | - Transition Types 22 | - Flexible Layout Direction 23 | - Text Styling Options 24 | - Shadow Customization 25 | - Corner Radius Control 26 | - Optional Subtitle 27 | - Adaptive Stack Alignment 28 | - Smooth Animations 29 | - Manual Close Button 30 | - Responsive Design 31 | 32 | --------- 33 | ### GlassEffect 34 | 35 | ![glassGif](https://github.com/user-attachments/assets/09430eb5-fed7-4864-b865-ac3fa7b1e56b) 36 | 37 | ```swift 38 | VStack { 39 | 40 | } 41 | .frame(maxWidth: .infinity, maxHeight: .infinity) 42 | //simple usage 43 | .glassToast(isVisible: $isVisible, title: title) 44 | 45 | //with full parameters 46 | .glassToast( 47 | isVisible: $isVisible2, 48 | title: title, 49 | subtitle: "if you have iOS 26 you can use this toast", 50 | glassColor: .red.opacity(0.5), 51 | titleFontColor: .black, 52 | subtitleFontColor: .black, 53 | maxWidth: false, 54 | transitionType: .custom(.asymmetric(insertion: .move(edge: .trailing), removal: .move(edge: .leading)).combined(with: .opacity)), 55 | animation: .smooth, 56 | vDirection: .bottom 57 | ) 58 | ``` 59 | 60 | ##### Configuration ⚙️ 61 | | Parameter | Type | Default Value | Description | 62 | |-------------------|------------------------------|--------------------------------|-------------| 63 | | `isVisible` | Binding | — | Binding to control visibility. | 64 | | `title` | String | — | The main message displayed in the toast. | 65 | | `subtitle` | String | `""` | Subtitle text for additional info. | 66 | | `titleFontColor` | Color | `.white` | Font color of the title. | 67 | | `subtitleFontColor` | Color | `.white` | Font color of the subtitle. | 68 | | `transitionType` | ToastTransitionType | .move(edge: .top) | The transition animation for how the toast appears/disappears. | 69 | | `animation` | Animation | .snappy | Animation used to present and dismiss the toast. | 70 | | `vDirection` | VerticalDirection | .top | Vertical position of the toast (`.top` or `.bottom`). | 71 | | `maxWidth` | Bool | false | If `true`, toast takes maximum available width. | 72 | 73 | ### Success / Warning / Error/ Info - Toasts 74 | ![simples](https://github.com/user-attachments/assets/f3af5442-fd4b-4ab3-a01d-9bf95d374656) 75 | 76 | ##### simple toast 77 | ```swift 78 | VStack { 79 | 80 | } 81 | .frame(maxWidth: .infinity, maxHeight: .infinity) 82 | .successToast(isVisible: $isVisible, title: "Success") 83 | .warningToast(isVisible: $isVisible, title: "Warning") 84 | .errorToast(isVisible: $isVisible, title: "Error") 85 | .infoToast(isVisible: $isVisible, title: "Info") 86 | ``` 87 | 88 | ##### with full parameters 89 | ```swift 90 | VStack { 91 | 92 | } 93 | .successToast(isVisible: $isVisible, title: "success full width", toastColor: .success, animation: .snappy, titleFontColor: .white, maxWidth: false) 94 | .warningToast(isVisible: $isVisible, title: "warning full width", toastColor: .warning, animation: .snappy, titleFontColor: .white, maxWidth: false) 95 | .errorToast(isVisible: $isVisible, title: "error full width", toastColor: .error, animation: .snappy, titleFontColor: .white, maxWidth: false) 96 | .infoToast(isVisible: $isVisible, title: "info full width", toastColor: .info, animation: .snappy, titleFontColor: .white, maxWidth: false) 97 | ``` 98 | 99 | ##### Configuration ⚙️ 100 | | Parameter | Type | Default Value | Description | 101 | | :----------------------| :--------------------- | :---------------------- | :---------------------------------- | 102 | | `isVisible ` | Binding | — | Binding to control visibility. | 103 | | `title` | String | — | The main message displayed in the toast. | 104 | | `toastColor` | ToastColorTypes | .success / .warning / .error / .info | The visual style or color theme of the toast . | 105 | | `animation` | Animation | .snappy | Animation used to present and dismiss the toast. | 106 | | `titleFontColor` | Color | .white | The color of the toast message text. | 107 | | `maxWidth` | Bool | false | Whether the toast should stretch to the maximum width. | 108 | 109 | --------- 110 | ### Bottom - Toasts 111 | ![bottom](https://github.com/user-attachments/assets/c9f086f6-13d4-42be-a008-ab1c81937674) 112 | 113 | ##### simple toast 114 | ```swift 115 | VStack { 116 | 117 | } 118 | .bottomToast(isVisible: $isVisible, title: "bottom") 119 | ``` 120 | 121 | ##### with full parameters 122 | ```swift 123 | VStack { 124 | 125 | } 126 | .bottomToast( 127 | isVisible: $isVisible, 128 | title: "bottom", 129 | toastColor: .custom(.indigo), 130 | animation: .bouncy, 131 | titleFontColor: .white, 132 | maxWidth: false 133 | ) 134 | ``` 135 | 136 | ##### Configuration ⚙️ 137 | 138 | | Parameter | Type | Default Value | Description | 139 | |------------------|-------------------|----------------|-------------| 140 | | `isVisible` | Binding | — | Binding to control visibility. | 141 | | `title` | String | — | The main message displayed in the toast. | 142 | | `toastColor` | ToastColorTypes | .success | The color type or style of the toast (`.success`, `.error`, `.info`, `.warning`, `.custom(Color)`). | 143 | | `animation` | Animation | .snappy | Animation used to present and dismiss the toast. | 144 | | `titleFontColor` | Color | .white | The color of the toast message text. | 145 | | `maxWidth` | Bool | false | If true, toast stretches to maximum available width. | 146 | ---------------- 147 | 148 | 149 | ### Edge Slide Toast - Toasts 150 | ![slide](https://github.com/user-attachments/assets/4fd902f8-3afe-4b36-8e20-0dfd0a7d4639) 151 | 152 | ##### simple toast 153 | ```swift 154 | VStack { 155 | 156 | } 157 | .edgeSlideToast(isVisible: $isVisible, title: "slide") 158 | ``` 159 | ##### with full parameters 160 | ```swift 161 | VStack { 162 | 163 | } 164 | .edgeSlideToast( 165 | isVisible: $isVisible, 166 | title: "slide", 167 | toastColor: .info, 168 | animation: .bouncy, 169 | hDirection: .leading, 170 | vDirection: .top, 171 | titleFontColor: .white, 172 | maxWidth: false 173 | ) 174 | ``` 175 | ##### Configuration ⚙️ 176 | 177 | | Parameter | Type | Default Value | Description | 178 | |------------------|-----------------------|----------------|-------------| 179 | | `isVisible` | Binding | — | Binding to control visibility. | 180 | | `title` | String | — | The main message displayed in the toast. | 181 | | `toastColor` | ToastColorTypes | .success | The color type or style of the toast (`.success`, `.error`, `.info`, `.warning`, `.custom(Color)`). | 182 | | `animation` | Animation | .snappy | Animation used to present and dismiss the toast. | 183 | | `hDirection` | HorizontalDirection | .trailing | Slide from horizontal edge (`.leading` or `.trailing`). | 184 | | `vDirection` | VerticalDirection | .top | Vertical position of the toast (`.top` or `.bottom`). | 185 | | `titleFontColor` | Color | .white | The color of the toast message text. | 186 | | `maxWidth` | Bool | false | If `true`, toast stretches to maximum available width. | 187 | ---------------- 188 | 189 | ### Toast with SF Symbol 190 | ![sf](https://github.com/user-attachments/assets/9c45e226-4193-4252-beb7-7c09b782ce08) 191 | 192 | ##### simple toast 193 | ```swift 194 | VStack { 195 | 196 | } 197 | .toastWithSFSymbol( 198 | isVisible: $isVisivble, 199 | title: "Toast with SF symbol", 200 | sfSymbolName: "sun.max" 201 | ) 202 | ``` 203 | ##### with full parameters 204 | ```swift 205 | VStack { 206 | 207 | } 208 | .toastWithSFSymbol( 209 | isVisible: $isVisivble, 210 | title: "Toast with SF symbol", 211 | toastColor: .custom(.indigo), 212 | titleFontColor: .white, 213 | sfSymbolName: "sun.max", 214 | sfSymbolSize: 18, 215 | sfSymbolColor: .black, 216 | transitionType: .scale, 217 | animation: .smooth, 218 | vDirection: .top, 219 | maxWidth: false, 220 | layoutDirection: .leftToRight 221 | ) 222 | ``` 223 | ##### Configuration ⚙️ 224 | 225 | | Parameter | Type | Default Value | Description | 226 | |-------------------|------------------------------|--------------------------------|-------------| 227 | | `isVisible` | Binding | — | Binding to control visibility. | 228 | | `title` | String | — | The main message displayed in the toast. | 229 | | `toastColor` | ToastColorTypes | .success | The color type or style of the toast (`.success`, `.error`, `.info`, `.warning`, `.custom(Color)`). | 230 | | `titleFontColor` | Color | .white | The color of the toast message text. | 231 | | `sfSymbolName` | String? | nil | Optional name of an SF Symbol. | 232 | | `sfSymbolSize` | CGFloat? | 24 | Size of the SF Symbol icon. | 233 | | `sfSymbolColor` | Color? | .white | Color of the SF Symbol icon. | 234 | | `transitionType` | ToastTransitionType | .move(edge: .top) | The transition animation for how the toast appears/disappears. | 235 | | `animation` | Animation | .snappy | Animation used to present and dismiss the toast. | 236 | | `vDirection` | VerticalDirection | .top | Vertical position of the toast (`.top` or `.bottom`). | 237 | | `maxWidth` | Bool | false | If `true`, toast takes maximum available width. | 238 | | `layoutDirection` | LayoutDirection | .leftToRight | Layout direction of content (`.leftToRight` or `.rightToLeft`). | 239 | ----------- 240 | 241 | ### Toast with custom icon or image 242 | ![custom](https://github.com/user-attachments/assets/cfb46343-79c0-48f8-847b-c292530fb580) 243 | 244 | ##### simple toast 245 | ```swift 246 | VStack { 247 | 248 | } 249 | .toastWithIcon( 250 | isVisible: $isVisivble, 251 | title: "with custom icon", 252 | iconName: "swift" 253 | ) 254 | ``` 255 | 256 | ##### with full parameters 257 | ```swift 258 | VStack { 259 | 260 | } 261 | .toastWithIcon( 262 | isVisible: $showWithCustomIconToast, 263 | title: "with custom icon", 264 | toastColor: .custom(.orange), 265 | iconName: "swift", 266 | iconSize: 18, 267 | iconColor: nil, 268 | transitionType: .move(edge: .top), 269 | animation: .bouncy, 270 | vDirection: .top, 271 | titleFontColor: .white, 272 | maxWidth: false, 273 | layoutDirection: .leftToRight 274 | ) 275 | ``` 276 | ##### Configuration ⚙️ 277 | 278 | | Parameter | Type | Default Value | Description | 279 | |-------------------|------------------------------|--------------------------------|-------------| 280 | | `isVisible` | Binding | — | Binding to control visibility. | 281 | | `title` | String | — | The main message displayed in the toast. | 282 | | `toastColor` | ToastColorTypes | .success | The color type or style of the toast (`.success`, `.error`, `.info`, `.warning`, `.custom(Color)`). | 283 | | `iconName` | String? | nil | Optional name of a custom icon (from asset). | 284 | | `iconSize` | CGFloat? | 24 | Size of the custom icon. | 285 | | `iconColor` | Color? | .white | Color of the custom icon. | 286 | | `transitionType` | ToastTransitionType | .move(edge: .top) | The transition animation for how the toast appears/disappears. | 287 | | `animation` | Animation | .snappy | Animation used to present and dismiss the toast. | 288 | | `vDirection` | VerticalDirection | .top | Vertical position of the toast (`.top` or `.bottom`). | 289 | | `titleFontColor` | Color | .white | The color of the toast message text. | 290 | | `maxWidth` | Bool | false | If `true`, toast takes maximum available width. | 291 | | `layoutDirection` | LayoutDirection | .leftToRight | Layout direction of content (`.leftToRight` or `.rightToLeft`). | 292 | 293 | ------------ 294 | 295 | ## 🍞 Toast Stack 296 | ![simplestack](https://github.com/user-attachments/assets/565c010b-55b6-4976-982e-0800e153f6f9) 297 | ![customstack](https://github.com/user-attachments/assets/4fafbb4c-9138-4239-9ced-2a3fa2005da1) 298 | 299 | ### With ToastStackManager, you can show toasts at the same time!  300 | 301 | #### Certainly, you can utilize toast stacks from your view model. Here’s an example: 302 | ```swift 303 | // in your ViewModel 304 | 305 | import ToastKit 306 | import Combine 307 | 308 | final class ViewModel: ObservableObject { 309 | let toastManager: ToastStackManager 310 | 311 | init(toastManager: ToastStackManager = ToastStackManager()) { 312 | self.toastManager = toastManager 313 | } 314 | 315 | @MainActor func foo() { 316 | // Your logic 317 | toastManager.show(title: "foo success toast", toastColor: .success, autoDisappearDuration: 3.0) 318 | 319 | // Your logic 320 | toastManager.show(title: "foo info toast", toastColor: .info) 321 | } 322 | } 323 | ``` 324 | 325 | #### in your view 326 | 327 | ```swift 328 | import ToastKit 329 | import SwiftUI 330 | 331 | struct ProfileView: View { 332 | @StateObject private var vm: ViewModel 333 | 334 | init(vm: ViewModel = ViewModel()) { 335 | _vm = StateObject(wrappedValue: vm) 336 | } 337 | 338 | var body: some View { 339 | ZStack { 340 | // Your view 341 | 342 | ToastStackView(vm: vm.toastManager) 343 | // Alternatively, you can utilize it with a custom transition. 344 | ToastStackView(vm: vm.toastManager, transitionType: .move(edge: .trailing).combined(with: .opacity)) 345 | } 346 | } 347 | } 348 | ``` 349 | 350 | ##### ToastStackView Configuration ⚙️ 351 | | Parameter | Type | Default Value | Description | 352 | |-------------------|------------------------------|--------------------------------|-------------| 353 | | `title` | String | - | The main message displayed in the toast. | 354 | | `toastColor` | ToastColorTypes | - | The color type or style of the toast. | 355 | | `autoDisappearDuration`| TimeInterval | 2.0 | Duration before toast disappears. | 356 | 357 | ##### ToastStackManager Configuration ⚙️ 358 | | Parameter | Type | Default Value | Description | 359 | |-------------------|------------------------------|--------------------------------|-------------| 360 | | `vm` | ToastStackManager | - | The view model that manages | 361 | | `transitionType`| AnyTransition | - | Transition animation for appearing/disappearing. | 362 | 363 | ----------- 364 | 365 | 366 | ## ⚠️ Alternatively, you can utilize the `.toast` method to construct a fully customizable toast by specifying the following parameters: 367 | 368 | ##### Configuration ⚙️ 369 | | Parameter | Type | Default Value | Description | 370 | |-------------------|------------------------------|--------------------------------|-------------| 371 | | `isVisible` | Binding | — | Binding to control visibility. | 372 | | `title` | String | — | The main toast message. | 373 | | `toastColor` | ToastColorTypes | `.success` | The color type or style of the toast. | 374 | | `transitionType` | ToastTransitionType | `.move(edge: .top)` | Transition animation for appearing/disappearing. | 375 | | `animation` | Animation | `.snappy` | Animation used to show/hide the toast. | 376 | | `autoDisappear` | Bool | `true` | If `true`, toast disappears automatically. | 377 | | `autoDisappearDuration`| TimeInterval | `2.0` | Duration before toast disappears. | 378 | | `maxWidth` | Bool | `false` | If `true`, toast takes maximum width. | 379 | | `subtitle` | String | `""` | Subtitle text for additional info. | 380 | | `font` | String | `"SFProDisplay"` | Name of the font used in text. | 381 | | `titleFontSize` | CGFloat | `16` | Font size of the title. | 382 | | `titleFontWeight` | Font.Weight | `.semibold` | Font weight of the title. | 383 | | `titleFontColor` | Color | `.white` | Font color of the title. | 384 | | `subtitleFontSize` | CGFloat | `14` | Font size of the subtitle. | 385 | | `subtitleFontWeight` | Font.Weight | `.regular` | Font weight of the subtitle. | 386 | | `subtitleFontColor` | Color | `.white` | Font color of the subtitle. | 387 | | `multilineTextAlignment`| TextAlignment | `.center` | Alignment of multiline text. | 388 | | `innerHpadding` | CGFloat | `20` | Inner horizontal padding. | 389 | | `innerVpadding` | CGFloat | `10` | Inner vertical padding. | 390 | | `outterHpadding` | CGFloat | `20` | Outer horizontal padding. | 391 | | `stackAligment` | Alignment | `.top` | Stack alignment inside the toast. | 392 | | `isStackMaxHeight` | Bool | `true` | occupies the maximum available height | 393 | | `cornerRadius` | CGFloat | `12` | Corner radius of the toast. | 394 | | `shadowColor` | Color | `.black.opacity(0.2)` | Shadow color. | 395 | | `shadowRadius` | CGFloat | `10` | Radius of the toast's shadow. | 396 | | `shadowX` | CGFloat | `0` | Horizontal offset of shadow. | 397 | | `shadowY` | CGFloat | `4` | Vertical offset of shadow. | 398 | | `withIcon` | Bool | `false` | Whether to show a custom icon. | 399 | | `iconName` | String? | `nil` | Name of the custom icon. | 400 | | `iconSize` | CGFloat? | `nil` | Size of the custom icon. | 401 | | `iconColor` | Color? | `nil` | Color of the custom icon. | 402 | | `withSfsymbol` | Bool | `false` | Whether to show an SF Symbol. | 403 | | `sfSymbolName` | String? | `nil` | Name of the SF Symbol. | 404 | | `sfSymbolSize` | CGFloat? | `nil` | Size of the SF Symbol. | 405 | | `sfSymbolColor` | Color? | `nil` | Color of the SF Symbol. | 406 | | `layoutDirection` | LayoutDirection | `.leftToRight` | Layout direction of content. | 407 | | `closeSFicon` | String | `"x.circle"` | SF Symbol used as close button. | 408 | | `closeSFiconSize` | CGFloat | `18` | Size of the close icon. | 409 | | `closeSFiconColor` | Color | `.white` | Color of the close icon. | 410 | ---- 411 | ## Installation via Swift Package Manager 🖥️ 412 | - Open your project. 413 | - Go to File → Add Package Dependencies. 414 | - Enter URL: https://github.com/Desp0o/ToastKit.git 415 | - Click Add Package. 416 | 417 | ## Contact 📬 418 | 419 | - Email: tornike.despotashvili@gmail.com 420 | - LinkedIn: https://www.linkedin.com/in/tornike-despotashvili-250150219/ 421 | - github: https://github.com/Desp0o 422 | 423 | 424 | -------------------------------------------------------------------------------- /Sources/ToastKit/Enums/ToastEnums.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ToastTransitionType.swift 3 | // ToastKit 4 | // 5 | // Created by Despo on 15.04.25. 6 | // 7 | 8 | import SwiftUI 9 | 10 | @available(macOS 14.0, *) 11 | @available(iOS 17, *) 12 | public enum ToastTransitionType { 13 | case fade 14 | case scale 15 | case slide 16 | case move(edge: Edge) 17 | case custom(AnyTransition) 18 | } 19 | 20 | @available(macOS 14.0, *) 21 | @available(iOS 17, *) 22 | public enum ToastColorTypes { 23 | case success 24 | case warning 25 | case error 26 | case info 27 | case custom(Color) 28 | case glass 29 | 30 | var value: Color { 31 | switch self { 32 | case .success: 33 | return .green 34 | case .warning: 35 | return .yellow 36 | case .error: 37 | return .red 38 | case .info: 39 | return .blue 40 | case .glass: 41 | return .clear 42 | case .custom(let color): 43 | return color 44 | } 45 | } 46 | } 47 | 48 | @available(macOS 14.0, *) 49 | @available(iOS 17, *) 50 | public enum HorizontalDirection { 51 | case leading 52 | case trailing 53 | 54 | var value: Edge { 55 | switch self { 56 | case .leading: 57 | return .leading 58 | case .trailing: 59 | return .trailing 60 | } 61 | } 62 | } 63 | 64 | @available(macOS 14.0, *) 65 | @available(iOS 17, *) 66 | public enum VerticalDirection { 67 | case top 68 | case bottom 69 | 70 | var value: Alignment { 71 | switch self { 72 | case .top: 73 | return .top 74 | case .bottom: 75 | return .bottom 76 | } 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /Sources/ToastKit/Extensions/Extension+View.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // ToastKit 4 | // 5 | // Created by Despo on 15.04.25. 6 | // 7 | 8 | import SwiftUI 9 | 10 | @available(macOS 14.0, *) 11 | @available(iOS 17.0, *) 12 | 13 | public extension View { 14 | @ViewBuilder func `if`(_ condition: Bool, transform: (Self) -> Content) -> some View { 15 | if condition { 16 | transform(self) 17 | } else { 18 | self 19 | } 20 | } 21 | } 22 | 23 | @available(macOS 14.0, *) 24 | @available(iOS 17, *) 25 | public extension View { 26 | func toast( 27 | isVisible: Binding, 28 | title: String, 29 | toastColor: ToastColorTypes = .success, 30 | transitionType: ToastTransitionType = .move(edge: .top), 31 | animation: Animation = .snappy, 32 | autoDisappear: Bool = true, 33 | autoDisappearDuration: TimeInterval = 2.0, 34 | maxWidth: Bool = false, 35 | subtitle: String = "", 36 | font: String = "SFProDisplay", 37 | titleFontSize: CGFloat = 16, 38 | titleFontWeight: Font.Weight = .semibold, 39 | titleFontColor: Color = .white, 40 | subtitleFontSize: CGFloat = 14, 41 | subtitleFontWeight: Font.Weight = .regular, 42 | subtitleFontColor: Color = .white, 43 | multilineTextAlignment: TextAlignment = .center, 44 | innerHpadding: CGFloat = 30, 45 | innerVpadding: CGFloat = 10, 46 | outterHpadding: CGFloat = 20, 47 | stackAligment: Alignment = .top, 48 | cornerRadius: CGFloat = 12, 49 | shadowColor: Color = .black.opacity(0.2), 50 | shadowRadius: CGFloat = 10, 51 | shadowX: CGFloat = 0, 52 | shadowY: CGFloat = 4, 53 | withIcon: Bool = false, 54 | iconName: String? = nil, 55 | iconSize: CGFloat? = nil, 56 | iconColor: Color? = nil, 57 | withSfsymbol: Bool = false, 58 | sfSymbolName: String? = "x.circle", 59 | sfSymbolSize: CGFloat? = 18, 60 | sfSymbolColor: Color? = .white, 61 | layoutDirection: LayoutDirection = .leftToRight, 62 | closeSFicon: String = "x.circle", 63 | closeSFiconSize: CGFloat = 18, 64 | closeSFiconColor: Color = .white, 65 | isGlass: Bool = false, 66 | glassColor: Color = .clear 67 | ) -> some View { 68 | modifier( 69 | ToastModifier( 70 | isVisible: isVisible, 71 | toast: CustomToast( 72 | isVisible: isVisible, 73 | title: title, 74 | toastColor: toastColor, 75 | transitionType: transitionType, 76 | animation: animation, 77 | autoDisappear: autoDisappear, 78 | autoDisappearDuration: autoDisappearDuration, 79 | maxWidth: maxWidth, 80 | subtitle: subtitle, 81 | font: font, 82 | titleFontSize: titleFontSize, 83 | titleFontWeight: titleFontWeight, 84 | titleFontColor: titleFontColor, 85 | subtitleFontSize: subtitleFontSize, 86 | subtitleFontWeight: subtitleFontWeight, 87 | subtitleFontColor: subtitleFontColor, 88 | multilineTextAlignment: multilineTextAlignment, 89 | innerHpadding: innerHpadding, 90 | innerVpadding: innerVpadding, 91 | outterHpadding: outterHpadding, 92 | stackAligment: stackAligment, 93 | cornerRadius: cornerRadius, 94 | shadowColor: shadowColor, 95 | shadowRadius: shadowRadius, 96 | shadowX: shadowX, 97 | shadowY: shadowY, 98 | withIcon: withIcon, 99 | iconName: iconName, 100 | iconSize: iconSize, 101 | iconColor: iconColor, 102 | withSfsymbol: withSfsymbol, 103 | sfSymbolName: sfSymbolName, 104 | sfSymbolSize: sfSymbolSize, 105 | sfSymbolColor: sfSymbolColor, 106 | layoutDirection: layoutDirection, 107 | closeSFicon: closeSFicon, 108 | closeSFiconSize: closeSFiconSize, 109 | closeSFiconColor: closeSFiconColor, 110 | isGlass: isGlass, 111 | glassColor: glassColor 112 | ) 113 | ) 114 | ) 115 | } 116 | } 117 | 118 | 119 | @available(macOS 14.0, *) 120 | @available(iOS 17, *) 121 | public extension View { 122 | func successToast( 123 | isVisible: Binding, 124 | title: String, 125 | toastColor: ToastColorTypes = .success, 126 | animation: Animation = .snappy, 127 | titleFontColor: Color = .white, 128 | maxWidth: Bool = false 129 | ) -> some View { 130 | toast( 131 | isVisible: isVisible, 132 | title: title, 133 | toastColor: toastColor, 134 | animation: animation, 135 | maxWidth: maxWidth, 136 | titleFontColor: titleFontColor 137 | ) 138 | } 139 | } 140 | 141 | 142 | @available(macOS 14.0, *) 143 | @available(iOS 17, *) 144 | public extension View { 145 | func warningToast( 146 | isVisible: Binding, 147 | title: String, 148 | toastColor: ToastColorTypes = .warning, 149 | animation: Animation = .snappy, 150 | titleFontColor: Color = .white, 151 | maxWidth: Bool = false 152 | ) -> some View { 153 | toast( 154 | isVisible: isVisible, 155 | title: title, 156 | toastColor: toastColor, 157 | animation: animation, 158 | maxWidth: maxWidth, 159 | titleFontColor: titleFontColor 160 | ) 161 | } 162 | } 163 | 164 | 165 | @available(macOS 14.0, *) 166 | @available(iOS 17, *) 167 | public extension View { 168 | func errorToast( 169 | isVisible: Binding, 170 | title: String, 171 | toastColor: ToastColorTypes = .error, 172 | animation: Animation = .snappy, 173 | titleFontColor: Color = .white, 174 | maxWidth: Bool = false 175 | ) -> some View { 176 | toast( 177 | isVisible: isVisible, 178 | title: title, 179 | toastColor: toastColor, 180 | animation: animation, 181 | maxWidth: maxWidth, 182 | titleFontColor: titleFontColor 183 | ) 184 | } 185 | } 186 | 187 | 188 | @available(macOS 14.0, *) 189 | @available(iOS 17, *) 190 | public extension View { 191 | func bottomToast( 192 | isVisible: Binding, 193 | title: String, 194 | toastColor: ToastColorTypes = .success, 195 | animation: Animation = .snappy, 196 | titleFontColor: Color = .white, 197 | maxWidth: Bool = false 198 | ) -> some View { 199 | toast( 200 | isVisible: isVisible, 201 | title: title, 202 | toastColor: toastColor, 203 | transitionType: .move(edge: .bottom), 204 | animation: animation, 205 | maxWidth: maxWidth, 206 | titleFontColor: titleFontColor, 207 | stackAligment: .bottom 208 | ) 209 | } 210 | } 211 | 212 | 213 | @available(macOS 14.0, *) 214 | @available(iOS 17, *) 215 | public extension View { 216 | func edgeSlideToast( 217 | isVisible: Binding, 218 | title: String, 219 | toastColor: ToastColorTypes = .success, 220 | animation: Animation = .snappy, 221 | hDirection: HorizontalDirection = .trailing, 222 | vDirection: VerticalDirection = .top, 223 | titleFontColor: Color = .white, 224 | maxWidth: Bool = false 225 | ) -> some View { 226 | toast( 227 | isVisible: isVisible, 228 | title: title, 229 | toastColor: toastColor, 230 | transitionType: .move(edge: hDirection.value), 231 | animation: animation, 232 | maxWidth: maxWidth, 233 | titleFontColor: titleFontColor, 234 | stackAligment: vDirection.value 235 | ) 236 | } 237 | } 238 | 239 | 240 | @available(macOS 14.0, *) 241 | @available(iOS 17, *) 242 | public extension View { 243 | func infoToast( 244 | isVisible: Binding, 245 | title: String, 246 | toastColor: ToastColorTypes = .info, 247 | animation: Animation = .snappy, 248 | titleFontColor: Color = .white, 249 | maxWidth: Bool = false 250 | ) -> some View { 251 | toast( 252 | isVisible: isVisible, 253 | title: title, 254 | toastColor: .info, 255 | animation: animation, 256 | maxWidth: maxWidth, 257 | titleFontColor: titleFontColor 258 | ) 259 | } 260 | } 261 | 262 | 263 | @available(macOS 14.0, *) 264 | @available(iOS 17, *) 265 | public extension View { 266 | func toastWithIcon( 267 | isVisible: Binding, 268 | title: String, 269 | toastColor: ToastColorTypes = .success, 270 | iconName: String?, 271 | iconSize: CGFloat? = 24, 272 | iconColor: Color? = nil, 273 | transitionType: ToastTransitionType = .move(edge: .top), 274 | animation: Animation = .snappy, 275 | vDirection: VerticalDirection = .top, 276 | titleFontColor: Color = .white, 277 | maxWidth: Bool = false 278 | ) -> some View { 279 | toast( 280 | isVisible: isVisible, 281 | title: title, 282 | toastColor: toastColor, 283 | transitionType: transitionType, 284 | animation: animation, 285 | maxWidth: maxWidth, 286 | titleFontColor: titleFontColor, 287 | stackAligment: vDirection.value, 288 | withIcon: true, 289 | iconName: iconName, 290 | iconSize: iconSize, 291 | iconColor: iconColor, 292 | ) 293 | } 294 | } 295 | 296 | 297 | @available(macOS 14.0, *) 298 | @available(iOS 17, *) 299 | public extension View { 300 | func toastWithSFSymbol( 301 | isVisible: Binding, 302 | title: String, 303 | toastColor: ToastColorTypes = .success, 304 | titleFontColor: Color = .white, 305 | sfSymbolName: String?, 306 | sfSymbolSize: CGFloat? = 24, 307 | sfSymbolColor: Color? = .white, 308 | transitionType: ToastTransitionType = .move(edge: .top), 309 | animation: Animation = .snappy, 310 | vDirection: VerticalDirection = .top, 311 | maxWidth: Bool = false, 312 | layoutDirection: LayoutDirection = .leftToRight 313 | ) -> some View { 314 | toast( 315 | isVisible: isVisible, 316 | title: title, 317 | toastColor: toastColor, 318 | transitionType: transitionType, 319 | animation: animation, 320 | maxWidth: maxWidth, 321 | titleFontColor: titleFontColor, 322 | stackAligment: vDirection.value, 323 | withSfsymbol: true, 324 | sfSymbolName: sfSymbolName, 325 | sfSymbolSize: sfSymbolSize, 326 | sfSymbolColor: sfSymbolColor, 327 | layoutDirection: layoutDirection 328 | ) 329 | } 330 | } 331 | 332 | 333 | @available(macOS 26.0, *) 334 | @available(iOS 26, *) 335 | public extension View { 336 | func glassToast( 337 | isVisible: Binding, 338 | title: String, 339 | subtitle: String = "", 340 | glassColor: Color = .clear, 341 | titleFontColor: Color = .white, 342 | subtitleFontColor: Color = .white, 343 | maxWidth: Bool = false, 344 | transitionType: ToastTransitionType = .move(edge: .top), 345 | animation: Animation = .snappy, 346 | vDirection: VerticalDirection = .top, 347 | layoutDirection: LayoutDirection = .leftToRight 348 | ) -> some View { 349 | modifier( 350 | ToastModifier( 351 | isVisible: isVisible, 352 | toast: CustomToast( 353 | isVisible: isVisible, 354 | title: title, 355 | toastColor: .glass, 356 | transitionType: transitionType, 357 | animation: animation, 358 | maxWidth: maxWidth, 359 | subtitle: subtitle, 360 | titleFontColor: titleFontColor, 361 | subtitleFontColor: subtitleFontColor, 362 | stackAligment: vDirection.value, 363 | layoutDirection: layoutDirection, isGlass: true, 364 | glassColor: glassColor 365 | ) 366 | ) 367 | ) 368 | } 369 | } 370 | -------------------------------------------------------------------------------- /Sources/ToastKit/ToastKit.swift: -------------------------------------------------------------------------------- 1 | // The Swift Programming Language 2 | // https://docs.swift.org/swift-book 3 | 4 | import SwiftUI 5 | 6 | @available(macOS 14.0, *) 7 | @available(iOS 17, *) 8 | public struct CustomToast: View { 9 | @State private var disappearTask: Task<(), Never>? 10 | @Binding var isVisible: Bool 11 | let title: String 12 | let toastColor: ToastColorTypes 13 | let transitionType: ToastTransitionType 14 | let animation: Animation 15 | let autoDisappear: Bool 16 | let autoDisappearDuration: TimeInterval 17 | let maxWidth: Bool 18 | 19 | let subtitle: String 20 | 21 | let font: String 22 | let titleFontSize: CGFloat 23 | let titleFontWeight: Font.Weight 24 | let titleFontColor: Color 25 | 26 | let subtitleFontSize: CGFloat 27 | let subtitleFontWeight: Font.Weight 28 | let subtitleFontColor: Color 29 | 30 | let multilineTextAlignment: TextAlignment 31 | 32 | let innerHpadding: CGFloat 33 | let innerVpadding: CGFloat 34 | let outterHpadding: CGFloat 35 | let stackAligment: Alignment 36 | let isStackMaxHeight: Bool 37 | 38 | let cornerRadius: CGFloat 39 | 40 | let shadowColor: Color 41 | let shadowRadius: CGFloat 42 | let shadowX: CGFloat 43 | let shadowY: CGFloat 44 | 45 | let withIcon: Bool 46 | let iconName: String? 47 | let iconSize: CGFloat? 48 | let iconColor: Color? 49 | 50 | let withSfsymbol: Bool 51 | let sfSymbolName: String? 52 | let sfSymbolSize: CGFloat? 53 | let sfSymbolColor: Color? 54 | 55 | let layoutDirection: LayoutDirection 56 | 57 | let closeSFicon: String 58 | let closeSFiconSize: CGFloat 59 | let closeSFiconColor: Color 60 | 61 | let isGlass: Bool 62 | let glassColor: Color 63 | 64 | init( 65 | isVisible: Binding, 66 | title: String, 67 | toastColor: ToastColorTypes = .success, 68 | transitionType: ToastTransitionType = .move(edge: .top), 69 | animation: Animation = .snappy, 70 | autoDisappear: Bool = true, 71 | autoDisappearDuration: TimeInterval = 2.0, 72 | maxWidth: Bool = false, 73 | 74 | subtitle: String = "", 75 | 76 | font: String = "SFProDisplay", 77 | titleFontSize: CGFloat = 16, 78 | titleFontWeight: Font.Weight = .regular, 79 | titleFontColor: Color = .white, 80 | 81 | subtitleFontSize: CGFloat = 14, 82 | subtitleFontWeight: Font.Weight = .regular, 83 | subtitleFontColor: Color = .white, 84 | 85 | multilineTextAlignment: TextAlignment = .center, 86 | 87 | innerHpadding: CGFloat = 20, 88 | innerVpadding: CGFloat = 10, 89 | outterHpadding: CGFloat = 20, 90 | stackAligment: Alignment = .top, 91 | isStackMaxHeight: Bool = true, 92 | 93 | cornerRadius: CGFloat = 12, 94 | 95 | shadowColor: Color = .black.opacity(0.2), 96 | shadowRadius: CGFloat = 10, 97 | shadowX: CGFloat = 0, 98 | shadowY: CGFloat = 4, 99 | 100 | withIcon: Bool = false, 101 | iconName: String? = nil, 102 | iconSize: CGFloat? = nil, 103 | iconColor: Color? = nil, 104 | 105 | withSfsymbol: Bool = false, 106 | sfSymbolName: String? = nil, 107 | sfSymbolSize: CGFloat? = nil, 108 | sfSymbolColor: Color? = nil, 109 | 110 | layoutDirection: LayoutDirection = .leftToRight, 111 | 112 | closeSFicon: String = "x.circle", 113 | closeSFiconSize: CGFloat = 18, 114 | closeSFiconColor: Color = .white, 115 | 116 | isGlass: Bool = false, 117 | glassColor: Color = .clear 118 | ) { 119 | _isVisible = isVisible 120 | self.title = title 121 | self.toastColor = toastColor 122 | self.transitionType = transitionType 123 | self.subtitle = subtitle 124 | self.autoDisappear = autoDisappear 125 | self.autoDisappearDuration = autoDisappearDuration 126 | self.animation = animation 127 | self.maxWidth = maxWidth 128 | 129 | self.font = font 130 | self.titleFontSize = titleFontSize 131 | self.titleFontWeight = titleFontWeight 132 | self.titleFontColor = titleFontColor 133 | 134 | self.subtitleFontSize = subtitleFontSize 135 | self.subtitleFontWeight = subtitleFontWeight 136 | self.subtitleFontColor = subtitleFontColor 137 | 138 | self.multilineTextAlignment = multilineTextAlignment 139 | 140 | self.innerHpadding = innerHpadding 141 | self.innerVpadding = innerVpadding 142 | self.outterHpadding = outterHpadding 143 | self.stackAligment = stackAligment 144 | self.isStackMaxHeight = isStackMaxHeight 145 | 146 | self.cornerRadius = cornerRadius 147 | 148 | self.shadowColor = shadowColor 149 | self.shadowRadius = shadowRadius 150 | self.shadowX = shadowX 151 | self.shadowY = shadowY 152 | 153 | self.withIcon = withIcon 154 | self.iconName = iconName 155 | self.iconSize = iconSize 156 | self.iconColor = iconColor 157 | 158 | self.withSfsymbol = withSfsymbol 159 | self.sfSymbolName = sfSymbolName 160 | self.sfSymbolSize = sfSymbolSize 161 | self.sfSymbolColor = sfSymbolColor 162 | 163 | self.layoutDirection = layoutDirection 164 | 165 | self.closeSFicon = closeSFicon 166 | self.closeSFiconSize = closeSFiconSize 167 | self.closeSFiconColor = closeSFiconColor 168 | 169 | self.isGlass = isGlass 170 | self.glassColor = glassColor 171 | } 172 | 173 | public var body: some View { 174 | ZStack(alignment: stackAligment) { 175 | if isVisible { 176 | HStack { 177 | if !withIcon && !withSfsymbol { 178 | VStack { 179 | Text(title) 180 | .font(.custom(font, size: titleFontSize)) 181 | .font(.system(size: titleFontSize)) 182 | .fontWeight(titleFontWeight) 183 | .foregroundStyle(titleFontColor) 184 | .multilineTextAlignment(multilineTextAlignment) 185 | 186 | if !subtitle.isEmpty { 187 | Text(subtitle) 188 | .font(.custom(font, size: subtitleFontSize)) 189 | .fontWeight(subtitleFontWeight) 190 | .foregroundStyle(subtitleFontColor) 191 | .multilineTextAlignment(multilineTextAlignment) 192 | } 193 | } 194 | } else { 195 | HStack(spacing: 20) { 196 | if withSfsymbol { 197 | Image(systemName: sfSymbolName ?? "") 198 | .renderingMode(.template) 199 | .resizable() 200 | .scaledToFit() 201 | .frame(width: sfSymbolSize, height: sfSymbolSize) 202 | .foregroundStyle(sfSymbolColor ?? .clear) 203 | } else { 204 | Image(iconName ?? "") 205 | .resizable() 206 | .renderingMode(iconColor != nil ? .template : .original) 207 | .scaledToFit() 208 | .foregroundStyle(iconColor ?? .clear) 209 | .frame(width: iconSize, height: iconSize) 210 | } 211 | 212 | Text(title) 213 | .font(.custom(font, size: titleFontSize)) 214 | .fontWeight(titleFontWeight) 215 | .foregroundStyle(titleFontColor) 216 | .multilineTextAlignment(multilineTextAlignment) 217 | } 218 | .environment(\.layoutDirection, layoutDirection) 219 | } 220 | } 221 | .padding(.horizontal, innerHpadding) 222 | .padding(.vertical, innerVpadding) 223 | .if(maxWidth) { $0.frame(maxWidth: .infinity)} 224 | .background(toastColor.value) 225 | .clipShape(RoundedRectangle(cornerRadius: cornerRadius)) 226 | .transition(transition(for: transitionType)) 227 | .shadow(color: shadowColor, radius: shadowRadius, x: shadowX, y: shadowY) 228 | .overlay { 229 | if !autoDisappear { 230 | ZStack { 231 | Button { 232 | isVisible = false 233 | } label: { 234 | Image(systemName: closeSFicon) 235 | .renderingMode(.template) 236 | .resizable() 237 | .scaledToFit() 238 | .frame(width: closeSFiconSize, height: closeSFiconSize) 239 | .foregroundStyle(closeSFiconColor) 240 | } 241 | .offset(x: -7, y: 10) 242 | } 243 | .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topTrailing) 244 | } 245 | } 246 | .padding(.horizontal, outterHpadding) 247 | .background { 248 | if #available(iOS 26.0, *), #available(macOS 26.0, *), isGlass { 249 | Color.clear.glassEffect(.regular.tint(glassColor)) 250 | } 251 | } 252 | } 253 | } 254 | .frame(maxWidth: .infinity, alignment: stackAligment) 255 | .if(isStackMaxHeight) { $0.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: stackAligment)} 256 | .onChange(of: isVisible) { _, newValue in 257 | if newValue { 258 | disappearTask?.cancel() 259 | if autoDisappear { 260 | disappearTask = Task { 261 | try? await Task.sleep(nanoseconds: UInt64(autoDisappearDuration * 1_000_000_000)) 262 | if !Task.isCancelled { 263 | isVisible = false 264 | } 265 | } 266 | } 267 | } else { 268 | disappearTask?.cancel() 269 | disappearTask = nil 270 | } 271 | } 272 | .animation(animation, value: isVisible) 273 | } 274 | 275 | func transition(for type: ToastTransitionType) -> AnyTransition { 276 | switch type { 277 | case .fade: 278 | return .opacity 279 | case .scale: 280 | return .scale 281 | case .slide: 282 | return .slide 283 | case .move(let edge): 284 | return .move(edge: edge).combined(with: .opacity) 285 | case .custom(let transition): 286 | return transition 287 | } 288 | } 289 | } 290 | -------------------------------------------------------------------------------- /Sources/ToastKit/ToastModifier.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ToastModifier.swift 3 | // ToastKit 4 | // 5 | // Created by Despo on 16.04.25. 6 | // 7 | 8 | import SwiftUI 9 | 10 | @available(macOS 14.0, *) 11 | @available(iOS 17.0, *) 12 | public struct ToastModifier: ViewModifier { 13 | @Binding var isVisible: Bool 14 | let toast: CustomToast 15 | 16 | public func body(content: Content) -> some View { 17 | content 18 | .overlay { 19 | toast 20 | } 21 | } 22 | } 23 | 24 | -------------------------------------------------------------------------------- /Sources/ToastKit/ToastStack/ToastItemModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ToastItemModel.swift 3 | // ToastKit 4 | // 5 | // Created by Despo on 18.04.25. 6 | // 7 | 8 | import SwiftUI 9 | 10 | @available(macOS 14.0, *) 11 | @available(iOS 17, *) 12 | public struct ToastItemModel: Identifiable, Equatable { 13 | public let id = UUID() 14 | let title: String 15 | let toastColor: ToastColorTypes 16 | let autoDisappearDuration: TimeInterval 17 | let isStackMaxHeight: Bool = false 18 | 19 | public static func == (lhs: ToastItemModel, rhs: ToastItemModel) -> Bool { 20 | return lhs.id == rhs.id 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /Sources/ToastKit/ToastStack/ToastStackManager.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ToastStackManager.swift 3 | // ToastKit 4 | // 5 | // Created by Despo on 18.04.25. 6 | // 7 | import SwiftUI 8 | 9 | @available(macOS 14.0, *) 10 | @available(iOS 17, *) 11 | 12 | public class ToastStackManager: ObservableObject { 13 | @Published var toasts: [ToastItemModel] = [] 14 | 15 | public init() { } 16 | 17 | @MainActor public func show(title: String, toastColor: ToastColorTypes, autoDisappearDuration: TimeInterval = 2.0) { 18 | let toast = ToastItemModel( 19 | title: title, 20 | toastColor: toastColor, 21 | autoDisappearDuration: autoDisappearDuration 22 | ) 23 | 24 | toasts.insert(toast, at: 0) 25 | 26 | Task { 27 | try await Task.sleep(nanoseconds: UInt64(autoDisappearDuration * 1_000_000_000)) 28 | self.removeToast(toast) 29 | } 30 | } 31 | 32 | func removeToast(_ toast: ToastItemModel) { 33 | toasts.removeAll { $0.id == toast.id } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /Sources/ToastKit/ToastStack/ToastStackView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ToastStackView.swift 3 | // ToastKit 4 | // 5 | // Created by Despo on 18.04.25. 6 | // 7 | 8 | import SwiftUI 9 | 10 | @available(macOS 14.0, *) 11 | @available(iOS 17, *) 12 | public struct ToastStackView: View { 13 | @StateObject var vm: ToastStackManager 14 | let transitionType: AnyTransition 15 | let isGlass: Bool 16 | let glassColor: Color 17 | 18 | public init( 19 | vm: ToastStackManager, 20 | transitionType: AnyTransition = .move(edge: .top).combined(with: .opacity), 21 | isGlass: Bool = false, 22 | glassColor: Color = .clear 23 | ) { 24 | _vm = StateObject(wrappedValue: vm) 25 | self.transitionType = transitionType 26 | self.isGlass = isGlass 27 | self.glassColor = glassColor 28 | } 29 | 30 | public var body: some View { 31 | VStack { 32 | ForEach(vm.toasts, id: \.id) { toast in 33 | ZStack { 34 | if #available(iOS 26.0, *), isGlass { 35 | CustomToast( 36 | isVisible: .constant(true), 37 | title: toast.title, 38 | toastColor: toast.toastColor, 39 | isStackMaxHeight: toast.isStackMaxHeight, 40 | isGlass: isGlass, 41 | glassColor: glassColor 42 | ) 43 | } else { 44 | CustomToast( 45 | isVisible: .constant(true), 46 | title: toast.title, 47 | toastColor: toast.toastColor, 48 | isStackMaxHeight: toast.isStackMaxHeight 49 | ) 50 | } 51 | } 52 | .transition(transitionType) 53 | } 54 | } 55 | .frame(maxWidth: .infinity, maxHeight: .infinity,alignment: .top) 56 | .animation(.bouncy, value: vm.toasts) 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /Tests/ToastKitTests/EnumsTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // EnumsTests.swift 3 | // ToastKit 4 | // 5 | // Created by Despo on 18.04.25. 6 | // 7 | 8 | import XCTest 9 | @testable import ToastKit 10 | 11 | final class ToastColorTypesTests: XCTestCase { 12 | func testSuccessType() { 13 | XCTAssertEqual(ToastColorTypes.success.value, .green) 14 | } 15 | 16 | func testWarninType() { 17 | XCTAssertEqual(ToastColorTypes.warning.value, .yellow) 18 | } 19 | 20 | func testErrorType() { 21 | XCTAssertEqual(ToastColorTypes.error.value, .red) 22 | } 23 | 24 | func testInfoType() { 25 | XCTAssertEqual(ToastColorTypes.info.value, .blue) 26 | } 27 | 28 | func testCustomType() { 29 | XCTAssertEqual(ToastColorTypes.custom(.teal).value, .teal) 30 | } 31 | } 32 | 33 | final class ToastDirectionsTests: XCTestCase { 34 | func testLeadingDirection() { 35 | XCTAssertEqual(HorizontalDirection.leading.value, .leading) 36 | } 37 | 38 | func testTrailinDirection() { 39 | XCTAssertEqual(HorizontalDirection.trailing.value, .trailing) 40 | } 41 | } 42 | 43 | final class VerticalDirectionTests: XCTestCase { 44 | func testTopDirection() { 45 | XCTAssertEqual(VerticalDirection.top.value, .top) 46 | } 47 | 48 | func testBottomDirection() { 49 | XCTAssertEqual(VerticalDirection.bottom.value, .bottom) 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /Tests/ToastKitTests/ToastStackTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import ToastKit 3 | 4 | final class ToastStackTests: XCTestCase { 5 | private var sut: ToastStackManager! 6 | 7 | override func setUpWithError() throws { 8 | sut = ToastStackManager() 9 | } 10 | 11 | override func tearDownWithError() throws { 12 | sut = nil 13 | } 14 | 15 | @MainActor func testAddToastToStack() throws { 16 | //Given 17 | XCTAssertTrue(sut.toasts.isEmpty) 18 | 19 | //When 20 | sut.show(title: "test", toastColor: .success, autoDisappearDuration: 2) 21 | 22 | //Then 23 | XCTAssertEqual(sut.toasts.count, 1) 24 | } 25 | 26 | @MainActor func testToastDissapearWithDuration() async throws { 27 | // Given 28 | sut.show(title: "Auto Disappear", toastColor: .info, autoDisappearDuration: 2.0) 29 | XCTAssertEqual(sut.toasts.count, 1) 30 | 31 | Task { 32 | // When 33 | try await Task.sleep(nanoseconds: 2_000_000_000) 34 | 35 | // Then 36 | XCTAssertEqual(sut.toasts.count, 0) 37 | } 38 | } 39 | 40 | func testRemoveToastFromToasts() throws { 41 | //Given 42 | let toast1 = ToastItemModel(title: "One", toastColor: .info, autoDisappearDuration: 2.0) 43 | sut.toasts = [toast1] 44 | 45 | XCTAssertEqual(sut.toasts.count, 1) 46 | 47 | //When 48 | sut.removeToast(toast1) 49 | 50 | //Then 51 | XCTAssertTrue(sut.toasts.isEmpty) 52 | } 53 | } 54 | --------------------------------------------------------------------------------