fix(ios): NWC Dynamic Island lifecycle when host app is dismissed
This commit is contained in:
@@ -128,7 +128,7 @@ enum NWCLiveActivityShared {
|
||||
)
|
||||
let content = ActivityContent(
|
||||
state: state,
|
||||
staleDate: Date().addingTimeInterval(3600)
|
||||
staleDate: Date().addingTimeInterval(30)
|
||||
)
|
||||
await act.update(content)
|
||||
NSLog("[NWCLiveActivityShared] pushed – track=%@ muted=%d rev=%u",
|
||||
|
||||
@@ -56,6 +56,9 @@ struct NWCStopIntent: LiveActivityIntent {
|
||||
static var title: LocalizedStringResource = "Stop NWC"
|
||||
func perform() async throws -> some IntentResult {
|
||||
postDarwin(kNWCStop)
|
||||
if #available(iOS 16.2, *) {
|
||||
await NWCLiveActivityShared.endDuplicateActivities(keepingId: nil)
|
||||
}
|
||||
return .result()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -110,37 +110,98 @@ private struct NWCExpandedView: View {
|
||||
}
|
||||
}
|
||||
|
||||
private struct NWCStaleView: View {
|
||||
var body: some View {
|
||||
Button(intent: NWCStopIntent()) {
|
||||
HStack(spacing: 12) {
|
||||
ZeusIcon(size: 28)
|
||||
.opacity(0.35)
|
||||
|
||||
VStack(alignment: .leading, spacing: 2) {
|
||||
Text("Session ended")
|
||||
.font(.system(size: 15, weight: .semibold))
|
||||
.foregroundStyle(.white.opacity(0.6))
|
||||
Text("Tap to dismiss")
|
||||
.font(.system(size: 11))
|
||||
.foregroundStyle(.white.opacity(0.4))
|
||||
}
|
||||
|
||||
Spacer(minLength: 8)
|
||||
|
||||
Image(systemName: "xmark.circle.fill")
|
||||
.font(.system(size: 22))
|
||||
.foregroundStyle(.red.opacity(0.75))
|
||||
}
|
||||
.padding(.horizontal, 4)
|
||||
.padding(.vertical, 6)
|
||||
.contentShape(Rectangle())
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
}
|
||||
}
|
||||
|
||||
struct NWCWidgetLiveActivity: Widget {
|
||||
var body: some WidgetConfiguration {
|
||||
ActivityConfiguration(for: NWCLiveActivityAttributes.self) { context in
|
||||
NWCExpandedView(context: context)
|
||||
.padding(.horizontal, 20)
|
||||
.padding(.vertical, 14)
|
||||
.background(Color.black)
|
||||
Group {
|
||||
if context.isStale {
|
||||
NWCStaleView()
|
||||
} else {
|
||||
NWCExpandedView(context: context)
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, 20)
|
||||
.padding(.vertical, 14)
|
||||
.background(Color.black)
|
||||
} dynamicIsland: { context in
|
||||
DynamicIsland {
|
||||
DynamicIslandExpandedRegion(.bottom) {
|
||||
NWCExpandedView(context: context)
|
||||
.padding(.horizontal, 8)
|
||||
.padding(.bottom, 6)
|
||||
Group {
|
||||
if context.isStale {
|
||||
NWCStaleView()
|
||||
} else {
|
||||
NWCExpandedView(context: context)
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, 8)
|
||||
.padding(.bottom, 6)
|
||||
}
|
||||
} compactLeading: {
|
||||
ZeusIcon(size: 20)
|
||||
.opacity(context.isStale ? 0.35 : 1)
|
||||
} compactTrailing: {
|
||||
Button(intent: NWCToggleMuteIntent()) {
|
||||
Image(systemName: context.state.isMuted
|
||||
? "speaker.slash.fill"
|
||||
: "speaker.wave.2.fill")
|
||||
.font(.system(size: 12, weight: .medium))
|
||||
.foregroundStyle(context.state.isMuted ? .orange : .white)
|
||||
if context.isStale {
|
||||
Button(intent: NWCStopIntent()) {
|
||||
Image(systemName: "xmark.circle.fill")
|
||||
.font(.system(size: 12, weight: .medium))
|
||||
.foregroundStyle(.red.opacity(0.85))
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.padding(.trailing, 2)
|
||||
} else {
|
||||
Button(intent: NWCToggleMuteIntent()) {
|
||||
Image(systemName: context.state.isMuted
|
||||
? "speaker.slash.fill"
|
||||
: "speaker.wave.2.fill")
|
||||
.font(.system(size: 12, weight: .medium))
|
||||
.foregroundStyle(context.state.isMuted ? .orange : .white)
|
||||
}
|
||||
.id(context.state.contentRevision)
|
||||
.buttonStyle(.plain)
|
||||
.padding(.trailing, 2)
|
||||
}
|
||||
.id(context.state.contentRevision)
|
||||
.buttonStyle(.plain)
|
||||
.padding(.trailing, 2)
|
||||
} minimal: {
|
||||
ZeusIcon(size: 16)
|
||||
if context.isStale {
|
||||
Image(systemName: "xmark.circle.fill")
|
||||
.font(.system(size: 14))
|
||||
.foregroundStyle(.red.opacity(0.85))
|
||||
} else {
|
||||
ZeusIcon(size: 16)
|
||||
}
|
||||
}
|
||||
.keylineTint(Color(red: 1, green: 0.82, blue: 0))
|
||||
.keylineTint(context.isStale
|
||||
? Color.gray.opacity(0.5)
|
||||
: Color(red: 1, green: 0.82, blue: 0))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,7 +44,7 @@ import UIKit
|
||||
|
||||
// MARK: - Private
|
||||
|
||||
private var staleDate: Date { Date().addingTimeInterval(3600) }
|
||||
private var staleDate: Date { Date().addingTimeInterval(30) }
|
||||
|
||||
private func runOnMain(_ block: @escaping () -> Void) {
|
||||
if Thread.isMainThread {
|
||||
@@ -186,7 +186,7 @@ import UIKit
|
||||
private func startRefreshTimer() {
|
||||
stopRefreshTimer()
|
||||
let timer = DispatchSource.makeTimerSource(queue: .main)
|
||||
timer.schedule(deadline: .now() + 15, repeating: 15)
|
||||
timer.schedule(deadline: .now() + 10, repeating: 10)
|
||||
timer.setEventHandler { [weak self] in
|
||||
guard let self else { return }
|
||||
guard self.resolveLiveActivity() != nil else { return }
|
||||
|
||||
Reference in New Issue
Block a user