UIDatePicker odd behavior when setting minuteInter

2020-06-19 05:31发布

问题:

The following code displays an odd behavior under iOS 4.3 (maybe others version too). In this example, a UIDatePicker whose date is set to 4 Aug 2011 2:31 PM is displayed. The UILabel below the UIDatePicker displays the date for reference. The three UIButtons below, labeled 1, 5, 10 set the minuteInterval on the UIDatePicker.

Tapping 1 - shows the selected date in the UIDatePicker to be 4 Aug 2011 2:31 PM, and the minute interval is 1, which is to be expected.

Tapping 5 - shows the selected date in the UIDatePicker to be 4 Aug 2011 2:35 PM, and the minute interval is 5, which is to be expected (one could argue the time should round down, but that is not a huge issue).

Tapping 10 - shows the selected date in the UIDatePicker to be 4 Aug 2011 2:10 PM, and the minute interval is 10. Okay the minute interval is correct, but the selected time is 2:10? One would have expected 2:40 (if rounded up) or 2:30 (if rounded down).

BugDatePickerVC.h

#import <UIKit/UIKit.h>

@interface BugDatePickerVC : UIViewController {
    NSDateFormatter *dateFormatter;
    NSDate *date;
    UIDatePicker *datePicker;
    UILabel *dateL;
    UIButton *oneB;
    UIButton *fiveB;
    UIButton *tenB;
}

- (void) buttonEventTouchDown:(id)sender;

@end

BugDatePickerVC.m

import "BugDatePickerVC.h"

@implementation BugDatePickerVC

- (id) init
{
    if ( !(self = [super init]) )
    {
        return self;
    }

    dateFormatter = [[NSDateFormatter alloc] init];
    dateFormatter.dateFormat = @"d MMM yyyy h:mm a";

    date = [[dateFormatter dateFromString:@"4 Aug 2011 2:31 PM"] retain];

    // = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = =
    // Date picker
    datePicker = [[UIDatePicker alloc] initWithFrame:CGRectMake(0.0f, 0.0f, 320.0f, 216.0f)];
    datePicker.date = date;
    datePicker.minuteInterval = 1;
    [self.view addSubview:datePicker];

    // = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = =
    // Label with the date.
    dateL = [[UILabel alloc] initWithFrame:CGRectMake(10.0f, 230.0f, 300.0f, 32.0f)];
    dateL.text = [dateFormatter stringFromDate:date];
    [self.view addSubview:dateL];

    // = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = =
    // Button that set the date picker's minute interval to 1.
    oneB = [UIButton buttonWithType:UIButtonTypeRoundedRect];
    oneB.frame = CGRectMake(10.0f, 270.0f, 100.0f, 32.0f);
    oneB.tag = 1;
    [oneB setTitle:@"1" forState:UIControlStateNormal];
    [oneB   addTarget:self
               action:@selector(buttonEventTouchDown:)
     forControlEvents:UIControlEventTouchDown];
    [self.view addSubview:oneB];

    // = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = =
    // Button that set the date picker's minute interval to 5.
    fiveB = [UIButton buttonWithType:UIButtonTypeRoundedRect];
    fiveB.frame = CGRectMake(10.0f, 310.0f, 100.0f, 32.0f);
    fiveB.tag = 5;
    [fiveB setTitle:@"5" forState:UIControlStateNormal];
    [fiveB  addTarget:self
               action:@selector(buttonEventTouchDown:)
     forControlEvents:UIControlEventTouchDown];
    [self.view addSubview:fiveB];

    // = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = =
    // Button that set the date picker's minute interval to 10.
    tenB = [UIButton buttonWithType:UIButtonTypeRoundedRect];
    tenB.frame = CGRectMake(10.0f, 350.0f, 100.0f, 32.0f);
    tenB.tag = 10;
    [tenB setTitle:@"10" forState:UIControlStateNormal];
    [tenB   addTarget:self
               action:@selector(buttonEventTouchDown:)
     forControlEvents:UIControlEventTouchDown];
    [self.view addSubview:tenB];

    return self;
}

- (void) dealloc
{
    [dateFormatter release];
    [date release];
    [datePicker release];
    [dateL release];
    [oneB release];
    [fiveB release];
    [tenB release];

    [super dealloc];
}

- (void) buttonEventTouchDown:(id)sender
{
    datePicker.minuteInterval = [sender tag];
}

回答1:

Okay so I was able to change the behavior by explicitly setting the UIDatePicker date value to the date rounded to the minute interval using the following code:

- (void) handleUIControlEventTouchDown:(id)sender
{
    // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    // Set the date picker's minute interval.
    NSInteger minuteInterval  = [sender tag];

    // Setting the date picker's minute interval can change what is selected on
    // the date picker's UI to a wrong date, it does not effect the date
    // picker's date value.
    //
    // For example the date picker's date value is 2:31 and then minute interval
    // is set to 10.  The date value is still 2:31, but 2:10 is selected on the
    // UI, not 2:40 (rounded up) or 2:30 (rounded down).
    //
    // The code that follow's setting the date picker's minute interval
    // addresses fixing the date value (and the selected date on the UI display)
    // .  In the example above both would be 2:30.
    datePicker.minuteInterval = minuteInterval;

    // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    // Calculate the proper date value (and the date to be selected on the UI
    // display) by rounding down to the nearest minute interval.
    NSDateComponents *dateComponents = [[NSCalendar currentCalendar] components:NSMinuteCalendarUnit fromDate:date];
    NSInteger minutes = [dateComponents minute];
    NSInteger minutesRounded = ( (NSInteger)(minutes / minuteInterval) ) * minuteInterval;
    NSDate *roundedDate = [[NSDate alloc] initWithTimeInterval:60.0 * (minutesRounded - minutes) sinceDate:date];

    // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    // Set the date picker's value (and the selected date on the UI display) to
    // the rounded date.
    if ([roundedDate isEqualToDate:datePicker.date])
    {
        // We need to set the date picker's value to something different than
        // the rounded date, because the second call to set the date picker's
        // date with the same value is ignored. Which could be bad since the
        // call above to set the date picker's minute interval can leave the
        // date picker with the wrong selected date (the whole reason why we are
        // doing this).
        NSDate *diffrentDate = [[NSDate alloc] initWithTimeInterval:60 sinceDate:roundedDate];
        datePicker.date = diffrentDate;
        [diffrentDate release];
    }
    datePicker.date = roundedDate;
    [roundedDate release];
}

Pay attention to the part where the UIDatePicker's date is set twice. It was fun figuring that out.

Anyone know how to turn the animation off for the call to minuteInterval? The phantom scrolling when clicking 5 then 10 is a little unsightly.



回答2:

I used the above solution by mmoris and created a method that returns rounded date.. (for ARC)

- (NSDate *)getRoundedDate:(NSDate *)inDate{

    NSDate *returnDate;
    NSInteger minuteInterval = 10;
    NSDateComponents *dateComponents = [[NSCalendar currentCalendar] components:NSMinuteCalendarUnit fromDate:inDate];
    NSInteger minutes = [dateComponents minute];
    NSInteger minutesRounded = ( (NSInteger)(minutes / minuteInterval) ) * minuteInterval;
    NSDate *roundedDate = [[NSDate alloc] initWithTimeInterval:60.0 * (minutesRounded - minutes) sinceDate:inDate];

    // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    // Set the date picker's value (and the selected date on the UI display) to
    // the rounded date.
    if ([roundedDate isEqualToDate:inDate])
    {
       // We need to set the date picker's value to something different than
       // the rounded date, because the second call to set the date picker's
       // date with the same value is ignored. Which could be bad since the
       // call above to set the date picker's minute interval can leave the
       // date picker with the wrong selected date (the whole reason why we are
       // doing this).
       NSDate *diffrentDate = [[NSDate alloc] initWithTimeInterval:60 sinceDate:roundedDate];
       returnDate = diffrentDate;
       //[diffrentDate release];
    }

    returnDate = roundedDate;
    return returnDate;
}


回答3:

Here's yet another approach, with an Objective-C category!

I took the spirit of @zurbergram's rounding behavior (up/down to closest) and @mmorris's overall answer and came up with this category:

#import <UIKit/UIKit.h>

@interface UIDatePicker (SetDateRounded)

-(void)setMinimumDateRoundedByMinuteInterval:(NSDate *)minimumDate;
-(void)setDateRoundedByMinuteInterval:(NSDate *)date animated:(BOOL)animatedYesNo;

@end

@implementation UIDatePicker (SetDateRounded)

-(void)setDateRoundedByMinuteInterval:(NSDate *)date animated:(BOOL)animatedYesNo
{
    NSDateComponents *dateComponents = [[NSCalendar currentCalendar] components:NSMinuteCalendarUnit fromDate:date];
    NSInteger minutes = [dateComponents minute];
    NSInteger minutesRounded = roundf((float)minutes / (float)[self minuteInterval]) * self.minuteInterval;
    NSDate *roundedDate = [[NSDate alloc] initWithTimeInterval:60.0 * (minutesRounded - minutes) sinceDate:date];
    [self setDate:roundedDate animated:animatedYesNo];
}

-(void)setMinimumDateRoundedByMinuteInterval:(NSDate *)date
{
    NSDateComponents *dateComponents = [[NSCalendar currentCalendar] components:NSMinuteCalendarUnit fromDate:date];
    NSInteger minutes = [dateComponents minute];
    NSInteger minutesRounded = roundf((float)minutes / (float)[self minuteInterval]) * self.minuteInterval;
    NSDate *roundedDate = [[NSDate alloc] initWithTimeInterval:60.0 * (minutesRounded - minutes) sinceDate:date];
    [self setMinimumDate:roundedDate];
}

@end

Then in your implementation, you can do something like this:

#import "UIDatePicker+SetDateRounded.h"

...

- (void)viewDidLoad
{
    [super viewDidLoad];

    _datePicker.minuteInterval = 15;

    [_datePicker setMinimumDateRoundedByMinuteInterval:[NSDate date]];
    [_datePicker setDateRoundedByMinuteInterval:[NSDate date] animated:YES];
}

...

Bonus method: setMinimumDateRoundedByMinuteInterval: lets you set the picker's initial minimum to match the same behavior. One refactor would be to abstract the actual calculation part out into its own method, instead of the copy pasta, but I'm sure folks can optimize that for themselves.



回答4:

Here is an update version of getRoundedDate: that rounds up or down so that 1:03 pm rounds down to 1:00 pm and 1:12 pm rounds up to 1:15pm

-(NSDate *)getRoundedDate:(NSDate *)inDate
{
    NSInteger minuteInterval = 15;
    NSDateComponents *dateComponents = [[NSCalendar currentCalendar] components:NSMinuteCalendarUnit fromDate:inDate];
    NSInteger minutes = [dateComponents minute];

    float minutesF = [[NSNumber numberWithInteger:minutes] floatValue];
    float minuteIntervalF = [[NSNumber numberWithInteger:minuteInterval] floatValue];

    // Determine whether to add 0 or the minuteInterval to time found by rounding down
    NSInteger roundingAmount = (fmodf(minutesF, minuteIntervalF)) > minuteIntervalF/2.0 ? minuteInterval : 0;
    NSInteger minutesRounded = ( (NSInteger)(minutes / minuteInterval) ) * minuteInterval;
    NSDate *roundedDate = [[NSDate alloc] initWithTimeInterval:60.0 * (minutesRounded + roundingAmount - minutes) sinceDate:inDate];

    return roundedDate;
}


回答5:

I had the same problem with a UIDatePicker only with hours and minutes, every time I selected a time the picker add 20 minutes in the UI, not in the selected time. The solution was quite easy in my case, set the picker.minuteInterval=5 before setting the value of the picker.

Hope this will help to other people.



回答6:

zurbergram code in Swift :

func getRoundedDate(inDate: NSDate) -> NSDate {

    let minuteInterval = 15
    let dateComponents = NSCalendar.currentCalendar().components(NSCalendarUnit.MinuteCalendarUnit, fromDate: inDate)
    let minutes = dateComponents.minute

    let minutesF = NSNumber(integer: minutes).floatValue
    let minuteIntervalF = NSNumber(integer: minuteInterval).floatValue

    // Determine whether to add 0 or the minuteInterval to time found by rounding down
    let roundingAmount = (fmodf(minutesF, minuteIntervalF)) > minuteIntervalF/2.0 ? minuteInterval : 0
    let minutesRounded = (minutes / minuteInterval) * minuteInterval
    let timeInterval = NSNumber(integer: (60 * (minutesRounded + roundingAmount - minutes))).doubleValue
    let roundedDate = NSDate(timeInterval: timeInterval, sinceDate: inDate )

    return roundedDate
}


回答7:

Swift 4 version

func round(date: Date, for minuteInterval: Int) -> Date {
        let dateComponents = Calendar.current.dateComponents([.minute], from: date)

        let minutes = dateComponents.minute!

        // Determine whether to add 0 or the minuteInterval to time found by rounding down

        let intervalRemainder = Double(minutes).truncatingRemainder(
            dividingBy: Double(minuteInterval)
        )

        let halfInterval = Double(minuteInterval) / 2.0

        let roundingAmount: Int
        if  intervalRemainder > halfInterval {
            roundingAmount = minuteInterval
        } else {
            roundingAmount = 0
        }

        let minutesRounded = minutes / minuteInterval * minuteInterval
        let timeInterval = TimeInterval(
            60 * (minutesRounded + roundingAmount - minutes)
        )

        let roundedDate = Date(timeInterval: timeInterval, since: date)

        return roundedDate
    }