Masilotti.com

NSDate Manipulation in iOS 8

Apple quietly introduced a whole new suite of public API methods to NSCalendar in iOS 8 titled “Calendrical Calculations”. For some reason they seemed to have forgotten to include them in the public documentation on their developer site. Fortunately, digging in to the header file in Xcode reveals lots of descriptive comments about how to use these powerful new ways of interacting with NSDate objects.

In iOS 7 all NSDate manipulation required working with NSDateComponents directly. While there is nothing inherently wrong with this class it is terribly verbose. Most of the manipulations require converting an NSDate to its components, fiddling with one or more of the values, then creating a new date by hand. I’ll compare a few common operations and you can decide which are more legible, terse, and easy to understand.

Note that these new APIs don’t add any new functionality to the framework. Think of them as shortcuts and more readable ways of doing standard date operations.

Follow along with the code via this gist.

Create an NSDate Three Hours in the Future

To create an NSDate object three hours from now we only have one API call (after getting an NSCalendar instance).

NSCalendar *calendar = [NSCalendar currentCalendar];
NSDate *threeHoursFromNow;
threeHoursFromNow = [calendar dateByAddingUnit:NSCalendarUnitHour
                                        value:3
                                       toDate:[NSDate date]
                                      options:kNilOptions];

This method will account for overflow as expected. Try passing in 24 for the value parameter and watch the date’s day increase. Following this same approach you can easily manipulate other units by setting the first parameter to your desired granularity.

Comparing this to the old approach of fishing out NSDateComponents, then setting the specific property, then creating an NSDate from said components you can quickly see how powerful and readable the new API is. Here is a rundown of how we could have done it “the old way”. Note that all of the NSCalendarUnits are required to ensure the newly created date has all of the same properties from the original one.

NSCalendar *calendar = [NSCalendar currentCalendar];
NSCalendarUnit calendarUnits = NSCalendarUnitTimeZone | NSCalendarUnitYear
| NSCalendarUnitMonth | NSCalendarUnitDay | NSCalendarUnitHour
| NSCalendarUnitMinute | NSCalendarUnitSecond;
NSDateComponents *components = [calendar components:calendarUnits 
                                           fromDate:[NSDate date]];
components.hour += 3;
NSDate *threeHoursFromNow = [calendar dateFromComponents:components];

Getting the Next Occurrence of a Time

Let’s say you need to compute when the next occurrence of a time happens. For example, you could be trying to schedule a local notification to fire the next time it’s 9:30am. (Remember, scheduling a UILocalNotification in the past makes it fire immediately!) Again, using the new API we can achieve this in one call.

NSCalendar *calendar = [NSCalendar currentCalendar];
NSDate *nextNineThirty = [calendar nextDateAfterDate:[NSDate date] 
                                        matchingHour:9
                                              minute:30
                                              second:0
                                             options:NSCalendarMatchNextTime];

Using the old APIs would require us to take a few more steps. First we create a new date from today’s date and set the relevant components. If the new date's hour and minute are “more than” todays’s then return the new date, otherwise increment the day component.

Note the annoying and cryptic if statement doing the actual calculation. You can easily imagine how this would snowball if we cared about more components.

NSCalendar *calendar = [NSCalendar currentCalendar];
NSCalendarUnit calendarUnits = NSCalendarUnitTimeZone | NSCalendarUnitYear
| NSCalendarUnitMonth | NSCalendarUnitDay | NSCalendarUnitHour 
| NSCalendarUnitMinute | NSCalendarUnitSecond;
NSDateComponents *components = [calendar components:calendarUnits 
                                           fromDate:date];

if (components.hour > hour || 
    (components.hour == hour && components.minute > minute)) {
    components.day++;
}
components.hour = hour;
components.minute = minute;

NSDate *nextDate = [calendar dateFromComponents:components];

Comparing Dates (with Granularity)

Gone are the days of breaking down dates into seconds then doing math to compute comparisons. Two new methods introduced help us compare NSDates with unit granularity. This can simplify a lot of existing code where you would need to know if a date is sort of bigger or smaller than the other. For example, comparing if a day is after another day (disregarding time) you could specify the NSCalendarUnitDay granularity.

NSDate *today = [NSDate date];

NSDate *laterToday = [calendar dateByAddingUnit:NSCalendarUnitMinute
                                          value:20
                                         toDate:today
                                        options:kNilOptions];
NSComparisonResult result1 = [calendar compareDate:today
                                            toDate:laterToday
                                 toUnitGranularity:NSCalendarUnitDay];
// result1 = NSOrderedSame

NSDate *tomorrow = [calendar dateByAddingUnit:NSCalendarUnitHour
                                        value:24
                                       toDate:today
                                      options:kNilOptions];
NSComparisonResult result2 = [calendar compareDate:today
                                            toDate:tomorrow
                                 toUnitGranularity:NSCalendarUnitDay];
// result2 = NSOrderedAscending

As mentioned above, the second date manipulation handles the overflow of the hours and increments the day.

One approach to a solution to this problem using the old APIs is to generate new NSDates with only the relevant components. You can then compare the two as the other attributes will be striped. Unfortunately this is quite cryptic. Coming back to this code in a month or two you might not remember why you only grabbed three components when everywhere else in the app you use more.

NSCalendar *calendar = [NSCalendar currentCalendar];
NSCalendarUnit calendarUnits;
calendarUnits = NSCalendarUnitYear | NSCalendarUnitMonth | NSCalendarUnitDay;

NSDateComponents *components1 = [calendar components:calendarUnits 
                                            fromDate:date1];
NSDateComponents *components2 = [calendar components:calendarUnits 
                                            fromDate:date2];

NSDate *lessGranularDate1 = [calendar dateFromComponents:components1];
NSDate *lessGranularDate2 = [calendar dateFromComponents:components2];

NSComparisonResult result = [lessGranularDate1 compare:lessGranularDate2];

Is This Date Today? Tomorrow? Yesterday?

Finally, let’s look at a few new conveinence methods that build on the previous method. If you have very specific requirements for figuring out if a particular NSDate is today, tomorrow, or yesterday you are in luck.

NSDate *today = [NSDate date];
[calendar isDateInToday:today];
[calendar isDateInTomorrow:today];
[calendar isDateInYesterday:today];

iOS 7 compatible code can follow the same basic approach as the -isDateInToday: method. Essentially, only grab components that are necessary then compare.

NSCalendar *calendar = [NSCalendar currentCalendar];
NSCalendarUnit calendarUnits;
calendarUnits = NSCalendarUnitYear | NSCalendarUnitMonth | NSCalendarUnitDay;

NSDateComponents *components = [calendar components:calendarUnits 
                                           fromDate:date];
NSDateComponents *todayComponents = [calendar components:calendarUnits 
                                                fromDate:[NSDate date]];

NSDate *lessGranularDate1 = [calendar dateFromComponents:components];
NSDate *lessGranularToday = [calendar dateFromComponents:todayComponents];

BOOL isInToday = [lessGranularDate1 isEqualToDate:lessGranularToday];

Benefits of Highly Abstracted APIs

The new high level NSCalendar API gives developers more concise, terse, and legible ways of manipulating dates as we no longer have to muck around with crufty NSDateComponents and NSCalendarUnit bit masks. There is less moving parts and more direct manipulation. This makes code easier to read and understand leading to faster development time and fewer bugs. It makes you wonder why Apple didn’t include these in earlier versions of iOS.

From my count there are 32 new API methods added to NSCalendar in iOS 8 and I didn’t even touch upon validating dates, iterating over a series of dates, or matching dates by component. If you’re interested in learning more take a peek at the header file and add a comment with your findings below.

If you liked this post, you can share it with your followers or follow me on Twitter.

More on iOS Tips