AlarmStepperSection.swift 3.0 KB

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