From 9f0435d011b6b3a9d744c71ecd640508c3e8f874 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20Bj=C3=B6rkert?= Date: Sat, 11 Apr 2026 16:26:36 +0200 Subject: [PATCH] Exclude Live Activity code from Mac Catalyst builds Wrap all Live Activity references behind #if !targetEnvironment(macCatalyst) compiler directives to fix build errors when targeting Mac Catalyst, since ActivityKit is not available on that platform. --- LoopFollow/Application/SceneDelegate.swift | 8 +- .../LiveActivity/LiveActivityManager.swift | 4 +- .../StorageCurrentGlucoseStateProvider.swift | 10 +- LoopFollow/LiveActivitySettingsView.swift | 131 +++++++++--------- LoopFollow/Settings/SettingsMenuView.swift | 20 ++- .../ViewControllers/MainViewController.swift | 5 +- RestartLiveActivityIntent.swift | 72 +++++----- 7 files changed, 135 insertions(+), 115 deletions(-) diff --git a/LoopFollow/Application/SceneDelegate.swift b/LoopFollow/Application/SceneDelegate.swift index e702db267..882db04e6 100644 --- a/LoopFollow/Application/SceneDelegate.swift +++ b/LoopFollow/Application/SceneDelegate.swift @@ -39,9 +39,11 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { // scene(_:openURLContexts:) fires after sceneDidBecomeActive when the app // foregrounds from background. Post on the next run loop so the view // hierarchy (including any presented modals) is fully settled. - DispatchQueue.main.async { - NotificationCenter.default.post(name: .liveActivityDidForeground, object: nil) - } + #if !targetEnvironment(macCatalyst) + DispatchQueue.main.async { + NotificationCenter.default.post(name: .liveActivityDidForeground, object: nil) + } + #endif } func sceneWillResignActive(_: UIScene) { diff --git a/LoopFollow/LiveActivity/LiveActivityManager.swift b/LoopFollow/LiveActivity/LiveActivityManager.swift index fb73c3409..180fdc1c1 100644 --- a/LoopFollow/LiveActivity/LiveActivityManager.swift +++ b/LoopFollow/LiveActivity/LiveActivityManager.swift @@ -780,10 +780,10 @@ final class LiveActivityManager { } } -#endif - extension Notification.Name { /// Posted when the user taps the Live Activity or Dynamic Island. /// Observers navigate to the Home or Snoozer tab as appropriate. static let liveActivityDidForeground = Notification.Name("liveActivityDidForeground") } + +#endif diff --git a/LoopFollow/LiveActivity/StorageCurrentGlucoseStateProvider.swift b/LoopFollow/LiveActivity/StorageCurrentGlucoseStateProvider.swift index 38bde98d0..2ee27bd59 100644 --- a/LoopFollow/LiveActivity/StorageCurrentGlucoseStateProvider.swift +++ b/LoopFollow/LiveActivity/StorageCurrentGlucoseStateProvider.swift @@ -131,8 +131,12 @@ struct StorageCurrentGlucoseStateProvider: CurrentGlucoseStateProviding { // MARK: - Renewal var showRenewalOverlay: Bool { - let renewBy = Storage.shared.laRenewBy.value - let now = Date().timeIntervalSince1970 - return renewBy > 0 && now >= renewBy - LiveActivityManager.renewalWarning + #if targetEnvironment(macCatalyst) + return false + #else + let renewBy = Storage.shared.laRenewBy.value + let now = Date().timeIntervalSince1970 + return renewBy > 0 && now >= renewBy - LiveActivityManager.renewalWarning + #endif } } diff --git a/LoopFollow/LiveActivitySettingsView.swift b/LoopFollow/LiveActivitySettingsView.swift index 2264881bb..f10e3a07d 100644 --- a/LoopFollow/LiveActivitySettingsView.swift +++ b/LoopFollow/LiveActivitySettingsView.swift @@ -1,40 +1,58 @@ // LoopFollow // LiveActivitySettingsView.swift -import SwiftUI +#if !targetEnvironment(macCatalyst) -struct LiveActivitySettingsView: View { - @State private var laEnabled: Bool = Storage.shared.laEnabled.value - @State private var restartConfirmed = false - @State private var slots: [LiveActivitySlotOption] = LAAppGroupSettings.slots() - @State private var smallWidgetSlot: LiveActivitySlotOption = LAAppGroupSettings.smallWidgetSlot() + import SwiftUI - private let slotLabels = ["Top left", "Top right", "Bottom left", "Bottom right"] + struct LiveActivitySettingsView: View { + @State private var laEnabled: Bool = Storage.shared.laEnabled.value + @State private var restartConfirmed = false + @State private var slots: [LiveActivitySlotOption] = LAAppGroupSettings.slots() + @State private var smallWidgetSlot: LiveActivitySlotOption = LAAppGroupSettings.smallWidgetSlot() - var body: some View { - Form { - Section(header: Text("Live Activity")) { - Toggle("Enable Live Activity", isOn: $laEnabled) - } + private let slotLabels = ["Top left", "Top right", "Bottom left", "Bottom right"] + + var body: some View { + Form { + Section(header: Text("Live Activity")) { + Toggle("Enable Live Activity", isOn: $laEnabled) + } - if laEnabled { - Section { - Button(restartConfirmed ? "Live Activity Restarted" : "Restart Live Activity") { - LiveActivityManager.shared.forceRestart() - restartConfirmed = true - DispatchQueue.main.asyncAfter(deadline: .now() + 2) { - restartConfirmed = false + if laEnabled { + Section { + Button(restartConfirmed ? "Live Activity Restarted" : "Restart Live Activity") { + LiveActivityManager.shared.forceRestart() + restartConfirmed = true + DispatchQueue.main.asyncAfter(deadline: .now() + 2) { + restartConfirmed = false + } } + .disabled(restartConfirmed) } - .disabled(restartConfirmed) } - } - Section(header: Text("Grid Slots - Live Activity")) { - ForEach(0 ..< 4, id: \.self) { index in - Picker(slotLabels[index], selection: Binding( - get: { slots[index] }, - set: { selectSlot($0, at: index) } + Section(header: Text("Grid Slots - Live Activity")) { + ForEach(0 ..< 4, id: \.self) { index in + Picker(slotLabels[index], selection: Binding( + get: { slots[index] }, + set: { selectSlot($0, at: index) } + )) { + ForEach(LiveActivitySlotOption.allCases, id: \.self) { option in + Text(option.displayName).tag(option) + } + } + } + } + + Section(header: Text("Grid Slot - CarPlay / Watch")) { + Picker("Right slot", selection: Binding( + get: { smallWidgetSlot }, + set: { newValue in + smallWidgetSlot = newValue + LAAppGroupSettings.setSmallWidgetSlot(newValue) + LiveActivityManager.shared.refreshFromCurrentState(reason: "small widget slot changed") + } )) { ForEach(LiveActivitySlotOption.allCases, id: \.self) { option in Text(option.displayName).tag(option) @@ -42,48 +60,33 @@ struct LiveActivitySettingsView: View { } } } - - Section(header: Text("Grid Slot - CarPlay / Watch")) { - Picker("Right slot", selection: Binding( - get: { smallWidgetSlot }, - set: { newValue in - smallWidgetSlot = newValue - LAAppGroupSettings.setSmallWidgetSlot(newValue) - LiveActivityManager.shared.refreshFromCurrentState(reason: "small widget slot changed") - } - )) { - ForEach(LiveActivitySlotOption.allCases, id: \.self) { option in - Text(option.displayName).tag(option) - } - } + .onReceive(Storage.shared.laEnabled.$value) { newValue in + if newValue != laEnabled { laEnabled = newValue } } - } - .onReceive(Storage.shared.laEnabled.$value) { newValue in - if newValue != laEnabled { laEnabled = newValue } - } - .onChange(of: laEnabled) { newValue in - Storage.shared.laEnabled.value = newValue - if newValue { - LiveActivityManager.shared.forceRestart() - } else { - LiveActivityManager.shared.end(dismissalPolicy: .immediate) + .onChange(of: laEnabled) { newValue in + Storage.shared.laEnabled.value = newValue + if newValue { + LiveActivityManager.shared.forceRestart() + } else { + LiveActivityManager.shared.end(dismissalPolicy: .immediate) + } } + .preferredColorScheme(Storage.shared.appearanceMode.value.colorScheme) + .navigationTitle("Live Activity") + .navigationBarTitleDisplayMode(.inline) } - .preferredColorScheme(Storage.shared.appearanceMode.value.colorScheme) - .navigationTitle("Live Activity") - .navigationBarTitleDisplayMode(.inline) - } - /// Selects an option for the given slot index, enforcing uniqueness: - /// if the chosen option is already in another slot, that slot is cleared to `.none`. - private func selectSlot(_ option: LiveActivitySlotOption, at index: Int) { - if option != .none { - for i in 0 ..< slots.count where i != index && slots[i] == option { - slots[i] = .none + /// Selects an option for the given slot index, enforcing uniqueness: + /// if the chosen option is already in another slot, that slot is cleared to `.none`. + private func selectSlot(_ option: LiveActivitySlotOption, at index: Int) { + if option != .none { + for i in 0 ..< slots.count where i != index && slots[i] == option { + slots[i] = .none + } } + slots[index] = option + LAAppGroupSettings.setSlots(slots) + LiveActivityManager.shared.refreshFromCurrentState(reason: "slot config changed") } - slots[index] = option - LAAppGroupSettings.setSlots(slots) - LiveActivityManager.shared.refreshFromCurrentState(reason: "slot config changed") } -} +#endif diff --git a/LoopFollow/Settings/SettingsMenuView.swift b/LoopFollow/Settings/SettingsMenuView.swift index 80ae07f16..5c0ade26f 100644 --- a/LoopFollow/Settings/SettingsMenuView.swift +++ b/LoopFollow/Settings/SettingsMenuView.swift @@ -71,11 +71,13 @@ struct SettingsMenuView: View { settingsPath.value.append(Sheet.apn) } - NavigationRow(title: "Live Activity", - icon: "dot.radiowaves.left.and.right") - { - settingsPath.value.append(Sheet.liveActivity) - } + #if !targetEnvironment(macCatalyst) + NavigationRow(title: "Live Activity", + icon: "dot.radiowaves.left.and.right") + { + settingsPath.value.append(Sheet.liveActivity) + } + #endif if !nightscoutURL.value.isEmpty { NavigationRow(title: "Remote", @@ -171,7 +173,9 @@ private enum Sheet: Hashable, Identifiable { case infoDisplay case alarmSettings case apn - case liveActivity + #if !targetEnvironment(macCatalyst) + case liveActivity + #endif case remote case importExport case calendar, contact @@ -192,7 +196,9 @@ private enum Sheet: Hashable, Identifiable { case .infoDisplay: InfoDisplaySettingsView(viewModel: .init()) case .alarmSettings: AlarmSettingsView() case .apn: APNSettingsView() - case .liveActivity: LiveActivitySettingsView() + #if !targetEnvironment(macCatalyst) + case .liveActivity: LiveActivitySettingsView() + #endif case .remote: RemoteSettingsView(viewModel: .init()) case .importExport: ImportExportSettingsView() case .calendar: CalendarSettingsView() diff --git a/LoopFollow/ViewControllers/MainViewController.swift b/LoopFollow/ViewControllers/MainViewController.swift index ac1f19a24..c82ecb590 100644 --- a/LoopFollow/ViewControllers/MainViewController.swift +++ b/LoopFollow/ViewControllers/MainViewController.swift @@ -194,7 +194,10 @@ class MainViewController: UIViewController, UITableViewDataSource, ChartViewDele // when runMigrationsIfNeeded() is called. This catches migrations deferred by a // background BGAppRefreshTask launch in Before-First-Unlock state. notificationCenter.addObserver(self, selector: #selector(appDidBecomeActive), name: UIApplication.didBecomeActiveNotification, object: nil) - notificationCenter.addObserver(self, selector: #selector(navigateOnLAForeground), name: .liveActivityDidForeground, object: nil) + + #if !targetEnvironment(macCatalyst) + notificationCenter.addObserver(self, selector: #selector(navigateOnLAForeground), name: .liveActivityDidForeground, object: nil) + #endif // Setup the Graph if firstGraphLoad { diff --git a/RestartLiveActivityIntent.swift b/RestartLiveActivityIntent.swift index c594d5fa9..95563dafc 100644 --- a/RestartLiveActivityIntent.swift +++ b/RestartLiveActivityIntent.swift @@ -1,45 +1,47 @@ // LoopFollow // RestartLiveActivityIntent.swift -import AppIntents -import UIKit - -@available(iOS 16.4, *) -struct RestartLiveActivityIntent: AppIntent, ForegroundContinuableIntent { - static var title: LocalizedStringResource = "Restart Live Activity" - static var description = IntentDescription("Starts or restarts the LoopFollow Live Activity.") +#if !targetEnvironment(macCatalyst) + import AppIntents + import UIKit + + @available(iOS 16.4, *) + struct RestartLiveActivityIntent: AppIntent, ForegroundContinuableIntent { + static var title: LocalizedStringResource = "Restart Live Activity" + static var description = IntentDescription("Starts or restarts the LoopFollow Live Activity.") + + func perform() async throws -> some IntentResult & ProvidesDialog { + Storage.shared.laEnabled.value = true + + let keyId = Storage.shared.lfKeyId.value + let apnsKey = Storage.shared.lfApnsKey.value + + if keyId.isEmpty || apnsKey.isEmpty { + if let url = URL(string: "loopfollow://settings/live-activity") { + await MainActor.run { UIApplication.shared.open(url) } + } + return .result(dialog: "Please enter your APNs credentials in LoopFollow settings to use the Live Activity.") + } - func perform() async throws -> some IntentResult & ProvidesDialog { - Storage.shared.laEnabled.value = true + if #available(iOS 26.0, *) { + try await continueInForeground() + } - let keyId = Storage.shared.lfKeyId.value - let apnsKey = Storage.shared.lfApnsKey.value + await MainActor.run { LiveActivityManager.shared.forceRestart() } - if keyId.isEmpty || apnsKey.isEmpty { - if let url = URL(string: "loopfollow://settings/live-activity") { - await MainActor.run { UIApplication.shared.open(url) } - } - return .result(dialog: "Please enter your APNs credentials in LoopFollow settings to use the Live Activity.") + return .result(dialog: "Live Activity restarted.") } + } - if #available(iOS 26.0, *) { - try await continueInForeground() + @available(iOS 16.4, *) + struct LoopFollowAppShortcuts: AppShortcutsProvider { + static var appShortcuts: [AppShortcut] { + AppShortcut( + intent: RestartLiveActivityIntent(), + phrases: ["Restart Live Activity in \(.applicationName)"], + shortTitle: "Restart Live Activity", + systemImageName: "dot.radiowaves.left.and.right" + ) } - - await MainActor.run { LiveActivityManager.shared.forceRestart() } - - return .result(dialog: "Live Activity restarted.") - } -} - -@available(iOS 16.4, *) -struct LoopFollowAppShortcuts: AppShortcutsProvider { - static var appShortcuts: [AppShortcut] { - AppShortcut( - intent: RestartLiveActivityIntent(), - phrases: ["Restart Live Activity in \(.applicationName)"], - shortTitle: "Restart Live Activity", - systemImageName: "dot.radiowaves.left.and.right" - ) } -} +#endif