12

With UIKit, the UIDatePicker allow set mode to UIDatePicker.Mode.countDownTimer and we can have set duration.

With SwiftUI, I don't see any native way to solve this. The only way would be doing interface with UIKit?

This is how I need

[UPDATE]
Only solution found

DurationPickerView.swift

import SwiftUI
import UIKit

struct DurationPickerView: UIViewRepresentable {
    @Binding var time: Time

    func makeCoordinator() -> DurationPickerView.Coordinator {
        Coordinator(self)
    }

    func makeUIView(context: Context) -> UIDatePicker {
        let datePicker = UIDatePicker()
        datePicker.datePickerMode = .countDownTimer
        datePicker.addTarget(context.coordinator, action: #selector(Coordinator.onDateChanged), for: .valueChanged)
        return datePicker
    }

    func updateUIView(_ datePicker: UIDatePicker, context: Context) {
        let date = Calendar.current.date(bySettingHour: time.hour, minute: time.minute, second: time.second, of: datePicker.date)!
        datePicker.setDate(date, animated: true)
    }

    class Coordinator: NSObject {
        var durationPicker: DurationPickerView

        init(_ durationPicker: DurationPickerView) {
            self.durationPicker = durationPicker
        }

        @objc func onDateChanged(sender: UIDatePicker) {
            print(sender.date)
            let calendar = Calendar.current
            let date = sender.date
            durationPicker.time = Time(hour: calendar.component(.hour, from: date), minute: calendar.component(.minute, from: date), second: calendar.component(.second, from: date))
        }
    }
}

Time.swift

import Foundation

struct Time {
    var hour: Int
    var minute: Int
    var second: Int = 0
}
  • I can’t find anything about this either. You could use a two segment standard picker. – Chris Oct 27 '19 at 11:36

2 Answers2

9

The only way to get the countdown behavior right now is by wrapping UIDatePicker in a custom view.

Here is a simplified version of Ailton Vieira Pinto Filho's code using countDownDuration.

import SwiftUI

struct DurationPicker: UIViewRepresentable {
    @Binding var duration: TimeInterval

    func makeUIView(context: Context) -> UIDatePicker {
        let datePicker = UIDatePicker()
        datePicker.datePickerMode = .countDownTimer
        datePicker.addTarget(context.coordinator, action: #selector(Coordinator.updateDuration), for: .valueChanged)
        return datePicker
    }

    func updateUIView(_ datePicker: UIDatePicker, context: Context) {
        datePicker.countDownDuration = duration
    }

    func makeCoordinator() -> Coordinator {
        Coordinator(self)
    }

    class Coordinator: NSObject {
        let parent: DurationPicker

        init(_ parent: DurationPicker) {
            self.parent = parent
        }

        @objc func updateDuration(datePicker: UIDatePicker) {
            parent.duration = datePicker.countDownDuration
        }
    }
}
bcause
  • 1,303
  • 12
  • 29
0

Here’s another solution. It’s just putting two pickers next to each other so it doesn’t look as nice. But it does avoid the bug in the other answer where it won’t update the value chosen in the picker the first time.

import SwiftUI

struct CountDownPicker: View {
    
    var hours = Array(0...23)
    var min = Array(0...59)
    
    @Binding var selectedHours: Int
    @Binding var selectedMins: Int
    
    var body: some View {
        GeometryReader { geometry in
            HStack {
                Picker(selection: $selectedHours, label: Text("hrs")) {
                    ForEach(0..<self.hours.count) {
                        Text("\(self.hours[$0]) hrs")
                            .bold()
                    }
                }
                .frame(maxWidth: geometry.size.width / 2)
                .clipped()
                .pickerStyle(.wheel)
                
                Picker(selection: self.$selectedMins, label: Text("mins")) {
                    ForEach(0..<self.min.count) {
                        Text("\(self.min[$0]) mins")
                            .bold()
                    }
                }
                .frame(maxWidth: geometry.size.width / 2)
                .clipped()
                .pickerStyle(.wheel)
            }
        }
        .offset(y: -100)
        .padding()
        .frame(width: .infinity, height: 140, alignment: .center)
        
    }
}
  • 1
    Please help future readers out by offering an explanation of how this differs from the accepted solution, and why people should consider using it instead. You simply not that it's "another solution", but that's not very descriptive, and especially when providing so much code. Can you [edit] your answer to explain how it differs, and why you prefer this approach? – Jeremy Caney Dec 04 '21 at 00:43