Salva
Salva

Reputation: 807

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

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!

Upvotes: 15

Views: 16980

Answers (4)

Asperi
Asperi

Reputation: 257869

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()
    }
}

Upvotes: 16

threepartment
threepartment

Reputation: 329

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
});

Upvotes: 20

Wasim
Wasim

Reputation: 1140

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()
        }

    }

}

Upvotes: 1

Simone Pistecchia
Simone Pistecchia

Reputation: 2842

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()
    }
}

Upvotes: 1

Related Questions