@capgo/capacitor-pretty-toast 8.1.8 → 8.1.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -36,6 +36,24 @@ class PassThroughWindow: UIWindow, ObservableObject {
|
|
|
36
36
|
return frame.insetBy(dx: -8, dy: -8).contains(point)
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
+
func resolvedSafeAreaInsets(geometryInsets: EdgeInsets) -> UIEdgeInsets {
|
|
40
|
+
let geometrySafeArea = UIEdgeInsets(
|
|
41
|
+
top: geometryInsets.top,
|
|
42
|
+
left: geometryInsets.leading,
|
|
43
|
+
bottom: geometryInsets.bottom,
|
|
44
|
+
right: geometryInsets.trailing
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
guard let scene = windowScene else {
|
|
48
|
+
return mergedSafeAreaInsets(geometrySafeArea, safeAreaInsets)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return scene.windows
|
|
52
|
+
.filter { $0 !== self && !$0.isHidden }
|
|
53
|
+
.map(\.safeAreaInsets)
|
|
54
|
+
.reduce(mergedSafeAreaInsets(geometrySafeArea, safeAreaInsets), mergedSafeAreaInsets)
|
|
55
|
+
}
|
|
56
|
+
|
|
39
57
|
// MARK: - Backdrop sampling
|
|
40
58
|
|
|
41
59
|
func startBackdropSampling() {
|
|
@@ -137,3 +155,12 @@ class PassThroughWindow: UIWindow, ObservableObject {
|
|
|
137
155
|
}
|
|
138
156
|
}
|
|
139
157
|
}
|
|
158
|
+
|
|
159
|
+
private func mergedSafeAreaInsets(_ lhs: UIEdgeInsets, _ rhs: UIEdgeInsets) -> UIEdgeInsets {
|
|
160
|
+
UIEdgeInsets(
|
|
161
|
+
top: max(lhs.top, rhs.top),
|
|
162
|
+
left: max(lhs.left, rhs.left),
|
|
163
|
+
bottom: max(lhs.bottom, rhs.bottom),
|
|
164
|
+
right: max(lhs.right, rhs.right)
|
|
165
|
+
)
|
|
166
|
+
}
|
|
@@ -5,26 +5,17 @@ struct PrettyToastView: View {
|
|
|
5
5
|
@State private var measuredContentHeight: CGFloat = 0
|
|
6
6
|
|
|
7
7
|
var body: some View {
|
|
8
|
-
GeometryReader {
|
|
9
|
-
let safeArea =
|
|
10
|
-
let
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
// Nudge up 0.5pt so the centered stroke clears the DI's top line
|
|
17
|
-
// instead of sitting half-behind it.
|
|
18
|
-
let expandedTopOffset: CGFloat = topOffset - 0.5
|
|
19
|
-
|
|
20
|
-
let expandedWidth = size.width - (topOffset * 2)
|
|
21
|
-
let baseHeight: CGFloat = haveDynamicIsland ? 90 : 70
|
|
22
|
-
let baseContentArea: CGFloat = haveDynamicIsland ? (baseHeight - dynamicIslandHeight - 12) : (baseHeight - 20)
|
|
23
|
-
let overflow = max(0, measuredContentHeight - baseContentArea)
|
|
24
|
-
let expandedHeight: CGFloat = baseHeight + overflow
|
|
8
|
+
GeometryReader { proxy in
|
|
9
|
+
let safeArea = window.resolvedSafeAreaInsets(geometryInsets: proxy.safeAreaInsets)
|
|
10
|
+
let layout = PrettyToastLayout(
|
|
11
|
+
size: proxy.size,
|
|
12
|
+
safeAreaTop: safeArea.top,
|
|
13
|
+
measuredContentHeight: measuredContentHeight,
|
|
14
|
+
useDynamicIsland: window.useDynamicIsland
|
|
15
|
+
)
|
|
25
16
|
|
|
26
|
-
let scaleX: CGFloat = isExpanded ? 1 : (
|
|
27
|
-
let scaleY: CGFloat = isExpanded ? 1 : (
|
|
17
|
+
let scaleX: CGFloat = isExpanded ? 1 : (layout.compactWidth / layout.expandedWidth)
|
|
18
|
+
let scaleY: CGFloat = isExpanded ? 1 : (layout.compactHeight / layout.expandedHeight)
|
|
28
19
|
|
|
29
20
|
let swipeGesture = DragGesture(minimumDistance: 2).onEnded { value in
|
|
30
21
|
if value.translation.height < -8 || value.predictedEndTranslation.height < -40 {
|
|
@@ -36,17 +27,17 @@ struct PrettyToastView: View {
|
|
|
36
27
|
Group {
|
|
37
28
|
let pill = toastBackground()
|
|
38
29
|
.overlay {
|
|
39
|
-
toastContent(
|
|
40
|
-
.frame(width: expandedWidth, height: expandedHeight)
|
|
30
|
+
toastContent(layout)
|
|
31
|
+
.frame(width: layout.expandedWidth, height: layout.expandedHeight)
|
|
41
32
|
.scaleEffect(x: scaleX, y: scaleY)
|
|
42
33
|
}
|
|
43
34
|
.frame(
|
|
44
|
-
width: isExpanded ? expandedWidth :
|
|
45
|
-
height: isExpanded ? expandedHeight :
|
|
35
|
+
width: isExpanded ? layout.expandedWidth : layout.compactWidth,
|
|
36
|
+
height: isExpanded ? layout.expandedHeight : layout.compactHeight
|
|
46
37
|
)
|
|
47
|
-
.opacity(
|
|
38
|
+
.opacity(layout.hasDynamicIsland ? 1 : (isExpanded ? 1 : 0))
|
|
48
39
|
.modifier(CapsuleOpacityModifier(
|
|
49
|
-
haveDynamicIsland:
|
|
40
|
+
haveDynamicIsland: layout.hasDynamicIsland,
|
|
50
41
|
isExpanded: isExpanded
|
|
51
42
|
))
|
|
52
43
|
.modifier(GeometryGroupModifier())
|
|
@@ -74,10 +65,10 @@ struct PrettyToastView: View {
|
|
|
74
65
|
pill
|
|
75
66
|
}
|
|
76
67
|
}
|
|
77
|
-
.offset(y:
|
|
68
|
+
.offset(y: layout.hasDynamicIsland ? (isExpanded ? layout.expandedTopOffset : layout.topOffset) : 0)
|
|
78
69
|
}
|
|
79
70
|
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top)
|
|
80
|
-
.padding(.top,
|
|
71
|
+
.padding(.top, layout.hasDynamicIsland ? 0 : (isExpanded ? max(safeArea.top, 10) : 0))
|
|
81
72
|
.ignoresSafeArea()
|
|
82
73
|
.coordinateSpace(name: "overlayWindow")
|
|
83
74
|
.animation(.bouncy(duration: 0.3, extraBounce: 0), value: isExpanded)
|
|
@@ -85,83 +76,79 @@ struct PrettyToastView: View {
|
|
|
85
76
|
}
|
|
86
77
|
|
|
87
78
|
@ViewBuilder
|
|
88
|
-
func toastContent(_
|
|
79
|
+
func toastContent(_ layout: PrettyToastLayout) -> some View {
|
|
89
80
|
if let toast = window.toast {
|
|
90
81
|
VStack(spacing: 0) {
|
|
91
|
-
if
|
|
92
|
-
Spacer(minLength:
|
|
82
|
+
if layout.contentTopClearance > 0 {
|
|
83
|
+
Spacer(minLength: layout.contentTopClearance)
|
|
93
84
|
}
|
|
94
85
|
|
|
95
|
-
|
|
96
|
-
ToastIconView(toast: toast, isExpanded: isExpanded)
|
|
97
|
-
.frame(width: 50)
|
|
98
|
-
|
|
99
|
-
VStack(alignment: .leading, spacing: 4) {
|
|
100
|
-
Text(toast.title)
|
|
101
|
-
.font(.callout)
|
|
102
|
-
.fontWeight(.semibold)
|
|
103
|
-
.foregroundStyle(.white)
|
|
104
|
-
|
|
105
|
-
if !toast.message.isEmpty {
|
|
106
|
-
Text(toast.message)
|
|
107
|
-
.font(.caption)
|
|
108
|
-
.foregroundColor(.white.opacity(0.6))
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
.frame(maxWidth: .infinity, alignment: .leading)
|
|
112
|
-
|
|
113
|
-
if let label = toast.actionLabel, !label.isEmpty {
|
|
114
|
-
Button(action: { window.actionTapped = true }) {
|
|
115
|
-
Text(label)
|
|
116
|
-
.font(.footnote)
|
|
117
|
-
.fontWeight(.semibold)
|
|
118
|
-
.foregroundStyle(toast.accentColor)
|
|
119
|
-
.padding(.horizontal, 12)
|
|
120
|
-
.padding(.vertical, 6)
|
|
121
|
-
.background(
|
|
122
|
-
Capsule().fill(Color.white.opacity(0.12))
|
|
123
|
-
)
|
|
124
|
-
}
|
|
125
|
-
.buttonStyle(.plain)
|
|
126
|
-
}
|
|
127
|
-
}
|
|
86
|
+
toastRow(toast)
|
|
128
87
|
}
|
|
129
88
|
.padding(.horizontal, 20)
|
|
130
|
-
.padding(.bottom,
|
|
89
|
+
.padding(.bottom, layout.contentBottomPadding)
|
|
131
90
|
.compositingGroup()
|
|
132
91
|
.blur(radius: isExpanded ? 0 : 5)
|
|
133
92
|
.opacity(isExpanded ? 1 : 0)
|
|
134
93
|
|
|
135
94
|
// Hidden measurer — drives measuredContentHeight so the pill grows
|
|
136
95
|
// for overflowing text.
|
|
137
|
-
|
|
138
|
-
|
|
96
|
+
toastRow(toast, isMeasuring: true)
|
|
97
|
+
.padding(.horizontal, 20)
|
|
98
|
+
.fixedSize(horizontal: false, vertical: true)
|
|
99
|
+
.background(
|
|
100
|
+
GeometryReader { geo in
|
|
101
|
+
Color.clear.preference(
|
|
102
|
+
key: ContentHeightKey.self,
|
|
103
|
+
value: geo.size.height
|
|
104
|
+
)
|
|
105
|
+
}
|
|
106
|
+
)
|
|
107
|
+
.hidden()
|
|
108
|
+
.onPreferenceChange(ContentHeightKey.self) { height in
|
|
109
|
+
measuredContentHeight = height
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
139
113
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
114
|
+
@ViewBuilder
|
|
115
|
+
private func toastRow(_ toast: Toast, isMeasuring: Bool = false) -> some View {
|
|
116
|
+
HStack(spacing: 10) {
|
|
117
|
+
ToastIconView(toast: toast, isExpanded: isMeasuring ? true : isExpanded)
|
|
118
|
+
.frame(width: 50)
|
|
144
119
|
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
120
|
+
VStack(alignment: .leading, spacing: 4) {
|
|
121
|
+
Text(toast.title)
|
|
122
|
+
.font(.callout)
|
|
123
|
+
.fontWeight(.semibold)
|
|
124
|
+
.foregroundStyle(.white)
|
|
125
|
+
|
|
126
|
+
if !toast.message.isEmpty {
|
|
127
|
+
Text(toast.message)
|
|
128
|
+
.font(.caption)
|
|
129
|
+
.foregroundColor(.white.opacity(0.6))
|
|
149
130
|
}
|
|
150
|
-
.frame(maxWidth: .infinity, alignment: .leading)
|
|
151
131
|
}
|
|
152
|
-
.
|
|
153
|
-
|
|
154
|
-
.
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
132
|
+
.frame(maxWidth: .infinity, alignment: .leading)
|
|
133
|
+
|
|
134
|
+
if let label = toast.actionLabel, !label.isEmpty {
|
|
135
|
+
Button(action: {
|
|
136
|
+
if !isMeasuring {
|
|
137
|
+
window.actionTapped = true
|
|
138
|
+
}
|
|
139
|
+
}, label: {
|
|
140
|
+
Text(label)
|
|
141
|
+
.font(.footnote)
|
|
142
|
+
.fontWeight(.semibold)
|
|
143
|
+
.foregroundStyle(toast.accentColor)
|
|
144
|
+
.padding(.horizontal, 12)
|
|
145
|
+
.padding(.vertical, 6)
|
|
146
|
+
.background(
|
|
147
|
+
Capsule().fill(Color.white.opacity(0.12))
|
|
148
|
+
)
|
|
149
|
+
})
|
|
150
|
+
.buttonStyle(.plain)
|
|
151
|
+
.allowsHitTesting(!isMeasuring)
|
|
165
152
|
}
|
|
166
153
|
}
|
|
167
154
|
}
|
|
@@ -220,6 +207,51 @@ struct PrettyToastView: View {
|
|
|
220
207
|
}
|
|
221
208
|
}
|
|
222
209
|
|
|
210
|
+
struct PrettyToastLayout {
|
|
211
|
+
static let dynamicIslandSafeAreaThreshold: CGFloat = 59
|
|
212
|
+
static let compactIslandWidth: CGFloat = 120
|
|
213
|
+
static let compactIslandHeight: CGFloat = 36
|
|
214
|
+
private static let dynamicIslandBaseHeight: CGFloat = 90
|
|
215
|
+
private static let standardBaseHeight: CGFloat = 70
|
|
216
|
+
private static let topOffsetBase: CGFloat = 11
|
|
217
|
+
private static let expandedTopNudge: CGFloat = -0.5
|
|
218
|
+
private static let dynamicIslandContentGap: CGFloat = 2
|
|
219
|
+
private static let dynamicIslandBottomPadding: CGFloat = 12
|
|
220
|
+
private static let standardContentInset: CGFloat = 20
|
|
221
|
+
|
|
222
|
+
let hasDynamicIsland: Bool
|
|
223
|
+
let compactWidth: CGFloat
|
|
224
|
+
let compactHeight: CGFloat
|
|
225
|
+
let topOffset: CGFloat
|
|
226
|
+
let expandedTopOffset: CGFloat
|
|
227
|
+
let expandedWidth: CGFloat
|
|
228
|
+
let expandedHeight: CGFloat
|
|
229
|
+
let contentTopClearance: CGFloat
|
|
230
|
+
let contentBottomPadding: CGFloat
|
|
231
|
+
let baseContentArea: CGFloat
|
|
232
|
+
|
|
233
|
+
init(size: CGSize, safeAreaTop: CGFloat, measuredContentHeight: CGFloat, useDynamicIsland: Bool) {
|
|
234
|
+
hasDynamicIsland = safeAreaTop >= Self.dynamicIslandSafeAreaThreshold && useDynamicIsland
|
|
235
|
+
compactWidth = Self.compactIslandWidth
|
|
236
|
+
compactHeight = Self.compactIslandHeight
|
|
237
|
+
topOffset = Self.topOffsetBase + max(safeAreaTop - Self.dynamicIslandSafeAreaThreshold, 0)
|
|
238
|
+
// Nudge up 0.5pt so the centered stroke clears the DI's top line
|
|
239
|
+
// instead of sitting half-behind it.
|
|
240
|
+
expandedTopOffset = topOffset + Self.expandedTopNudge
|
|
241
|
+
expandedWidth = max(1, size.width - (topOffset * 2))
|
|
242
|
+
|
|
243
|
+
let baseHeight = hasDynamicIsland ? Self.dynamicIslandBaseHeight : Self.standardBaseHeight
|
|
244
|
+
contentTopClearance = hasDynamicIsland ? Self.compactIslandHeight + Self.dynamicIslandContentGap : 0
|
|
245
|
+
contentBottomPadding = hasDynamicIsland ? Self.dynamicIslandBottomPadding : 0
|
|
246
|
+
let reservedContentInset = hasDynamicIsland
|
|
247
|
+
? contentTopClearance + contentBottomPadding
|
|
248
|
+
: Self.standardContentInset
|
|
249
|
+
baseContentArea = max(1, baseHeight - reservedContentInset)
|
|
250
|
+
let overflow = max(0, measuredContentHeight - baseContentArea)
|
|
251
|
+
expandedHeight = baseHeight + overflow
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
223
255
|
struct ToastIconView: View {
|
|
224
256
|
let toast: Toast
|
|
225
257
|
let isExpanded: Bool
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import XCTest
|
|
2
|
+
import CoreGraphics
|
|
2
3
|
@testable import PrettyToastPlugin
|
|
3
4
|
|
|
4
5
|
final class PrettyToastPluginTests: XCTestCase {
|
|
@@ -18,4 +19,50 @@ final class PrettyToastPluginTests: XCTestCase {
|
|
|
18
19
|
let color = PrettyToastColorParser.parse("#FF0000")
|
|
19
20
|
XCTAssertNotNil(color)
|
|
20
21
|
}
|
|
22
|
+
|
|
23
|
+
func testDynamicIslandLayoutReservesCutoutClearanceBeforeTextMeasurement() {
|
|
24
|
+
let layout = PrettyToastLayout(
|
|
25
|
+
size: CGSize(width: 402, height: 874),
|
|
26
|
+
safeAreaTop: 62,
|
|
27
|
+
measuredContentHeight: 0,
|
|
28
|
+
useDynamicIsland: true
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
XCTAssertTrue(layout.hasDynamicIsland)
|
|
32
|
+
XCTAssertGreaterThan(layout.contentTopClearance, PrettyToastLayout.compactIslandHeight)
|
|
33
|
+
XCTAssertEqual(layout.contentBottomPadding, 12)
|
|
34
|
+
XCTAssertGreaterThan(layout.baseContentArea, 0)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
func testDynamicIslandLayoutGrowsWithoutChangingCutoutClearance() {
|
|
38
|
+
let initialLayout = PrettyToastLayout(
|
|
39
|
+
size: CGSize(width: 402, height: 874),
|
|
40
|
+
safeAreaTop: 62,
|
|
41
|
+
measuredContentHeight: 0,
|
|
42
|
+
useDynamicIsland: true
|
|
43
|
+
)
|
|
44
|
+
let overflowingContentHeight = initialLayout.baseContentArea + 24
|
|
45
|
+
let measuredLayout = PrettyToastLayout(
|
|
46
|
+
size: CGSize(width: 402, height: 874),
|
|
47
|
+
safeAreaTop: 62,
|
|
48
|
+
measuredContentHeight: overflowingContentHeight,
|
|
49
|
+
useDynamicIsland: true
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
XCTAssertEqual(measuredLayout.contentTopClearance, initialLayout.contentTopClearance)
|
|
53
|
+
XCTAssertEqual(measuredLayout.expandedHeight, initialLayout.expandedHeight + 24, accuracy: 0.001)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
func testStandardLayoutDoesNotApplyDynamicIslandClearance() {
|
|
57
|
+
let layout = PrettyToastLayout(
|
|
58
|
+
size: CGSize(width: 390, height: 844),
|
|
59
|
+
safeAreaTop: 47,
|
|
60
|
+
measuredContentHeight: 0,
|
|
61
|
+
useDynamicIsland: true
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
XCTAssertFalse(layout.hasDynamicIsland)
|
|
65
|
+
XCTAssertEqual(layout.contentTopClearance, 0)
|
|
66
|
+
XCTAssertEqual(layout.contentBottomPadding, 0)
|
|
67
|
+
}
|
|
21
68
|
}
|
package/package.json
CHANGED