import Charts import Foundation import SwiftUI import Swinject import UniformTypeIdentifiers struct LiveActivityBottomRowConfiguration: BaseView { let resolver: Resolver @ObservedObject var state: LiveActivitySettings.StateModel @State private var selectedItems: [LiveActivityItem] = [] @State private var showAddItemDialog: Bool = false @State private var isEditMode: Bool = false @State private var draggingItem: LiveActivityItem? @State private var showDeleteAlert: Bool = false @Environment(\.colorScheme) var colorScheme private var color: LinearGradient { colorScheme == .dark ? LinearGradient( gradient: Gradient(colors: [ Color.bgDarkBlue, Color.bgDarkerDarkBlue ]), startPoint: .top, endPoint: .bottom ) : LinearGradient( gradient: Gradient(colors: [Color.gray.opacity(0.1)]), startPoint: .top, endPoint: .bottom ) } // Dummy data for glucose levels private var glucoseData: [DummyChart] { var data = [DummyChart]() let totalMinutes = 6 * 60 // 6 hours in minutes let interval = 5 // 5 minutes interval for each data point for minute in stride(from: 0, to: totalMinutes, by: interval) { let time = Double(minute) / 60.0 // Convert minutes to hours let glucoseLevel = 100 + 20 * sin(time) // Oscillating sine wave pattern data.append(DummyChart(time: Double(minute), glucoseLevel: glucoseLevel)) } return data } var body: some View { VStack { Group { VStack(alignment: .leading, spacing: 0) { Text("Live Activity Personalization".uppercased()) .frame(maxWidth: .infinity, alignment: .leading) .foregroundColor(.secondary) .font(.footnote) .padding(.leading) } VStack { Text( "Trio offers you to customize your Live Activity lock screen widget. The default configuration will display current glucose, IOB, COB and the time of last algorithm run." ) .padding() .font(.footnote) .foregroundColor(.secondary) } .background( RoundedRectangle(cornerRadius: 10, style: .continuous) .fill(Color.chart) ) } GroupBox { VStack { dummyChart HStack { if selectedItems.isEmpty { Spacer() Button(action: { showAddItemDialog.toggle() }) { VStack { Image(systemName: "plus") .font(.title2) .foregroundColor(.gray) } .frame(width: 60, height: 40) .overlay( RoundedRectangle(cornerRadius: 12) .stroke(style: StrokeStyle(lineWidth: 2, dash: [5])) .foregroundColor(.gray) ) } Spacer() } else { ForEach(Array(selectedItems.enumerated()), id: \.element) { index, item in if index > 0 { Divider() .frame(height: 50) } ZStack(alignment: .topTrailing) { getItemPreview(for: item) .frame(width: 50, height: 50) .padding(5) .background( draggingItem == item ? Color.blue.opacity(0.2) : Color.clear ) .cornerRadius(12) .overlay( RoundedRectangle(cornerRadius: 12) .stroke( draggingItem == item ? Color.blue : Color.primary, lineWidth: draggingItem == item ? 2 : 1 ) ) .opacity(draggingItem == item ? 0.85 : 1.0) .onDrag { self.draggingItem = item return NSItemProvider(object: item.rawValue as NSString) } .onDrop( of: [UTType.text], delegate: DropViewDelegate( item: item, items: $selectedItems, draggingItem: $draggingItem ) ) .disabled(!isEditMode) .rotationEffect(.degrees(isEditMode ? 2.5 : 0)) .rotation3DEffect(.degrees(isEditMode ? 2.5 : 0), axis: (x: 0, y: -5, z: 0)) .animation( isEditMode ? Animation.easeInOut(duration: 0.15) .repeatForever(autoreverses: true) : .default, value: isEditMode ) if isEditMode { Button(action: { showDeleteAlert = true }) { Image(systemName: "minus.circle.fill") .foregroundColor(Color(UIColor.systemGray2)) // Opaque foreground color .background(Color.white) // Adding a background for contrast .clipShape(Circle()) // Make sure the background stays circular .font(.system(size: 20)) } .offset(x: -45, y: -10) .alert(isPresented: $showDeleteAlert) { Alert( title: Text("Delete Widget"), message: Text("Are you sure you want to delete this widget?"), primaryButton: .destructive(Text("Delete")) { removeItem(item) }, secondaryButton: .cancel() ) } } } .animation(.easeInOut, value: draggingItem) .frame(maxWidth: .infinity) } } } .padding() .overlay( RoundedRectangle(cornerRadius: 12) .stroke(style: StrokeStyle(lineWidth: 2, dash: [5])) .foregroundColor(.gray) ) .cornerRadius(12) } }.padding(.vertical).groupBoxStyle(.dummyChart) if isEditMode { HStack { Image(systemName: "hand.draw.fill") Text("Tap 'Add +' to add a widget. Press, drag and drop a widget to re-order a widget.") }.frame(maxWidth: .infinity, alignment: .leading) .foregroundColor(.secondary) .font(.footnote) .padding(.horizontal) } Spacer() } .padding() .scrollContentBackground(.hidden).background(color) .navigationTitle("Widget Configuration") .navigationBarTitleDisplayMode(.automatic) .toolbar { ToolbarItem(placement: .topBarTrailing) { Button { isEditMode.toggle() } label: { HStack { Text("Edit") } } } ToolbarItem(placement: .topBarTrailing) { Button { showAddItemDialog.toggle() } label: { HStack { Text("Add") Image(systemName: "plus") } } } } .confirmationDialog("Add Widget", isPresented: $showAddItemDialog, titleVisibility: .visible) { ForEach(LiveActivityItem.allCases, id: \.self) { item in Button(item.displayName) { addItem(item) }.disabled(selectedItems.contains(item)) } } .onAppear { configureView() loadOrder() } } private func getItemPreview(for item: LiveActivityItem) -> some View { switch item { case .currentGlucose: return AnyView(currentGlucosePreview) case .cob: return AnyView(cobPreview) case .iob: return AnyView(iobPreview) case .updatedLabel: return AnyView(updatedLabelPreview) } } private var dummyChart: some View { Chart { ForEach(glucoseData) { data in PointMark( x: .value("Time", data.time), y: .value("Glucose Level", data.glucoseLevel) ).foregroundStyle(.green.gradient).symbolSize(15) } } .chartPlotStyle { plotContent in plotContent .background( RoundedRectangle(cornerRadius: 12) .fill(Color.cyan.opacity(0.15)) ) .clipShape(RoundedRectangle(cornerRadius: 12)) } .chartYAxis { AxisMarks(position: .trailing) { _ in AxisGridLine(stroke: .init(lineWidth: 0.2, dash: [2, 3])).foregroundStyle(Color.primary) } } .chartYAxis(.hidden) .chartYScale(domain: 40 ... 200) .chartXAxis { AxisMarks(position: .automatic) { _ in AxisGridLine(stroke: .init(lineWidth: 0.2, dash: [2, 3])).foregroundStyle(Color.primary) } } .chartXAxis(.hidden) .frame(height: 100) } private var currentGlucosePreview: some View { VStack { HStack(alignment: .center) { Text("123") .fontWeight(.bold) .font(.caption) } HStack(spacing: -5) { HStack { Text("\u{2192}") Text("+6") }.foregroundStyle(.primary).font(.caption2) } } } private var cobPreview: some View { VStack(spacing: 2) { Text("25 g").fontWeight(.bold).font(.caption) Text("COB").font(.caption2).foregroundStyle(.primary) } } private var iobPreview: some View { VStack(spacing: 2) { Text("2 U").fontWeight(.bold).font(.caption) Text("IOB").font(.caption2).foregroundStyle(.primary) } } private var updatedLabelPreview: some View { VStack { Text("19:05") .fontWeight(.bold) .font(.caption) .foregroundStyle(.primary) Text("Updated").font(.caption2).foregroundStyle(.primary) } } private func loadOrder() { if let savedItems = UserDefaults.standard.loadLiveActivityOrder() { selectedItems = savedItems } else { selectedItems = LiveActivityItem.defaultItems saveOrder() } print("Loaded order: \(selectedItems.map(\.rawValue))") updateVisibilityForSelectedItems() } private func saveOrder() { print("Saving order: \(selectedItems.map(\.rawValue))") UserDefaults.standard.saveLiveActivityOrder(selectedItems) } private func addItem(_ item: LiveActivityItem) { setItemVisibility(item: item, isVisible: true) selectedItems.append(item) saveOrder() } private func removeItem(_ item: LiveActivityItem) { setItemVisibility(item: item, isVisible: false) selectedItems.removeAll { $0 == item } saveOrder() } private func setItemVisibility(item: LiveActivityItem, isVisible: Bool) { switch item { case .currentGlucose: state.showCurrentGlucose = isVisible case .iob: state.showIOB = isVisible case .cob: state.showCOB = isVisible case .updatedLabel: state.showUpdatedLabel = isVisible } } private func updateVisibilityForSelectedItems() { for item in selectedItems { setItemVisibility(item: item, isVisible: true) } let allItems = LiveActivityItem.allCases let hiddenItems = allItems.filter { !selectedItems.contains($0) } for item in hiddenItems { setItemVisibility(item: item, isVisible: false) } } } struct DropViewDelegate: DropDelegate { let item: LiveActivityItem @Binding var items: [LiveActivityItem] @Binding var draggingItem: LiveActivityItem? func dropEntered(info _: DropInfo) { guard let draggingItem = draggingItem else { return } if draggingItem != item { let fromIndex = items.firstIndex(of: draggingItem)! let toIndex = items.firstIndex(of: item)! withAnimation { items.move(fromOffsets: IndexSet(integer: fromIndex), toOffset: toIndex > fromIndex ? toIndex + 1 : toIndex) } // Save to User Defaults saveOrder() // Trigger Live Activity Update Foundation.NotificationCenter.default.post(name: .liveActivityOrderDidChange, object: nil) } } func performDrop(info _: DropInfo) -> Bool { draggingItem = nil return true } private func saveOrder() { UserDefaults.standard.saveLiveActivityOrder(items) } } // Extension for UserDefaults to save and load the order extension UserDefaults { private enum Keys { static let liveActivityOrder = "liveActivityOrder" } func saveLiveActivityOrder(_ items: [LiveActivityItem]) { let itemStrings = items.map(\.rawValue) set(itemStrings, forKey: Keys.liveActivityOrder) } func loadLiveActivityOrder() -> [LiveActivityItem]? { if let itemStrings = array(forKey: Keys.liveActivityOrder) as? [String] { return itemStrings.compactMap { LiveActivityItem(rawValue: $0) } } return nil } } // Enum to represent each live activity item enum LiveActivityItem: String, CaseIterable, Identifiable { case currentGlucose case iob case cob case updatedLabel var id: String { rawValue } static var defaultItems: [LiveActivityItem] { [.currentGlucose, .iob, .cob, .updatedLabel] } var displayName: String { switch self { case .currentGlucose: return "Current Glucose" case .iob: return "IOB" case .cob: return "COB" case .updatedLabel: return "Updated Label" } } } struct DummyChart: Identifiable { let id = UUID() let time: Double // Time in hours let glucoseLevel: Double // Glucose level in mg/dL } struct DummyChartGroupBoxStyle: GroupBoxStyle { func makeBody(configuration: Configuration) -> some View { VStack { configuration.content } .padding() .clipShape(RoundedRectangle(cornerRadius: 12, style: .continuous)) .background(Color.chart, in: RoundedRectangle(cornerRadius: 12)) .frame(width: UIScreen.main.bounds.width * 0.9) } } extension GroupBoxStyle where Self == DummyChartGroupBoxStyle { static var dummyChart: DummyChartGroupBoxStyle { .init() } }