From 8aa11bdcf9b71887328dbcde95f26c451cbe8c6b Mon Sep 17 00:00:00 2001 From: Paul Schmiedmayer Date: Fri, 8 Dec 2023 01:58:38 -0800 Subject: [PATCH] Updates Medication Schedule & Dosage Selection (#5) # Updates Medication Schedule & Dosage Selection ## :gear: Release Notes - Associates a dosage with a medication - Updates the API surface including convenience methods for getting dates from the schedule - Improve the date picker logic to only submit the date once the picker is dismissed - Improve encoding format - Improve Schedule and ScheduledTime with additional conformances - Improve UI and accessibility setup ## :pencil: Code of Conduct & Contributing Guidelines By submitting creating this pull request, you agree to follow our [Code of Conduct](https://github.com/StanfordSpezi/.github/blob/main/CODE_OF_CONDUCT.md) and [Contributing Guidelines](https://github.com/StanfordSpezi/.github/blob/main/CONTRIBUTING.md): - [x] I agree to follow the [Code of Conduct](https://github.com/StanfordSpezi/.github/blob/main/CODE_OF_CONDUCT.md) and [Contributing Guidelines](https://github.com/StanfordSpezi/.github/blob/main/CONTRIBUTING.md). --- .../Extensions/UIDatePicker+SwiftUI.swift | 73 ++++++++++++ .../SpeziMedication/Models/Frequency.swift | 46 ++++++++ .../Models/MedicationInstance.swift | 2 +- Sources/SpeziMedication/Models/Schedule.swift | 73 +++++++++++- .../SpeziMedication/Models/ScheduleTime.swift | 53 --------- .../Models/ScheduledTime.swift | 84 ++++++++++++++ Sources/SpeziMedication/Models/Weekdays.swift | 26 +++++ .../AddMedicationSchedule.swift | 10 +- .../ModifyMedication/EditFrequency.swift | 6 +- .../ModifyMedication/EditMedication.swift | 2 +- .../ModifyMedication/EditScheduleTime.swift | 107 +++++++++--------- .../EditScheduleTimeRow.swift | 72 ++++++++++++ .../ModifyMedication/ScheduleFrequency.swift | 10 +- .../Resources/Localizable.xcstrings | 6 +- .../ExampleMedicationSettingsViewModel.swift | 12 +- .../TestAppUITests/TestAppUITests.swift | 2 +- 16 files changed, 455 insertions(+), 129 deletions(-) create mode 100644 Sources/SpeziMedication/Extensions/UIDatePicker+SwiftUI.swift delete mode 100644 Sources/SpeziMedication/Models/ScheduleTime.swift create mode 100644 Sources/SpeziMedication/Models/ScheduledTime.swift create mode 100644 Sources/SpeziMedication/ModifyMedication/EditScheduleTimeRow.swift diff --git a/Sources/SpeziMedication/Extensions/UIDatePicker+SwiftUI.swift b/Sources/SpeziMedication/Extensions/UIDatePicker+SwiftUI.swift new file mode 100644 index 0000000..7ce1331 --- /dev/null +++ b/Sources/SpeziMedication/Extensions/UIDatePicker+SwiftUI.swift @@ -0,0 +1,73 @@ +// +// This source file is part of the Stanford Spezi open-source project +// +// SPDX-FileCopyrightText: 2022 Stanford University and the project authors (see CONTRIBUTORS.md) +// +// SPDX-License-Identifier: MIT +// + +import SwiftUI + + +struct ScheduledTimeDatePicker: UIViewRepresentable { + class Coordinator: NSObject { + private var lastDate: Date + private let date: Binding + private let excludedDates: [Date] + + + fileprivate init(date: Binding, excludedDates: [Date]) { + self.date = date + self.lastDate = date.wrappedValue + self.excludedDates = excludedDates + } + + + @objc + fileprivate func valueChanged(datePicker: UIDatePicker, forEvent event: UIEvent) { + guard !excludedDates.contains(datePicker.date) else { + datePicker.date = lastDate + return + } + + lastDate = datePicker.date + } + + @objc + fileprivate func editingDidEnd(datePicker: UIDatePicker, forEvent event: UIEvent) { + self.date.wrappedValue = datePicker.date + } + } + + + static let minuteInterval = 5 + + + @Binding private var date: Date + private let excludedDates: [Date] + + + init(date: Binding, excludedDates: [Date]) { + self._date = date + self.excludedDates = excludedDates + } + + + func makeCoordinator() -> Self.Coordinator { + Coordinator(date: $date, excludedDates: excludedDates) + } + + func makeUIView(context: Context) -> UIDatePicker { + let datePicker = UIDatePicker() + datePicker.datePickerMode = .time + datePicker.preferredDatePickerStyle = .compact + datePicker.minuteInterval = Self.minuteInterval + datePicker.addTarget(context.coordinator, action: #selector(Coordinator.valueChanged), for: .valueChanged) + datePicker.addTarget(context.coordinator, action: #selector(Coordinator.editingDidEnd), for: .editingDidEnd) + return datePicker + } + + func updateUIView(_ datePicker: UIDatePicker, context: Context) { + datePicker.date = date + } +} diff --git a/Sources/SpeziMedication/Models/Frequency.swift b/Sources/SpeziMedication/Models/Frequency.swift index 14365b9..8a4199c 100644 --- a/Sources/SpeziMedication/Models/Frequency.swift +++ b/Sources/SpeziMedication/Models/Frequency.swift @@ -16,6 +16,13 @@ public enum Frequency: Codable, CustomStringConvertible, Equatable, Hashable { case asNeeded + enum CodingKeys: CodingKey { + case regularDayIntervals + case specificDaysOfWeek + case asNeeded + } + + public var description: String { switch self { case let .regularDayIntervals(dayInterval): @@ -33,4 +40,43 @@ public enum Frequency: Codable, CustomStringConvertible, Equatable, Hashable { String(localized: "As Needed", bundle: .module) } } + + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + var allKeys = ArraySlice(container.allKeys) + + guard let onlyKey = allKeys.popFirst(), allKeys.isEmpty else { + throw DecodingError.typeMismatch( + Frequency.self, + DecodingError.Context.init( + codingPath: container.codingPath, + debugDescription: "Invalid number of keys found, expected one.", + underlyingError: nil + ) + ) + } + + switch onlyKey { + case .regularDayIntervals: + self = Frequency.regularDayIntervals(try container.decode(Int.self, forKey: CodingKeys.regularDayIntervals)) + case .specificDaysOfWeek: + self = Frequency.specificDaysOfWeek(try container.decode(Weekdays.self, forKey: CodingKeys.specificDaysOfWeek)) + case .asNeeded: + self = Frequency.asNeeded + } + } + + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + switch self { + case let .regularDayIntervals(dayInterval): + try container.encode(dayInterval, forKey: .regularDayIntervals) + case let .specificDaysOfWeek(weekdays): + try container.encode(weekdays, forKey: .specificDaysOfWeek) + case .asNeeded: + try container.encode(true, forKey: .asNeeded) + } + } } diff --git a/Sources/SpeziMedication/Models/MedicationInstance.swift b/Sources/SpeziMedication/Models/MedicationInstance.swift index 70d9d79..4643eb3 100644 --- a/Sources/SpeziMedication/Models/MedicationInstance.swift +++ b/Sources/SpeziMedication/Models/MedicationInstance.swift @@ -40,6 +40,6 @@ extension MedicationInstance { extension MedicationInstance { /// See Equatable public static func == (lhs: Self, rhs: Self) -> Bool { - lhs.type == rhs.type && lhs.dosage == rhs.dosage + lhs.type == rhs.type && lhs.dosage == rhs.dosage && lhs.schedule == rhs.schedule } } diff --git a/Sources/SpeziMedication/Models/Schedule.swift b/Sources/SpeziMedication/Models/Schedule.swift index 978485a..aa49a9e 100644 --- a/Sources/SpeziMedication/Models/Schedule.swift +++ b/Sources/SpeziMedication/Models/Schedule.swift @@ -10,18 +10,81 @@ import Foundation /// Schedule of a medication. -public struct Schedule: Codable, Equatable { - /// The frequency of the Schedule, see ``Frequency`.` +@Observable +public class Schedule: Codable, Equatable, Hashable { + enum CodingKeys: CodingKey { + case frequency + case times + case startDate + } + + + /// The frequency of the Schedule, see ``Frequency`.`. public var frequency: Frequency - /// The times of the Schedule, that are associated with the ``Schedule/frequency`.` - public var times: [ScheduleTime] + /// The times of the Schedule, that are associated with the ``Schedule/frequency`.`. + public var times: [ScheduledTime] + /// Start date of the schedule. + public var startDate: Date /// - Parameters: /// - frequency: The frequency of the Schedule, see ``Frequency`.` /// - times: The times of the Schedule, that are associated with the ``Schedule/frequency`.` - init(frequency: Frequency, times: [ScheduleTime]) { + public init(frequency: Frequency = .asNeeded, times: [ScheduledTime] = [], startDate: Date = .now) { self.frequency = frequency self.times = times + self.startDate = startDate + } + + public required init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + self.frequency = try container.decode(Frequency.self, forKey: .frequency) + self.times = try container.decode([ScheduledTime].self, forKey: .times) + self.startDate = try container.decode(Date.self, forKey: .startDate) + } + + + /// See Equatable + public static func == (lhs: Schedule, rhs: Schedule) -> Bool { + lhs.frequency == rhs.frequency && lhs.times.sorted() == rhs.times.sorted() && lhs.startDate == rhs.startDate + } + + + public func timesScheduled(onDay date: Date = .now) -> [Date] { + switch frequency { + case let .regularDayIntervals(dayInterval): + guard let dayDifference = Calendar.current.dateComponents([.day], from: startDate, to: date).day, + dayDifference.isMultiple(of: dayInterval) else { + return [] + } + case let .specificDaysOfWeek(weekdays): + guard let weekday = Weekdays(weekDay: Calendar.current.component(.weekday, from: date)), + weekdays.contains(weekday) else { + return [] + } + case .asNeeded: + break + } + + return times.compactMap { scheduledTime -> Date? in + guard let hour = scheduledTime.time.hour, let minute = scheduledTime.time.minute else { + return nil + } + + return Calendar.current.date(bySettingHour: hour, minute: minute, second: 0, of: date) + } + } + + public func hash(into hasher: inout Hasher) { + hasher.combine(frequency) + hasher.combine(times.sorted()) + hasher.combine(startDate) + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(self.frequency, forKey: .frequency) + try container.encode(self.times, forKey: .times) + try container.encode(self.startDate, forKey: .startDate) } } diff --git a/Sources/SpeziMedication/Models/ScheduleTime.swift b/Sources/SpeziMedication/Models/ScheduleTime.swift deleted file mode 100644 index 3cadd3f..0000000 --- a/Sources/SpeziMedication/Models/ScheduleTime.swift +++ /dev/null @@ -1,53 +0,0 @@ -// -// This source file is part of the Stanford Spezi open-source project -// -// SPDX-FileCopyrightText: 2022 Stanford University and the project authors (see CONTRIBUTORS.md) -// -// SPDX-License-Identifier: MIT -// - -import Foundation - - -public struct ScheduleTime: Codable, Identifiable, Hashable, Equatable, Comparable { - public let uuid: UUID - public let time: DateComponents - - - public var id: String { - "\(time.hour ?? 0):\(time.minute ?? 0)" - } - - var date: Date { - Calendar.current.date(bySettingHour: self.time.hour ?? 0, minute: self.time.minute ?? 0, second: 0, of: .now) ?? .now - } - - - init(time: DateComponents) { - precondition(time.hour != nil && time.minute != nil) - - self.uuid = UUID() - self.time = time - } - - init(date: Date) { - self.init(time: Calendar.current.dateComponents([.hour, .minute], from: date)) - } - - - public static func == (lhs: ScheduleTime, rhs: ScheduleTime) -> Bool { - lhs.time.hour == rhs.time.hour && lhs.time.minute == rhs.time.minute - } - - public static func < (lhs: ScheduleTime, rhs: ScheduleTime) -> Bool { - guard lhs.time.hour == rhs.time.hour else { - return lhs.time.hour ?? 0 < rhs.time.hour ?? 0 - } - - return lhs.time.minute ?? 0 < rhs.time.minute ?? 0 - } - - public func hash(into hasher: inout Hasher) { - hasher.combine(id) - } -} diff --git a/Sources/SpeziMedication/Models/ScheduledTime.swift b/Sources/SpeziMedication/Models/ScheduledTime.swift new file mode 100644 index 0000000..a29a725 --- /dev/null +++ b/Sources/SpeziMedication/Models/ScheduledTime.swift @@ -0,0 +1,84 @@ +// +// This source file is part of the Stanford Spezi open-source project +// +// SPDX-FileCopyrightText: 2022 Stanford University and the project authors (see CONTRIBUTORS.md) +// +// SPDX-License-Identifier: MIT +// + +import Foundation +import SwiftUI + + +@Observable +public class ScheduledTime: Codable, Identifiable, Hashable, Equatable, Comparable { + enum CodingKeys: CodingKey { + case time + case dosage + } + + + public var time: DateComponents + public var dosage: Double + + + public var id: String { + "\(time.hour ?? 0):\(time.minute ?? 0)" + } + + var date: Date { + Calendar.current.date(bySettingHour: self.time.hour ?? 0, minute: self.time.minute ?? 0, second: 0, of: .now) ?? .now + } + + var dateBinding: Binding { + Binding( + get: { + self.date + }, + set: { newValue in + self.time = Calendar.current.dateComponents([.hour, .minute], from: newValue) + } + ) + } + + + public init(time: DateComponents, dosage: Double = 1.0) { + precondition(time.hour != nil && time.minute != nil) + + self.time = time + self.dosage = dosage + } + + public convenience init(date: Date, dosage: Double = 1.0) { + self.init(time: Calendar.current.dateComponents([.hour, .minute], from: date), dosage: dosage) + } + + public required init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + self.time = try container.decode(DateComponents.self, forKey: .time) + self.dosage = try container.decode(Double.self, forKey: .dosage) + } + + + public static func == (lhs: ScheduledTime, rhs: ScheduledTime) -> Bool { + lhs.time.hour == rhs.time.hour && lhs.time.minute == rhs.time.minute + } + + public static func < (lhs: ScheduledTime, rhs: ScheduledTime) -> Bool { + if lhs.time.hour == rhs.time.hour { + return lhs.time.minute ?? 0 < rhs.time.minute ?? 0 + } + + return lhs.time.hour ?? 0 < rhs.time.hour ?? 0 + } + + public func hash(into hasher: inout Hasher) { + hasher.combine(id) + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(self.time, forKey: .time) + try container.encode(self.dosage, forKey: .dosage) + } +} diff --git a/Sources/SpeziMedication/Models/Weekdays.swift b/Sources/SpeziMedication/Models/Weekdays.swift index cce8ddd..8932ec1 100644 --- a/Sources/SpeziMedication/Models/Weekdays.swift +++ b/Sources/SpeziMedication/Models/Weekdays.swift @@ -53,6 +53,32 @@ public struct Weekdays: OptionSet, Codable, Hashable, CaseIterable, Identifiable } + /// Creates a Weekday instance using the index of Gregorian calendar weekdays. + /// + /// The weekday units are the numbers 1 through N (where for the Gregorian calendar N=7 and 1 is Sunday). + /// - Parameter weekDay: The number of the weekday (N=7 and 1 is Sunday). + public init?(weekDay: Int) { + switch weekDay { + case 1: + self = .sunday + case 2: + self = .monday + case 3: + self = .tuesday + case 4: + self = .wednesday + case 5: + self = .thursday + case 6: + self = .friday + case 7: + self = .saturday + default: + return nil + } + } + + private func descriptionFrom(weekdayDescriptions: [String]) -> String { if self == .all { return String(localized: "Every Day", bundle: .module) diff --git a/Sources/SpeziMedication/ModifyMedication/AddMedicationSchedule.swift b/Sources/SpeziMedication/ModifyMedication/AddMedicationSchedule.swift index cfbee1b..af5674c 100644 --- a/Sources/SpeziMedication/ModifyMedication/AddMedicationSchedule.swift +++ b/Sources/SpeziMedication/ModifyMedication/AddMedicationSchedule.swift @@ -15,7 +15,8 @@ struct AddMedicationSchedule: View { @Binding private var isPresented: Bool @State private var frequency: Frequency = .regularDayIntervals(1) - @State private var times: [ScheduleTime] = [] + @State private var startDate: Date = .now + @State private var times: [ScheduledTime] = [] private let medicationOption: MI.InstanceType private let dosage: MI.InstanceDosage @@ -25,7 +26,10 @@ struct AddMedicationSchedule: View { VStack(spacing: 0) { Form { titleSection - EditFrequency(frequency: $frequency) + .onTapGesture { + UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil) + } + EditFrequency(frequency: $frequency, startDate: $startDate) EditScheduleTime(times: $times) } VStack(alignment: .center) { @@ -35,7 +39,7 @@ struct AddMedicationSchedule: View { viewModel.createMedicationInstance( medicationOption, dosage, - Schedule(frequency: frequency, times: times) + Schedule(frequency: frequency, times: times, startDate: startDate) ) ) isPresented = false diff --git a/Sources/SpeziMedication/ModifyMedication/EditFrequency.swift b/Sources/SpeziMedication/ModifyMedication/EditFrequency.swift index 5aeef85..765a6b8 100644 --- a/Sources/SpeziMedication/ModifyMedication/EditFrequency.swift +++ b/Sources/SpeziMedication/ModifyMedication/EditFrequency.swift @@ -11,6 +11,7 @@ import SwiftUI struct EditFrequency: View { @Binding private var frequency: Frequency + @Binding private var startDate: Date @State private var showFrequencySheet = false @@ -32,12 +33,13 @@ struct EditFrequency: View { ) } .sheet(isPresented: $showFrequencySheet) { - ScheduleFrequencyView(frequency: $frequency) + ScheduleFrequencyView(frequency: $frequency, startDate: $startDate) } } - init(frequency: Binding) { + init(frequency: Binding, startDate: Binding) { self._frequency = frequency + self._startDate = startDate } } diff --git a/Sources/SpeziMedication/ModifyMedication/EditMedication.swift b/Sources/SpeziMedication/ModifyMedication/EditMedication.swift index be92ae5..b10627c 100644 --- a/Sources/SpeziMedication/ModifyMedication/EditMedication.swift +++ b/Sources/SpeziMedication/ModifyMedication/EditMedication.swift @@ -33,7 +33,7 @@ struct EditMedication: View { .labelsHidden() } Section(String(localized: "Schedule", bundle: .module)) { - EditFrequency(frequency: $schedule.frequency) + EditFrequency(frequency: $schedule.frequency, startDate: $schedule.startDate) } Section(String(localized: "Schedule Times", bundle: .module)) { EditScheduleTime(times: $schedule.times) diff --git a/Sources/SpeziMedication/ModifyMedication/EditScheduleTime.swift b/Sources/SpeziMedication/ModifyMedication/EditScheduleTime.swift index df9bffb..aa743b2 100644 --- a/Sources/SpeziMedication/ModifyMedication/EditScheduleTime.swift +++ b/Sources/SpeziMedication/ModifyMedication/EditScheduleTime.swift @@ -10,84 +10,81 @@ import SwiftUI struct EditScheduleTime: View { - @Binding private var times: [ScheduleTime] + // We assume that a user doesn't take a single medication more than the number of possible times which are 12 * 24 for 5 minute intervals. + private static let maxTimesCount = (60 / ScheduledTimeDatePicker.minuteInterval) * 24 + + @Binding private var times: [ScheduledTime] + + private let numberOfDosageFormatter: NumberFormatter = { + let formatter = NumberFormatter() + formatter.numberStyle = .decimal + return formatter + }() var body: some View { Section { - List { - ForEach(times.sorted()) { time in - HStack { - Button( - action: { - times.removeAll(where: { $0 == time }) - }, - label: { - Image(systemName: "minus.circle.fill") - .accessibilityLabel(Text("Delete", bundle: .module)) - .foregroundStyle(Color.red) - } - ) - DatePicker( - "Time", - selection: dateBinding(time: time), - displayedComponents: .hourAndMinute - ) - .labelsHidden() - } - } + if !times.isEmpty { + timesList + } + if times.count < Self.maxTimesCount { + addTimeButton } - Button( - action: { - addNewTime() - }, - label: { - HStack { - Image(systemName: "plus.circle.fill") - .accessibilityHidden(true) - .foregroundStyle(Color.green) - Text("Add a time") - } - } - ) } + .onChange(of: times, initial: true) { + withAnimation { + times.sort() + } + } } - init(times: Binding<[ScheduleTime]>) { - self._times = times + private var timesList: some View { + List($times) { $time in + EditScheduleTimeRow(time: $time, times: $times, excludedDates: times.map(\.date)) + } } - - private func dateBinding(time: ScheduleTime) -> Binding { - Binding( - get: { - time.date + private var addTimeButton: some View { + Button( + action: { + addNewTime() }, - set: { newValue in - times.removeAll(where: { $0 == time }) - let newScheduleTime = ScheduleTime(date: newValue) - - guard !times.contains(newScheduleTime) else { - return + label: { + HStack { + Image(systemName: "plus.circle.fill") + .accessibilityHidden(true) + .foregroundStyle(Color.green) + Text("Add a time") } - - times.append(newScheduleTime) } ) } + init(times: Binding<[ScheduledTime]>) { + self._times = times + } + + private func addNewTime() { var endlessLoopCounter = 0 - var newTimeAdded = times.last?.time.date?.addingTimeInterval(60) ?? Date.now + let possibleNewTime = times.last?.time.date?.addingTimeInterval(Double(ScheduledTimeDatePicker.minuteInterval) * 60) ?? Date.now + let possibleNewTimeMinute = Calendar.current.dateComponents([.minute], from: possibleNewTime) + + guard var newTimeAdded = Calendar.current.date( + bySetting: .minute, + value: ((possibleNewTimeMinute.minute ?? 0) / 5) * 5, + of: possibleNewTime + ) else { + return + } - // We assume that a user doesn't take a single medication more than the loop limit. - while endlessLoopCounter < 100 { - let newScheduleTime = ScheduleTime(time: Calendar.current.dateComponents([.hour, .minute], from: newTimeAdded)) + while endlessLoopCounter <= Self.maxTimesCount { + let newScheduleTime = ScheduledTime(date: newTimeAdded) guard !times.contains(newScheduleTime) else { - newTimeAdded.addTimeInterval(60) + newTimeAdded.addTimeInterval(Double(ScheduledTimeDatePicker.minuteInterval) * 60) endlessLoopCounter += 1 continue } diff --git a/Sources/SpeziMedication/ModifyMedication/EditScheduleTimeRow.swift b/Sources/SpeziMedication/ModifyMedication/EditScheduleTimeRow.swift new file mode 100644 index 0000000..a0ee6e4 --- /dev/null +++ b/Sources/SpeziMedication/ModifyMedication/EditScheduleTimeRow.swift @@ -0,0 +1,72 @@ +// +// This source file is part of the Stanford Spezi open-source project +// +// SPDX-FileCopyrightText: 2022 Stanford University and the project authors (see CONTRIBUTORS.md) +// +// SPDX-License-Identifier: MIT +// + +import SwiftUI + + +struct EditScheduleTimeRow: View { + @Binding var time: ScheduledTime + @Binding var times: [ScheduledTime] + + let excludedDates: [Date] + + @FocusState private var dosageFieldIsFocused: Bool + + + private let numberOfDosageFormatter: NumberFormatter = { + let formatter = NumberFormatter() + formatter.numberStyle = .decimal + return formatter + }() + + + var body: some View { + HStack { + Button( + action: { + times.removeAll(where: { $0 == $time.wrappedValue }) + }, + label: { + Image(systemName: "minus.circle.fill") + .accessibilityLabel(Text("Delete", bundle: .module)) + .foregroundStyle(Color.red) + } + ) + .buttonStyle(.borderless) + ScheduledTimeDatePicker( + date: $time.wrappedValue.dateBinding, + excludedDates: excludedDates + ) + .frame(width: 100) + Spacer() + TextField( + String(localized: "Quantity", bundle: .module), + value: $time.dosage, + formatter: numberOfDosageFormatter + ) + .focused($dosageFieldIsFocused) + .textFieldStyle(.roundedBorder) + .keyboardType(.decimalPad) + .frame(maxWidth: 90) + } + .background { + Color.clear + .frame(maxWidth: .infinity, maxHeight: .infinity) + .contentShape(Rectangle()) + .onTapGesture { + dosageFieldIsFocused = false + } + .padding(-32) + } + .onChange(of: time.time) { + withAnimation { + times.sort() + } + } + } +} diff --git a/Sources/SpeziMedication/ModifyMedication/ScheduleFrequency.swift b/Sources/SpeziMedication/ModifyMedication/ScheduleFrequency.swift index 61dd5bb..9e8c077 100644 --- a/Sources/SpeziMedication/ModifyMedication/ScheduleFrequency.swift +++ b/Sources/SpeziMedication/ModifyMedication/ScheduleFrequency.swift @@ -12,11 +12,11 @@ import SwiftUI struct ScheduleFrequencyView: View { @Environment(\.dismiss) private var dismiss - @Binding var outsideFrequency: Frequency + @Binding private var outsideFrequency: Frequency + @Binding private var startDate: Date @State private var frequency: Frequency @State private var regularInterval: Int = 1 @State private var daysOfTheWeek: Weekdays = .all - @State private var startDate: Date = .now var body: some View { NavigationStack { @@ -114,8 +114,9 @@ struct ScheduleFrequencyView: View { } - init(frequency: Binding) { + init(frequency: Binding, startDate: Binding) { self._outsideFrequency = frequency + self._startDate = startDate self._frequency = State(wrappedValue: frequency.wrappedValue) switch frequency.wrappedValue { @@ -153,6 +154,7 @@ struct ScheduleFrequencyView: View { #Preview { @State var frequency: Frequency = .specificDaysOfWeek(.all) + @State var startDate: Date = .now - return ScheduleFrequencyView(frequency: $frequency) + return ScheduleFrequencyView(frequency: $frequency, startDate: $startDate) } diff --git a/Sources/SpeziMedication/Resources/Localizable.xcstrings b/Sources/SpeziMedication/Resources/Localizable.xcstrings index f397162..7b41694 100644 --- a/Sources/SpeziMedication/Resources/Localizable.xcstrings +++ b/Sources/SpeziMedication/Resources/Localizable.xcstrings @@ -94,6 +94,9 @@ }, "On Specific Days of the Week" : { + }, + "Quantity" : { + }, "Save Dosage" : { @@ -112,9 +115,6 @@ }, "Start Date" : { - }, - "Time" : { - }, "Use the \"+\" button at the top to add all the medications you take." : { diff --git a/Tests/UITests/TestApp/ExampleMedicationSettingsViewModel.swift b/Tests/UITests/TestApp/ExampleMedicationSettingsViewModel.swift index 9d65213..9bad144 100644 --- a/Tests/UITests/TestApp/ExampleMedicationSettingsViewModel.swift +++ b/Tests/UITests/TestApp/ExampleMedicationSettingsViewModel.swift @@ -24,7 +24,17 @@ class ExampleMedicationSettingsViewModel: Module, MedicationSettingsViewModel, C return medicationInstances .map { medicationInstance in - "\(medicationInstance.localizedDescription) - \(medicationInstance.dosage.localizedDescription) - \(medicationInstance.schedule)" + let scheduleDescription: String + switch medicationInstance.schedule.frequency { + case let .regularDayIntervals(dayInterval): + scheduleDescription = "RegularDayIntervals: \(dayInterval)" + case let .specificDaysOfWeek(weekdays): + scheduleDescription = "SpecificDaysOfWeek: \(weekdays)" + case .asNeeded: + scheduleDescription = "AsNeeded" + } + + return "\(medicationInstance.localizedDescription) - \(medicationInstance.dosage.localizedDescription) - \(scheduleDescription)" } .joined(separator: ", ") } diff --git a/Tests/UITests/TestAppUITests/TestAppUITests.swift b/Tests/UITests/TestAppUITests/TestAppUITests.swift index d0af0c9..60ca19b 100644 --- a/Tests/UITests/TestAppUITests/TestAppUITests.swift +++ b/Tests/UITests/TestAppUITests/TestAppUITests.swift @@ -36,7 +36,7 @@ class TestAppUITests: XCTestCase { try await Task.sleep(for: .seconds(2)) - XCTAssertTrue(app.staticTexts["Medication 1 - Dosage 1.1 - Schedule(frequency: Every Day, times: [])"].waitForExistence(timeout: 2)) + XCTAssertTrue(app.staticTexts["Medication 1 - Dosage 1.1 - RegularDayIntervals: 1"].waitForExistence(timeout: 2)) } func testSpeziMedicationDelete() throws {