expo-live-activity 0.4.3-alpha1 → 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.
@@ -0,0 +1,127 @@
1
+ import SwiftUI
2
+ import WidgetKit
3
+
4
+ struct LiveActivityMediumView: View {
5
+ let contentState: LiveActivityAttributes.ContentState
6
+ let attributes: LiveActivityAttributes
7
+ @Binding var imageContainerSize: CGSize?
8
+ let alignedImage: (String, HorizontalAlignment, Bool) -> AnyView
9
+
10
+ private var hasImage: Bool {
11
+ contentState.imageName != nil
12
+ }
13
+
14
+ private var isLeftImage: Bool {
15
+ (attributes.imagePosition ?? "right").hasPrefix("left")
16
+ }
17
+
18
+ private var isStretch: Bool {
19
+ (attributes.imagePosition ?? "right").contains("Stretch")
20
+ }
21
+
22
+ private var effectiveStretch: Bool {
23
+ isStretch && hasImage
24
+ }
25
+
26
+ private var progressViewTint: Color? {
27
+ attributes.progressViewTint.map { Color(hex: $0) }
28
+ }
29
+
30
+ var body: some View {
31
+ let padding = attributes.resolvedPadding(defaultPadding: 24)
32
+
33
+ let _ = contentState.logSegmentedProgressWarningIfNeeded()
34
+
35
+ VStack(alignment: .leading) {
36
+ HStack(alignment: .center) {
37
+ if hasImage, isLeftImage {
38
+ if let imageName = contentState.imageName {
39
+ alignedImage(imageName, .leading, false)
40
+ }
41
+ }
42
+
43
+ VStack(alignment: .leading, spacing: 2) {
44
+ Text(contentState.title)
45
+ .font(.title2)
46
+ .fontWeight(.semibold)
47
+ .modifier(ConditionalForegroundViewModifier(color: attributes.titleColor))
48
+
49
+ if let subtitle = contentState.subtitle {
50
+ Text(subtitle)
51
+ .font(.title3)
52
+ .modifier(ConditionalForegroundViewModifier(color: attributes.subtitleColor))
53
+ }
54
+
55
+ if effectiveStretch {
56
+ if contentState.hasSegmentedProgress,
57
+ let currentStep = contentState.currentStep,
58
+ let totalSteps = contentState.totalSteps,
59
+ totalSteps > 0
60
+ {
61
+ SegmentedProgressView(
62
+ currentStep: currentStep,
63
+ totalSteps: totalSteps,
64
+ activeColor: attributes.segmentActiveColor,
65
+ inactiveColor: attributes.segmentInactiveColor
66
+ )
67
+ } else if let startDate = contentState.elapsedTimerStartDateInMilliseconds {
68
+ ElapsedTimerText(
69
+ startTimeMilliseconds: startDate,
70
+ color: attributes.progressViewLabelColor.map { Color(hex: $0) }
71
+ )
72
+ .font(.title3)
73
+ .fontWeight(.medium)
74
+ } else if let date = contentState.timerEndDateInMilliseconds {
75
+ ProgressView(timerInterval: Date.toTimerInterval(miliseconds: date))
76
+ .tint(progressViewTint)
77
+ .modifier(ConditionalForegroundViewModifier(color: attributes.progressViewLabelColor))
78
+ } else if let progress = contentState.progress {
79
+ ProgressView(value: progress)
80
+ .tint(progressViewTint)
81
+ .modifier(ConditionalForegroundViewModifier(color: attributes.progressViewLabelColor))
82
+ }
83
+ }
84
+ }.layoutPriority(1)
85
+
86
+ if hasImage, !isLeftImage {
87
+ if let imageName = contentState.imageName {
88
+ alignedImage(imageName, .trailing, false)
89
+ }
90
+ }
91
+ }
92
+
93
+ if !effectiveStretch {
94
+ if contentState.hasSegmentedProgress,
95
+ let currentStep = contentState.currentStep,
96
+ let totalSteps = contentState.totalSteps,
97
+ totalSteps > 0
98
+ {
99
+ SegmentedProgressView(
100
+ currentStep: currentStep,
101
+ totalSteps: totalSteps,
102
+ activeColor: attributes.segmentActiveColor,
103
+ inactiveColor: attributes.segmentInactiveColor
104
+ )
105
+ } else if let startDate = contentState.elapsedTimerStartDateInMilliseconds {
106
+ ElapsedTimerText(
107
+ startTimeMilliseconds: startDate,
108
+ color: attributes.progressViewLabelColor.map { Color(hex: $0) }
109
+ )
110
+ .font(.title2)
111
+ .fontWeight(.semibold)
112
+ .frame(maxWidth: .infinity, alignment: .leading)
113
+ .padding(.top, 4)
114
+ } else if let date = contentState.timerEndDateInMilliseconds {
115
+ ProgressView(timerInterval: Date.toTimerInterval(miliseconds: date))
116
+ .tint(progressViewTint)
117
+ .modifier(ConditionalForegroundViewModifier(color: attributes.progressViewLabelColor))
118
+ } else if let progress = contentState.progress {
119
+ ProgressView(value: progress)
120
+ .tint(progressViewTint)
121
+ .modifier(ConditionalForegroundViewModifier(color: attributes.progressViewLabelColor))
122
+ }
123
+ }
124
+ }
125
+ .padding(padding)
126
+ }
127
+ }
@@ -0,0 +1,178 @@
1
+ import SwiftUI
2
+ import WidgetKit
3
+
4
+ #if canImport(ActivityKit)
5
+ import ActivityKit
6
+
7
+ struct LiveActivitySmallView: View {
8
+ let contentState: LiveActivityAttributes.ContentState
9
+ let attributes: LiveActivityAttributes
10
+ @Binding var imageContainerSize: CGSize?
11
+ let alignedImage: (String, HorizontalAlignment, Bool) -> AnyView
12
+
13
+ // CarPlay Live Activity views don't have fixed dimensions (unlike Apple Watch),
14
+ // because the system may scale them to fit the vehicle display.
15
+ // These thresholds are derived from the CarPlay live activities test sizes listed in the documentation.
16
+ private let carPlayWidthThreshold: CGFloat = 200
17
+ private let carPlayTallHeightThreshold: CGFloat = 90
18
+
19
+ private var resolvedImageName: String? {
20
+ contentState.smallImageName ?? contentState.imageName
21
+ }
22
+
23
+ private var hasImage: Bool {
24
+ resolvedImageName != nil
25
+ }
26
+
27
+ private var isLeftImage: Bool {
28
+ (attributes.imagePosition ?? "right").hasPrefix("left")
29
+ }
30
+
31
+ private var progressViewTint: Color? {
32
+ attributes.progressViewTint.map { Color(hex: $0) }
33
+ }
34
+
35
+ private var isSubtitleDisplayed: Bool {
36
+ contentState.subtitle != nil
37
+ }
38
+
39
+ private var isTimerShownAsText: Bool {
40
+ attributes.timerType == .digital && contentState.timerEndDateInMilliseconds != nil
41
+ }
42
+
43
+ private var shouldShowProgressBar: Bool {
44
+ let hasProgress = contentState.progress != nil
45
+ let hasTimer = contentState.timerEndDateInMilliseconds != nil
46
+ return hasProgress || (hasTimer && !isSubtitleDisplayed && !isTimerShownAsText) || contentState.hasSegmentedProgress
47
+ }
48
+
49
+ var body: some View {
50
+ GeometryReader { proxy in
51
+ let w = proxy.size.width
52
+ let h = proxy.size.height
53
+
54
+ let carPlayView = w > carPlayWidthThreshold
55
+ let carPlayTallView = carPlayView && h > carPlayTallHeightThreshold
56
+
57
+ let padding = attributes.resolvedPadding(defaultPadding: carPlayView ? 14 : 8)
58
+
59
+ let fixedImageSize: CGFloat = carPlayView ? 28 : 23
60
+
61
+ let _ = contentState.logSegmentedProgressWarningIfNeeded()
62
+
63
+ VStack(alignment: .leading, spacing: 4) {
64
+ VStack(alignment: .leading, spacing: shouldShowProgressBar ? 0 : nil) {
65
+ HStack(alignment: .center, spacing: 8) {
66
+ if hasImage, isLeftImage, !isTimerShownAsText, let imageName = resolvedImageName {
67
+ if carPlayView, !carPlayTallView {
68
+ fixedSizeImage(name: imageName, size: fixedImageSize)
69
+ Spacer()
70
+ } else {
71
+ alignedImage(imageName, .leading, true)
72
+ }
73
+ }
74
+
75
+ VStack(alignment: .leading, spacing: 0) {
76
+ Text(contentState.title)
77
+ .font(carPlayView
78
+ ? (isSubtitleDisplayed || contentState.hasSegmentedProgress ? .body : .footnote)
79
+ : (isSubtitleDisplayed ? .footnote : .callout))
80
+ .fontWeight(.semibold)
81
+ .lineLimit(1)
82
+ .modifier(ConditionalForegroundViewModifier(color: attributes.titleColor))
83
+
84
+ if let subtitle = contentState.subtitle, !(carPlayView && !carPlayTallView) {
85
+ Text(subtitle)
86
+ .font(carPlayView ? .body : .footnote)
87
+ .fontWeight(.semibold)
88
+ .lineLimit(1)
89
+ .modifier(ConditionalForegroundViewModifier(color: attributes.subtitleColor))
90
+ }
91
+
92
+ if let startDate = contentState.elapsedTimerStartDateInMilliseconds {
93
+ ElapsedTimerText(
94
+ startTimeMilliseconds: startDate,
95
+ color: attributes.progressViewLabelColor.map { Color(hex: $0) }
96
+ )
97
+ .font(carPlayView
98
+ ? (isSubtitleDisplayed ? .footnote : .title2)
99
+ : (isSubtitleDisplayed ? .footnote : .callout))
100
+ .fontWeight(carPlayView && !isSubtitleDisplayed ? .semibold : .medium)
101
+ .padding(.top, isSubtitleDisplayed ? 3 : 0)
102
+ } else if let date = contentState.timerEndDateInMilliseconds, !isTimerShownAsText, !(carPlayView && isSubtitleDisplayed) {
103
+ smallTimerText(endDate: date, isSubtitleDisplayed: isSubtitleDisplayed, carPlayView: carPlayView, labelColor: attributes.progressViewLabelColor)
104
+ }
105
+ }
106
+ .layoutPriority(1)
107
+
108
+ if hasImage, !isLeftImage, !isTimerShownAsText, let imageName = resolvedImageName {
109
+ if carPlayView, !carPlayTallView {
110
+ Spacer()
111
+ fixedSizeImage(name: imageName, size: fixedImageSize)
112
+ } else {
113
+ alignedImage(imageName, .trailing, true)
114
+ }
115
+ }
116
+ }
117
+ if isTimerShownAsText, let date = contentState.timerEndDateInMilliseconds {
118
+ HStack {
119
+ if let imageName = resolvedImageName, hasImage, isLeftImage {
120
+ fixedSizeImage(name: imageName, size: fixedImageSize)
121
+ Spacer()
122
+ }
123
+ smallTimerText(endDate: date, isSubtitleDisplayed: false, carPlayView: carPlayView, labelColor: attributes.progressViewLabelColor).frame(maxWidth: .infinity, alignment: isLeftImage ? .trailing : .leading)
124
+ if let imageName = resolvedImageName, hasImage, !isLeftImage {
125
+ Spacer()
126
+ fixedSizeImage(name: imageName, size: fixedImageSize)
127
+ }
128
+ }
129
+ .frame(maxWidth: .infinity)
130
+ } else {
131
+ if contentState.hasSegmentedProgress,
132
+ let currentStep = contentState.currentStep,
133
+ let totalSteps = contentState.totalSteps,
134
+ totalSteps > 0
135
+ {
136
+ SegmentedProgressView(
137
+ currentStep: currentStep,
138
+ totalSteps: totalSteps,
139
+ activeColor: attributes.segmentActiveColor,
140
+ inactiveColor: attributes.segmentInactiveColor,
141
+ height: 6
142
+ ).padding(.bottom, 6)
143
+ } else if let progress = contentState.progress {
144
+ styledLinearProgressView(tint: progressViewTint, labelColor: attributes.progressViewLabelColor) {
145
+ ProgressView(value: progress)
146
+ }
147
+ } else if let date = contentState.timerEndDateInMilliseconds, !isSubtitleDisplayed || carPlayView {
148
+ HStack(spacing: 4) {
149
+ styledLinearProgressView(tint: progressViewTint, labelColor: attributes.progressViewLabelColor) {
150
+ ProgressView(
151
+ timerInterval: Date.toTimerInterval(miliseconds: date),
152
+ countsDown: false,
153
+ label: { EmptyView() },
154
+ currentValueLabel: { EmptyView() }
155
+ )
156
+ }
157
+ .frame(minWidth: 0, maxWidth: .infinity, alignment: .leading)
158
+
159
+ if carPlayView, isSubtitleDisplayed {
160
+ Text(timerInterval: Date.toTimerInterval(miliseconds: date))
161
+ .font(.footnote)
162
+ .monospacedDigit()
163
+ .lineLimit(1)
164
+ .modifier(ConditionalForegroundViewModifier(color: attributes.progressViewLabelColor))
165
+ .multilineTextAlignment(.trailing)
166
+ .frame(maxWidth: 60, alignment: .trailing)
167
+ }
168
+ }
169
+ }
170
+ }
171
+ }
172
+ }
173
+ .padding(padding)
174
+ }
175
+ }
176
+ }
177
+
178
+ #endif
@@ -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
@@ -54,69 +76,142 @@ import WidgetKit
54
76
  }
55
77
  }
56
78
 
57
- private func alignedImage(imageName: String, horizontalAlignment: HorizontalAlignment) -> some View {
58
- let defaultHeight: CGFloat = 64
59
- let defaultWidth: CGFloat = 64
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
- let hasWidthConstraint = (attributes.imageWidthPercent != nil) || (attributes.imageWidth != nil)
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 = attributes.imageHeightPercent {
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 = attributes.imageHeight {
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 (64pt)
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 = attributes.imageWidthPercent {
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 = attributes.imageWidth {
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
192
  let fit = attributes.contentFit ?? "contain"
98
193
  switch fit {
99
194
  case "contain":
100
- Image.dynamic(assetNameOrPath: imageName).resizable().scaledToFit().frame(width: computedWidth, height: computedHeight)
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: computedWidth,
104
- height: computedHeight
198
+ width: resolvedWidth,
199
+ height: resolvedHeight
105
200
  )
106
201
  case "none":
107
- Image.dynamic(assetNameOrPath: imageName).renderingMode(.original).frame(width: computedWidth, height: computedHeight)
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 = computedHeight ?? uiImage.size.height
112
- let targetWidth = computedWidth ?? uiImage.size.width
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: computedWidth, height: computedHeight)
214
+ .frame(width: resolvedWidth, height: resolvedHeight)
120
215
  } else {
121
216
  Image(uiImage: uiImage)
122
217
  .renderingMode(.original)
@@ -127,8 +222,8 @@ import WidgetKit
127
222
  }
128
223
  case "cover":
129
224
  Image.dynamic(assetNameOrPath: imageName).resizable().scaledToFill().frame(
130
- width: computedWidth,
131
- height: computedHeight
225
+ width: resolvedWidth,
226
+ height: resolvedHeight
132
227
  ).clipped()
133
228
  default:
134
229
  DebugLog("⚠️[ExpoLiveActivity] Unknown contentFit '\(fit)'")
@@ -153,97 +248,28 @@ import WidgetKit
153
248
  }
154
249
  )
155
250
  }
251
+ }
156
252
 
157
- var body: some View {
158
- let defaultPadding = 24
159
-
160
- let top = CGFloat(
161
- attributes.paddingDetails?.top
162
- ?? attributes.paddingDetails?.vertical
163
- ?? attributes.padding
164
- ?? defaultPadding
165
- )
166
-
167
- let bottom = CGFloat(
168
- attributes.paddingDetails?.bottom
169
- ?? attributes.paddingDetails?.vertical
170
- ?? attributes.padding
171
- ?? defaultPadding
172
- )
173
-
174
- let leading = CGFloat(
175
- attributes.paddingDetails?.left
176
- ?? attributes.paddingDetails?.horizontal
177
- ?? attributes.padding
178
- ?? defaultPadding
179
- )
180
-
181
- let trailing = CGFloat(
182
- attributes.paddingDetails?.right
183
- ?? attributes.paddingDetails?.horizontal
184
- ?? attributes.padding
185
- ?? defaultPadding
186
- )
187
-
188
- VStack(alignment: .leading) {
189
- let position = attributes.imagePosition ?? "right"
190
- let isStretch = position.contains("Stretch")
191
- let isLeftImage = position.hasPrefix("left")
192
- let hasImage = contentState.imageName != nil
193
- let effectiveStretch = isStretch && hasImage
194
-
195
- HStack(alignment: .center) {
196
- if hasImage, isLeftImage {
197
- if let imageName = contentState.imageName {
198
- alignedImage(imageName: imageName, horizontalAlignment: .leading)
199
- }
200
- }
201
-
202
- VStack(alignment: .leading, spacing: 2) {
203
- Text(contentState.title)
204
- .font(.title2)
205
- .fontWeight(.semibold)
206
- .modifier(ConditionalForegroundViewModifier(color: attributes.titleColor))
207
-
208
- if let subtitle = contentState.subtitle {
209
- Text(subtitle)
210
- .font(.title3)
211
- .modifier(ConditionalForegroundViewModifier(color: attributes.subtitleColor))
212
- }
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?
213
258
 
214
- if effectiveStretch {
215
- if let date = contentState.timerEndDateInMilliseconds {
216
- ProgressView(timerInterval: Date.toTimerInterval(miliseconds: date))
217
- .tint(progressViewTint)
218
- .modifier(ConditionalForegroundViewModifier(color: attributes.progressViewLabelColor))
219
- } else if let progress = contentState.progress {
220
- ProgressView(value: progress)
221
- .tint(progressViewTint)
222
- .modifier(ConditionalForegroundViewModifier(color: attributes.progressViewLabelColor))
223
- }
224
- }
225
- }.layoutPriority(1)
259
+ let smallView: () -> Small
260
+ let mediumView: () -> Medium
226
261
 
227
- if hasImage, !isLeftImage { // right side (default)
228
- if let imageName = contentState.imageName {
229
- alignedImage(imageName: imageName, horizontalAlignment: .trailing)
230
- }
231
- }
232
- }
262
+ @Environment(\.activityFamily) private var activityFamily
233
263
 
234
- if !effectiveStretch {
235
- if let date = contentState.timerEndDateInMilliseconds {
236
- ProgressView(timerInterval: Date.toTimerInterval(miliseconds: date))
237
- .tint(progressViewTint)
238
- .modifier(ConditionalForegroundViewModifier(color: attributes.progressViewLabelColor))
239
- } else if let progress = contentState.progress {
240
- ProgressView(value: progress)
241
- .tint(progressViewTint)
242
- .modifier(ConditionalForegroundViewModifier(color: attributes.progressViewLabelColor))
243
- }
244
- }
264
+ var body: some View {
265
+ switch activityFamily {
266
+ case .small:
267
+ smallView()
268
+ case .medium:
269
+ mediumView()
270
+ @unknown default:
271
+ mediumView()
245
272
  }
246
- .padding(EdgeInsets(top: top, leading: leading, bottom: bottom, trailing: trailing))
247
273
  }
248
274
  }
249
275