expo-live-activity 0.4.2 → 0.5.0-alpha1
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.
- package/Package.swift +16 -0
- package/README.md +60 -7
- package/build/index.d.ts +41 -5
- package/build/index.d.ts.map +1 -1
- package/build/index.js +57 -33
- package/build/index.js.map +1 -1
- package/ios/ExpoLiveActivityModule.swift +96 -8
- package/ios/LiveActivityAttributes.swift +10 -0
- package/ios-files/LiveActivityHelpers.swift +58 -0
- package/ios-files/LiveActivityMediumView.swift +127 -0
- package/ios-files/LiveActivitySmallView.swift +178 -0
- package/ios-files/LiveActivityView.swift +137 -108
- package/ios-files/LiveActivityWidget.swift +204 -10
- package/ios-files/ViewHelpers.swift +41 -0
- package/package.json +8 -4
- package/plugin/build/index.js +2 -0
- package/plugin/build/lib/getWidgetFiles.js +1 -1
- package/plugin/build/types.d.ts +1 -0
- package/plugin/build/withUnsupportedOS.d.ts +4 -0
- package/plugin/build/withUnsupportedOS.js +9 -0
- package/plugin/src/index.ts +3 -0
- package/plugin/src/lib/getWidgetFiles.ts +1 -1
- package/plugin/src/types.ts +1 -0
- package/plugin/src/withUnsupportedOS.ts +10 -0
- package/plugin/tsconfig.tsbuildinfo +1 -1
- package/src/index.ts +88 -33
- package/.prettierignore +0 -5
|
@@ -18,6 +18,8 @@ import WidgetKit
|
|
|
18
18
|
struct DebugLog: View {
|
|
19
19
|
#if DEBUG
|
|
20
20
|
private let message: String
|
|
21
|
+
|
|
22
|
+
@discardableResult
|
|
21
23
|
init(_ message: String) {
|
|
22
24
|
self.message = message
|
|
23
25
|
print(message)
|
|
@@ -29,11 +31,31 @@ import WidgetKit
|
|
|
29
31
|
.foregroundStyle(.red)
|
|
30
32
|
}
|
|
31
33
|
#else
|
|
34
|
+
@discardableResult
|
|
32
35
|
init(_: String) {}
|
|
33
36
|
var body: some View { EmptyView() }
|
|
34
37
|
#endif
|
|
35
38
|
}
|
|
36
39
|
|
|
40
|
+
struct SegmentedProgressView: View {
|
|
41
|
+
let currentStep: Int
|
|
42
|
+
let totalSteps: Int
|
|
43
|
+
let activeColor: Color
|
|
44
|
+
let inactiveColor: Color
|
|
45
|
+
var height: CGFloat = 4
|
|
46
|
+
|
|
47
|
+
var body: some View {
|
|
48
|
+
let clampedCurrentStep = min(max(currentStep, 0), totalSteps)
|
|
49
|
+
HStack(spacing: 4) {
|
|
50
|
+
ForEach(0 ..< totalSteps, id: \.self) { index in
|
|
51
|
+
RoundedRectangle(cornerRadius: 2)
|
|
52
|
+
.fill(index < clampedCurrentStep ? activeColor : inactiveColor)
|
|
53
|
+
.frame(height: height)
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
37
59
|
struct LiveActivityView: View {
|
|
38
60
|
let contentState: LiveActivityAttributes.ContentState
|
|
39
61
|
let attributes: LiveActivityAttributes
|
|
@@ -43,7 +65,7 @@ import WidgetKit
|
|
|
43
65
|
attributes.progressViewTint.map { Color(hex: $0) }
|
|
44
66
|
}
|
|
45
67
|
|
|
46
|
-
private var
|
|
68
|
+
private var imageVerticalAlignment: VerticalAlignment {
|
|
47
69
|
switch attributes.imageAlign {
|
|
48
70
|
case "center":
|
|
49
71
|
return .center
|
|
@@ -54,69 +76,142 @@ import WidgetKit
|
|
|
54
76
|
}
|
|
55
77
|
}
|
|
56
78
|
|
|
57
|
-
private
|
|
58
|
-
|
|
59
|
-
|
|
79
|
+
private var hasImage: Bool {
|
|
80
|
+
contentState.imageName != nil
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
private var isLeftImage: Bool {
|
|
84
|
+
(attributes.imagePosition ?? "right").hasPrefix("left")
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
private var isStretch: Bool {
|
|
88
|
+
(attributes.imagePosition ?? "right").contains("Stretch")
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
var body: some View {
|
|
92
|
+
if #available(iOS 18.0, *) {
|
|
93
|
+
LiveActivityView_iOS18(
|
|
94
|
+
contentState: contentState,
|
|
95
|
+
attributes: attributes,
|
|
96
|
+
imageContainerSize: $imageContainerSize,
|
|
97
|
+
smallView: {
|
|
98
|
+
LiveActivitySmallView(
|
|
99
|
+
contentState: contentState,
|
|
100
|
+
attributes: attributes,
|
|
101
|
+
imageContainerSize: $imageContainerSize,
|
|
102
|
+
alignedImage: { imageName, horizontalAlignment, isSmallView in
|
|
103
|
+
AnyView(alignedImage(imageName: imageName, horizontalAlignment: horizontalAlignment, isSmallView: isSmallView))
|
|
104
|
+
}
|
|
105
|
+
)
|
|
106
|
+
},
|
|
107
|
+
mediumView: {
|
|
108
|
+
LiveActivityMediumView(
|
|
109
|
+
contentState: contentState,
|
|
110
|
+
attributes: attributes,
|
|
111
|
+
imageContainerSize: $imageContainerSize,
|
|
112
|
+
alignedImage: { imageName, horizontalAlignment, isSmallView in
|
|
113
|
+
AnyView(alignedImage(imageName: imageName, horizontalAlignment: horizontalAlignment, isSmallView: isSmallView))
|
|
114
|
+
}
|
|
115
|
+
)
|
|
116
|
+
}
|
|
117
|
+
)
|
|
118
|
+
} else {
|
|
119
|
+
// iOS 17: missing activityFamily in environment -> default to medium
|
|
120
|
+
LiveActivityMediumView(
|
|
121
|
+
contentState: contentState,
|
|
122
|
+
attributes: attributes,
|
|
123
|
+
imageContainerSize: $imageContainerSize,
|
|
124
|
+
alignedImage: { imageName, horizontalAlignment, isSmallView in
|
|
125
|
+
AnyView(alignedImage(imageName: imageName, horizontalAlignment: horizontalAlignment, isSmallView: isSmallView))
|
|
126
|
+
}
|
|
127
|
+
)
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
private func alignedImage(imageName: String, horizontalAlignment: HorizontalAlignment, isSmallView: Bool = false) -> some View {
|
|
132
|
+
let defaultHeight: CGFloat = isSmallView ? 28 : 64
|
|
133
|
+
let defaultWidth: CGFloat = isSmallView ? 28 : 64
|
|
60
134
|
let containerHeight = imageContainerSize?.height
|
|
61
135
|
let containerWidth = imageContainerSize?.width
|
|
62
|
-
|
|
136
|
+
|
|
137
|
+
// For small view, check small-specific dimensions first, then fall back to global
|
|
138
|
+
let imageWidth = isSmallView ? (attributes.smallImageWidth ?? attributes.imageWidth) : attributes.imageWidth
|
|
139
|
+
let imageHeight = isSmallView ? (attributes.smallImageHeight ?? attributes.imageHeight) : attributes.imageHeight
|
|
140
|
+
let imageWidthPercent = isSmallView ? (attributes.smallImageWidthPercent ?? attributes.imageWidthPercent) : attributes.imageWidthPercent
|
|
141
|
+
let imageHeightPercent = isSmallView ? (attributes.smallImageHeightPercent ?? attributes.imageHeightPercent) : attributes.imageHeightPercent
|
|
142
|
+
|
|
143
|
+
let hasWidthConstraint = (imageWidthPercent != nil) || (imageWidth != nil)
|
|
63
144
|
|
|
64
145
|
let computedHeight: CGFloat? = {
|
|
65
|
-
if let percent =
|
|
146
|
+
if let percent = imageHeightPercent {
|
|
66
147
|
let clamped = min(max(percent, 0), 100) / 100.0
|
|
67
148
|
// Use the row height as a base. Fallback to default when row height is not measured yet.
|
|
68
149
|
let base = (containerHeight ?? defaultHeight)
|
|
69
150
|
return base * clamped
|
|
70
|
-
} else if let size =
|
|
151
|
+
} else if let size = imageHeight {
|
|
71
152
|
return CGFloat(size)
|
|
72
153
|
} else if hasWidthConstraint {
|
|
73
154
|
// Mimic CSS: when only width is set, keep height automatic to preserve aspect ratio
|
|
74
155
|
return nil
|
|
75
156
|
} else {
|
|
76
157
|
// Mimic CSS: this works against CSS but provides a better default behavior.
|
|
77
|
-
// When no width/height is set, use a default size (
|
|
158
|
+
// When no width/height is set, use a default size (64 / 28pt)
|
|
78
159
|
// Width will adjust automatically base on aspect ratio
|
|
79
160
|
return defaultHeight
|
|
80
161
|
}
|
|
81
162
|
}()
|
|
82
163
|
|
|
83
164
|
let computedWidth: CGFloat? = {
|
|
84
|
-
if let percent =
|
|
165
|
+
if let percent = imageWidthPercent {
|
|
85
166
|
let clamped = min(max(percent, 0), 100) / 100.0
|
|
86
167
|
let base = (containerWidth ?? defaultWidth)
|
|
87
168
|
return base * clamped
|
|
88
|
-
} else if let size =
|
|
169
|
+
} else if let size = imageWidth {
|
|
89
170
|
return CGFloat(size)
|
|
90
171
|
} else {
|
|
91
172
|
return nil // Keep aspect fit based on height
|
|
92
173
|
}
|
|
93
174
|
}()
|
|
94
175
|
|
|
176
|
+
let resolvedHeight = computedHeight ?? defaultHeight
|
|
177
|
+
|
|
178
|
+
let resolvedWidth: CGFloat? = {
|
|
179
|
+
if let w = computedWidth { return w }
|
|
180
|
+
|
|
181
|
+
if let uiImage = UIImage.dynamic(assetNameOrPath: imageName) {
|
|
182
|
+
let h = max(uiImage.size.height, 1)
|
|
183
|
+
let ratio = uiImage.size.width / h
|
|
184
|
+
return resolvedHeight * ratio
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
return nil
|
|
188
|
+
}()
|
|
189
|
+
|
|
95
190
|
return ZStack(alignment: .center) {
|
|
96
191
|
Group {
|
|
97
|
-
let fit = attributes.contentFit ?? "
|
|
192
|
+
let fit = attributes.contentFit ?? "contain"
|
|
98
193
|
switch fit {
|
|
99
194
|
case "contain":
|
|
100
|
-
Image.dynamic(assetNameOrPath: imageName).resizable().scaledToFit().frame(width:
|
|
195
|
+
Image.dynamic(assetNameOrPath: imageName).resizable().scaledToFit().frame(width: resolvedWidth, height: resolvedHeight)
|
|
101
196
|
case "fill":
|
|
102
197
|
Image.dynamic(assetNameOrPath: imageName).resizable().frame(
|
|
103
|
-
width:
|
|
104
|
-
height:
|
|
198
|
+
width: resolvedWidth,
|
|
199
|
+
height: resolvedHeight
|
|
105
200
|
)
|
|
106
201
|
case "none":
|
|
107
|
-
Image.dynamic(assetNameOrPath: imageName).renderingMode(.original).frame(width:
|
|
202
|
+
Image.dynamic(assetNameOrPath: imageName).renderingMode(.original).frame(width: resolvedWidth, height: resolvedHeight)
|
|
108
203
|
case "scale-down":
|
|
109
204
|
if let uiImage = UIImage.dynamic(assetNameOrPath: imageName) {
|
|
110
205
|
// Determine the target box. When width/height are nil, we use image's intrinsic dimension for comparison.
|
|
111
|
-
let targetHeight =
|
|
112
|
-
let targetWidth =
|
|
206
|
+
let targetHeight = resolvedHeight
|
|
207
|
+
let targetWidth = resolvedWidth ?? uiImage.size.width
|
|
113
208
|
let shouldScaleDown = uiImage.size.height > targetHeight || uiImage.size.width > targetWidth
|
|
114
209
|
|
|
115
210
|
if shouldScaleDown {
|
|
116
211
|
Image(uiImage: uiImage)
|
|
117
212
|
.resizable()
|
|
118
213
|
.scaledToFit()
|
|
119
|
-
.frame(width:
|
|
214
|
+
.frame(width: resolvedWidth, height: resolvedHeight)
|
|
120
215
|
} else {
|
|
121
216
|
Image(uiImage: uiImage)
|
|
122
217
|
.renderingMode(.original)
|
|
@@ -127,15 +222,19 @@ import WidgetKit
|
|
|
127
222
|
}
|
|
128
223
|
case "cover":
|
|
129
224
|
Image.dynamic(assetNameOrPath: imageName).resizable().scaledToFill().frame(
|
|
130
|
-
width:
|
|
131
|
-
height:
|
|
225
|
+
width: resolvedWidth,
|
|
226
|
+
height: resolvedHeight
|
|
132
227
|
).clipped()
|
|
133
228
|
default:
|
|
134
229
|
DebugLog("⚠️[ExpoLiveActivity] Unknown contentFit '\(fit)'")
|
|
135
230
|
}
|
|
136
231
|
}
|
|
137
232
|
}
|
|
138
|
-
.frame(
|
|
233
|
+
.frame(
|
|
234
|
+
maxWidth: .infinity,
|
|
235
|
+
maxHeight: .infinity,
|
|
236
|
+
alignment: Alignment(horizontal: horizontalAlignment, vertical: imageVerticalAlignment)
|
|
237
|
+
)
|
|
139
238
|
.background(
|
|
140
239
|
GeometryReader { proxy in
|
|
141
240
|
Color.clear
|
|
@@ -149,98 +248,28 @@ import WidgetKit
|
|
|
149
248
|
}
|
|
150
249
|
)
|
|
151
250
|
}
|
|
251
|
+
}
|
|
152
252
|
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
?? attributes.paddingDetails?.vertical
|
|
159
|
-
?? attributes.padding
|
|
160
|
-
?? defaultPadding
|
|
161
|
-
)
|
|
162
|
-
|
|
163
|
-
let bottom = CGFloat(
|
|
164
|
-
attributes.paddingDetails?.bottom
|
|
165
|
-
?? attributes.paddingDetails?.vertical
|
|
166
|
-
?? attributes.padding
|
|
167
|
-
?? defaultPadding
|
|
168
|
-
)
|
|
169
|
-
|
|
170
|
-
let leading = CGFloat(
|
|
171
|
-
attributes.paddingDetails?.left
|
|
172
|
-
?? attributes.paddingDetails?.horizontal
|
|
173
|
-
?? attributes.padding
|
|
174
|
-
?? defaultPadding
|
|
175
|
-
)
|
|
176
|
-
|
|
177
|
-
let trailing = CGFloat(
|
|
178
|
-
attributes.paddingDetails?.right
|
|
179
|
-
?? attributes.paddingDetails?.horizontal
|
|
180
|
-
?? attributes.padding
|
|
181
|
-
?? defaultPadding
|
|
182
|
-
)
|
|
183
|
-
|
|
184
|
-
VStack(alignment: .leading) {
|
|
185
|
-
let position = attributes.imagePosition ?? "right"
|
|
186
|
-
let isStretch = position.contains("Stretch")
|
|
187
|
-
let isLeftImage = position.hasPrefix("left")
|
|
188
|
-
let hasImage = contentState.imageName != nil
|
|
189
|
-
let effectiveStretch = isStretch && hasImage
|
|
190
|
-
|
|
191
|
-
HStack(alignment: .center) {
|
|
192
|
-
if hasImage, isLeftImage {
|
|
193
|
-
if let imageName = contentState.imageName {
|
|
194
|
-
alignedImage(imageName: imageName)
|
|
195
|
-
}
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
VStack(alignment: .leading, spacing: 2) {
|
|
199
|
-
Text(contentState.title)
|
|
200
|
-
.font(.title2)
|
|
201
|
-
.fontWeight(.semibold)
|
|
202
|
-
.modifier(ConditionalForegroundViewModifier(color: attributes.titleColor))
|
|
253
|
+
@available(iOS 18.0, *)
|
|
254
|
+
private struct LiveActivityView_iOS18<Small: View, Medium: View>: View {
|
|
255
|
+
let contentState: LiveActivityAttributes.ContentState
|
|
256
|
+
let attributes: LiveActivityAttributes
|
|
257
|
+
@Binding var imageContainerSize: CGSize?
|
|
203
258
|
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
.font(.title3)
|
|
207
|
-
.modifier(ConditionalForegroundViewModifier(color: attributes.subtitleColor))
|
|
208
|
-
}
|
|
259
|
+
let smallView: () -> Small
|
|
260
|
+
let mediumView: () -> Medium
|
|
209
261
|
|
|
210
|
-
|
|
211
|
-
if let date = contentState.timerEndDateInMilliseconds {
|
|
212
|
-
ProgressView(timerInterval: Date.toTimerInterval(miliseconds: date))
|
|
213
|
-
.tint(progressViewTint)
|
|
214
|
-
.modifier(ConditionalForegroundViewModifier(color: attributes.progressViewLabelColor))
|
|
215
|
-
} else if let progress = contentState.progress {
|
|
216
|
-
ProgressView(value: progress)
|
|
217
|
-
.tint(progressViewTint)
|
|
218
|
-
.modifier(ConditionalForegroundViewModifier(color: attributes.progressViewLabelColor))
|
|
219
|
-
}
|
|
220
|
-
}
|
|
221
|
-
}.layoutPriority(1)
|
|
262
|
+
@Environment(\.activityFamily) private var activityFamily
|
|
222
263
|
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
if !effectiveStretch {
|
|
232
|
-
if let date = contentState.timerEndDateInMilliseconds {
|
|
233
|
-
ProgressView(timerInterval: Date.toTimerInterval(miliseconds: date))
|
|
234
|
-
.tint(progressViewTint)
|
|
235
|
-
.modifier(ConditionalForegroundViewModifier(color: attributes.progressViewLabelColor))
|
|
236
|
-
} else if let progress = contentState.progress {
|
|
237
|
-
ProgressView(value: progress)
|
|
238
|
-
.tint(progressViewTint)
|
|
239
|
-
.modifier(ConditionalForegroundViewModifier(color: attributes.progressViewLabelColor))
|
|
240
|
-
}
|
|
241
|
-
}
|
|
264
|
+
var body: some View {
|
|
265
|
+
switch activityFamily {
|
|
266
|
+
case .small:
|
|
267
|
+
smallView()
|
|
268
|
+
case .medium:
|
|
269
|
+
mediumView()
|
|
270
|
+
@unknown default:
|
|
271
|
+
mediumView()
|
|
242
272
|
}
|
|
243
|
-
.padding(EdgeInsets(top: top, leading: leading, bottom: bottom, trailing: trailing))
|
|
244
273
|
}
|
|
245
274
|
}
|
|
246
275
|
|
|
@@ -2,14 +2,42 @@ import ActivityKit
|
|
|
2
2
|
import SwiftUI
|
|
3
3
|
import WidgetKit
|
|
4
4
|
|
|
5
|
-
struct LiveActivityAttributes: ActivityAttributes {
|
|
6
|
-
struct ContentState: Codable, Hashable {
|
|
5
|
+
public struct LiveActivityAttributes: ActivityAttributes {
|
|
6
|
+
public struct ContentState: Codable, Hashable {
|
|
7
7
|
var title: String
|
|
8
8
|
var subtitle: String?
|
|
9
9
|
var timerEndDateInMilliseconds: Double?
|
|
10
10
|
var progress: Double?
|
|
11
11
|
var imageName: String?
|
|
12
12
|
var dynamicIslandImageName: String?
|
|
13
|
+
var smallImageName: String?
|
|
14
|
+
var elapsedTimerStartDateInMilliseconds: Double?
|
|
15
|
+
var currentStep: Int?
|
|
16
|
+
var totalSteps: Int?
|
|
17
|
+
|
|
18
|
+
public init(
|
|
19
|
+
title: String,
|
|
20
|
+
subtitle: String? = nil,
|
|
21
|
+
timerEndDateInMilliseconds: Double? = nil,
|
|
22
|
+
progress: Double? = nil,
|
|
23
|
+
imageName: String? = nil,
|
|
24
|
+
dynamicIslandImageName: String? = nil,
|
|
25
|
+
smallImageName: String? = nil,
|
|
26
|
+
elapsedTimerStartDateInMilliseconds: Double? = nil,
|
|
27
|
+
currentStep: Int? = nil,
|
|
28
|
+
totalSteps: Int? = nil
|
|
29
|
+
) {
|
|
30
|
+
self.title = title
|
|
31
|
+
self.subtitle = subtitle
|
|
32
|
+
self.timerEndDateInMilliseconds = timerEndDateInMilliseconds
|
|
33
|
+
self.progress = progress
|
|
34
|
+
self.imageName = imageName
|
|
35
|
+
self.dynamicIslandImageName = dynamicIslandImageName
|
|
36
|
+
self.smallImageName = smallImageName
|
|
37
|
+
self.elapsedTimerStartDateInMilliseconds = elapsedTimerStartDateInMilliseconds
|
|
38
|
+
self.currentStep = currentStep
|
|
39
|
+
self.totalSteps = totalSteps
|
|
40
|
+
}
|
|
13
41
|
}
|
|
14
42
|
|
|
15
43
|
var name: String
|
|
@@ -27,27 +55,100 @@ struct LiveActivityAttributes: ActivityAttributes {
|
|
|
27
55
|
var imageHeight: Int?
|
|
28
56
|
var imageWidthPercent: Double?
|
|
29
57
|
var imageHeightPercent: Double?
|
|
58
|
+
var smallImageWidth: Int?
|
|
59
|
+
var smallImageHeight: Int?
|
|
60
|
+
var smallImageWidthPercent: Double?
|
|
61
|
+
var smallImageHeightPercent: Double?
|
|
30
62
|
var imageAlign: String?
|
|
31
63
|
var contentFit: String?
|
|
64
|
+
var progressSegmentActiveColor: String?
|
|
65
|
+
var progressSegmentInactiveColor: String?
|
|
32
66
|
|
|
33
|
-
|
|
67
|
+
public init(
|
|
68
|
+
name: String,
|
|
69
|
+
backgroundColor: String? = nil,
|
|
70
|
+
titleColor: String? = nil,
|
|
71
|
+
subtitleColor: String? = nil,
|
|
72
|
+
progressViewTint: String? = nil,
|
|
73
|
+
progressViewLabelColor: String? = nil,
|
|
74
|
+
deepLinkUrl: String? = nil,
|
|
75
|
+
timerType: DynamicIslandTimerType? = nil,
|
|
76
|
+
padding: Int? = nil,
|
|
77
|
+
paddingDetails: PaddingDetails? = nil,
|
|
78
|
+
imagePosition: String? = nil,
|
|
79
|
+
imageWidth: Int? = nil,
|
|
80
|
+
imageHeight: Int? = nil,
|
|
81
|
+
imageWidthPercent: Double? = nil,
|
|
82
|
+
imageHeightPercent: Double? = nil,
|
|
83
|
+
smallImageWidth: Int? = nil,
|
|
84
|
+
smallImageHeight: Int? = nil,
|
|
85
|
+
smallImageWidthPercent: Double? = nil,
|
|
86
|
+
smallImageHeightPercent: Double? = nil,
|
|
87
|
+
imageAlign: String? = nil,
|
|
88
|
+
contentFit: String? = nil,
|
|
89
|
+
progressSegmentActiveColor: String? = nil,
|
|
90
|
+
progressSegmentInactiveColor: String? = nil
|
|
91
|
+
) {
|
|
92
|
+
self.name = name
|
|
93
|
+
self.backgroundColor = backgroundColor
|
|
94
|
+
self.titleColor = titleColor
|
|
95
|
+
self.subtitleColor = subtitleColor
|
|
96
|
+
self.progressViewTint = progressViewTint
|
|
97
|
+
self.progressViewLabelColor = progressViewLabelColor
|
|
98
|
+
self.deepLinkUrl = deepLinkUrl
|
|
99
|
+
self.timerType = timerType
|
|
100
|
+
self.padding = padding
|
|
101
|
+
self.paddingDetails = paddingDetails
|
|
102
|
+
self.imagePosition = imagePosition
|
|
103
|
+
self.imageWidth = imageWidth
|
|
104
|
+
self.imageHeight = imageHeight
|
|
105
|
+
self.imageWidthPercent = imageWidthPercent
|
|
106
|
+
self.imageHeightPercent = imageHeightPercent
|
|
107
|
+
self.smallImageWidth = smallImageWidth
|
|
108
|
+
self.smallImageHeight = smallImageHeight
|
|
109
|
+
self.smallImageWidthPercent = smallImageWidthPercent
|
|
110
|
+
self.smallImageHeightPercent = smallImageHeightPercent
|
|
111
|
+
self.imageAlign = imageAlign
|
|
112
|
+
self.contentFit = contentFit
|
|
113
|
+
self.progressSegmentActiveColor = progressSegmentActiveColor
|
|
114
|
+
self.progressSegmentInactiveColor = progressSegmentInactiveColor
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
public enum DynamicIslandTimerType: String, Codable {
|
|
34
118
|
case circular
|
|
35
119
|
case digital
|
|
36
120
|
}
|
|
37
121
|
|
|
38
|
-
struct PaddingDetails: Codable, Hashable {
|
|
122
|
+
public struct PaddingDetails: Codable, Hashable {
|
|
39
123
|
var top: Int?
|
|
40
124
|
var bottom: Int?
|
|
41
125
|
var left: Int?
|
|
42
126
|
var right: Int?
|
|
43
127
|
var vertical: Int?
|
|
44
128
|
var horizontal: Int?
|
|
129
|
+
|
|
130
|
+
public init(
|
|
131
|
+
top: Int? = nil,
|
|
132
|
+
bottom: Int? = nil,
|
|
133
|
+
left: Int? = nil,
|
|
134
|
+
right: Int? = nil,
|
|
135
|
+
vertical: Int? = nil,
|
|
136
|
+
horizontal: Int? = nil
|
|
137
|
+
) {
|
|
138
|
+
self.top = top
|
|
139
|
+
self.bottom = bottom
|
|
140
|
+
self.left = left
|
|
141
|
+
self.right = right
|
|
142
|
+
self.vertical = vertical
|
|
143
|
+
self.horizontal = horizontal
|
|
144
|
+
}
|
|
45
145
|
}
|
|
46
146
|
}
|
|
47
147
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
148
|
+
@available(iOS 16.1, *)
|
|
149
|
+
public struct LiveActivityWidget: Widget {
|
|
150
|
+
public var body: some WidgetConfiguration {
|
|
151
|
+
let baseConfiguration = ActivityConfiguration(for: LiveActivityAttributes.self) { context in
|
|
51
152
|
LiveActivityView(contentState: context.state, attributes: context.attributes)
|
|
52
153
|
.activityBackgroundTint(
|
|
53
154
|
context.attributes.backgroundColor.map { Color(hex: $0) }
|
|
@@ -70,12 +171,28 @@ struct LiveActivityWidget: Widget {
|
|
|
70
171
|
}
|
|
71
172
|
}
|
|
72
173
|
DynamicIslandExpandedRegion(.bottom) {
|
|
73
|
-
if let
|
|
174
|
+
if let startDate = context.state.elapsedTimerStartDateInMilliseconds {
|
|
175
|
+
ElapsedTimerText(
|
|
176
|
+
startTimeMilliseconds: startDate,
|
|
177
|
+
color: context.attributes.progressViewTint.map { Color(hex: $0) } ?? .white
|
|
178
|
+
)
|
|
179
|
+
.font(.title2)
|
|
180
|
+
.fontWeight(.semibold)
|
|
181
|
+
.padding(.top, 5)
|
|
182
|
+
.padding(.horizontal, 5)
|
|
183
|
+
.applyWidgetURL(from: context.attributes.deepLinkUrl)
|
|
184
|
+
} else if let date = context.state.timerEndDateInMilliseconds {
|
|
74
185
|
dynamicIslandExpandedBottom(
|
|
75
186
|
endDate: date, progressViewTint: context.attributes.progressViewTint
|
|
76
187
|
)
|
|
77
188
|
.padding(.horizontal, 5)
|
|
78
189
|
.applyWidgetURL(from: context.attributes.deepLinkUrl)
|
|
190
|
+
} else if let progress = context.state.progress {
|
|
191
|
+
dynamicIslandExpandedBottomProgress(
|
|
192
|
+
progress: progress, progressViewTint: context.attributes.progressViewTint
|
|
193
|
+
)
|
|
194
|
+
.padding(.horizontal, 5)
|
|
195
|
+
.applyWidgetURL(from: context.attributes.deepLinkUrl)
|
|
79
196
|
}
|
|
80
197
|
}
|
|
81
198
|
} compactLeading: {
|
|
@@ -85,25 +202,62 @@ struct LiveActivityWidget: Widget {
|
|
|
85
202
|
.applyWidgetURL(from: context.attributes.deepLinkUrl)
|
|
86
203
|
}
|
|
87
204
|
} compactTrailing: {
|
|
88
|
-
if let
|
|
205
|
+
if let startDate = context.state.elapsedTimerStartDateInMilliseconds {
|
|
206
|
+
ElapsedTimerText(
|
|
207
|
+
startTimeMilliseconds: startDate,
|
|
208
|
+
color: nil
|
|
209
|
+
)
|
|
210
|
+
.font(.system(size: 15))
|
|
211
|
+
.minimumScaleFactor(0.8)
|
|
212
|
+
.fontWeight(.semibold)
|
|
213
|
+
.frame(maxWidth: 60)
|
|
214
|
+
.multilineTextAlignment(.trailing)
|
|
215
|
+
.applyWidgetURL(from: context.attributes.deepLinkUrl)
|
|
216
|
+
} else if let date = context.state.timerEndDateInMilliseconds {
|
|
89
217
|
compactTimer(
|
|
90
218
|
endDate: date,
|
|
91
219
|
timerType: context.attributes.timerType ?? .circular,
|
|
92
220
|
progressViewTint: context.attributes.progressViewTint
|
|
93
221
|
).applyWidgetURL(from: context.attributes.deepLinkUrl)
|
|
222
|
+
} else if let progress = context.state.progress {
|
|
223
|
+
compactProgress(
|
|
224
|
+
progress: progress,
|
|
225
|
+
progressViewTint: context.attributes.progressViewTint
|
|
226
|
+
).applyWidgetURL(from: context.attributes.deepLinkUrl)
|
|
94
227
|
}
|
|
95
228
|
} minimal: {
|
|
96
|
-
if let
|
|
229
|
+
if let startDate = context.state.elapsedTimerStartDateInMilliseconds {
|
|
230
|
+
ElapsedTimerText(
|
|
231
|
+
startTimeMilliseconds: startDate,
|
|
232
|
+
color: context.attributes.progressViewTint.map { Color(hex: $0) }
|
|
233
|
+
)
|
|
234
|
+
.font(.system(size: 11))
|
|
235
|
+
.minimumScaleFactor(0.6)
|
|
236
|
+
.applyWidgetURL(from: context.attributes.deepLinkUrl)
|
|
237
|
+
} else if let date = context.state.timerEndDateInMilliseconds {
|
|
97
238
|
compactTimer(
|
|
98
239
|
endDate: date,
|
|
99
240
|
timerType: context.attributes.timerType ?? .circular,
|
|
100
241
|
progressViewTint: context.attributes.progressViewTint
|
|
101
242
|
).applyWidgetURL(from: context.attributes.deepLinkUrl)
|
|
243
|
+
} else if let progress = context.state.progress {
|
|
244
|
+
compactProgress(
|
|
245
|
+
progress: progress,
|
|
246
|
+
progressViewTint: context.attributes.progressViewTint
|
|
247
|
+
).applyWidgetURL(from: context.attributes.deepLinkUrl)
|
|
102
248
|
}
|
|
103
249
|
}
|
|
104
250
|
}
|
|
251
|
+
|
|
252
|
+
if #available(iOS 18.0, *) {
|
|
253
|
+
return baseConfiguration.supplementalActivityFamilies([.small])
|
|
254
|
+
} else {
|
|
255
|
+
return baseConfiguration
|
|
256
|
+
}
|
|
105
257
|
}
|
|
106
258
|
|
|
259
|
+
public init() {}
|
|
260
|
+
|
|
107
261
|
@ViewBuilder
|
|
108
262
|
private func compactTimer(
|
|
109
263
|
endDate: Double,
|
|
@@ -166,4 +320,44 @@ struct LiveActivityWidget: Widget {
|
|
|
166
320
|
)
|
|
167
321
|
.progressViewStyle(.circular)
|
|
168
322
|
}
|
|
323
|
+
|
|
324
|
+
private func compactProgress(
|
|
325
|
+
progress: Double,
|
|
326
|
+
progressViewTint: String?
|
|
327
|
+
) -> some View {
|
|
328
|
+
ProgressView(value: progress)
|
|
329
|
+
.progressViewStyle(.circular)
|
|
330
|
+
.tint(progressViewTint.map { Color(hex: $0) })
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
private func dynamicIslandExpandedBottomProgress(progress: Double, progressViewTint: String?) -> some View {
|
|
334
|
+
ProgressView(value: progress)
|
|
335
|
+
.foregroundStyle(.white)
|
|
336
|
+
.tint(progressViewTint.map { Color(hex: $0) })
|
|
337
|
+
.padding(.top, 5)
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
// MARK: - Elapsed Timer View
|
|
342
|
+
|
|
343
|
+
struct ElapsedTimerText: View {
|
|
344
|
+
let startTimeMilliseconds: Double
|
|
345
|
+
let color: Color?
|
|
346
|
+
|
|
347
|
+
private var startTime: Date {
|
|
348
|
+
Date(timeIntervalSince1970: startTimeMilliseconds / 1000)
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
var body: some View {
|
|
352
|
+
// Use Text with timerInterval for Live Activities - iOS handles the updates automatically
|
|
353
|
+
// The range goes from startTime to a far future date, with countsDown: false to count UP
|
|
354
|
+
Text(
|
|
355
|
+
timerInterval: startTime ... Date.distantFuture,
|
|
356
|
+
pauseTime: nil,
|
|
357
|
+
countsDown: false,
|
|
358
|
+
showsHours: true
|
|
359
|
+
)
|
|
360
|
+
.monospacedDigit()
|
|
361
|
+
.foregroundStyle(color ?? .primary)
|
|
362
|
+
}
|
|
169
363
|
}
|
|
@@ -11,6 +11,47 @@ func resizableImage(imageName: String, height: CGFloat?, width: CGFloat?) -> som
|
|
|
11
11
|
.frame(width: width, height: height)
|
|
12
12
|
}
|
|
13
13
|
|
|
14
|
+
@ViewBuilder
|
|
15
|
+
func fixedSizeImage(name: String, size: CGFloat) -> some View {
|
|
16
|
+
Image.dynamic(assetNameOrPath: name)
|
|
17
|
+
.resizable()
|
|
18
|
+
.scaledToFit()
|
|
19
|
+
.frame(width: size, height: size)
|
|
20
|
+
.layoutPriority(0)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
@ViewBuilder
|
|
24
|
+
func smallTimerText(
|
|
25
|
+
endDate: Double,
|
|
26
|
+
isSubtitleDisplayed: Bool,
|
|
27
|
+
carPlayView: Bool = false,
|
|
28
|
+
labelColor: String?
|
|
29
|
+
) -> some View {
|
|
30
|
+
Text(timerInterval: Date.toTimerInterval(miliseconds: endDate))
|
|
31
|
+
.font(carPlayView
|
|
32
|
+
? (isSubtitleDisplayed ? .footnote : .title2)
|
|
33
|
+
: (isSubtitleDisplayed ? .footnote : .callout))
|
|
34
|
+
.fontWeight(carPlayView && !isSubtitleDisplayed ? .semibold : .medium)
|
|
35
|
+
.modifier(ConditionalForegroundViewModifier(color: labelColor))
|
|
36
|
+
.padding(.top, isSubtitleDisplayed ? 3 : 0)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
@ViewBuilder
|
|
40
|
+
func styledLinearProgressView<Content: View>(
|
|
41
|
+
tint: Color?,
|
|
42
|
+
labelColor: String?,
|
|
43
|
+
@ViewBuilder content: () -> Content
|
|
44
|
+
) -> some View {
|
|
45
|
+
content()
|
|
46
|
+
.progressViewStyle(.linear)
|
|
47
|
+
.tint(tint)
|
|
48
|
+
.frame(height: 8.0)
|
|
49
|
+
.scaleEffect(x: 1, y: 2, anchor: .center)
|
|
50
|
+
.clipShape(RoundedRectangle(cornerRadius: 10))
|
|
51
|
+
.modifier(ConditionalForegroundViewModifier(color: labelColor))
|
|
52
|
+
.padding(.bottom, 6)
|
|
53
|
+
}
|
|
54
|
+
|
|
14
55
|
private struct ContainerSizeKey: PreferenceKey {
|
|
15
56
|
static var defaultValue: CGSize?
|
|
16
57
|
static func reduce(value: inout CGSize?, nextValue: () -> CGSize?) {
|