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.
- package/README.md +59 -6
- package/build/index.d.ts +27 -2
- package/build/index.d.ts.map +1 -1
- package/build/index.js +42 -29
- package/build/index.js.map +1 -1
- package/ios/ExpoLiveActivityModule.swift +87 -6
- 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 +130 -104
- package/ios-files/LiveActivityWidget.swift +95 -6
- package/ios-files/ViewHelpers.swift +41 -0
- package/package.json +2 -3
- package/plugin/build/index.js +2 -0
- 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/types.ts +1 -0
- package/plugin/src/withUnsupportedOS.ts +10 -0
- package/plugin/tsconfig.tsbuildinfo +1 -1
- package/src/index.ts +70 -28
- package/.prettierignore +0 -5
|
@@ -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
|
|
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
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,8 +222,8 @@ 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)'")
|
|
@@ -153,97 +248,28 @@ import WidgetKit
|
|
|
153
248
|
}
|
|
154
249
|
)
|
|
155
250
|
}
|
|
251
|
+
}
|
|
156
252
|
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
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
|
-
|
|
215
|
-
|
|
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
|
-
|
|
228
|
-
if let imageName = contentState.imageName {
|
|
229
|
-
alignedImage(imageName: imageName, horizontalAlignment: .trailing)
|
|
230
|
-
}
|
|
231
|
-
}
|
|
232
|
-
}
|
|
262
|
+
@Environment(\.activityFamily) private var activityFamily
|
|
233
263
|
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
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
|
|