Skip to content

Commit

Permalink
Updates Medication Schedule & Dosage Selection (#5)
Browse files Browse the repository at this point in the history
# Updates Medication Schedule & Dosage Selection 

## ⚙️ 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


## 📝 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).
  • Loading branch information
PSchmiedmayer authored Dec 8, 2023
1 parent c386e66 commit 8aa11bd
Show file tree
Hide file tree
Showing 16 changed files with 455 additions and 129 deletions.
73 changes: 73 additions & 0 deletions Sources/SpeziMedication/Extensions/UIDatePicker+SwiftUI.swift
Original file line number Diff line number Diff line change
@@ -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<Date>
private let excludedDates: [Date]


fileprivate init(date: Binding<Date>, 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<Date>, 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
}
}
46 changes: 46 additions & 0 deletions Sources/SpeziMedication/Models/Frequency.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand All @@ -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)
}
}
}
2 changes: 1 addition & 1 deletion Sources/SpeziMedication/Models/MedicationInstance.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
}
73 changes: 68 additions & 5 deletions Sources/SpeziMedication/Models/Schedule.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}
53 changes: 0 additions & 53 deletions Sources/SpeziMedication/Models/ScheduleTime.swift

This file was deleted.

84 changes: 84 additions & 0 deletions Sources/SpeziMedication/Models/ScheduledTime.swift
Original file line number Diff line number Diff line change
@@ -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<Date> {
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)
}
}
26 changes: 26 additions & 0 deletions Sources/SpeziMedication/Models/Weekdays.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Loading

0 comments on commit 8aa11bd

Please sign in to comment.