11

I'm adapting my iPad app to Mac with Mac Catalyst and am having a problem with the datePicker (it has a datePickerMode of time). On iPad the datePicker is a wheel and whenever the user scrolls on the date picker the dateChanged action is fired. But on Mac the date picker is not a scroller and is instead a type of text input. I can type and change all the time values on Mac, but the dateChanged action won't be fired until I press the return key.

I would like to get the dateChange action fired whenever a user is entering in a time. How can I do this? I tried adding different targets to the datePicker but nothing work.

I actually prefer to have the date scroller on the Mac so if anyone knows how to do this instead I would greatly appreciate it (I looked all over the internet for this and found nothing)!

Here's my code:

class DateVC: UIViewController {
     @IBOutlet weak var datePicker: UIDatePicker!

     override func viewDidLoad() {
          super.viewDidLoad()

          //Just show the time
          datePicker.datePickerMode = .time
    }

     //Action connected to datePicker. This is not called until I press enter on Mac
     @IBAction func datePickerChanged(_ sender: Any) {
        //do actions
     }

}
fphelp
  • 616
  • 5
  • 18
  • I don't have a date scroller for the Mac, but I have a nice SwiftUI clock with the hour and minutes hands that you can use in ios and maccatalyst. It maybe of interest. the lib at: https://github.com/workingDog/ClockTimePicker An example use at: https://github.com/workingDog/ClockPicker – workingdog May 30 '20 at 03:43
  • That sounds like a bug in Mac Catalyst. I have a mac app with NSDatePicker and the action is always fired even if the user enters the date with the keyboard. – jvarela May 31 '20 at 23:40
  • I've encountered the same bug and have filed a bug report via Xcode. Apple has followed up requesting more info so I made a sample project for them to run, so hopefully this will be fixed soon. Very frustrating. – gbotha Aug 23 '20 at 00:15

6 Answers6

0

Here is a SwiftUI solution that displays a date and time scroller picker on iPad, iPhone and Mac Catalyst. Works without pressing the return key. You can easily display just the HoursMinutesPicker if desired.

import SwiftUI

struct ContentView: View {
@State var date = Date()
var body: some View {
    NavigationView {
        NavigationLink(destination: DateHMPicker(date: self.$date)) {
            VStack {
                Text("Show time")
                Text("\(self.date)")
            }
        }
    }.navigationViewStyle(StackNavigationViewStyle())
}
}

struct DateHMPicker: View {
var titleKey: LocalizedStringKey = ""
@Binding var date: Date

var body: some View {
    HStack {
        Spacer()
        DatePicker(titleKey, selection: self.$date, displayedComponents: .date).datePickerStyle(WheelDatePickerStyle())
        HoursMinutesPicker(date: self.$date)
        Spacer()
    }
}
}

struct HoursMinutesPicker: View {
@Binding var date: Date
@State var hours: Int = 0
@State var minutes: Int = 0

var body: some View {
    HStack {
        Spacer()
        Picker("", selection: Binding<Int>(
            get: { self.hours},
            set : {
                self.hours = $0
                self.update()
        })) {
            ForEach(0..<24, id: \.self) { i in
                Text("\(i) hours").tag(i)
            }
        }.pickerStyle(WheelPickerStyle()).frame(width: 90).clipped()
        Picker("", selection: Binding<Int>(
            get: { self.minutes},
            set : {
                self.minutes = $0
                self.update()
        })) {
            ForEach(0..<60, id: \.self) { i in
                Text("\(i) min").tag(i)
            }
        }.pickerStyle(WheelPickerStyle()).frame(width: 90).clipped()
        Spacer()
    }.onAppear(perform: loadData)
}

func loadData() {
    self.hours = Calendar.current.component(.hour, from: date)
    self.minutes = Calendar.current.component(.minute, from: date)
}

func update() {
    if let newDate = Calendar.current.date(bySettingHour: self.hours, minute: self.minutes, second: 0, of: date) {
        date = newDate
    }
}
}
workingdog
  • 3,125
  • 1
  • 5
  • 13
  • Thanks but I'm not looking to have the WheelPickerStyle. The advantage of the UIDatePicker on Mac catalyst is it is a text field format so it allows users to use their keyboard to type in a date/time – Stephen May 30 '20 at 17:19
  • Sorry, I misread your question. I thought "I actually prefer to have the date scroller on the Mac" meant you wanted WheelPickerStyle. Are you looking for a textfield type for time only? – workingdog May 31 '20 at 01:31
  • Sorry the original question is not mine so I see your confusion. I’m looking to use it for date & time. In Mac catalyst it provides a nice calendar from the text field for the date – Stephen Jun 01 '20 at 02:24
0

How about using DatePicker for the date part, and the following textfields for the time input part.

import SwiftUI
import Combine

struct ContentView: View {
@State var date = Date()
var body: some View {
    NavigationView {
        NavigationLink(destination: DateHMPicker(date: self.$date)) {
            VStack {
                Text("Show time")
                Text("\(self.date)")
            }
        }
    }.navigationViewStyle(StackNavigationViewStyle())
}
}

struct DateHMPicker: View {
@State var labelText = ""
@Binding var date: Date

var body: some View {
    HStack {
        Spacer()
        DatePicker(labelText, selection: self.$date, displayedComponents: .date)
        Spacer()
        HoursMinutesPicker(date: self.$date).frame(width: 90)
    }.fixedSize()
}
}

struct TextFieldTime: View {
let range: ClosedRange<Int>
@Binding var value: Int
var handler: () -> Void

@State private var isGood = false
@State private var textValue = ""
@State private var digits = 2

var body: some View {
    TextField("", text: $textValue)
        .font(Font.body.monospacedDigit())
        .onReceive(Just(textValue)) { txt in
            // must be numbers
            var newTxt = txt.filter {"0123456789".contains($0)}
            if newTxt == txt {
                // restrict the digits
                if newTxt.count > self.digits {
                    newTxt = String(newTxt.dropLast())
                }
                // check the number
                self.isGood = false
                if let number = NumberFormatter().number(from: newTxt) {
                    if self.range.contains(number.intValue) {
                        self.textValue = newTxt
                        self.value = number.intValue
                        self.isGood = true
                    } else {
                        self.textValue = self.textValue.count == 1
                            ? String(self.range.lowerBound) : String(self.textValue.dropLast())
                    }
                }
                if self.value >= 0 && self.isGood {
                    self.handler()
                }
            } else {
                self.textValue = newTxt.isEmpty ? String(self.range.lowerBound) : newTxt
            }

    }.onAppear(perform: {
        self.textValue = String(self.value)
        self.digits = String(self.range.upperBound).count
    })
        .fixedSize()
}
}

struct HoursMinutesPicker: View {
@Binding var date: Date
@State var separator = ":"

@State var hours: Int = 0
@State var minutes: Int = 0

var body: some View {
    HStack (spacing: 1) {
        TextFieldTime(range: 0...23, value: self.$hours, handler: self.update)
        Text(separator)
        TextFieldTime(range: 0...59, value: self.$minutes, handler: self.update)
    }.onAppear(perform: loadData).padding(5)
}

func loadData() {
    self.hours = Calendar.current.component(.hour, from: date)
    self.minutes = Calendar.current.component(.minute, from: date)
}

func update() {
    let baseDate = Calendar.current.dateComponents([.year, .month, .day], from: date)
    var dc = DateComponents()
    dc.year = baseDate.year
    dc.month = baseDate.month
    dc.day = baseDate.day
    dc.hour = self.hours
    dc.minute = self.minutes
    if let newDate = Calendar.current.date(from: dc), date != newDate {
        date = newDate
    }
}
}
workingdog
  • 3,125
  • 1
  • 5
  • 13
  • doesn't this answer provide a workaround that doesn't require the user to hit enter after adjusting the time? – workingdog Jun 06 '20 at 03:55
0

Because your function linked with @IBAction which is to be called upon action, like 'button press'

you should follow different approach.

let datePicker = UIDatePicker()
    datePicker.datePickerMode = .date
    dateTextField.inputView = datePicker



datePicker.addTarget(self, action: #selector(datePickerChanged(picker:)), for: .valueChanged)

and here is your function:

@objc func datePickerChanged(picker: UIDatePicker) {
    //do your action here
}
0

I didn't find a solution to this using the built in UIDatePicker. I ended up moving to using JBCalendarDatePicker which accomplished the same look/feel.

Stephen
  • 1,388
  • 1
  • 16
  • 36
0

If you prefer the wheel format of the DatePicker in Mac Catalyst, then change your Date Picker style to Wheels. It will display correctly on the Mac.

I converted my iPhone/iPad app to run on Mac Catalyst. I'm using Xcode 11.4.1 on MacOS Catalina 10.15.5. I was having a problem displaying the DatePicker as a wheel on the Mac version of the app. On the Mac emulator, it displayed as a text field, but the calendar would display when clicking on one of the date fields. I felt it would be a better user experience to stay with the wheel display.

A very simple workaround is to select the DatePicker in Storyboard. Display the Attributes Inspector. Change your Style from "Automatic" to "Wheels", and the display will go back to displaying as a wheel on the Mac Catalyst version.

Michelle
  • 119
  • 4
  • I also found that you should check the Mode setting under Attributes inspector for Date and Time, Date, or Time so that the wheel shows the correct values. – Michelle Jul 16 '20 at 20:47
0

I have filed a bug report with Apple about 1 week ago. For now I a doing the following to force the datepicker to use the wheel format. This fires the onchangedlistener as the wheels are spun.

if #available(macCatalyst 13.4, *) {
    datePickerView.preferredDatePickerStyle = .wheels
}
gbotha
  • 1,025
  • 14
  • 21