I have spent about 3 hours trying to get a UIDatePicker to do what I want it to do. I have it setup as an action of a UITextField to segue to a new view controller with a UIDatePicker. This is all working. I have been able to select a date and send it back to the original view controller.
The problem is that I am using 5 minute time intervals. If I don't move the date picker "wheels" and select my done button to return to the parent controller the non-interval time is returned. So 11:37 returns 11:37 when I need 11:40. However, if I change the "wheels" it returns the rounded time. The datepicker is currently showing the 5 minute intervals on appear. Any clues to lead me to the right solution? I have tried setDate and minuteInterval on the UIDatePicker on viewDidAppear to no avail. Below is the code I have been using:
protocol DatePickerDelegate {
func datePickerDidSelect(selectedDate: String)
}
class DatePickerVC: UIViewController {
#IBOutlet weak var pickerView: UIDatePicker!
var selectedDate = ""
var delegate : DatePickerDelegate?
override func viewDidLoad() {
super.viewDidLoad()
}
override func viewDidAppear(_ animated: Bool) {
//setDate()
//Have tried using setDate and minuteInterval here
}
func formatDate() -> DateFormatter {
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "MMM d, yyyy h:mm a"
return dateFormatter
}
func setDate() {
let dateFormatter = formatDate()
selectedDate = dateFormatter.string(from: pickerView.date)
self.delegate?.datePickerDidSelect(selectedDate: selectedDate)
print(selectedDate)
}
#IBAction func datePickerChanged(_ sender: Any) {
setDate()
}
#IBAction func doneBtnPressed(_ sender: Any) {
self.dismiss(animated: true, completion: nil)
}
#IBAction func cancelBtnPressed(_ sender: Any) {
self.dismiss(animated: true, completion: nil)
}
}
you could check the date and round it to your needs like this:
override func viewDidLoad() {
super.viewDidLoad()
let calendar = Calendar.current
var dateComponents = calendar.dateComponents([.month, .day, .year, .hour, .minute], from: datePicker.date)
guard var hour = dateComponents.hour, var minute = dateComponents.minute else {
print("something went wrong")
return
}
let intervalRemainder = minute % datePicker.minuteInterval
if intervalRemainder > 0 {
// need to correct the date
minute += datePicker.minuteInterval - intervalRemainder
if minute >= 60 {
hour += 1
minute -= 60
}
// update datecomponents
dateComponents.hour = hour
dateComponents.minute = minute
// get the corrected date
guard let roundedDate = calendar.date(from: dateComponents) else {
print("something went wrong")
return
}
// update the datepicker
datePicker.date = roundedDate
}
}
feel free to ask if anything is unclear!
Another alternative is to create an extension that allows you to tweak intervals on the date value that's set when the UIDatePicker is initialized.
extension Date {
var nearest30Min: Date {
var dateComponents = Calendar.current.dateComponents([.year, .month, .day, .hour, .minute], from: self)
guard let hours = dateComponents.hour else { return self }
switch dateComponents.minute ?? 0 {
case 0...14:
dateComponents.minute = 0
case 15...44:
dateComponents.minute = 30
case 44...59:
dateComponents.minute = 0
dateComponents.hour = hours + 1
default:
break
}
return dateComponents.date ?? self
}
}
After your UIDatePicker is initialized, you could update the datePicker's value with the following code:
datePicker.setDate(datePicker.date.nearest30min, animated: false)
Related
I have start date and end date strings, how to show the end Sale timer like this image?
"Start-date":"Dec 18, 2019 05:15:00 +0000","End-date":"Dec 27, 2019 11:15:39 +0000"
Please help!
You should convert the date string to Date first, then you can use timer to update the timer labels based on start and end date.
Use Calendar and dateComponents method to find the day, hour, minute and second differences between dates and then set the value on day, hour, minute and second labels as like you attached image.
Example implementation:
var timer:Timer?
var endDate:Date?
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
startTimer()
}
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
stopTimer()
}
func startTimer(){
let endDateStr = "Dec 27, 2019 11:15:39 +0000"
let dateFormat = "MMM d, yyyy HH:mm:ss Z"
let dateFormater = DateFormatter()
dateFormater.dateFormat = dateFormat
endDate = dateFormater.date(from: endDateStr)
//stop timer if it's already running
stopTimer()
timer = Timer.scheduledTimer(timeInterval: 1, target: self, selector: #selector(updateSaleTime), userInfo: nil, repeats: true)
}
func stopTimer(){
if timer != nil{
timer!.invalidate()
timer = nil
}
}
func updateSaleTime(){
guard let d2 = endDate else {
stopTimer()//Check if the date-format is correct for end date string.
return
}
let cal = Calendar.current
let components = cal.dateComponents([.day, .hour, .minute, .second], from: Date(), to: d2)
let day = components.day!
let hour = components.hour!
let minute = components.minute!
let second = components.second!
//set the value on day, hour, minute and second labels as like you attached image.
}
If you calculate the difference between the startDate and the endDate it will alway be the same . Instead you can calculate the difference to endDate from the current date.
Check the following implementation.
class ViewController: UIViewController {
let endDate : Date? = {
// To create Date from date string received from server
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "MMM dd, yyyy HH:mm:ss Z"
// Convert to desired Timezone
return dateFormatter.date(from: "Dec 18, 2019 06:30:39 +0000")
}()
var timer : Timer?
private func starCountDown() {
timer = Timer.scheduledTimer(timeInterval: 1, target: self, selector: #selector(updateTime), userInfo: nil, repeats: true)
}
private func stopCountDown() {
timer?.invalidate()
}
#objc func updateTime() {
guard let endDate = endDate else {
stopCountDown()
return
}
let countdown = Calendar.current.dateComponents([.day, .hour, .minute, .second], from: Date(), to: endDate)
let days = countdown.day!
let hours = countdown.hour!
let minutes = countdown.minute!
let seconds = countdown.second!
if days <= 0 && hours <= 0 && minutes <= 0 && seconds <= 0 {
stopCountDown()
print("Offer Expired")
return
}
print(String(format: "%02d Days , %02d Hours, %02d Mins, %02d Sec", days, hours, minutes, seconds))
// set values as per your requirement
}
override func viewWillDisappear(_ animated: Bool) {
super.viewWillAppear(animated)
stopCountDown()
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
starCountDown()
}
}
My time label is displaying the time, when I open my app but it won't update it live. I had a look at other answers but they didn't make sense.
// CURRENT TIME
#IBOutlet weak var currentTimeLabel: UILabel!
override func viewDidLoad() {
super.viewDidLoad()
getCurrentTime()
}
// FORMAT TIME
func getCurrentTime(){
let formatter = DateFormatter()
formatter.dateFormat = "hh:mm"
let str = formatter.string(from: Date())
currentTimeLabel.text = str
}
I want my app to update the time label live. Thanks in advance. It's probably a really simple fix.
Use Timer for your requirement,
class ViewController: UIViewController {
#IBOutlet weak var currentTimeLabel: UILabel!
var timer = Timer()
override func viewDidLoad() {
super.viewDidLoad()
getCurrentTime()
}
private func getCurrentTime() {
timer = Timer.scheduledTimer(timeInterval: 1.0, target: self, selector:#selector(self.currentTime) , userInfo: nil, repeats: true)
}
#objc func currentTime() {
let formatter = DateFormatter()
formatter.dateFormat = "hh:mm"
currentTimeLabel.text = formatter.string(from: Date())
}
}
You can create Timer with repeating every minute (because you don't need seconds for anything) starting in the next minute (so call getCurrentTime() once before you start Timer).
Every minute code inside timer's closure gets executed so you can say that you want to call getCurrentTime(). Now your currentTimeLabel will be updated every minute
let now = Date()
let date = Calendar.current.date(bySettingHour: Calendar.current.component(.hour, from: now), minute: Calendar.current.component(.minute, from: now) + 1, second: 0, of: now)!
let timer = Timer(fire: date, timeInterval: 60, repeats: true) { _ in
self.getCurrentTime()
}
Also I would recommend you to have formatter variable outside of the method (in global scope)
lazy var formatter: DateFormatter = {
let formatter = DateFormatter()
formatter.dateFormat = "hh:mm" // or "hh:mm a" if you need to have am or pm symbols
return formatter
}()
and then in getCurrentTime() just get String and change text of currentTimeLabel
func getCurrentTime() {
currentTimeLabel.text = formatter.string(from: Date())
}
I get my date from server not device date.but there is problem with changing date
override func viewDidAppear(_ animated: Bool) {
datepicker2.datePickerMode = .date
datepicker2.timeZone = TimeZone(identifier: "IRST")
datepicker2.calendar = Calendar(identifier: .persian)
datepicker2.locale = Locale(identifier: "fa_IR")
datepicker2.date=self.minDate // this is a date var filled
datepicker2.addTarget(self, action: #selector(datepickerAction(_:)), for: .valueChanged)
}
each time I change the date prints just one date
func datepickerAction(_ sender: UIDatePicker){
let date = self.datepicker2.date
let components = Calendar.current.dateComponents([.month, .day,.year], from: date)
let hour = components.month
let minute = components.day
let yr=components.year
print(hour,minute,yr) // prints one date always
}
this is the date I get from server :
func CurrentdateAndTime(){
let url=URL(string: "\(address)date/getNewDate")
var urlrequest=URLRequest(url: url!)
print(url)
var time:String=""
urlrequest.httpMethod = "GET"
URLSession.shared.dataTask(with: urlrequest) { (data, response, error) in
if let data=data {
let json=String(data: data, encoding: String.Encoding.utf8)
let b:Double=Double(json!)!
// if !b.isZero{
self.setComperehensiveDate(longDate: b)
self.settime(longTime: b)
self.setDay(day: b)
self.setYear(year: b)
self.setmonth(month: b)
self.setHour(longTime: b)
self.setMin(longTime: b)
self.onComplete?("ok?")
}
}.resume()
}
func setComperehensiveDate(longDate:Double){
let dbl = TimeInterval(longDate)
let date = Date(timeIntervalSince1970: dbl / 1000)
//let formatter = DateFormatter()
print("long",longDate,dbl)
// formatter.calendar = Calendar(identifier: .persian)
// formatter.locale = Locale(identifier: "fa_IR")
// formatter.timeZone = TimeZone(identifier: "IRST")
//formatter.dateFormat = "yyyy-MM-dd HH:MM:ss"
//let a = formatter.string(from: date)
// resultDate = formatter.date(from: a)
resultDate = date
print("aaa",resultDate)
}
I get millisecond from server set on func that converts to date then I get it
and I get minDate data this way :
self.minDate=getcomperehensiveDate()
update : my problem is if user takes his device date to the past manually or future the picker shows the the past which I don't want this. I must show the current date thats why I'm setting date programatclly and facing this issue so what is your idea about this scenario ?
I can see issue with your line
let date = self.datepicker2.date
it must be getting the same date or something. I can see you are giving the same date
datepicker2.date=self.minDate (this is issue)
I just tried using
#IBAction func change(_ sender: Any) {
let components = Calendar.current.dateComponents([.month, .day,.year], from: datepicker.date)
let hour = components.month
let minute = components.day
let yr=components.year
print(hour,minute,yr)
}
it's same picker working fine.
please download the project from the link:
https://github.com/sanojKashyap/51799141_S
datepickerAction is called after changing the value of datepicker2 on a real device or the simulator.
The following should work normally:
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
datepicker2.datePickerMode = .date
datepicker2.timeZone = TimeZone(identifier: "IRST")
datepicker2.calendar = Calendar(identifier: .persian)
datepicker2.locale = Locale(identifier: "fa_IR")
// The following line is setting the datepicker2.date to Tomorrow, because you haven't posted the code giving self.minDate
datepicker.date = Date().addingTimeInterval(24 * 3600)
}
func datepickerAction(_ sender: UIDatePicker) {
let date = self.datepicker2.date
let components = Calendar.current.dateComponents([.month, .day,.year], from: date)
let month = components.month //This is the month component, not the hour like in your code
let day = components.day //This is the day component, not the hour like in your code
let yr = components.year
print(day!,month!,yr!)
}
If you want the hour and minute components you'll just have to define components like so:
let components = Calendar.current.dateComponents([.minute, .hour, .month, .day,.year], from: date)
let hour = components.hour
let minutes = components.minute
Your problem is because you set the UIDatePicker date in viewDidAppear. viewDidAppear and viewWillApear are not reliable enough to be used to call one time setters, use viewDidLoad as this is guaranteed to only be called once.
I learned this the hard way and after putting a breakpoint in the problematic date setter I traced it back to the fact it was set in the viewDidLayoutSubviews override. Another no-no apparently which led me to your question.
I was wondering if there's a way to select the date of the time picker to the next interval not to the nearest: If it's 1:47, I want the picker to display 2:00 and not 1:45.. is there any attribute for the UIDatePicker to choose the next Interval?
let datePicker = UIDatePicker()
datePicker.datePickerMode = UIDatePickerMode.time
datePicker.minuteInterval = 15
Thank you for any help.
What you need is to add a target for your date picker for UIControlEvents .valueChanged and get the minute component from the selected date. To calculate the next hour quarter minute you can use the following formula (x - x % 15 + 15) % 60:
import UIKit
class ViewController: UIViewController {
#IBOutlet weak var datePicker: UIDatePicker!
override func viewDidLoad() {
super.viewDidLoad()
datePicker.addTarget(self, action: #selector(datePickerValueChanged), for: .valueChanged)
}
#objc func datePickerValueChanged(datePicker: UIDatePicker) {
if datePicker.date.minute % 15 != 0 {
if datePicker.date.minute > 45 {
datePicker.date += 60*60
}
datePicker.date = datePicker.date.nextHourQuarter
}
}
}
extension Int {
var nextHourQuarter: Int {
return (self - self % 15 + 15) % 60
}
}
extension Date {
var hour: Int { return Calendar.current.component(.hour, from: self) }
var minute: Int { return Calendar.current.component(.minute, from: self) }
var nextHourQuarter: Date {
return Calendar.current.date(bySettingHour: hour, minute: minute.nextHourQuarter, second: 0, of: self)!
}
}
Sample
The UIDatePicker class does not have the functionality. You can only lock the selector to allow certain values (e.g. if you set datePicker.minuteInterval = 15, the available selections would start from the initial time and go in 15 minute ticks).
You can implement this behaviour yourself by listening to the value changed notification and setting the time manually (preferably with an animation).
I guess it hasn't been clear what I mean.
I've written this function which sets the picker time to the next interval.
I don't know if it's correct but it works:
Date 8:03 -> picker date is 8:15
Date 8:23 -> picker date is 8:30
Date 8:38 -> picker date is 8:45
Date 8:46 -> picker date is 9:00
viewDidLoad:
let datePicker = UIDatePicker()
datePicker.datePickerMode = UIDatePickerMode.time
datePicker.minuteInterval = 15
datePicker.date = setDatePickerTime(datePicker: datePicker)
Set DatePicker Time to next 15 min interval
func setDatePickerTime(datePicker: UIDatePicker) -> Date {
let calendar = Calendar.current
let currentHour = calendar.component(.hour, from: Date())
let currentMinute = calendar.component(.minute, from: Date())
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "HH:mm"
var pickerOutHour = ""
var pickerOutMinute = ""
if (currentMinute > 45 && currentMinute < 60){
pickerOutMinute = "00"
pickerOutHour = String((Int(currentHour) + 1) % 24)
} else if (currentMinute > -1 && currentMinute < 16){
pickerOutMinute = "15"
pickerOutHour = String(currentHour)
} else if (currentMinute > 15 && currentMinute < 31){
pickerOutMinute = "30"
pickerOutHour = String(currentHour)
} else if (currentMinute > 30 && currentMinute < 46){
pickerOutMinute = "45"
pickerOutHour = String(currentHour)
}
let pickerOutDate = dateFormatter.date(from: pickerOutHour + ":" + pickerOutMinute)
return pickerOutDate!
}
I have been struggling making a countdown in Swift where it shows only the days left until some date where the input is the DatePicker... I have creo experience with Swift so, I have been struggling for a while. I tried some similar answers here but didn't work, I watched a tutorial but is a normal countdown with months, days, minutes and seconds, this is the code.
import UIKit
class ViewController: UIViewController {
#IBOutlet weak var timeLabel: UILabel!
let formatter = DateFormatter()
let userCleander = Calendar.current;
let requestedComponent : Set<Calendar.Component> = [
Calendar.Component.month,
Calendar.Component.day
]
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
let timer = Timer.scheduledTimer(timeInterval: 1.0, target: self, selector: #selector(timePrinter), userInfo: nil, repeats: true)
timer.fire()
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
func timeCalculator(dateFormat: String, endTime: String, startTime: Date = Date()) -> DateComponents {
formatter.dateFormat = dateFormat
let _startTime = startTime
let _endTime = formatter.date(from: endTime)
let timeDifference = userCleander.dateComponents(requestedComponent, from: _startTime, to: _endTime!)
return timeDifference
}
func timePrinter() -> Void {
let time = timeCalculator(dateFormat: "MM/dd/yyyy a", endTime: "12/25/2018 a")
timeLabel.text = "\(time.month!) Months \(time.day!) Days"
}
}
Several things: Don't use strings to compare dates. Use Date objects and Calendar operations. (More on that in a second.)
Don't run a timer once a second. Save the current date to user defaults. When your app is launched, compare the saved date to the current date and see if the day has changed.
When running, listen for UIApplicationSignificantTimeChange notifications, and when you get one, check to see if the date has changed.
As for comparing the current date to the user-selected date, you've got the right idea using dateComponents(_:from:to:), but you should pass in components of just [.day].
EDIT:
Code like this would do the trick:
override func viewDidLoad() {
super.viewDidLoad()
//Set up the date picker to pick dates, not dates & times
datePicker.datePickerMode = .date
//Force the date picker to use midnight today as it's base date and
//to pick a date at least 1 day in the future
guard let today = Calendar.current.date(bySettingHour: 0, minute: 0, second: 0, of: Date()),
let tomorrow = Calendar.current.date(byAdding: .day, value: 1, to: today)
else {
return
}
datePicker.minimumDate = tomorrow
datePicker.date = tomorrow
}
#IBAction func datePickerChanged(_ sender: UIDatePicker) {
let future = datePicker.date
//Use midnight today as the starting date
guard let today = Calendar.current.date(bySettingHour: 0, minute: 0, second: 0, of: Date()) else { return }
//Calculate the number of days between today and the user's chosen day.
let difference = Calendar.current.dateComponents([.day], from: today, to: future)
guard let days = difference.day else { return }
let ess = days > 1 ? "s" : ""
infoLabel.text = "That date is \(days) day\(ess) away."
}