AlarmStepperSection.swift 2.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110
  1. // LoopFollow
  2. // AlarmStepperSection.swift
  3. import SwiftUI
  4. struct AlarmStepperSection: View {
  5. // MARK: – public parameters
  6. let header: String?
  7. let footer: String?
  8. let title: String
  9. let range: ClosedRange<Double>
  10. let step: Double
  11. let unitLabel: String?
  12. // MARK: – private binding (always Double?)
  13. @Binding
  14. private var value: Double?
  15. // MARK: – designated initialiser (Double?)
  16. init(
  17. header: String? = nil,
  18. footer: String? = nil,
  19. title: String,
  20. range: ClosedRange<Double>,
  21. step: Double,
  22. unitLabel: String? = nil,
  23. value: Binding<Double?>
  24. ) {
  25. self.header = header
  26. self.footer = footer
  27. self.title = title
  28. self.range = range
  29. self.step = step
  30. self.unitLabel = unitLabel
  31. _value = value
  32. }
  33. // MARK: – convenience initialiser (Int?)
  34. /// Same API but for **`Binding<Int?>`** — it bridges to Double internally.
  35. init(
  36. header: String? = nil,
  37. footer: String? = nil,
  38. title: String,
  39. range: ClosedRange<Double>,
  40. step: Double,
  41. unitLabel: String? = nil,
  42. value intValue: Binding<Int?>
  43. ) {
  44. self.init(
  45. header: header,
  46. footer: footer,
  47. title: title,
  48. range: range,
  49. step: step,
  50. unitLabel: unitLabel,
  51. value: Binding<Double?>(
  52. get: { intValue.wrappedValue.map(Double.init) },
  53. set: { newVal in intValue.wrappedValue = newVal.map(Int.init) }
  54. )
  55. )
  56. }
  57. // MARK: – derived non-optional Binding<Double>
  58. private var nonOptional: Binding<Double> {
  59. Binding(
  60. get: { value ?? range.lowerBound },
  61. set: { newVal in value = newVal }
  62. )
  63. }
  64. private var decimalPlaces: Int {
  65. // If step is a whole number (e.g., 1.0, 5.0), we want 0 decimal places.
  66. if step.truncatingRemainder(dividingBy: 1) == 0 {
  67. return 0
  68. }
  69. // Otherwise, count characters after the decimal in the step value.
  70. let stepString = String(step)
  71. if let decimalIndex = stepString.firstIndex(of: ".") {
  72. return stepString.distance(from: stepString.index(after: decimalIndex), to: stepString.endIndex)
  73. }
  74. return 1 // Fallback to 1
  75. }
  76. // MARK: – view
  77. var body: some View {
  78. Section(
  79. header: header.map(Text.init),
  80. footer: footer.map(Text.init)
  81. ) {
  82. Stepper(value: nonOptional, in: range, step: step) {
  83. HStack {
  84. Text(title)
  85. Spacer()
  86. Text(
  87. String(format: "%.\(decimalPlaces)f", nonOptional.wrappedValue) +
  88. (unitLabel.map { " \($0)" } ?? "")
  89. )
  90. .foregroundColor(.secondary)
  91. }
  92. }
  93. }
  94. }
  95. }