UPGRADE YOUR SKILLS: Learn advanced Swift and SwiftUI on Hacking with Swift+! >>

Problems with MultiComponentPicker

Forums > SwiftUI

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")

        }
    }            
}

3      

I made some (unexplicable) progress: changing the Form View into an HStack/VStack does the job. With this the Subview with multiple pickers is working as expected. Does anyone have an idea why?

3      

Hacking with Swift is sponsored by Essential Developer

SPONSORED Join a FREE crash course for mid/senior iOS devs who want to achieve an expert level of technical and practical skills – it’s the fast track to being a complete senior developer! Hurry up because it'll be available only until April 28th.

Click to save your free spot now

Sponsor Hacking with Swift and reach the world's largest Swift community!

Archived topic

This topic has been closed due to inactivity, so you can't reply. Please create a new topic if you need to.

All interactions here are governed by our code of conduct.

 
Unknown user

You are not logged in

Log in or create account
 

Link copied to your pasteboard.