When you set an appointment in the calendar app there is a control used for time measurements called the
UIDat
ePicker
. While not one of the most popular controls, it is very handy for working with dates. In this lesson we’ll find out how easy it is to work with. We also will cover some of the important date-related methods and classes you should know when you works with dates and times.
Set Up the Delivery Time app
As a demonstration, we’ll create a delivery time estimator. Create a new project named SwiftDatePickerDemo from the single view template. On the storyboard set up like this:
Open the assistant editor. From the UIDatePicker, Top label, and upper segmented control, add outlets to your view controller:
//MARK: Outlets and properties @IBOutlet weak var myDatePicker: UIDatePicker! @IBOutlet weak var deliveryTimeLabel: UILabel! @IBOutlet weak var deliveryDelayPreset: UISegmentedControl!
From the date picker and the both segmented controls, add actions.
//MARK: - Target Actions @IBAction func myDateView(sender: UIDatePicker) { } @IBAction func deliveryDelayPreset(sender: UISegmentedControl) { } @IBAction func deliveryDate(sender: AnyObject) { }
Configure the Date Picker
Close the assistant editor and go to the viewcontroller.swift
file. UIDatePicker
has a few properties you can set to control the behavior of the picker. In a delivery application for example, we don’t want to deliver into the past so the first date and time that shows up is the current date and time. The picker will default to the current date and time, but we can set it. We can also set a minimum time on the picker. We will set if the date will show on the picker. For a simple delivery app where we will deliver only the same day, we really don’t need the extra wheels for the date.
Change viewDidLoad
to the following:
//MARK: - Life Cycle override func viewDidLoad() { super.viewDidLoad() myDatePicker.datePickerMode = UIDatePickerMode.Time // 4- use time only let currentDate = NSDate() //5 - get the current date myDatePicker.minimumDate = currentDate //6- set the current date/time as a minimum myDatePicker.date = currentDate //7 - defaults to current time but shows how to use it. }
Line 4 uses the datePickerMode
property to set the mode to .Time
We then get a new instance of a NSDate
, which is the current time. We set that time in the minimumDate
property and the date
property.
Build and Run. While it doesn’t do much yet, you will find you cannot go to a time before the current time.
Displaying a Date with NSDateFormatter
When we set the date
property, we did so as a NSDate
Class. What we would like to do is display the time on the label, but for that we need a String. Apparently one of the best kept secrets (AKA poorly documeted) in Xcode is how to do this. There is a special class for this, NSDateFormatter
, which is not quite intuitive to use. To get a string of the date, you make a instance of NSDateFormatter
,then set the properties for the instance. You use the instance method stringFromDate
to return a string from a date. The reason for all this is localization. NSDateFormatter
uses the region settings to display the date properly for that user’s experience.
Above the life cycle MARK
, add the following:
//MARK: - Instance Methods func printDate(date:NSDate){ let dateFormatter = NSDateFormatter()//3 var theDateFormat = NSDateFormatterStyle.ShortStyle //5 let theTimeFormat = NSDateFormatterStyle.ShortStyle//6 dateFormatter.dateStyle = theDateFormat//8 dateFormatter.timeStyle = theTimeFormat//9 deliveryTimeLabel.text = "Delivered at: " + dateFormatter.stringFromDate(date)//11 }
Line 3 creates a date formatter. Both date and time are in a date, so we need to set how we want to show the date and time. our choices are NoStyle
, ShortStyle
, MediumStyle
and LongStyle
. In line 5 and 6 we set this for the time and date. In line 11, we use the stringFromDate
method to return a string and directly send it to the label.
There is a shorter way to do this from a class method of NSDateFormatter
, but I don’t find it very useful.
let deliveryTime = NSDateFormatter.localizedStringFromDate(date, dateStyle: .ShortStyle, timeStyle: .ShortStyle) deliveryTImeLabel.text = "Delivered at: " + deliveryTime
Generally if there is one date on something, there is more than one date or time. Consider this scene from Interval RunCalc, an App I wrote for calculating pace and distance given running intervals.
There are four times here: two in each table cell, one in the header and one in the footer. This is the common case, and having a date formatter around actually speeds up the process, since you set the style only once and then use the same stringfromDate
for all the occurrences.
Now that we can get a date into the label, we need to get the date. Change the myDateView
method to:
@IBAction func myDateView(sender: UIDatePicker) { printDate(sender.date) }
We already set the date
property. Here we get the date selected, and call the printDate
method to display it on the label.
Build and run, and now we can see the time in the label.
Calculating with Time: Adding a Delay
Of course no one delivers immediately, and the idea of this app is to calculate what time the delivery will be ready from that delay. We have a segmented control with the standard delay times in them. We will add those to the time and display the time plus the delay.
To do that we will need another property. Add this to the properties and outlets:
var delay:NSTimeInterval = 0.0
Add the following to the instance methods:
func delayTime() -> NSDate{ let pickerTime = myDatePicker.date return pickerTime.dateByAddingTimeInterval(delay)//3 }
Line three has NSDate
‘s method for taking an NSTimeInterval
and adding it to an NSDate
we get from the picker’s date
property. NSTimeIntervals
are really of type Double
, containing a time interval in number of seconds.
The times on the labels are not numbers, they are strings. We need to get them into type Double
to do anything with them.
@IBAction func deliveryDelayPreset(sender: UISegmentedControl) { let index = sender.selectedSegmentIndex //2 let delayString:NSString = sender.titleForSegmentAtIndex(index)! //3 delay = delayString.doubleValue * 60 //4 convert minutes to seconds printDate(delayTime()) //5 }
In lines 2 and 3 we get the title string back from the segment. In line 4 we use the doubleValue
method to convert the String
to a Double
, them multiply by 60 to get seconds. Line 5 prints the date again, this time with the delay.
We will need to change the mydateView
method as well so it reflects the delay:
@IBAction func myDateView(sender: UIDatePicker) { printDate(delayTime()) }
Time and Date with NSDateFormatter
When we making deliveries today, we don’t need the date — it’s today’s date. Let’s use the other segmented control to set a boolean variable that will then change the formatting.
Add this to the outlets and properties:
var today = true
Change deliveryDate
to this:
@IBAction func deliveryDate(sender: UISegmentedControl) { let index = sender.selectedSegmentIndex today = index == 0 if today { myDatePicker.datePickerMode = .Time } else{ myDatePicker.datePickerMode = .DateAndTime } printDate(delayTime()) }
We set a flag today
and toggle the picker mode to show and hide the date. In the printDate
function change this:
var theDateFormat = NSDateFormatterStyle.NoStyle
to this:
var theDateFormat = NSDateFormatterStyle.NoStyle if !today { theDateFormat = .ShortStyle }
Build and run. Switch the segment from today to future.
One More Bug
The is still a subtle bug in the application. If you run the app for a long time, you can send pizzas back in time. Run the app, and wait a few minutes. You find you can dial back the date picker to the original initialization time. If you use an minimum time of the current time, you will need to update it frequently. I did it this way:
func delayTime() -> NSDate{ myDatePicker.minimumDate = NSDate() let pickerTime = myDatePicker.date return pickerTime.dateByAddingTimeInterval(delay) }
Since the date calculation occurs here, By setting the minimum date in the calculation, we prevent and catch all backwards dates, and reset them to the current date in one statement.
The Whole Code
// // ViewController.swift // SwiftDatePickerDemo // // Created by Steven Lipton on 9/21/14. // Copyright (c) 2014 MakeAppPie.Com. All rights reserved. // import UIKit class ViewController: UIViewController { //MARK: Outlets and properties @IBOutlet weak var myDatePicker: UIDatePicker! @IBOutlet weak var deliveryTimeLabel: UILabel! @IBOutlet weak var deliveryDelayPreset: UISegmentedControl! var delay:NSTimeInterval = 0.0 var today = true //MARK: - Target Actions @IBAction func myDateView(sender: UIDatePicker) { printDate(delayTime()) } @IBAction func deliveryDelayPreset(sender: UISegmentedControl) { let index = sender.selectedSegmentIndex let delayString:NSString = sender.titleForSegmentAtIndex(index)! delay = delayString.doubleValue * 60 //convert minutes to seconds printDate(delayTime()) } @IBAction func deliveryDate(sender: UISegmentedControl) { let index = sender.selectedSegmentIndex today = index == 0 if today { myDatePicker.datePickerMode = .Time } else{ myDatePicker.datePickerMode = .DateAndTime } printDate(delayTime()) } //MARK: - Instance Methods func delayTime() -> NSDate{ myDatePicker.minimumDate = NSDate() let pickerTime = myDatePicker.date return pickerTime.dateByAddingTimeInterval(delay) } func printDate(date:NSDate){ let dateFormatter = NSDateFormatter() var theDateFormat = NSDateFormatterStyle.NoStyle if !today { theDateFormat = .ShortStyle } let theTimeFormat = NSDateFormatterStyle.ShortStyle dateFormatter.dateStyle = theDateFormat dateFormatter.timeStyle = theTimeFormat deliveryTimeLabel.text = "Delivered at: " + dateFormatter.stringFromDate(date) } //MARK: - Life Cycle override func viewDidLoad() { super.viewDidLoad() myDatePicker.datePickerMode = UIDatePickerMode.Time let currentDate = NSDate() myDatePicker.minimumDate = currentDate myDatePicker.date = currentDate //defaults to this but.. } }
Leave a Reply