Как управлять NSDate для разных часовых поясов

Я создал календарь в своем приложении, используя объект даты следующим образом:

    NSCalendar *gregorian = [[NSCalendar alloc] initWithCalendarIdentifier:NSGregorianCalendar];
    NSDateComponents *weekdayComponents = [gregorian components:(NSDayCalendarUnit | NSYearCalendarUnit | NSMonthCalendarUnit  | NSMinuteCalendarUnit)fromDate:[NSDate date]];
    NSInteger day    = [weekdayComponents day];
    NSInteger month  = [weekdayComponents month]; 
    NSInteger year   = [weekdayComponents year];

    m_dateFormatter.dateFormat = @"dd/MM/yyyy";
    [gregorian setTimeZone:[NSTimeZone timeZoneWithAbbreviation:@"UTC"]];
    NSDateComponents *timeZoneComps=[[NSDateComponents alloc] init];
    [timeZoneComps setDay:day];
    [timeZoneComps setMonth:month];
    [timeZoneComps setYear:year];
    [timeZoneComps setHour:00];
    [timeZoneComps setMinute:00];
    [timeZoneComps setSecond:01];

    m_currentDate         = [gregorian dateFromComponents:timeZoneComps];

Когда пользователь хочет перейти в следующем месяце, я выделяю первое число этого месяца. Таким образом, в этом случае дата будет 1-06-2014,00:00:01.

Вот код:

    - (void)showNextMonth
    {  
        // Move the date context to the next month
        NSCalendar *gregorian = [[NSCalendar alloc] initWithCalendarIdentifier:NSGregorianCalendar];
        NSDateComponents *dateComps = [[NSDateComponents alloc] init];
        [dateComps setMonth:1];

        m_currentMonthContext =[gregorian dateByAddingComponents:dateComps toDate:m_currentMonthContext options:0];


        NSDateComponents *weekdayComponents1 = [gregorian components:(NSDayCalendarUnit | NSWeekdayCalendarUnit | NSYearCalendarUnit | NSMonthCalendarUnit) fromDate:m_currentMonthContext];

        NSInteger nextMonth = [weekdayComponents1 month];
        NSInteger nextyear  = [weekdayComponents1 year];

        NSDateComponents *weekdayComponents2 = [gregorian components:(NSDayCalendarUnit | NSWeekdayCalendarUnit | NSYearCalendarUnit | NSMonthCalendarUnit) fromDate:m_currentDate];

        NSInteger currentDay   = [weekdayComponents2 day];
        NSInteger currentMonth = [weekdayComponents2 month];
        NSInteger currentYear  = [weekdayComponents2 year];

        NSInteger selectedDay = 1;

        if(nextMonth == currentMonth && nextyear == currentYear)
        {
            selectedDay = currentDay;
        }

        NSInteger month = nextMonth;

        [gregorian setTimeZone:[NSTimeZone timeZoneWithAbbreviation:@"UTC"]];

        NSDateComponents *timeZoneComps=[[NSDateComponents alloc] init];
        [timeZoneComps setDay:selectedDay];
        [timeZoneComps setMonth:month];
        [timeZoneComps setYear:nextyear];
        [timeZoneComps setHour:00];
        [timeZoneComps setMinute:00];
        [timeZoneComps setSecond:01];

        m_currentMonthContext =[gregorian dateFromComponents:timeZoneComps];

        [self createCalendar];
    }

Когда m_currentMonthContext вычисляется в предпоследней строке вышеуказанного метода, его значение равно 1-06-2014,00:00:01.

createCalendar реализация:

-(void)createCalendar
{
    NSCalendar *gregorian = [[NSCalendar alloc] initWithCalendarIdentifier:NSGregorianCalendar];
    NSDateComponents *weekdayComponents = [gregorian components:(NSDayCalendarUnit | NSWeekdayCalendarUnit | NSYearCalendarUnit | NSMonthCalendarUnit)fromDate:m_currentMonthContext];

    NSInteger month = [weekdayComponents month];
    NSInteger year  = [weekdayComponents year];     
}

Здесь я получаю month как 5 и year как 2014, но дата 01-06-2014. Это происходит только в часовом поясе США, во всех остальных часовых поясах все работает нормально.

Поэтому я хочу знать, как эффективно обрабатывать часовые пояса или, в другом смысле, как убедиться, что NSDate не изменится, даже если часовой пояс изменится.


person Ranjit    schedule 24.05.2014    source источник
comment
Что вы подразумеваете под Как управлять NSDate для разных часовых поясов? Как насчет того, чтобы перечислить некоторые тематические исследования? например: UserA перемещается из timeZoneA в timeZoneB. Какую дату следует ожидать? и так далее..   -  person Ricky    schedule 24.05.2014
comment
Привет, Рикки, я хочу, чтобы мой объект NSDate не менялся независимо от того, в каком часовом поясе я нахожусь.   -  person Ranjit    schedule 24.05.2014
comment
Вы имеете в виду, что если я из Юго-Восточной Азии и лечу в США, и я все еще вижу дату, основанную на Юго-Восточной Азии? Итак, вы хотите, чтобы ваше приложение придерживалось только одной даты независимо от того, где находится пользователь?   -  person Ricky    schedule 24.05.2014
comment
да рикки ты прав   -  person Ranjit    schedule 24.05.2014
comment
@Ranjit Я попытался немного прояснить ваш вопрос, переместив некоторые ключевые детали из внутренних блоков кода, которые, казалось, были частью самого тела вопроса, пожалуйста, исправьте, если я допустил здесь ошибку, спасибо.   -  person Carl Veazey    schedule 24.05.2014


Ответы (2)


Непосредственная причина заключается в том, что часовой пояс не всегда устанавливается в календаре при расчете дат и компонентов даты. Иногда вы устанавливаете часовой пояс на UTC, а иногда нет, что приведет к несоответствиям, так как иногда будут применяться смещения для местного времени, а иногда нет.

Подробно, в вашей ситуации m_currentMonthContext представляет собой NSDate, который представляет время UTC через одну секунду после полуночи 1 июня 2014 года. В вашем методе createCalendar вы создаете календарь, который является локальным временем пользователя, и вычисляете компоненты для таких свидание. Во всех часовых поясах США по-прежнему наступает май месяц через одну секунду после полуночи 1 июня 2014 года по всемирному координированному времени. Пример кода, который можно запустить изолированно:

    NSCalendar *utcCalendar = [[NSCalendar alloc] initWithCalendarIdentifier:NSGregorianCalendar];
    [utcCalendar setTimeZone:[NSTimeZone timeZoneWithAbbreviation:@"UTC"]];
    NSCalendar *localCalendar = [[NSCalendar alloc] initWithCalendarIdentifier:NSGregorianCalendar];
    NSDate *june = [NSDate dateWithTimeIntervalSince1970:1401580801];
    NSDateComponents *utcComponents = [utcCalendar components:(NSYearCalendarUnit|NSMonthCalendarUnit|NSDayCalendarUnit) fromDate:june];
    NSDateComponents *localComponents = [localCalendar components:(NSYearCalendarUnit|NSMonthCalendarUnit|NSDayCalendarUnit) fromDate:june];
    NSLog(@"utc : %@", utcComponents);
    NSLog(@"local: %@", localComponents);

Здесь, в часовом поясе MDT, это регистрируется:

utc : Календарный год: 2014 Месяц: 6 Високосный месяц: нет День: 1

местный: Календарный год: 2014 Месяц: 5 Високосный месяц: нет День: 31

Напомним, что вы храните в памяти дату, которая была рассчитана для представления определенной даты календаря по времени UTC, а затем вычисляете дату календаря по местному времени пользователя, но, похоже, у вас есть неправильное ожидание, что календари для разных часовых поясов будет интерпретировать одну и ту же дату одинаково.

Так что делать? Ваш пример довольно сложный, но кажется, что нет необходимости хранить компоненты даты иногда в часовом поясе UTC, а иногда нет - будьте последовательны. Теперь мне также кажется, что вы можете быть намного проще в своем коде, если просто хотите найти первый день следующего месяца:

    NSCalendar *cal = [[NSCalendar alloc] initWithCalendarIdentifier:NSGregorianCalendar];
    NSDateComponents *comps = [cal components:(NSYearCalendarUnit|NSMonthCalendarUnit|NSDayCalendarUnit) fromDate:[NSDate date]];
    [comps setMonth:[comps month] + 1];
    [comps setDay:1];

Я протестировал это с 15 декабря 2014 года, и это сработало, чтобы создать 1 января 2015 года по моему местному времени. Надеюсь, это последовательное поведение.

Подводя итог - очень вероятно, что ошибка не использует согласованный календарь для вычислений компонента даты. Иногда наличие UTC, а иногда и локального вызовет у вас кошмары. Кажется, что вы всегда должны рассчитывать по местному времени, но я не знаю всего контекста вашего приложения, поэтому не могу сделать для этого общее заявление. Кроме того, должно быть безопасно не полагаться на бесконечные преобразования между датами и компонентами даты, а вместо этого использовать компонент даты как источник правды. То есть я имею в виду, что кажется запутанным преобразовывать компоненты даты в даты всегда для хранения в переменных экземпляра, но затем немедленно преобразовывать даты обратно в компоненты даты каждый раз, когда они используются - кажется, лучше просто работать с компонентами даты столько же насколько это возможно.

person Carl Veazey    schedule 24.05.2014
comment
Здравствуйте @Carl, спасибо за ваш ценный источник информации, вы говорите, что я использовал где-то UTC, а где-то местное время, я этого не понял, где я это сделал? 2) Что вы подразумеваете под непоследовательными преобразованиями между датами и компонентами даты, можете ли вы это указать? 3) Могу ли я использовать defaultTimeZone вместо UTC? - person Ranjit; 24.05.2014
comment
@Ranjit 1) Один пример: предпоследняя строка -showNextMonth вы назначаете m_currentMonthContext дату, полученную из календаря, который вы установили в качестве часового пояса UTC. Затем в -createCalendar вы создаете экземпляр нового календаря и не устанавливаете часовой пояс явно, поэтому по умолчанию используется время UTC. Видите ли вы, что вы делаете это, и есть ли смысл, почему вы не должны этого делать? 2) Я сказал Incessant, чтобы многое подразумевать - все переменные экземпляра хранятся как NSDate, но кажется, что вы почти всегда используете их только для вычисления компонентов даты. Отредактировано для уточнения. 3) Где? - person Carl Veazey; 24.05.2014
comment
Эй, спасибо, так вы хотите сказать, что я должен явно использовать UTC при создании календаря? Я думал об использовании defaulttimeZone, где я использовал UTC, например, в моей инициализации функции даты выше. Надеюсь, вы поняли. - person Ranjit; 24.05.2014
comment
Это, вероятно, просто закроет ошибки, но я точно не знаю, каков ваш широкий вариант использования, поэтому, возможно, это сработает в контексте вашего приложения. Почему вы используете UTC вообще, если вы отображаете все на основе того, что имеет смысл для календаря пользователя? - person Carl Veazey; 24.05.2014
comment
Я думал об использовании UTC, потому что это универсально. Что-нибудь не так с этим? - person Ranjit; 24.05.2014
comment
А, интересно! Оно называется Универсальным скоординированным временем, но оно не является по-настоящему универсальным — это скорее базовый часовой пояс, чем универсальный часовой пояс. Так что, возможно, в этом случае вам нужно не указывать часовой пояс для ваших календарей, а вместо этого полагаться на настройки календаря пользователя. То есть удалите все настройки явного часового пояса и посмотрите, поможет ли это. - person Carl Veazey; 24.05.2014

Судя по комментарию, я надеюсь, что правильно понял ваш вопрос. Вы можете попробовать этот код: -

NSDate * nowDate = [NSDate date];
NSLog(@"nowDate: %@",nowDate);

NSDateFormatter *df = [NSDateFormatter new];
[df setDateFormat:@"dd/MM/yyyy HH:mm"];
df.timeZone = [NSTimeZone timeZoneForSecondsFromGMT:[NSTimeZone localTimeZone].secondsFromGMT];

NSString *localDate = [df stringFromDate:nowDate];
NSLog(@"localDate: %@", localDate);

Вывод:

2014-05-24 23:03:06.205 TestTimeZone[10214:60b] nowDate: 24-05-2014 15:03:06 +0000

2014-05-24 23:03:06.209 TestTimeZone[10214:60b] localDate: 24/05/2014 23:03

[NSDate date] всегда возвращает дату GMT+0, независимо от вашего часового пояса. Может просто использовать это? В то же время я использовал NSDateFormatter для установки моей локальной даты на основе моего ноутбука. Вы можете попробовать перейти на несколько разных часовых поясов на своем Mac, запустив приведенный выше код на симуляторе. [Дата NSDate] может быть именно тем, что вам нужно.

person Ricky    schedule 24.05.2014
comment
Не совсем правильно говорить, что [NSDate date] всегда возвращает время по Гринвичу, поскольку NSDate не имеет абсолютно никакого понятия о часовом поясе. - person Carl Veazey; 24.05.2014
comment
Действительно? Может быть, мне не хватает знаний об этом. До сих пор я тестировал разные часовые пояса на своем Mac, он по-прежнему отображается как +0000, что, как я полагаю, является GMT + 0. Если мы не назначим часовой пояс для NSDate, по умолчанию [NSDate date] всегда GMT + 0 верно? Я могу быть не прав. - person Ricky; 25.05.2014
comment
Это то, что он печатает, когда вы регистрируете его, что на самом деле создает экземпляр NSDateFormatter с определенным часовым поясом и локалью для его печати, но NSDate в основном просто отметка времени. - person Carl Veazey; 25.05.2014