|
|
@@ -1,4 +1,5 @@
|
|
|
import CoreData
|
|
|
+import Foundation
|
|
|
import SwiftUI
|
|
|
import Swinject
|
|
|
|
|
|
@@ -10,8 +11,14 @@ extension OverrideProfilesConfig {
|
|
|
@State private var isEditing = false
|
|
|
@State private var showAlert = false
|
|
|
@State private var showingDetail = false
|
|
|
+ @State private var selectedPreset: OverridePresets?
|
|
|
+ @State private var isEditSheetPresented: Bool = false
|
|
|
@State private var alertSring = ""
|
|
|
@State var isSheetPresented: Bool = false
|
|
|
+ @State private var originalPreset: OverridePresets?
|
|
|
+ @State private var showDeleteAlert = false
|
|
|
+ @State private var indexToDelete: Int?
|
|
|
+ @State private var profileNameToDelete: String = ""
|
|
|
|
|
|
@Environment(\.dismiss) var dismiss
|
|
|
@Environment(\.managedObjectContext) var moc
|
|
|
@@ -22,6 +29,7 @@ extension OverrideProfilesConfig {
|
|
|
format: "name != %@", "" as String
|
|
|
)
|
|
|
) var fetchedProfiles: FetchedResults<OverridePresets>
|
|
|
+ var units: GlucoseUnits = .mmolL
|
|
|
|
|
|
private var formatter: NumberFormatter {
|
|
|
let formatter = NumberFormatter()
|
|
|
@@ -43,144 +51,246 @@ extension OverrideProfilesConfig {
|
|
|
|
|
|
var presetPopover: some View {
|
|
|
Form {
|
|
|
- Section {
|
|
|
- TextField("Name Of Profile", text: $state.profileName)
|
|
|
- } header: { Text("Enter Name of Profile") }
|
|
|
-
|
|
|
+ nameSection(header: "Enter a name")
|
|
|
+ settingsSection(header: "Settings to save")
|
|
|
Section {
|
|
|
Button("Save") {
|
|
|
state.savePreset()
|
|
|
isSheetPresented = false
|
|
|
}
|
|
|
- .disabled(state.profileName.isEmpty || fetchedProfiles.filter({ $0.name == state.profileName }).isNotEmpty)
|
|
|
+ .disabled(
|
|
|
+ state.profileName.isEmpty || fetchedProfiles
|
|
|
+ .contains(where: { $0.name == state.profileName })
|
|
|
+ )
|
|
|
|
|
|
Button("Cancel") {
|
|
|
isSheetPresented = false
|
|
|
}
|
|
|
+ .tint(.red)
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- var body: some View {
|
|
|
+ var editPresetPopover: some View {
|
|
|
Form {
|
|
|
- if state.presets.isNotEmpty {
|
|
|
- Section {
|
|
|
- ForEach(fetchedProfiles) { preset in
|
|
|
- profilesView(for: preset)
|
|
|
- }.onDelete(perform: removeProfile)
|
|
|
+ nameSection(header: "Change name?")
|
|
|
+ settingsConfig(header: "Change settings")
|
|
|
+ Section {
|
|
|
+ Button("Save") {
|
|
|
+ guard let selectedPreset = selectedPreset else { return }
|
|
|
+ state.updatePreset(selectedPreset)
|
|
|
+ isEditSheetPresented = false
|
|
|
+ }
|
|
|
+ .disabled(!hasChanges())
|
|
|
+
|
|
|
+ Button("Cancel") {
|
|
|
+ isEditSheetPresented = false
|
|
|
}
|
|
|
+ .tint(.red)
|
|
|
}
|
|
|
- Section {
|
|
|
- VStack {
|
|
|
- Slider(
|
|
|
- value: $state.percentage,
|
|
|
- in: 10 ... 200,
|
|
|
- step: 1,
|
|
|
- onEditingChanged: { editing in
|
|
|
- isEditing = editing
|
|
|
- }
|
|
|
- ).accentColor(state.percentage >= 130 ? .red : .blue)
|
|
|
- Text("\(state.percentage.formatted(.number)) %")
|
|
|
- .foregroundColor(
|
|
|
- state
|
|
|
- .percentage >= 130 ? .red :
|
|
|
- (isEditing ? .orange : .blue)
|
|
|
- )
|
|
|
- .font(.largeTitle)
|
|
|
- Spacer()
|
|
|
- Toggle(isOn: $state._indefinite) {
|
|
|
- Text("Enable indefinitely")
|
|
|
+ }
|
|
|
+ .onAppear {
|
|
|
+ if let preset = selectedPreset {
|
|
|
+ originalPreset = preset
|
|
|
+ state.populateSettings(from: preset)
|
|
|
+ }
|
|
|
+ }
|
|
|
+ .onDisappear {
|
|
|
+ state.savedSettings()
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ @ViewBuilder private func nameSection(header: String) -> some View {
|
|
|
+ Section {
|
|
|
+ TextField("Profile override name", text: $state.profileName)
|
|
|
+ } header: {
|
|
|
+ Text(header)
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ @ViewBuilder private func settingsConfig(header: String) -> some View {
|
|
|
+ Section {
|
|
|
+ VStack {
|
|
|
+ Spacer()
|
|
|
+ Text("\(state.percentage.formatted(.number)) %")
|
|
|
+ .foregroundColor(
|
|
|
+ state
|
|
|
+ .percentage >= 130 ? .red :
|
|
|
+ (isEditing ? .orange : .blue)
|
|
|
+ )
|
|
|
+ .font(.largeTitle)
|
|
|
+ Slider(
|
|
|
+ value: $state.percentage,
|
|
|
+ in: 10 ... 200,
|
|
|
+ step: 1,
|
|
|
+ onEditingChanged: { editing in
|
|
|
+ isEditing = editing
|
|
|
}
|
|
|
+ ).accentColor(state.percentage >= 130 ? .red : .blue)
|
|
|
+ Spacer()
|
|
|
+ Toggle(isOn: $state._indefinite) {
|
|
|
+ Text("Enable indefinitely")
|
|
|
}
|
|
|
- if !state._indefinite {
|
|
|
- HStack {
|
|
|
- Text("Duration")
|
|
|
- DecimalTextField("0", value: $state.duration, formatter: formatter, cleanInput: false)
|
|
|
- Text("minutes").foregroundColor(.secondary)
|
|
|
- }
|
|
|
+ }
|
|
|
+ if !state._indefinite {
|
|
|
+ HStack {
|
|
|
+ Text("Duration")
|
|
|
+ DecimalTextField("0", value: $state.duration, formatter: formatter, cleanInput: false)
|
|
|
+ Text("minutes").foregroundColor(.secondary)
|
|
|
}
|
|
|
+ }
|
|
|
|
|
|
+ HStack {
|
|
|
+ Toggle(isOn: $state.override_target) {
|
|
|
+ Text("Override Profile Target")
|
|
|
+ }
|
|
|
+ }
|
|
|
+ if state.override_target {
|
|
|
HStack {
|
|
|
- Toggle(isOn: $state.override_target) {
|
|
|
- Text("Override Profile Target")
|
|
|
- }
|
|
|
+ Text("Target Glucose")
|
|
|
+ DecimalTextField("0", value: $state.target, formatter: glucoseFormatter, cleanInput: false)
|
|
|
+ Text(state.units.rawValue).foregroundColor(.secondary)
|
|
|
}
|
|
|
- if state.override_target {
|
|
|
- HStack {
|
|
|
- Text("Target Glucose")
|
|
|
- DecimalTextField("0", value: $state.target, formatter: glucoseFormatter, cleanInput: false)
|
|
|
- Text(state.units.rawValue).foregroundColor(.secondary)
|
|
|
- }
|
|
|
+ }
|
|
|
+ HStack {
|
|
|
+ Toggle(isOn: $state.advancedSettings) {
|
|
|
+ Text("More options")
|
|
|
}
|
|
|
+ }
|
|
|
+ if state.advancedSettings {
|
|
|
HStack {
|
|
|
- Toggle(isOn: $state.advancedSettings) {
|
|
|
- Text("More options")
|
|
|
+ Toggle(isOn: $state.smbIsOff) {
|
|
|
+ Text("Always Disable SMBs")
|
|
|
}
|
|
|
}
|
|
|
- if state.advancedSettings {
|
|
|
- HStack {
|
|
|
- Toggle(isOn: $state.smbIsOff) {
|
|
|
- Text("Always Disable SMBs")
|
|
|
- }
|
|
|
- }
|
|
|
- if !state.smbIsOff {
|
|
|
- HStack {
|
|
|
- Toggle(isOn: $state.smbIsScheduledOff) {
|
|
|
- Text("Schedule when SMBs are Off")
|
|
|
- }
|
|
|
- }
|
|
|
- if state.smbIsScheduledOff {
|
|
|
- HStack {
|
|
|
- Text("First Hour SMBs are Off (24 hours)")
|
|
|
- DecimalTextField("0", value: $state.start, formatter: formatter, cleanInput: false)
|
|
|
- Text("hour").foregroundColor(.secondary)
|
|
|
- }
|
|
|
- HStack {
|
|
|
- Text("First Hour SMBs are Resumed (24 hours)")
|
|
|
- DecimalTextField("0", value: $state.end, formatter: formatter, cleanInput: false)
|
|
|
- Text("hour").foregroundColor(.secondary)
|
|
|
- }
|
|
|
- }
|
|
|
- }
|
|
|
+ if !state.smbIsOff {
|
|
|
HStack {
|
|
|
- Toggle(isOn: $state.isfAndCr) {
|
|
|
- Text("Change ISF and CR")
|
|
|
+ Toggle(isOn: $state.smbIsScheduledOff) {
|
|
|
+ Text("Schedule when SMBs are Off")
|
|
|
}
|
|
|
}
|
|
|
- if !state.isfAndCr {
|
|
|
+ if state.smbIsScheduledOff {
|
|
|
HStack {
|
|
|
- Toggle(isOn: $state.isf) {
|
|
|
- Text("Change ISF")
|
|
|
- }
|
|
|
+ Text("First Hour SMBs are Off (24 hours)")
|
|
|
+ DecimalTextField("0", value: $state.start, formatter: formatter, cleanInput: false)
|
|
|
+ Text("hour").foregroundColor(.secondary)
|
|
|
}
|
|
|
HStack {
|
|
|
- Toggle(isOn: $state.cr) {
|
|
|
- Text("Change CR")
|
|
|
- }
|
|
|
+ Text("First Hour SMBs are Resumed (24 hours)")
|
|
|
+ DecimalTextField("0", value: $state.end, formatter: formatter, cleanInput: false)
|
|
|
+ Text("hour").foregroundColor(.secondary)
|
|
|
}
|
|
|
}
|
|
|
+ }
|
|
|
+ HStack {
|
|
|
+ Toggle(isOn: $state.isfAndCr) {
|
|
|
+ Text("Change ISF and CR")
|
|
|
+ }
|
|
|
+ }
|
|
|
+ if !state.isfAndCr {
|
|
|
HStack {
|
|
|
- Text("SMB Minutes")
|
|
|
- DecimalTextField(
|
|
|
- "0",
|
|
|
- value: $state.smbMinutes,
|
|
|
- formatter: formatter,
|
|
|
- cleanInput: false
|
|
|
- )
|
|
|
- Text("minutes").foregroundColor(.secondary)
|
|
|
+ Toggle(isOn: $state.isf) {
|
|
|
+ Text("Change ISF")
|
|
|
+ }
|
|
|
}
|
|
|
HStack {
|
|
|
- Text("UAM SMB Minutes")
|
|
|
- DecimalTextField(
|
|
|
- "0",
|
|
|
- value: $state.uamMinutes,
|
|
|
- formatter: formatter,
|
|
|
- cleanInput: false
|
|
|
- )
|
|
|
- Text("minutes").foregroundColor(.secondary)
|
|
|
+ Toggle(isOn: $state.cr) {
|
|
|
+ Text("Change CR")
|
|
|
+ }
|
|
|
}
|
|
|
}
|
|
|
+ HStack {
|
|
|
+ Text("SMB Minutes")
|
|
|
+ DecimalTextField(
|
|
|
+ "0",
|
|
|
+ value: $state.smbMinutes,
|
|
|
+ formatter: formatter,
|
|
|
+ cleanInput: false
|
|
|
+ )
|
|
|
+ Text("minutes").foregroundColor(.secondary)
|
|
|
+ }
|
|
|
+ HStack {
|
|
|
+ Text("UAM SMB Minutes")
|
|
|
+ DecimalTextField(
|
|
|
+ "0",
|
|
|
+ value: $state.uamMinutes,
|
|
|
+ formatter: formatter,
|
|
|
+ cleanInput: false
|
|
|
+ )
|
|
|
+ Text("minutes").foregroundColor(.secondary)
|
|
|
+ }
|
|
|
+ }
|
|
|
+ } header: {
|
|
|
+ Text(header)
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ @ViewBuilder private func settingsSection(header: String) -> some View {
|
|
|
+ Section(header: Text(header)) {
|
|
|
+ let percentString = Text("Override: \(Int(state.percentage))%")
|
|
|
+ let targetString = state
|
|
|
+ .target != 0 ? Text("Target: \(state.target.formatted()) \(state.units.rawValue)") : Text("")
|
|
|
+ let durationString = state
|
|
|
+ ._indefinite ? Text("Duration: Indefinite") : Text("Duration: \(state.duration.formatted()) minutes")
|
|
|
+ let isfString = state.isf ? Text("Change ISF") : Text("")
|
|
|
+ let crString = state.cr ? Text("Change CR") : Text("")
|
|
|
+ let smbString = state.smbIsOff ? Text("Disable SMB") : Text("")
|
|
|
+ let scheduledSMBString = state.smbIsScheduledOff ? Text("SMB Schedule On") : Text("")
|
|
|
+ let maxMinutesSMBString = state
|
|
|
+ .smbMinutes != 0 ? Text("\(state.smbMinutes.formatted()) SMB Basal minutes") : Text("")
|
|
|
+ let maxMinutesUAMString = state
|
|
|
+ .uamMinutes != 0 ? Text("\(state.uamMinutes.formatted()) UAM Basal minutes") : Text("")
|
|
|
+
|
|
|
+ VStack(alignment: .leading, spacing: 2) {
|
|
|
+ percentString
|
|
|
+ if targetString != Text("") { targetString }
|
|
|
+ if durationString != Text("") { durationString }
|
|
|
+ if isfString != Text("") { isfString }
|
|
|
+ if crString != Text("") { crString }
|
|
|
+ if smbString != Text("") { smbString }
|
|
|
+ if scheduledSMBString != Text("") { scheduledSMBString }
|
|
|
+ if maxMinutesSMBString != Text("") { maxMinutesSMBString }
|
|
|
+ if maxMinutesUAMString != Text("") { maxMinutesUAMString }
|
|
|
+ }
|
|
|
+ .foregroundColor(.secondary)
|
|
|
+ .font(.caption)
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ var body: some View {
|
|
|
+ Form {
|
|
|
+ if state.presets.isNotEmpty {
|
|
|
+ Section {
|
|
|
+ ForEach(fetchedProfiles.indices, id: \.self) { index in
|
|
|
+ let preset = fetchedProfiles[index]
|
|
|
+ profilesView(for: preset)
|
|
|
+ .swipeActions {
|
|
|
+ Button(role: .none) {
|
|
|
+ indexToDelete = index
|
|
|
+ profileNameToDelete = preset.name ?? "this profile"
|
|
|
+ showDeleteAlert = true
|
|
|
+ } label: {
|
|
|
+ Label("Delete", systemImage: "trash")
|
|
|
+ }.tint(.red)
|
|
|
|
|
|
+ Button {
|
|
|
+ selectedPreset = preset
|
|
|
+ state.profileName = preset.name ?? ""
|
|
|
+ isEditSheetPresented = true
|
|
|
+ } label: {
|
|
|
+ Label("Edit", systemImage: "square.and.pencil")
|
|
|
+ }.tint(.blue)
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ header: { Text("Activate profile override") }
|
|
|
+ footer: { VStack(alignment: .leading) {
|
|
|
+ Text("Swipe left on a profile to edit or delete it.")
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ settingsConfig(header: "Insulin")
|
|
|
+ Section {
|
|
|
HStack {
|
|
|
Button("Start new Profile") {
|
|
|
showAlert.toggle()
|
|
|
@@ -244,6 +354,7 @@ extension OverrideProfilesConfig {
|
|
|
.tint(.orange)
|
|
|
.frame(maxWidth: .infinity, alignment: .trailing)
|
|
|
.buttonStyle(BorderlessButtonStyle())
|
|
|
+ .font(.callout)
|
|
|
.controlSize(.mini)
|
|
|
.disabled(unChanged())
|
|
|
}
|
|
|
@@ -251,8 +362,6 @@ extension OverrideProfilesConfig {
|
|
|
presetPopover
|
|
|
}
|
|
|
}
|
|
|
-
|
|
|
- header: { Text("Insulin") }
|
|
|
footer: {
|
|
|
Text(
|
|
|
"Your profile basal insulin will be adjusted with the override percentage and your profile ISF and CR will be inversly adjusted with the percentage."
|
|
|
@@ -273,46 +382,47 @@ extension OverrideProfilesConfig {
|
|
|
.navigationBarTitle("Profiles")
|
|
|
.navigationBarTitleDisplayMode(.automatic)
|
|
|
.navigationBarItems(leading: Button("Close", action: state.hideModal))
|
|
|
+ .sheet(isPresented: $isEditSheetPresented) {
|
|
|
+ editPresetPopover
|
|
|
+ .padding()
|
|
|
+ }
|
|
|
+ .alert(isPresented: $showDeleteAlert) {
|
|
|
+ Alert(
|
|
|
+ title: Text("Delete profile override"),
|
|
|
+ message: Text("Are you sure you want to delete\n\(profileNameToDelete)?"),
|
|
|
+ primaryButton: .destructive(Text("Delete")) {
|
|
|
+ if let index = indexToDelete {
|
|
|
+ removeProfile(at: IndexSet(integer: index))
|
|
|
+ }
|
|
|
+ },
|
|
|
+ secondaryButton: .cancel()
|
|
|
+ )
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
@ViewBuilder private func profilesView(for preset: OverridePresets) -> some View {
|
|
|
- let target = state.units == .mmolL ? (((preset.target ?? 0) as NSDecimalNumber) as Decimal)
|
|
|
- .asMmolL : (preset.target ?? 0) as Decimal
|
|
|
- let duration = (preset.duration ?? 0) as Decimal
|
|
|
- let name = ((preset.name ?? "") == "") || (preset.name?.isEmpty ?? true) ? "" : preset.name!
|
|
|
- let percent = preset.percentage / 100
|
|
|
- let perpetual = preset.indefinite
|
|
|
- let durationString = perpetual ? "" : "\(formatter.string(from: duration as NSNumber)!)"
|
|
|
- let scheduledSMBstring = (preset.smbIsOff && preset.smbIsScheduledOff) ? "Scheduled SMBs" : ""
|
|
|
- let smbString = (preset.smbIsOff && scheduledSMBstring == "") ? "SMBs are off" : ""
|
|
|
- let targetString = target != 0 ? "\(glucoseFormatter.string(from: target as NSNumber)!)" : ""
|
|
|
- let maxMinutesSMB = (preset.smbMinutes as Decimal?) != nil ? (preset.smbMinutes ?? 0) as Decimal : 0
|
|
|
- let maxMinutesUAM = (preset.uamMinutes as Decimal?) != nil ? (preset.uamMinutes ?? 0) as Decimal : 0
|
|
|
- let isfString = preset.isf ? "ISF" : ""
|
|
|
- let crString = preset.cr ? "CR" : ""
|
|
|
- let dash = crString != "" ? "/" : ""
|
|
|
- let isfAndCRstring = isfString + dash + crString
|
|
|
+ let data = state.profileViewData(for: preset)
|
|
|
|
|
|
- if name != "" {
|
|
|
+ if data.name != "" {
|
|
|
HStack {
|
|
|
VStack {
|
|
|
HStack {
|
|
|
- Text(name)
|
|
|
+ Text(data.name)
|
|
|
Spacer()
|
|
|
}
|
|
|
HStack(spacing: 5) {
|
|
|
- Text(percent.formatted(.percent.grouping(.never).rounded().precision(.fractionLength(0))))
|
|
|
- if targetString != "" {
|
|
|
- Text(targetString)
|
|
|
- Text(targetString != "" ? state.units.rawValue : "")
|
|
|
+ Text(data.percent.formatted(.percent.grouping(.never).rounded().precision(.fractionLength(0))))
|
|
|
+ if data.targetString != "" {
|
|
|
+ Text(data.targetString)
|
|
|
+ Text(data.targetString != "" ? state.units.rawValue : "")
|
|
|
}
|
|
|
- if durationString != "" { Text(durationString + (perpetual ? "" : "min")) }
|
|
|
- if smbString != "" { Text(smbString).foregroundColor(.secondary).font(.caption) }
|
|
|
- if scheduledSMBstring != "" { Text(scheduledSMBstring) }
|
|
|
+ if data.durationString != "" { Text(data.durationString + (data.perpetual ? "" : "min")) }
|
|
|
+ if data.smbString != "" { Text(data.smbString).foregroundColor(.secondary).font(.caption) }
|
|
|
+ if data.scheduledSMBString != "" { Text(data.scheduledSMBString) }
|
|
|
if preset.advancedSettings {
|
|
|
- Text(maxMinutesSMB == 0 ? "" : maxMinutesSMB.formatted() + " SMB")
|
|
|
- Text(maxMinutesUAM == 0 ? "" : maxMinutesUAM.formatted() + " UAM")
|
|
|
- Text(isfAndCRstring)
|
|
|
+ Text(data.maxMinutesSMB == 0 ? "" : data.maxMinutesSMB.formatted() + " SMB")
|
|
|
+ Text(data.maxMinutesUAM == 0 ? "" : data.maxMinutesUAM.formatted() + " UAM")
|
|
|
+ Text(data.isfAndCRString)
|
|
|
}
|
|
|
Spacer()
|
|
|
}
|
|
|
@@ -339,6 +449,39 @@ extension OverrideProfilesConfig {
|
|
|
return defaultProfile || noDurationSpecified || targetZeroWithOverride || allSettingsDefault
|
|
|
}
|
|
|
|
|
|
+ private func hasChanges() -> Bool {
|
|
|
+ guard let originalPreset = originalPreset else { return false }
|
|
|
+
|
|
|
+ let targetInStateUnits: Decimal
|
|
|
+ let targetInPresetUnits: Decimal
|
|
|
+
|
|
|
+ if state.units == .mmolL {
|
|
|
+ targetInStateUnits = state.target
|
|
|
+ targetInPresetUnits = (originalPreset.target as NSDecimalNumber?)?.decimalValue.asMmolL ?? 0
|
|
|
+ } else {
|
|
|
+ targetInStateUnits = state.target
|
|
|
+ targetInPresetUnits = (originalPreset.target as NSDecimalNumber?)?.decimalValue ?? 0
|
|
|
+ }
|
|
|
+
|
|
|
+ let hasChanges = state.profileName != originalPreset.name ||
|
|
|
+ state.percentage != originalPreset.percentage ||
|
|
|
+ state.duration != (originalPreset.duration ?? 0) as Decimal ||
|
|
|
+ state._indefinite != originalPreset.indefinite ||
|
|
|
+ state.override_target != (originalPreset.target != nil) ||
|
|
|
+ (state.override_target && targetInStateUnits != targetInPresetUnits) ||
|
|
|
+ state.smbIsOff != originalPreset.smbIsOff ||
|
|
|
+ state.smbIsScheduledOff != originalPreset.smbIsScheduledOff ||
|
|
|
+ state.isf != originalPreset.isf ||
|
|
|
+ state.cr != originalPreset.cr ||
|
|
|
+ state.smbMinutes != (originalPreset.smbMinutes ?? 0) as Decimal ||
|
|
|
+ state.uamMinutes != (originalPreset.uamMinutes ?? 0) as Decimal ||
|
|
|
+ state.isfAndCr != originalPreset.isfAndCr ||
|
|
|
+ state.start != (originalPreset.start ?? 0) as Decimal ||
|
|
|
+ state.end != (originalPreset.end ?? 0) as Decimal
|
|
|
+
|
|
|
+ return hasChanges
|
|
|
+ }
|
|
|
+
|
|
|
private func removeProfile(at offsets: IndexSet) {
|
|
|
for index in offsets {
|
|
|
let language = fetchedProfiles[index]
|