Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 5 additions & 3 deletions LoopFollow/Application/SceneDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
4 changes: 2 additions & 2 deletions LoopFollow/LiveActivity/LiveActivityManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
10 changes: 7 additions & 3 deletions LoopFollow/LiveActivity/StorageCurrentGlucoseStateProvider.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
}
131 changes: 67 additions & 64 deletions LoopFollow/LiveActivitySettingsView.swift
Original file line number Diff line number Diff line change
@@ -1,89 +1,92 @@
// 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)
}
}
}
}

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
20 changes: 13 additions & 7 deletions LoopFollow/Settings/SettingsMenuView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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
Expand All @@ -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()
Expand Down
5 changes: 4 additions & 1 deletion LoopFollow/ViewControllers/MainViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
72 changes: 37 additions & 35 deletions RestartLiveActivityIntent.swift
Original file line number Diff line number Diff line change
@@ -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