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
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import SwiftUI
|
|
2
|
+
import WidgetKit
|
|
3
|
+
|
|
4
|
+
extension LiveActivityAttributes.ContentState {
|
|
5
|
+
var hasSegmentedProgress: Bool {
|
|
6
|
+
currentStep != nil && (totalSteps ?? 0) > 0
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
func logSegmentedProgressWarningIfNeeded() {
|
|
10
|
+
#if DEBUG
|
|
11
|
+
if hasSegmentedProgress,
|
|
12
|
+
elapsedTimerStartDateInMilliseconds != nil
|
|
13
|
+
|| timerEndDateInMilliseconds != nil
|
|
14
|
+
|| progress != nil
|
|
15
|
+
{
|
|
16
|
+
DebugLog("⚠️[ExpoLiveActivity] Both segmented and regular progress provided; showing segmented")
|
|
17
|
+
}
|
|
18
|
+
#endif
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
extension LiveActivityAttributes {
|
|
23
|
+
var segmentActiveColor: Color {
|
|
24
|
+
progressSegmentActiveColor.map { Color(hex: $0) } ?? Color.blue
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
var segmentInactiveColor: Color {
|
|
28
|
+
progressSegmentInactiveColor.map { Color(hex: $0) } ?? Color.gray.opacity(0.3)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
func resolvedPadding(defaultPadding: Int) -> EdgeInsets {
|
|
32
|
+
let top = CGFloat(
|
|
33
|
+
paddingDetails?.top
|
|
34
|
+
?? paddingDetails?.vertical
|
|
35
|
+
?? padding
|
|
36
|
+
?? defaultPadding
|
|
37
|
+
)
|
|
38
|
+
let bottom = CGFloat(
|
|
39
|
+
paddingDetails?.bottom
|
|
40
|
+
?? paddingDetails?.vertical
|
|
41
|
+
?? padding
|
|
42
|
+
?? defaultPadding
|
|
43
|
+
)
|
|
44
|
+
let leading = CGFloat(
|
|
45
|
+
paddingDetails?.left
|
|
46
|
+
?? paddingDetails?.horizontal
|
|
47
|
+
?? padding
|
|
48
|
+
?? defaultPadding
|
|
49
|
+
)
|
|
50
|
+
let trailing = CGFloat(
|
|
51
|
+
paddingDetails?.right
|
|
52
|
+
?? paddingDetails?.horizontal
|
|
53
|
+
?? padding
|
|
54
|
+
?? defaultPadding
|
|
55
|
+
)
|
|
56
|
+
return EdgeInsets(top: top, leading: leading, bottom: bottom, trailing: trailing)
|
|
57
|
+
}
|
|
58
|
+
}
|
|
@@ -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
|