https://www.hackingwithswift.com/forums/swiftui/custom-multicomponent-picker-pure-swiftui/2236 is a great post and adresses some interesting problems of Pickers within SwiftUI.
I am having an additional issue using a View with multiple pickers as a subview within a playground I am working on
Overall goal of a small project to learn SwiftUI I am currently workin on is to allow to add timers to a list of which each timer has a name and a duration (TimeInterval). So my views should work like this:
- ContentView
- List
- Timers (name and duration as text)
- Button to add --> pulling up the sheet
- AddTimer (add a Timer)
- HStack
- Text
- TimeValuePicker (see below - does not work)
The interval could be given as text (e.g. 1h 30m 15s) but I wanted to allow for "dialing" the interval.
So I designed a "TimeValuePicker"
- to allow me to select hours, minutes and seconds to define a TimeInterval
- consiting of three instances of a custom "ValuePicker" which is based on the original Picker
Embedding the TimeValuePicker on the Top Level of a NavigationView does work the way I want it.
I tested it by adding TimeValuePicker (temp) to the structure given above as follows:
- ContentView
- TimeValuePicker (does work!)
- List
- ....
If it works it shows three dials to select the individual values...
BUT:
When I try and use the same TimeValuePicker on a "sheet" within the NavigationView which is pulled up to add timers to a list, the TimeValuePicker collapses into a single line, no dials can be seen.
Clicking on the TimeValuePicker (line) starts the NavigationView back and forth: first to the options for seconds, then for the minutes and then for the hours... I can not select the individual values.
Here are some code Snippets:
Root View
var body: some View {
NavigationView() {
VStack() {
// Works here: TimePickerView(durationString: $testTime)
List(selection: $selection) { // bound to selection for later deletion
ForEach(Array(timers.items.enumerated()), id: \.element.id) {index, actitem in
HStack() { // this HStack is required as the if the else is interpreted as multiple ambiguous views...
if (self.editMode != .active) {
NavigationLink(destination:
EditTimerView(timers: self.timers,
timerid: actitem.id,
name: actitem.name,
time: actitem.interval)) {
TimerView(timers: self.timers,
id: actitem.id,
name: actitem.name,
time: actitem.interval,
editMode: self.editMode,
totalTime: Date.hourAndMinuteAsString(date: self.timers.accumulatedTime(startTime: self.startTime, atIndex: index)))
}
} else {
TimerView(timers: self.timers,
id: actitem.id,
name: actitem.name,
time: actitem.interval,
editMode: self.editMode,
totalTime: "") // Date.hourAndMinuteAsString(date: accumulatedStartTime))
}
}
}
.onDelete(perform: removeItems)
.onMove(perform: moveItems)
}
.environment(\.editMode, self.$editMode)
.sheet(isPresented: $showingAddTimer) {
AddTimerView(timers: self.timers)
}
if (self.editMode != .active) {
Button(action: {
self.showingAddTimer = true
}) {
Image(systemName: "plus")
}
}
Spacer()
}
.navigationBarItems(leading: editButton, trailing: deleteButton)
}
.navigationViewStyle(StackNavigationViewStyle())
}
AddView
struct AddTimerView: View {
@ObservedObject var timers : XTimers
@State private var name = ""
@State private var time = ""
@Environment(\.presentationMode) var presentationMode
var body: some View {
NavigationView {
Form {
TextField("Name of timer", text: $name)
// this works to add this as a string '3h 1m 4s': TextField("Duration", text: $time)
TimePickerView(durationString: $time) // DOES NOT WORK
}
.navigationBarTitle("Add new Timer")
.navigationBarItems(trailing: Button("Save") {
let item = XTimer(name: self.name,
interval: self.time)
self.timers.items.append(item)
self.presentationMode.wrappedValue.dismiss()
})
}
.navigationViewStyle(StackNavigationViewStyle())
}
}
TimePickerView (separate file hence declared public)
public struct TimePickerView: View {
@Binding var durationString : String
@State private var durationHoursState: Int = 0
@State private var durationMinutesState: Int = 0
@State private var durationSecondsState: Int = 0
public init(durationString: Binding<String>) {
self._durationString = durationString
}
public var body: some View {
// splitting the durationString into multiple bindings for the Valuepickers
let durationSeconds = Binding<Int>(
get: {
let time = NSInteger(TimeInterval.timeInterval(fromAbbreviatedString: self.durationString))
let seconds = time % 60
return seconds
}, set: {
var duration = $0
duration += self.durationMinutesState * 60
duration += self.durationHoursState * 3600
self.durationSecondsState = $0
self.durationString = TimeInterval.abbreviatedFormat(duration: Double(duration))
})
let durationMinutes = Binding<Int>(
get: {
let time = NSInteger(TimeInterval.timeInterval(fromAbbreviatedString: self.durationString))
let minutes = (time / 60) % 60
return minutes
}, set: {
self.durationMinutesState = $0
var duration = $0 * 60
duration += self.durationSecondsState
duration += self.durationHoursState * 3600
self.durationString = TimeInterval.abbreviatedFormat(duration: Double(duration))
})
let durationHours = Binding<Int>(
get: {
let time = NSInteger(TimeInterval.timeInterval(fromAbbreviatedString: self.durationString))
let hours = (time / 3600) % 24
return hours
}, set: {
self.durationHoursState = $0
var duration = $0 * 3600
duration += self.durationSecondsState
duration += self.durationMinutesState * 60
self.durationString = TimeInterval.abbreviatedFormat(duration: Double(duration))
})
return // GeometryReader { geometry in
// duration = TimeInterval.timeInterval(timeString: durationString)
HStack() {
ValuePicker(units: durationHours, unitsToStartWith: 0, unitsToAdd: 1, unitsToEndWith: 24, unitsName: "hours")
.pickerStyle(SegmentedPickerStyle())
ValuePicker(units: durationMinutes, unitsToStartWith: 0, unitsToAdd: 5, unitsToEndWith: 60, unitsName: "minutes")
ValuePicker(units: durationSeconds, unitsToStartWith: 0, unitsToAdd: 15, unitsToEndWith: 60, unitsName: "seconds")
}
}
}