How to detect a value change of a Datepicker using SwiftUI and Combine?
Asked Answered
I

4

13

How would you detect a change of value of a Datepicker while using SwiftUI and Combine? I need to invoke a method whenever the datepicker wheel is moved, to update a Text and a Slider.

I have looked for specific methods to identify the value change (using UIKit it was possible to associate an action to an event), but apparently I haven't found anything useful in the documentation (I've tried the onTapGesture methods, but that's not what I want, since it forces the user to tap the picker to update the other views, whereas I would like to have an automatic update whenever the user moves the wheel).

import SwiftUI

struct ContentView: View {

    private var calendar = Calendar.current

    @State private var date = Date()
    @State private var weekOfYear = Double(Calendar.current.component(.weekOfYear, from: Date()) )
    @State private var lastWeekOfThisYear = 53.0
    @State private var weekDay: String = { () -> String in 
        let dateFormatter = DateFormatter()
        dateFormatter.dateFormat = "EEEE"
        let weekDay = dateFormatter.string(from: Date())
        return weekDay
    }()

    var body: some View {

        VStack {

            // Date Picker
            DatePicker(selection: $date, displayedComponents: .date, label:{ Text("Please enter a date") }
            )
            .labelsHidden()
            .datePickerStyle(WheelDatePickerStyle())
            .onTapGesture {
                self.updateWeekAndDayFromDate()
            }

            // Week number and day
            Text("Week \(Int(weekOfYear.rounded()))")
            Text("\(weekDay)")

            // Slider
            Slider(value: $weekOfYear, in: 1...lastWeekOfThisYear, onEditingChanged: { _ in
                    self.updateDateFromWeek()
                })
            }

    }

    func updateWeekAndDayFromDate() {
        // To do
    }

    func updateDateFromWeek() {
        // To do
    }

    func setToday() {
        // To do
    }

    func getWeekDay(_ date: Date) -> String {
        //To do
    }
}

I guess this could be solved using Combine (observableobject, published, sink, etc.), but I'm not experienced yet with Combine, therefore I'd like to ask for some help... any ideas? :)

Thanks a lot!

Interlining answered 29/10, 2019 at 8:46 Comment(2)
This can all be done using bindings and states. Could you share your code for the slider and text as well, please?Homogony
Sure, I have added some more code in my post. What I'm doing is using the datepicker wheel to select a date, and update a label showing the year week (from 1 to 53) and the position of the slider (also representing the year week). At the same time, when I move the slider I want to update the status of the picker (this last part I can already do, since I use on the slider 'onEditingChanged' to call my update method, even if this update the picker only when I terminate the movement of the slider, whereas I would prefer a continuous update). I couldn't do the opposite (update the slider/laber).Interlining
F
16

Please find below one possible approach (with a bit modified your code). Here is an idea to have source state transparent to activate dependent within it. Hope it will be helpful somehow.

Here is demo

dependent binding of DatePicker

Here is code

struct TestDatePicker: View {

    private static func weekOfYear(for date: Date) -> Double {
        Double(Calendar.current.component(.weekOfYear, from: date))
    }

    private static func weekDay(for date: Date) -> String {
        let dateFormatter = DateFormatter()
        dateFormatter.dateFormat = "EEEE"
        let weekDay = dateFormatter.string(from: date)
        return weekDay
    }

    @State private var date: Date
    @State private var weekOfYear: Double
    @State private var weekDay: String
    @State private var lastWeekOfThisYear = 53.0

    private var dateProxy:Binding<Date> {
        Binding<Date>(get: {self.date }, set: {
            self.date = $0
            self.updateWeekAndDayFromDate()
        })
    }

    init() {
        let now = Date()
        self._date = State<Date>(initialValue: now)
        self._weekOfYear = State<Double>(initialValue: Self.weekOfYear(for: now))
        self._weekDay = State<String>(initialValue: Self.weekDay(for: now))
    }

    var body: some View {

        VStack {

            // Date Picker
            DatePicker(selection: dateProxy, displayedComponents: .date, label:{ Text("Please enter a date") }
            )
            .labelsHidden()
            .datePickerStyle(WheelDatePickerStyle())

            // Week number and day
            Text("Week \(Int(weekOfYear.rounded()))")
            Text("\(weekDay)")

            // Slider
            Slider(value: $weekOfYear, in: 1...lastWeekOfThisYear, onEditingChanged: { _ in
                    self.updateDateFromWeek()
                })
            }

    }

    func updateWeekAndDayFromDate() {
        self.weekOfYear = Self.weekOfYear(for: self.date)
        self.weekDay = Self.weekDay(for: self.date)
    }

    func updateDateFromWeek() {
        // To do
    }

    func setToday() {
        // To do
    }

    func getWeekDay(_ date: Date) -> String {
        ""
    }
}

struct TestDatePicker_Previews: PreviewProvider {
    static var previews: some View {
        TestDatePicker()
    }
}
Forfar answered 25/11, 2019 at 11:4 Comment(0)
I
18

An easier way is using .onChange on DatePicker:

DatePicker(
    "Datum",
    selection: $date,
    displayedComponents: [.date]
).onChange(of: date, perform: { value in
     // Do what you want with "date", like array.timeStamp = date
});
Indecorous answered 27/9, 2021 at 15:36 Comment(4)
This is good, however only available in iOS14+, if you need this for iOS13 have a look at the last example in this article.Nollie
Also I've found that if you change the month/date from the larger DatePicker view it considers that a selection as well so I'm looking for a way to only close the view if they pick a date inside the month view.Nikolai
this doesn't seem to work if you select the same date that's already selectedGarpike
this doesn't work on macOSAfoot
F
16

Please find below one possible approach (with a bit modified your code). Here is an idea to have source state transparent to activate dependent within it. Hope it will be helpful somehow.

Here is demo

dependent binding of DatePicker

Here is code

struct TestDatePicker: View {

    private static func weekOfYear(for date: Date) -> Double {
        Double(Calendar.current.component(.weekOfYear, from: date))
    }

    private static func weekDay(for date: Date) -> String {
        let dateFormatter = DateFormatter()
        dateFormatter.dateFormat = "EEEE"
        let weekDay = dateFormatter.string(from: date)
        return weekDay
    }

    @State private var date: Date
    @State private var weekOfYear: Double
    @State private var weekDay: String
    @State private var lastWeekOfThisYear = 53.0

    private var dateProxy:Binding<Date> {
        Binding<Date>(get: {self.date }, set: {
            self.date = $0
            self.updateWeekAndDayFromDate()
        })
    }

    init() {
        let now = Date()
        self._date = State<Date>(initialValue: now)
        self._weekOfYear = State<Double>(initialValue: Self.weekOfYear(for: now))
        self._weekDay = State<String>(initialValue: Self.weekDay(for: now))
    }

    var body: some View {

        VStack {

            // Date Picker
            DatePicker(selection: dateProxy, displayedComponents: .date, label:{ Text("Please enter a date") }
            )
            .labelsHidden()
            .datePickerStyle(WheelDatePickerStyle())

            // Week number and day
            Text("Week \(Int(weekOfYear.rounded()))")
            Text("\(weekDay)")

            // Slider
            Slider(value: $weekOfYear, in: 1...lastWeekOfThisYear, onEditingChanged: { _ in
                    self.updateDateFromWeek()
                })
            }

    }

    func updateWeekAndDayFromDate() {
        self.weekOfYear = Self.weekOfYear(for: self.date)
        self.weekDay = Self.weekDay(for: self.date)
    }

    func updateDateFromWeek() {
        // To do
    }

    func setToday() {
        // To do
    }

    func getWeekDay(_ date: Date) -> String {
        ""
    }
}

struct TestDatePicker_Previews: PreviewProvider {
    static var previews: some View {
        TestDatePicker()
    }
}
Forfar answered 25/11, 2019 at 11:4 Comment(0)
R
1

A possible approach using ObservableObject

class TestPikerModel: ObservableObject {
    @Published var expireDate: Date = Date()
}

struct TestPikerView: View {

    @ObservedObject var testPikerModel = TestPikerModel()
    @State private var weekOfYear = Double(Calendar.current.component(.weekOfYear, from: Date()) )

    var body: some View {
    
        VStack {
            Text("\(Int(weekOfYear))")
            DatePicker("", selection: $testPikerModel.expireDate, displayedComponents: .date)
            .datePickerStyle(CompactDatePickerStyle())
            .clipped()
            .labelsHidden()
        }
        .onReceive(testPikerModel.$expireDate) { date in
            weekOfYear = Double(Calendar.current.component(.weekOfYear, from: date))
            updateWeekAndDayFromDate()
        }
    }

    func updateWeekAndDayFromDate() {
        print("updateWeekAndDayFromDate performed")
    }
}

struct TestPikerView_Previews: PreviewProvider {
    static var previews: some View {
        TestPikerView()
    }
}
Revolve answered 1/11, 2020 at 16:39 Comment(0)
O
1

Another approach would be, where observer in viewModel is called whenever date is changed

class TestPikerModel: ObservableObject {
    @Published var expireDate: Date = Date()

init() {
       setupObserver()
    }
    
private func setupObserver() {
        $expireDate
            .sink { date in
                // do some networking call
            }
            .store(in: &cancellables)
    }
}

struct TestPikerView: View {

    @ObservedObject var testPikerModel = TestPikerModel()
    @State private var weekOfYear = Double(Calendar.current.component(.weekOfYear, from: Date()) )

    var body: some View {
    
        VStack {
            Text("\(Int(weekOfYear))")
            DatePicker("", selection: $testPikerModel.expireDate, displayedComponents: .date)
            .datePickerStyle(CompactDatePickerStyle())
            .clipped()
            .labelsHidden()
        }

    }

}
Oslo answered 9/12, 2020 at 11:57 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.