From 7f9913f67a5c461dab5fc1dec149c6803f5e139e Mon Sep 17 00:00:00 2001 From: Gabriel Royer Date: Wed, 12 Feb 2025 10:06:58 -0800 Subject: [PATCH 1/7] Added SwiftUI wrapper for DurationPicker under new DurationPickerSwiftUI library --- Package.swift | 8 + .../Public/DurationPickerMode.swift | 47 +++++ .../Public/DurationPickerView.swift | 173 ++++++++++++++++++ 3 files changed, 228 insertions(+) create mode 100644 Sources/DurationPickerSwiftUI/Public/DurationPickerMode.swift create mode 100644 Sources/DurationPickerSwiftUI/Public/DurationPickerView.swift diff --git a/Package.swift b/Package.swift index 8305f89..55e33d4 100644 --- a/Package.swift +++ b/Package.swift @@ -12,13 +12,21 @@ let package = Package( .library( name: "DurationPicker", targets: ["DurationPicker"]), + .library( + name: "DurationPickerSwiftUI", + targets: ["DurationPickerSwiftUI"]), ], dependencies: [], targets: [ .target( name: "DurationPicker", dependencies: []), + .target( + name: "DurationPickerSwiftUI", + dependencies: ["DurationPicker"] + ), .testTarget( name: "DurationPickerTests", dependencies: ["DurationPicker"]), + ]) diff --git a/Sources/DurationPickerSwiftUI/Public/DurationPickerMode.swift b/Sources/DurationPickerSwiftUI/Public/DurationPickerMode.swift new file mode 100644 index 0000000..f6896db --- /dev/null +++ b/Sources/DurationPickerSwiftUI/Public/DurationPickerMode.swift @@ -0,0 +1,47 @@ +/// MIT License +/// +/// Copyright (c) 2025 Vis Fitness Inc. +/// +/// Permission is hereby granted, free of charge, to any person obtaining a copy +/// of this software and associated documentation files (the "Software"), to deal +/// in the Software without restriction, including without limitation the rights +/// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +/// copies of the Software, and to permit persons to whom the Software is +/// furnished to do so, subject to the following conditions: +/// +/// The above copyright notice and this permission notice shall be included in all +/// copies or substantial portions of the Software. +/// +/// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +/// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +/// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +/// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +/// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +/// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +/// SOFTWARE. + +import SwiftUI +import DurationPicker + +struct DurationPickerModeEnvironmentKey: EnvironmentKey { + static let defaultValue: DurationPickerView.Mode = .hourMinuteSecond +} + +extension EnvironmentValues { + var durationPickerMode: DurationPickerView.Mode { + get { + self[DurationPickerModeEnvironmentKey.self] + } set { + self[DurationPickerModeEnvironmentKey.self] = newValue + } + } +} + +public extension View { + /// Sets the mode displayed by the duration picker. + /// + /// The mode determines which combination of hours, minutes, and seconds are displayed. You can set and retrieve the mode value through the ``DurationPicker/pickerMode`` property. + func durationPickerMode(_ mode: DurationPickerView.Mode) -> some View { + environment(\.durationPickerMode, mode) + } +} diff --git a/Sources/DurationPickerSwiftUI/Public/DurationPickerView.swift b/Sources/DurationPickerSwiftUI/Public/DurationPickerView.swift new file mode 100644 index 0000000..f162334 --- /dev/null +++ b/Sources/DurationPickerSwiftUI/Public/DurationPickerView.swift @@ -0,0 +1,173 @@ +/// MIT License +/// +/// Copyright (c) 2025 Vis Fitness Inc. +/// +/// Permission is hereby granted, free of charge, to any person obtaining a copy +/// of this software and associated documentation files (the "Software"), to deal +/// in the Software without restriction, including without limitation the rights +/// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +/// copies of the Software, and to permit persons to whom the Software is +/// furnished to do so, subject to the following conditions: +/// +/// The above copyright notice and this permission notice shall be included in all +/// copies or substantial portions of the Software. +/// +/// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +/// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +/// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +/// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +/// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +/// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +/// SOFTWARE. + +import SwiftUI +import DurationPicker + +/// A customizable control for inputting time values ranging between 0 and 24 hours. It serves as a drop-in replacement of [DatePicker](https://developer.apple.com/documentation/swiftui/datepicker) with [hourMinuteAndSecond](https://developer.apple.com/documentation/swiftui/datepickercomponents/hourminuteandsecond) displayed components with additional functionality for time input. +/// +/// You can use a duration picker to allow a user to enter a time interval between 0 and 24 hours. +public struct DurationPickerView: UIViewRepresentable { + public init(duration: Binding, hourInterval: Int = 1, minuteInterval: Int = 1, secondInterval: Int = 1, minumumDuration: TimeInterval? = nil, maximumDuration: TimeInterval? = nil) { + self._duration = duration + self.hourInterval = hourInterval + self.minuteInterval = minuteInterval + self.secondInterval = secondInterval + self.minumumDuration = minumumDuration + self.maximumDuration = maximumDuration + } + + // This has to be public to comply with UIViewRepresentable, but we don't actually want it to show up in the doc. + @_documentation(visibility: internal) + public typealias UIViewType = DurationPicker + + /// The mode displayed by the duration picker. + /// + /// The mode determines which combination of hours, minutes, and seconds are displayed. You can set and retrieve the mode value through the ``DurationPicker/pickerMode`` property. + public typealias Mode = DurationPicker.Mode + + @Binding private var duration: TimeInterval + + @Environment(\.durationPickerMode) private var mode + + private var hourInterval: Int + private var minuteInterval: Int + private var secondInterval: Int + private var minumumDuration: TimeInterval? + private var maximumDuration: TimeInterval? + + // This has to be public to comply with UIViewRepresentable, but we don't actually want it to show up in the doc. + @_documentation(visibility: internal) + public func makeUIView(context: Context) -> DurationPicker { + let timeDurationPicker = DurationPicker() + + timeDurationPicker.pickerMode = mode + timeDurationPicker.hourInterval = hourInterval + timeDurationPicker.minuteInterval = minuteInterval + timeDurationPicker.secondInterval = secondInterval + timeDurationPicker.minimumDuration = minumumDuration + timeDurationPicker.maximumDuration = maximumDuration + + timeDurationPicker.addTarget(context.coordinator, action: #selector(Coordinator.changed(_:)), for: .valueChanged) + return timeDurationPicker + } + + // This has to be public to comply with UIViewRepresentable, but we don't actually want it to show up in the doc. + @_documentation(visibility: internal) + public func updateUIView(_ uiView: DurationPicker, context: Context) { + uiView.duration = duration + uiView.pickerMode = mode + uiView.hourInterval = hourInterval + uiView.minuteInterval = minuteInterval + uiView.secondInterval = secondInterval + uiView.minimumDuration = minumumDuration + uiView.maximumDuration = maximumDuration + } + + // This has to be public to comply with UIViewRepresentable, but we don't actually want it to show up in the doc. + @_documentation(visibility: internal) + public func makeCoordinator() -> DurationPickerView.Coordinator { + Coordinator(duration: $duration) + } + + + // This has to be public to comply with UIViewRepresentable, but we don't actually want it to show up in the doc. + @_documentation(visibility: internal) + public class Coordinator: NSObject { + private var duration: Binding + + init(duration: Binding) { + self.duration = duration + } + + @objc func changed(_ sender: DurationPicker) { + self.duration.wrappedValue = sender.duration + } + } +} + +@available(iOS 17.0,*) +#Preview { + @Previewable @State var duration: TimeInterval = 60.0 * 30.0 + @Previewable @State var mode: DurationPickerView.Mode = .hourMinuteSecond + @Previewable @State var minimumDuration: TimeInterval? = nil + @Previewable @State var maximumDuration: TimeInterval? = nil + @Previewable @State var hourInterval: Int = 1 + @Previewable @State var minuteInterval: Int = 1 + @Previewable @State var secondInterval: Int = 1 + + // Can't make it case iterable since its defined as an extension. + let modes: [DurationPickerView.Mode] = [.hour, .hourMinute, .hourMinuteSecond, .minute, .minuteSecond, .second] + + List { + DurationPickerView( + duration: $duration, + hourInterval: hourInterval, + minuteInterval: minuteInterval, + secondInterval: secondInterval, + minumumDuration: minimumDuration, + maximumDuration: maximumDuration + ).durationPickerMode(mode) + + LabeledContent { + Text(Date().. Date: Wed, 12 Feb 2025 10:19:15 -0800 Subject: [PATCH 2/7] Changed picker mode to and made it passable as an argument rather than an environemnt variable to better align with --- .../{DurationPickerMode.swift => .swift} | 0 .../Public/DurationPickerView.swift | 20 ++++++++++--------- 2 files changed, 11 insertions(+), 9 deletions(-) rename Sources/DurationPickerSwiftUI/Public/{DurationPickerMode.swift => .swift} (100%) diff --git a/Sources/DurationPickerSwiftUI/Public/DurationPickerMode.swift b/Sources/DurationPickerSwiftUI/Public/.swift similarity index 100% rename from Sources/DurationPickerSwiftUI/Public/DurationPickerMode.swift rename to Sources/DurationPickerSwiftUI/Public/.swift diff --git a/Sources/DurationPickerSwiftUI/Public/DurationPickerView.swift b/Sources/DurationPickerSwiftUI/Public/DurationPickerView.swift index f162334..f632a20 100644 --- a/Sources/DurationPickerSwiftUI/Public/DurationPickerView.swift +++ b/Sources/DurationPickerSwiftUI/Public/DurationPickerView.swift @@ -27,8 +27,9 @@ import DurationPicker /// /// You can use a duration picker to allow a user to enter a time interval between 0 and 24 hours. public struct DurationPickerView: UIViewRepresentable { - public init(duration: Binding, hourInterval: Int = 1, minuteInterval: Int = 1, secondInterval: Int = 1, minumumDuration: TimeInterval? = nil, maximumDuration: TimeInterval? = nil) { + public init(_ duration: Binding, components: Components = .hourMinuteSecond, hourInterval: Int = 1, minuteInterval: Int = 1, secondInterval: Int = 1, minumumDuration: TimeInterval? = nil, maximumDuration: TimeInterval? = nil) { self._duration = duration + self.mode = components self.hourInterval = hourInterval self.minuteInterval = minuteInterval self.secondInterval = secondInterval @@ -40,14 +41,14 @@ public struct DurationPickerView: UIViewRepresentable { @_documentation(visibility: internal) public typealias UIViewType = DurationPicker - /// The mode displayed by the duration picker. + /// The componentns displayed by the duration picker. /// /// The mode determines which combination of hours, minutes, and seconds are displayed. You can set and retrieve the mode value through the ``DurationPicker/pickerMode`` property. - public typealias Mode = DurationPicker.Mode + public typealias Components = DurationPicker.Mode @Binding private var duration: TimeInterval - @Environment(\.durationPickerMode) private var mode + private var mode: Components private var hourInterval: Int private var minuteInterval: Int @@ -108,7 +109,7 @@ public struct DurationPickerView: UIViewRepresentable { @available(iOS 17.0,*) #Preview { @Previewable @State var duration: TimeInterval = 60.0 * 30.0 - @Previewable @State var mode: DurationPickerView.Mode = .hourMinuteSecond + @Previewable @State var components: DurationPickerView.Components = .hourMinuteSecond @Previewable @State var minimumDuration: TimeInterval? = nil @Previewable @State var maximumDuration: TimeInterval? = nil @Previewable @State var hourInterval: Int = 1 @@ -116,17 +117,18 @@ public struct DurationPickerView: UIViewRepresentable { @Previewable @State var secondInterval: Int = 1 // Can't make it case iterable since its defined as an extension. - let modes: [DurationPickerView.Mode] = [.hour, .hourMinute, .hourMinuteSecond, .minute, .minuteSecond, .second] + let modes: [DurationPickerView.Components] = [.hour, .hourMinute, .hourMinuteSecond, .minute, .minuteSecond, .second] List { DurationPickerView( - duration: $duration, + $duration, + components: components, hourInterval: hourInterval, minuteInterval: minuteInterval, secondInterval: secondInterval, minumumDuration: minimumDuration, maximumDuration: maximumDuration - ).durationPickerMode(mode) + ) LabeledContent { Text(Date().. Date: Wed, 12 Feb 2025 10:20:19 -0800 Subject: [PATCH 3/7] Fixed typo --- Sources/DurationPickerSwiftUI/Public/DurationPickerView.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/DurationPickerSwiftUI/Public/DurationPickerView.swift b/Sources/DurationPickerSwiftUI/Public/DurationPickerView.swift index f632a20..324fb79 100644 --- a/Sources/DurationPickerSwiftUI/Public/DurationPickerView.swift +++ b/Sources/DurationPickerSwiftUI/Public/DurationPickerView.swift @@ -41,7 +41,7 @@ public struct DurationPickerView: UIViewRepresentable { @_documentation(visibility: internal) public typealias UIViewType = DurationPicker - /// The componentns displayed by the duration picker. + /// The components displayed by the duration picker. /// /// The mode determines which combination of hours, minutes, and seconds are displayed. You can set and retrieve the mode value through the ``DurationPicker/pickerMode`` property. public typealias Components = DurationPicker.Mode From 42aa2a839010fd624e5ba0f858a86c00356d21d1 Mon Sep 17 00:00:00 2001 From: Gabriel Royer Date: Wed, 12 Feb 2025 10:27:17 -0800 Subject: [PATCH 4/7] Remove extra newline --- Package.swift | 1 - 1 file changed, 1 deletion(-) diff --git a/Package.swift b/Package.swift index 55e33d4..24293aa 100644 --- a/Package.swift +++ b/Package.swift @@ -28,5 +28,4 @@ let package = Package( .testTarget( name: "DurationPickerTests", dependencies: ["DurationPicker"]), - ]) From bcd23633d5846e83299cbba0a98007386f25d0f6 Mon Sep 17 00:00:00 2001 From: Gabriel Royer Date: Wed, 12 Feb 2025 10:28:26 -0800 Subject: [PATCH 5/7] Cleaned up unused file --- Sources/DurationPickerSwiftUI/Public/.swift | 47 --------------------- 1 file changed, 47 deletions(-) delete mode 100644 Sources/DurationPickerSwiftUI/Public/.swift diff --git a/Sources/DurationPickerSwiftUI/Public/.swift b/Sources/DurationPickerSwiftUI/Public/.swift deleted file mode 100644 index f6896db..0000000 --- a/Sources/DurationPickerSwiftUI/Public/.swift +++ /dev/null @@ -1,47 +0,0 @@ -/// MIT License -/// -/// Copyright (c) 2025 Vis Fitness Inc. -/// -/// Permission is hereby granted, free of charge, to any person obtaining a copy -/// of this software and associated documentation files (the "Software"), to deal -/// in the Software without restriction, including without limitation the rights -/// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -/// copies of the Software, and to permit persons to whom the Software is -/// furnished to do so, subject to the following conditions: -/// -/// The above copyright notice and this permission notice shall be included in all -/// copies or substantial portions of the Software. -/// -/// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -/// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -/// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -/// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -/// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -/// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -/// SOFTWARE. - -import SwiftUI -import DurationPicker - -struct DurationPickerModeEnvironmentKey: EnvironmentKey { - static let defaultValue: DurationPickerView.Mode = .hourMinuteSecond -} - -extension EnvironmentValues { - var durationPickerMode: DurationPickerView.Mode { - get { - self[DurationPickerModeEnvironmentKey.self] - } set { - self[DurationPickerModeEnvironmentKey.self] = newValue - } - } -} - -public extension View { - /// Sets the mode displayed by the duration picker. - /// - /// The mode determines which combination of hours, minutes, and seconds are displayed. You can set and retrieve the mode value through the ``DurationPicker/pickerMode`` property. - func durationPickerMode(_ mode: DurationPickerView.Mode) -> some View { - environment(\.durationPickerMode, mode) - } -} From ab8e38ffd84cf9ea7fb3e409eaa1c05af292f46b Mon Sep 17 00:00:00 2001 From: Gabriel Royer Date: Wed, 12 Feb 2025 12:36:46 -0800 Subject: [PATCH 6/7] Added change check to updateUIView as it's triggering the coordinator on every set --- .../Public/DurationPickerView.swift | 35 +++++++++++++------ 1 file changed, 24 insertions(+), 11 deletions(-) diff --git a/Sources/DurationPickerSwiftUI/Public/DurationPickerView.swift b/Sources/DurationPickerSwiftUI/Public/DurationPickerView.swift index 324fb79..0b2ce40 100644 --- a/Sources/DurationPickerSwiftUI/Public/DurationPickerView.swift +++ b/Sources/DurationPickerSwiftUI/Public/DurationPickerView.swift @@ -68,28 +68,41 @@ public struct DurationPickerView: UIViewRepresentable { timeDurationPicker.minimumDuration = minumumDuration timeDurationPicker.maximumDuration = maximumDuration - timeDurationPicker.addTarget(context.coordinator, action: #selector(Coordinator.changed(_:)), for: .valueChanged) + timeDurationPicker.addTarget(context.coordinator, action: #selector(Coordinator.changed(_:)), for: .primaryActionTriggered) return timeDurationPicker } // This has to be public to comply with UIViewRepresentable, but we don't actually want it to show up in the doc. @_documentation(visibility: internal) public func updateUIView(_ uiView: DurationPicker, context: Context) { - uiView.duration = duration - uiView.pickerMode = mode - uiView.hourInterval = hourInterval - uiView.minuteInterval = minuteInterval - uiView.secondInterval = secondInterval - uiView.minimumDuration = minumumDuration - uiView.maximumDuration = maximumDuration + if (uiView.duration != duration) { + uiView.duration = duration + } + if (uiView.pickerMode != mode) { + uiView.pickerMode = mode + } + if (uiView.hourInterval != hourInterval) { + uiView.hourInterval = hourInterval + } + if (uiView.minuteInterval != minuteInterval) { + uiView.minuteInterval = minuteInterval + } + if (uiView.secondInterval != secondInterval) { + uiView.secondInterval = secondInterval + } + if (uiView.minimumDuration != minumumDuration) { + uiView.minimumDuration = minumumDuration + } + if (uiView.maximumDuration != maximumDuration) { + uiView.maximumDuration = maximumDuration + } } // This has to be public to comply with UIViewRepresentable, but we don't actually want it to show up in the doc. @_documentation(visibility: internal) public func makeCoordinator() -> DurationPickerView.Coordinator { - Coordinator(duration: $duration) + return Coordinator(duration: $duration) } - // This has to be public to comply with UIViewRepresentable, but we don't actually want it to show up in the doc. @_documentation(visibility: internal) @@ -97,7 +110,7 @@ public struct DurationPickerView: UIViewRepresentable { private var duration: Binding init(duration: Binding) { - self.duration = duration + self.duration = duration } @objc func changed(_ sender: DurationPicker) { From d1a8f963b787fb0a99908a11f70bf36d1a183978 Mon Sep 17 00:00:00 2001 From: Gabriel Royer Date: Sat, 15 Feb 2025 05:49:31 -0800 Subject: [PATCH 7/7] Fixed comments --- Sources/DurationPickerSwiftUI/Public/DurationPickerView.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/DurationPickerSwiftUI/Public/DurationPickerView.swift b/Sources/DurationPickerSwiftUI/Public/DurationPickerView.swift index 0b2ce40..f1cd750 100644 --- a/Sources/DurationPickerSwiftUI/Public/DurationPickerView.swift +++ b/Sources/DurationPickerSwiftUI/Public/DurationPickerView.swift @@ -23,7 +23,7 @@ import SwiftUI import DurationPicker -/// A customizable control for inputting time values ranging between 0 and 24 hours. It serves as a drop-in replacement of [DatePicker](https://developer.apple.com/documentation/swiftui/datepicker) with [hourMinuteAndSecond](https://developer.apple.com/documentation/swiftui/datepickercomponents/hourminuteandsecond) displayed components with additional functionality for time input. +/// A customizable control for inputting time values ranging between 0 and 24 hours. It serves as a drop-in replacement of [UIDatePicker](https://developer.apple.com/documentation/uikit/uidatepicker) with [countDownTimer](https://developer.apple.com/documentation/uikit/uidatepicker/mode/countdowntimer) mode with additional functionality for time input. /// /// You can use a duration picker to allow a user to enter a time interval between 0 and 24 hours. public struct DurationPickerView: UIViewRepresentable { @@ -43,7 +43,7 @@ public struct DurationPickerView: UIViewRepresentable { /// The components displayed by the duration picker. /// - /// The mode determines which combination of hours, minutes, and seconds are displayed. You can set and retrieve the mode value through the ``DurationPicker/pickerMode`` property. + /// The mode determines which combination of hours, minutes, and seconds are displayed. You can set and retrieve the mode value through the ``DurationPickerView/mode`` property. public typealias Components = DurationPicker.Mode @Binding private var duration: TimeInterval