Skip to content

Commit 4cd95d2

Browse files
committed
Add ToastView and toast modifiers
1 parent 5c785cb commit 4cd95d2

File tree

7 files changed

+256
-35
lines changed

7 files changed

+256
-35
lines changed

Package.resolved

+23
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
{
2+
"pins" : [
3+
{
4+
"identity" : "swift-case-paths",
5+
"kind" : "remoteSourceControl",
6+
"location" : "https://github.com/pointfreeco/swift-case-paths",
7+
"state" : {
8+
"revision" : "b4a872984463070c71e2e97e5c02c73a07d0fe36",
9+
"version" : "0.9.0"
10+
}
11+
},
12+
{
13+
"identity" : "swiftui-navigation",
14+
"kind" : "remoteSourceControl",
15+
"location" : "https://github.com/pointfreeco/swiftui-navigation",
16+
"state" : {
17+
"revision" : "2694c03284a368168b3e0b8d7ab52626802d2246",
18+
"version" : "0.1.0"
19+
}
20+
}
21+
],
22+
"version" : 2
23+
}

Package.swift

+26-23
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,31 @@
1-
// swift-tools-version: 5.6
2-
// The swift-tools-version declares the minimum version of Swift required to build this package.
1+
// swift-tools-version: 5.5
32

43
import PackageDescription
54

65
let package = Package(
7-
name: "ToastUI",
8-
products: [
9-
// Products define the executables and libraries a package produces, and make them visible to other packages.
10-
.library(
11-
name: "ToastUI",
12-
targets: ["ToastUI"]),
13-
],
14-
dependencies: [
15-
// Dependencies declare other packages that this package depends on.
16-
// .package(url: /* package url */, from: "1.0.0"),
17-
],
18-
targets: [
19-
// Targets are the basic building blocks of a package. A target can define a module or a test suite.
20-
// Targets can depend on other targets in this package, and on products in packages this package depends on.
21-
.target(
22-
name: "ToastUI",
23-
dependencies: []),
24-
.testTarget(
25-
name: "ToastUITests",
26-
dependencies: ["ToastUI"]),
27-
]
6+
name: "ToastUI",
7+
platforms: [
8+
.iOS(.v15),
9+
.macOS(.v12),
10+
.tvOS(.v15),
11+
.watchOS(.v8),
12+
],
13+
products: [
14+
.library(
15+
name: "ToastUI",
16+
targets: ["ToastUI"])
17+
],
18+
dependencies: [
19+
.package(url: "https://github.com/pointfreeco/swiftui-navigation", from: "0.1.0")
20+
],
21+
targets: [
22+
.target(
23+
name: "ToastUI",
24+
dependencies: [
25+
.product(name: "SwiftUINavigation", package: "swiftui-navigation")
26+
]),
27+
.testTarget(
28+
name: "ToastUITests",
29+
dependencies: ["ToastUI"]),
30+
]
2831
)

Sources/ToastUI/ToastDefaults.swift

+8
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import SwiftUI
2+
3+
public enum ToastDefaults {
4+
public static var duration: TimeInterval = 2
5+
public static var tapToDismiss: Bool = true
6+
public static var position: ToastPosition = .bottom
7+
public static var offsetY: CGFloat = 0
8+
}

Sources/ToastUI/ToastModifier.swift

+139
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
import SwiftUI
2+
import SwiftUINavigation
3+
4+
public enum ToastPosition {
5+
case top, bottom
6+
}
7+
8+
struct ToastModifier<Toast: View>: ViewModifier {
9+
@Binding var isPresented: Bool
10+
let duration: TimeInterval
11+
let tapToDismiss: Bool
12+
let position: ToastPosition
13+
var offsetY: CGFloat
14+
@ViewBuilder var toast: Toast
15+
let completion: (() -> Void)?
16+
17+
func body(content: Content) -> some View {
18+
ZStack {
19+
content
20+
21+
main
22+
.offset(y: offsetY)
23+
.animation(.spring(), value: isPresented)
24+
}
25+
}
26+
27+
@ViewBuilder
28+
private var main: some View {
29+
if isPresented {
30+
VStack(spacing: 0) {
31+
if position == .bottom {
32+
Spacer()
33+
}
34+
toast
35+
if position == .top {
36+
Spacer()
37+
}
38+
}
39+
.task { @MainActor in
40+
try? await Task.sleep(nanoseconds: NSEC_PER_SEC * UInt64(duration))
41+
withAnimation(.spring()) {
42+
isPresented = false
43+
}
44+
}
45+
.onDisappear { completion?() }
46+
.transition(
47+
.move(edge: position == .top ? .top : .bottom)
48+
.combined(with: .opacity)
49+
)
50+
}
51+
}
52+
}
53+
54+
extension View {
55+
public func toast<Toast: View>(
56+
isPresented: Binding<Bool>,
57+
position: ToastPosition = ToastDefaults.position,
58+
duration: TimeInterval = ToastDefaults.duration,
59+
tapToDismiss: Bool = ToastDefaults.tapToDismiss,
60+
offsetY: CGFloat = ToastDefaults.offsetY,
61+
onDismiss: (() -> Void)? = nil,
62+
@ViewBuilder content: () -> Toast
63+
) -> some View {
64+
modifier(
65+
ToastModifier(
66+
isPresented: isPresented,
67+
duration: duration,
68+
tapToDismiss: tapToDismiss,
69+
position: position,
70+
offsetY: offsetY,
71+
toast: content,
72+
completion: onDismiss
73+
)
74+
)
75+
}
76+
77+
public func toast<Item, Toast: View>(
78+
item: Binding<Item?>,
79+
position: ToastPosition = ToastDefaults.position,
80+
duration: TimeInterval = ToastDefaults.duration,
81+
tapToDismiss: Bool = ToastDefaults.tapToDismiss,
82+
offsetY: CGFloat = ToastDefaults.offsetY,
83+
onDismiss: (() -> Void)? = nil,
84+
@ViewBuilder content: (Item) -> Toast
85+
) -> some View {
86+
self.toast(
87+
isPresented: item.isPresent(),
88+
position: position,
89+
duration: duration,
90+
tapToDismiss: tapToDismiss,
91+
offsetY: offsetY,
92+
onDismiss: onDismiss
93+
) {
94+
item.wrappedValue.map(content)
95+
}
96+
}
97+
98+
public func toast<Value, Toast: View>(
99+
unwrapping value: Binding<Value?>,
100+
position: ToastPosition = ToastDefaults.position,
101+
duration: TimeInterval = ToastDefaults.duration,
102+
tapToDismiss: Bool = ToastDefaults.tapToDismiss,
103+
offsetY: CGFloat = ToastDefaults.offsetY,
104+
onDismiss: (() -> Void)? = nil,
105+
@ViewBuilder content: (Binding<Value>) -> Toast
106+
) -> some View {
107+
self.toast(
108+
isPresented: value.isPresent(),
109+
position: position,
110+
duration: duration,
111+
tapToDismiss: tapToDismiss,
112+
offsetY: offsetY,
113+
onDismiss: onDismiss
114+
) {
115+
Binding(unwrapping: value).map(content)
116+
}
117+
}
118+
119+
public func toast<Enum, Case, Toast: View>(
120+
unwrapping enum: Binding<Enum?>,
121+
case casePath: CasePath<Enum, Case>,
122+
position: ToastPosition = ToastDefaults.position,
123+
duration: TimeInterval = ToastDefaults.duration,
124+
tapToDismiss: Bool = ToastDefaults.tapToDismiss,
125+
offsetY: CGFloat = ToastDefaults.offsetY,
126+
onDismiss: (() -> Void)? = nil,
127+
@ViewBuilder content: (Binding<Case>) -> Toast
128+
) -> some View {
129+
self.toast(
130+
unwrapping: `enum`.case(casePath),
131+
position: position,
132+
duration: duration,
133+
tapToDismiss: tapToDismiss,
134+
offsetY: offsetY,
135+
onDismiss: onDismiss,
136+
content: content
137+
)
138+
}
139+
}

Sources/ToastUI/ToastUI.swift

-6
This file was deleted.

Sources/ToastUI/ToastView.swift

+57
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import SwiftUI
2+
3+
public struct ToastView: View {
4+
public enum Style {
5+
case success
6+
case failure
7+
case warning
8+
case info
9+
case regular
10+
11+
var backgroundColor: Color {
12+
switch self {
13+
case .success: return .green
14+
case .failure: return .red
15+
case .warning: return .yellow
16+
case .info: return .blue
17+
case .regular: return .clear
18+
}
19+
}
20+
}
21+
22+
public let style: Style
23+
public let icon: Image?
24+
public let title: String
25+
public let subtitle: String?
26+
27+
public init(
28+
style: Style = .regular,
29+
icon: Image? = nil,
30+
title: String,
31+
subtitle: String? = nil
32+
) {
33+
self.style = style
34+
self.icon = icon
35+
self.title = title
36+
self.subtitle = subtitle
37+
}
38+
39+
public var body: some View {
40+
VStack(alignment: .leading, spacing: 12) {
41+
HStack(spacing: 8) {
42+
icon
43+
Text(title)
44+
}
45+
subtitle.map { Text($0) }
46+
}
47+
.font(.headline.bold())
48+
.multilineTextAlignment(.leading)
49+
.padding(12)
50+
#if !os(watchOS)
51+
.background(.regularMaterial)
52+
#endif
53+
.background(style.backgroundColor)
54+
.clipShape(RoundedRectangle(cornerRadius: 6, style: .continuous))
55+
.padding(16)
56+
}
57+
}

Tests/ToastUITests/ToastUITests.swift

+3-6
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,8 @@
11
import XCTest
2+
23
@testable import ToastUI
34

45
final class ToastUITests: XCTestCase {
5-
func testExample() throws {
6-
// This is an example of a functional test case.
7-
// Use XCTAssert and related functions to verify your tests produce the correct
8-
// results.
9-
XCTAssertEqual(ToastUI().text, "Hello, World!")
10-
}
6+
func testExample() throws {
7+
}
118
}

0 commit comments

Comments
 (0)