feat(ios): add NWCActivityManager to start and update Live Activities

This commit is contained in:
ajaysehwal
2026-06-04 10:52:42 -04:00
committed by Evan Kaloudis
parent 72779a58dd
commit d1a56a1309
8 changed files with 424 additions and 4 deletions
Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.0 KiB

+199 -3
View File
@@ -226,6 +226,11 @@
NWCAUDIO0000000000000011 /* Fireplace.m4a in Resources */ = {isa = PBXBuildFile; fileRef = NWCAUDIO0000000000000015 /* Fireplace.m4a */; };
NWCAUDIO0000000000000012 /* White Noise.m4a in Resources */ = {isa = PBXBuildFile; fileRef = NWCAUDIO0000000000000016 /* White Noise.m4a */; };
NWCAUDIO0000000000000013 /* Gentle Rain.m4a in Resources */ = {isa = PBXBuildFile; fileRef = NWCAUDIO0000000000000017 /* Gentle Rain.m4a */; };
NWCLA000000000000000001 /* NWCWidgetAttributes.swift in Sources */ = {isa = PBXBuildFile; fileRef = NWCLA000000000000000007 /* NWCWidgetAttributes.swift */; };
NWCLA000000000000000002 /* NWCWidgetBridge.swift in Sources */ = {isa = PBXBuildFile; fileRef = NWCLA000000000000000008 /* NWCWidgetBridge.swift */; };
NWCLA000000000000000003 /* NWCActivityManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = NWCLA000000000000000009 /* NWCActivityManager.swift */; };
NWCLA000000000000000004 /* NWCWidgetBridgeImpl.swift in Sources */ = {isa = PBXBuildFile; fileRef = NWCLA000000000000000010 /* NWCWidgetBridgeImpl.swift */; };
NWCLA000000000000000005 /* NWCWidget.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = NWCLA000000000000000006 /* NWCWidget.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
/* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */
@@ -404,6 +409,13 @@
remoteGlobalIDString = 358F4ED71D1E81A9004DF814;
remoteInfo = RCTBlob;
};
NWCLA000000000000000019 /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = 83CBB9F71A601CBA00E9B192 /* Project object */;
proxyType = 1;
remoteGlobalIDString = NWCLA000000000000000011;
remoteInfo = NWCWidget;
};
/* End PBXContainerItemProxy section */
/* Begin PBXCopyFilesBuildPhase section */
@@ -414,6 +426,7 @@
dstSubfolderSpec = 13;
files = (
5134B5512EB7C1B000A6AD03 /* ShareQR.appex in Embed Foundation Extensions */,
NWCLA000000000000000005 /* NWCWidget.appex in Embed Foundation Extensions */,
);
name = "Embed Foundation Extensions";
runOnlyForDeploymentPostprocessing = 0;
@@ -703,9 +716,14 @@
FDE09436C88E4DCA980DA7DE /* zeus_illustration_6a.jpg */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = unknown; name = zeus_illustration_6a.jpg; path = ../assets/images/zeus_illustration_6a.jpg; sourceTree = "<group>"; };
NWC1A2B3C4D5E6F700000002 /* NWCAudioKeepAlive.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = NWCAudioKeepAlive.m; path = zeus/NWCAudioKeepAlive.m; sourceTree = "<group>"; };
NWC1A2B3C4D5E6F700000003 /* NWCAudioKeepAlive.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = NWCAudioKeepAlive.h; path = zeus/NWCAudioKeepAlive.h; sourceTree = "<group>"; };
NWCAUDIO0000000000000015 /* Fireplace.m4a */ = {isa = PBXFileReference; lastKnownFileType = file; name = Fireplace.m4a; path = zeus/Fireplace.m4a; sourceTree = "<group>"; };
NWCAUDIO0000000000000016 /* White Noise.m4a */ = {isa = PBXFileReference; lastKnownFileType = file; name = "White Noise.m4a"; path = "zeus/White Noise.m4a"; sourceTree = "<group>"; };
NWCAUDIO0000000000000017 /* Gentle Rain.m4a */ = {isa = PBXFileReference; lastKnownFileType = file; name = "Gentle Rain.m4a"; path = "zeus/Gentle Rain.m4a"; sourceTree = "<group>"; };
NWCAUDIO0000000000000015 /* Fireplace.m4a */ = {isa = PBXFileReference; lastKnownFileType = "audio.x-m4a"; name = Fireplace.m4a; path = zeus/Fireplace.m4a; sourceTree = "<group>"; };
NWCAUDIO0000000000000016 /* White Noise.m4a */ = {isa = PBXFileReference; lastKnownFileType = "audio.x-m4a"; name = "White Noise.m4a"; path = "zeus/White Noise.m4a"; sourceTree = "<group>"; };
NWCAUDIO0000000000000017 /* Gentle Rain.m4a */ = {isa = PBXFileReference; lastKnownFileType = "audio.x-m4a"; name = "Gentle Rain.m4a"; path = "zeus/Gentle Rain.m4a"; sourceTree = "<group>"; };
NWCLA000000000000000006 /* NWCWidget.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = NWCWidget.appex; sourceTree = BUILT_PRODUCTS_DIR; };
NWCLA000000000000000007 /* NWCWidgetAttributes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = NWCWidgetAttributes.swift; path = NWCWidget/NWCWidgetAttributes.swift; sourceTree = "<group>"; };
NWCLA000000000000000008 /* NWCWidgetBridge.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = NWCWidgetBridge.swift; path = NWCWidget/NWCWidgetBridge.swift; sourceTree = "<group>"; };
NWCLA000000000000000009 /* NWCActivityManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = NWCActivityManager.swift; path = zeus/NWCActivityManager.swift; sourceTree = "<group>"; };
NWCLA000000000000000010 /* NWCWidgetBridgeImpl.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = NWCWidgetBridgeImpl.swift; path = zeus/NWCWidgetBridgeImpl.swift; sourceTree = "<group>"; };
/* End PBXFileReference section */
/* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */
@@ -716,6 +734,14 @@
);
target = 5134B5462EB7C1B000A6AD03 /* ShareQR */;
};
NWCLA000000000000000013 /* Exceptions for "NWCWidget" folder in "NWCWidget" target */ = {
isa = PBXFileSystemSynchronizedBuildFileExceptionSet;
membershipExceptions = (
Info.plist,
NWCWidget.entitlements,
);
target = NWCLA000000000000000011 /* NWCWidget */;
};
/* End PBXFileSystemSynchronizedBuildFileExceptionSet section */
/* Begin PBXFileSystemSynchronizedRootGroup section */
@@ -727,6 +753,14 @@
path = ShareQR;
sourceTree = "<group>";
};
NWCLA000000000000000015 /* NWCWidget */ = {
isa = PBXFileSystemSynchronizedRootGroup;
exceptions = (
NWCLA000000000000000013 /* Exceptions for "NWCWidget" folder in "NWCWidget" target */,
);
path = NWCWidget;
sourceTree = "<group>";
};
/* End PBXFileSystemSynchronizedRootGroup section */
/* Begin PBXFrameworksBuildPhase section */
@@ -778,6 +812,13 @@
);
runOnlyForDeploymentPostprocessing = 0;
};
NWCLA00000000000000001B /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */
@@ -874,6 +915,10 @@
13B07FB01A68108700A75B9A /* AppDelegate.mm */,
NWC1A2B3C4D5E6F700000003 /* NWCAudioKeepAlive.h */,
NWC1A2B3C4D5E6F700000002 /* NWCAudioKeepAlive.m */,
NWCLA000000000000000009 /* NWCActivityManager.swift */,
NWCLA000000000000000010 /* NWCWidgetBridgeImpl.swift */,
NWCLA000000000000000007 /* NWCWidgetAttributes.swift */,
NWCLA000000000000000008 /* NWCWidgetBridge.swift */,
NWCAUDIO0000000000000015 /* Fireplace.m4a */,
NWCAUDIO0000000000000016 /* White Noise.m4a */,
NWCAUDIO0000000000000017 /* Gentle Rain.m4a */,
@@ -982,6 +1027,7 @@
832341AE1AAA6A7D00B99B32 /* Libraries */,
00E356EF1AD99517003FC87E /* zeusTests */,
5134B5482EB7C1B000A6AD03 /* ShareQR */,
NWCLA000000000000000015 /* NWCWidget */,
83CBBA001A601CBA00E9B192 /* Products */,
2D16E6871FA4F8E400B85C8A /* Frameworks */,
C313040DFF224D8A9EC46990 /* Resources */,
@@ -1003,6 +1049,7 @@
2D02E47B1E0B4A5D006451C7 /* zeus-tvOS.app */,
2D02E4901E0B4A5D006451C7 /* zeus-tvOSTests.xctest */,
5134B5472EB7C1B000A6AD03 /* ShareQR.appex */,
NWCLA000000000000000006 /* NWCWidget.appex */,
);
name = Products;
sourceTree = "<group>";
@@ -1318,6 +1365,7 @@
);
dependencies = (
5134B5502EB7C1B000A6AD03 /* PBXTargetDependency */,
NWCLA000000000000000020 /* PBXTargetDependency */,
);
name = zeus;
productName = "Hello World";
@@ -1380,6 +1428,26 @@
productReference = 5134B5472EB7C1B000A6AD03 /* ShareQR.appex */;
productType = "com.apple.product-type.app-extension";
};
NWCLA000000000000000011 /* NWCWidget */ = {
isa = PBXNativeTarget;
buildConfigurationList = NWCLA000000000000000016 /* Build configuration list for PBXNativeTarget "NWCWidget" */;
buildPhases = (
NWCLA000000000000000012 /* Sources */,
NWCLA00000000000000001B /* Frameworks */,
NWCLA000000000000000014 /* Resources */,
);
buildRules = (
);
dependencies = (
);
fileSystemSynchronizedGroups = (
NWCLA000000000000000015 /* NWCWidget */,
);
name = NWCWidget;
productName = NWCWidget;
productReference = NWCLA000000000000000006 /* NWCWidget.appex */;
productType = "com.apple.product-type.app-extension";
};
/* End PBXNativeTarget section */
/* Begin PBXProject section */
@@ -1412,6 +1480,9 @@
5134B5462EB7C1B000A6AD03 = {
CreatedOnToolsVersion = 16.4;
};
NWCLA000000000000000011 = {
CreatedOnToolsVersion = 16.4;
};
};
};
buildConfigurationList = 83CBB9FA1A601CBA00E9B192 /* Build configuration list for PBXProject "zeus" */;
@@ -1478,6 +1549,7 @@
2D02E47A1E0B4A5D006451C7 /* zeus-tvOS */,
2D02E48F1E0B4A5D006451C7 /* zeus-tvOSTests */,
5134B5462EB7C1B000A6AD03 /* ShareQR */,
NWCLA000000000000000011 /* NWCWidget */,
);
};
/* End PBXProject section */
@@ -1850,6 +1922,13 @@
);
runOnlyForDeploymentPostprocessing = 0;
};
NWCLA000000000000000014 /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXResourcesBuildPhase section */
/* Begin PBXShellScriptBuildPhase section */
@@ -1990,6 +2069,10 @@
745388452A54C2B6000927CE /* LndMobileScheduledSync.swift in Sources */,
13B07FBC1A68108700A75B9A /* AppDelegate.mm in Sources */,
NWC1A2B3C4D5E6F700000001 /* NWCAudioKeepAlive.m in Sources */,
NWCLA000000000000000001 /* NWCWidgetAttributes.swift in Sources */,
NWCLA000000000000000002 /* NWCWidgetBridge.swift in Sources */,
NWCLA000000000000000003 /* NWCActivityManager.swift in Sources */,
NWCLA000000000000000004 /* NWCWidgetBridgeImpl.swift in Sources */,
745388442A54C2B6000927CE /* lightning.pb.swift in Sources */,
74B637822A5A350B00750202 /* LncModule.mm in Sources */,
74B637842A5A350B00750202 /* Callback.mm in Sources */,
@@ -2025,6 +2108,13 @@
);
runOnlyForDeploymentPostprocessing = 0;
};
NWCLA000000000000000012 /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXSourcesBuildPhase section */
/* Begin PBXTargetDependency section */
@@ -2043,6 +2133,11 @@
target = 5134B5462EB7C1B000A6AD03 /* ShareQR */;
targetProxy = 5134B54F2EB7C1B000A6AD03 /* PBXContainerItemProxy */;
};
NWCLA000000000000000020 /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
target = NWCLA000000000000000011 /* NWCWidget */;
targetProxy = NWCLA000000000000000019 /* PBXContainerItemProxy */;
};
/* End PBXTargetDependency section */
/* Begin PBXVariantGroup section */
@@ -2566,6 +2661,98 @@
};
name = Release;
};
NWCLA000000000000000017 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
CLANG_ANALYZER_NONNULL = YES;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
CLANG_ENABLE_OBJC_WEAK = YES;
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CODE_SIGN_ENTITLEMENTS = NWCWidget/NWCWidget.entitlements;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEBUG_INFORMATION_FORMAT = dwarf;
DEVELOPMENT_TEAM = 9TU7M3555F;
ENABLE_USER_SCRIPT_SANDBOXING = YES;
GCC_C_LANGUAGE_STANDARD = gnu17;
GCC_PREPROCESSOR_DEFINITIONS = (
"DEBUG=1",
"$(inherited)",
);
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = NWCWidget/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = "Zeus NWC";
INFOPLIST_KEY_NSHumanReadableCopyright = "";
IPHONEOS_DEPLOYMENT_TARGET = 17.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
MARKETING_VERSION = 13.1.0;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES;
PRODUCT_BUNDLE_IDENTIFIER = com.zeusln.zeus.NWCWidget;
PRODUCT_NAME = "$(TARGET_NAME)";
SDKROOT = iphoneos;
SKIP_INSTALL = YES;
SUPPORTS_MACCATALYST = NO;
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)";
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Debug;
};
NWCLA000000000000000018 /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
CLANG_ANALYZER_NONNULL = YES;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
CLANG_ENABLE_OBJC_WEAK = YES;
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CODE_SIGN_ENTITLEMENTS = NWCWidget/NWCWidget.entitlements;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
COPY_PHASE_STRIP = NO;
CURRENT_PROJECT_VERSION = 1;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DEVELOPMENT_TEAM = 9TU7M3555F;
ENABLE_USER_SCRIPT_SANDBOXING = YES;
GCC_C_LANGUAGE_STANDARD = gnu17;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = NWCWidget/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = "Zeus NWC";
INFOPLIST_KEY_NSHumanReadableCopyright = "";
IPHONEOS_DEPLOYMENT_TARGET = 17.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
MARKETING_VERSION = 13.1.0;
MTL_FAST_MATH = YES;
PRODUCT_BUNDLE_IDENTIFIER = com.zeusln.zeus.NWCWidget;
PRODUCT_NAME = "$(TARGET_NAME)";
SDKROOT = iphoneos;
SKIP_INSTALL = YES;
SUPPORTS_MACCATALYST = NO;
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Release;
};
/* End XCBuildConfiguration section */
/* Begin XCConfigurationList section */
@@ -2623,6 +2810,15 @@
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
NWCLA000000000000000016 /* Build configuration list for PBXNativeTarget "NWCWidget" */ = {
isa = XCConfigurationList;
buildConfigurations = (
NWCLA000000000000000017 /* Debug */,
NWCLA000000000000000018 /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
/* End XCConfigurationList section */
};
rootObject = 83CBB9F71A601CBA00E9B192 /* Project object */;
+26 -1
View File
@@ -50,6 +50,18 @@ static void ClearKeychainIfNecessary() {
[RNNotifications startMonitorNotifications];
ClearKeychainIfNecessary();
// Initialize NWCActivityManager early so the widget bridge is set up before
// any Live Activity intent fires (intents run in the host-app process).
if (@available(iOS 16.1, *)) {
Class managerClass = NSClassFromString(@"NWCActivityManager");
if (managerClass && [managerClass respondsToSelector:@selector(shared)]) {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
[managerClass performSelector:@selector(shared)];
#pragma clang diagnostic pop
}
}
return [super application:application didFinishLaunchingWithOptions:launchOptions];
}
@@ -86,7 +98,20 @@ static void ClearKeychainIfNecessary() {
}
- (void)applicationWillTerminate:(UIApplication *)application {
NSLog(@"App will terminate");
NSLog(@"App will terminate stopping Live Activity");
if (@available(iOS 16.1, *)) {
Class managerClass = NSClassFromString(@"NWCActivityManager");
if (managerClass && [managerClass respondsToSelector:@selector(shared)]) {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
id manager = [managerClass performSelector:@selector(shared)];
#pragma clang diagnostic pop
if ([manager respondsToSelector:@selector(stopActivity)]) {
[manager performSelector:@selector(stopActivity)];
}
}
}
[[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:2.0]];
}
@end
+4
View File
@@ -128,6 +128,10 @@
<string>fetch</string>
<string>remote-notification</string>
</array>
<key>NSSupportsLiveActivities</key>
<true/>
<key>NSSupportsLiveActivitiesFrequentUpdates</key>
<true/>
<key>UIFileSharingEnabled</key>
<true/>
<key>UILaunchStoryboardName</key>
+157
View File
@@ -0,0 +1,157 @@
import Foundation
import ActivityKit
/// Manages the NWC Live Activity (Dynamic Island + lock-screen banner).
@available(iOS 16.1, *)
@objc final class NWCActivityManager: NSObject {
@objc static let shared = NWCActivityManager()
// Callbacks wired by NWCAudioKeepAlive.m so widget button taps reach the audio engine.
@objc var onNextTrack: (() -> Void)?
@objc var onPrevTrack: (() -> Void)?
@objc var onToggleMute: (() -> Void)?
@objc var onStop: (() -> Void)?
// Only adopt an existing activity that is still live (not ended/dismissed).
// Orphaned activities from previous crashes are ended in init().
private var activity: Activity<NWCLiveActivityAttributes>?
override private init() {
super.init()
NWCBridge = NWCWidgetBridgeImpl(manager: self)
cleanupOrphanedActivities()
}
// ObjC entry points
@objc func startActivity(trackName: String, isMuted: Bool) {
DispatchQueue.main.async { [weak self] in
self?.startActivityOnMain(trackName: trackName, isMuted: isMuted)
}
}
@objc func updateActivity(trackName: String?, isMuted: Bool) {
DispatchQueue.main.async { [weak self] in
self?.updateActivityOnMain(trackName: trackName, isMuted: isMuted)
}
}
@objc func stopActivity() {
DispatchQueue.main.async { [weak self] in
self?.stopActivityOnMain()
}
}
// Main-queue implementation
// 5-minute rolling staleDate safety net if the app is killed mid-session.
private var staleDate: Date { Date().addingTimeInterval(300) }
private func startActivityOnMain(trackName: String, isMuted: Bool) {
let auth = ActivityAuthorizationInfo()
guard auth.areActivitiesEnabled else {
NSLog("[NWCActivity] Live Activities disabled go to Settings → ZEUS → Live Activities")
return
}
// Re-use any existing live activity (active OR stale both accept updates).
if let existing = activity, isLive(existing) {
NSLog("[NWCActivity] activity already active, updating instead")
updateActivityOnMain(trackName: trackName, isMuted: isMuted)
return
}
let attrs = NWCLiveActivityAttributes(startedAt: .now)
let state = NWCLiveActivityAttributes.ContentState(
currentTrackName: trackName,
isMuted: isMuted
)
do {
if #available(iOS 16.2, *) {
let content = ActivityContent(state: state, staleDate: staleDate)
activity = try Activity.request(attributes: attrs, content: content, pushType: .none)
} else {
activity = try Activity.request(attributes: attrs, contentState: state, pushType: .none)
}
NSLog("[NWCActivity] started id=%@ track=%@", activity?.id ?? "?", trackName)
} catch {
NSLog("[NWCActivity] FAILED to start %@ (%@)",
error.localizedDescription, String(describing: error))
}
}
private func updateActivityOnMain(trackName: String?, isMuted: Bool) {
guard let current = activity, isLive(current) else {
NSLog("[NWCActivity] update skipped no live activity")
return
}
let state = NWCLiveActivityAttributes.ContentState(
currentTrackName: trackName,
isMuted: isMuted
)
Task {
if #available(iOS 16.2, *) {
// Rolling the staleDate forward keeps the activity alive during a long session.
let content = ActivityContent(state: state, staleDate: staleDate)
await current.update(content)
} else {
await current.update(using: state)
}
NSLog("[NWCActivity] updated track=%@ muted=%d",
trackName ?? "nil", isMuted ? 1 : 0)
}
}
private func stopActivityOnMain() {
guard let current = activity else { return }
activity = nil
let finalState = NWCLiveActivityAttributes.ContentState(
currentTrackName: nil,
isMuted: false
)
Task {
if #available(iOS 16.2, *) {
let content = ActivityContent(state: finalState, staleDate: Date())
await current.end(content, dismissalPolicy: .immediate)
} else {
await current.end(using: finalState, dismissalPolicy: .immediate)
}
NSLog("[NWCActivity] ended")
}
}
// Helpers
/// True when the activity can still be updated (active or stale but not ended/dismissed).
private func isLive(_ act: Activity<NWCLiveActivityAttributes>) -> Bool {
if act.activityState == .ended { return false }
if #available(iOS 16.2, *) {
if act.activityState == .dismissed { return false }
}
return true
}
/// End any activities left over from a previous session (e.g. a crash that
/// prevented the normal stopActivity() call from completing).
private func cleanupOrphanedActivities() {
let orphans = Activity<NWCLiveActivityAttributes>.activities
guard !orphans.isEmpty else { return }
NSLog("[NWCActivity] cleaning up %d orphaned activit(y|ies)", orphans.count)
Task {
for orphan in orphans {
if #available(iOS 16.2, *) {
let content = ActivityContent(
state: NWCLiveActivityAttributes.ContentState(
currentTrackName: nil, isMuted: false),
staleDate: Date()
)
await orphan.end(content, dismissalPolicy: .immediate)
} else {
await orphan.end(using: nil, dismissalPolicy: .immediate)
}
}
}
}
}
+38
View File
@@ -0,0 +1,38 @@
import Foundation
/// Real bridge implementation compiled into the main zeus app target only.
/// Routes Live Activity button taps back to NWCActivityManager's callback blocks
/// (which NWCAudioKeepAlive.m registers at session start).
@available(iOS 16.1, *)
final class NWCWidgetBridgeImpl: NWCWidgetBridgeProtocol {
private weak var manager: NWCActivityManager?
init(manager: NWCActivityManager) {
self.manager = manager
}
func nextTrack() {
DispatchQueue.main.async { [weak self] in
self?.manager?.onNextTrack?()
}
}
func prevTrack() {
DispatchQueue.main.async { [weak self] in
self?.manager?.onPrevTrack?()
}
}
func toggleMute() {
DispatchQueue.main.async { [weak self] in
self?.manager?.onToggleMute?()
}
}
func stopNWC() {
DispatchQueue.main.async { [weak self] in
self?.manager?.onStop?()
}
}
}