Правильная инициализация NSCalendar - PullRequest
0 голосов
/ 17 июня 2020

Наше приложение немного работает с датами, но в настоящее время мы поддерживаем только григорианский календарь, а экземпляр NSCalendar для всего приложения инициализируется следующим образом:

NSCalendar *appCalendar = [NSCalendar calendarWithIdentifier:NSCalendarIdentifierGregorian];

В документации вышеуказанного метода указано, что «Возвращенный календарь по умолчанию использует текущий языковой стандарт и часовой пояс по умолчанию». Однако при запуске приложения на устройстве с регионом «Великобритания» при вызове [appCalendar firstWeekday] возвращалось значение 1 (воскресенье), а не ожидаемые 2 (понедельник). Если я запустил [[NSCalendar currentCalendar] firstWeekday], вернется правильное значение 2. Сначала я подумал, что локаль не может быть установлена ​​в «appCalendar», но журнал показал, что он есть, хотя ему не хватало кода страны et c., Который есть у экземпляра «currentCalendar» и который позволяет ему возвращать правильный firstWeekDay.

Должен ли языковой стандарт быть явно установлен для объекта, возвращаемого из calendarWithIdentifier, и если да, то есть ли при этом какие-либо соображения?

Обновить

Основываясь на ответе zrzka ниже, я рекомендую явно указывать языковой стандарт при инициализации календаря с идентификатором, например,

NSCalendar *appCalendar = [NSCalendar calendarWithIdentifier:NSCalendarIdentifierGregorian];

appCalendar.locale = [NSLocale currentLocale];

1 Ответ

2 голосов
/ 17 июня 2020

Документация неверна:

Возвращаемый календарь по умолчанию соответствует текущему языку и часовому поясу по умолчанию.

Это должно быть:

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

CFCalendar.c:

CFCalendarRef CFCalendarCreateWithIdentifier(CFAllocatorRef allocator, CFStringRef identifier) {
    if (allocator == NULL) allocator = __CFGetDefaultAllocator();
    __CFGenericValidateType(allocator, CFAllocatorGetTypeID());
    __CFGenericValidateType(identifier, CFStringGetTypeID());
    // return NULL until Chinese calendar is available
    if (identifier != kCFGregorianCalendar && identifier != kCFBuddhistCalendar && identifier != kCFJapaneseCalendar && identifier != kCFIslamicCalendar && identifier != kCFIslamicCivilCalendar && identifier != kCFHebrewCalendar) {
//    if (identifier != kCFGregorianCalendar && identifier != kCFBuddhistCalendar && identifier != kCFJapaneseCalendar && identifier != kCFIslamicCalendar && identifier != kCFIslamicCivilCalendar && identifier != kCFHebrewCalendar && identifier != kCFChineseCalendar) {
    if (CFEqual(kCFGregorianCalendar, identifier)) identifier = kCFGregorianCalendar;
    else if (CFEqual(kCFBuddhistCalendar, identifier)) identifier = kCFBuddhistCalendar;
    else if (CFEqual(kCFJapaneseCalendar, identifier)) identifier = kCFJapaneseCalendar;
    else if (CFEqual(kCFIslamicCalendar, identifier)) identifier = kCFIslamicCalendar;
    else if (CFEqual(kCFIslamicCivilCalendar, identifier)) identifier = kCFIslamicCivilCalendar;
    else if (CFEqual(kCFHebrewCalendar, identifier)) identifier = kCFHebrewCalendar;
//  else if (CFEqual(kCFChineseCalendar, identifier)) identifier = kCFChineseCalendar;
    else return NULL;
    }
    struct __CFCalendar *calendar = NULL;
    uint32_t size = sizeof(struct __CFCalendar) - sizeof(CFRuntimeBase);
    calendar = (struct __CFCalendar *)_CFRuntimeCreateInstance(allocator, CFCalendarGetTypeID(), size, NULL);
    if (NULL == calendar) {
    return NULL;
    }
    calendar->_identifier = (CFStringRef)CFRetain(identifier);
    calendar->_locale = NULL;
    calendar->_localeID = CFLocaleGetIdentifier(CFLocaleGetSystem());
    calendar->_tz = CFTimeZoneCopyDefault();
    calendar->_cal = NULL;
    return (CFCalendarRef)calendar;
}

_locale инициализируется с помощью NULL, а _localeID инициализируется идентификатором языкового стандарта системного языкового стандарта (который является пустой строкой в ​​iPhone и симуляторе). _cal установлено на NULL.

CFIndex CFCalendarGetFirstWeekday(CFCalendarRef calendar) {
    CF_OBJC_FUNCDISPATCHV(CFCalendarGetTypeID(), CFIndex, calendar, firstWeekday);
    __CFGenericValidateType(calendar, CFCalendarGetTypeID());
    if (!calendar->_cal) __CFCalendarSetupCal(calendar);
    if (calendar->_cal) {
    return ucal_getAttribute(calendar->_cal, UCAL_FIRST_DAY_OF_WEEK);
    }
    return -1;
}

Итак, поскольку _cal равно NULL, вызывается __CFCalendarSetupCal.

static void __CFCalendarSetupCal(CFCalendarRef calendar) {
    calendar->_cal = __CFCalendarCreateUCalendar(calendar->_identifier, calendar->_localeID, calendar->_tz);
}

Что вызывает __CFCalendarCreateUCalendar с _localeID, которая является пустой строкой.

Я могу подтвердить это поведение на iOS 11, 12 и 13. Исходный код предназначен для чего-то под названием CF-Lite , но я пошел дальше и разобрал реальную структуру CoreFoundation, и он делает то же самое ...

call       _CFLocaleGetSystem             ; _CFLocaleGetSystem
mov        rdi, rax                       ; argument "cf" for method _CFRetain
call       _CFRetain                      ; _CFRetain
mov        qword [r15+0x18], rax
call       _CFTimeZoneCopyDefault         ; _CFTimeZoneCopyDefault
mov        qword [r15+0x20], rax
mov        rbx, qword [r15+0x10]
mov        rdi, qword [r15+0x18]          ; argument "locale" for method _CFLocaleGetIdentifier
call       _CFLocaleGetIdentifier         ; _CFLocaleGetIdentifier
mov        rdx, qword [r15+0x20]          ; argument #3 for method ___CFCalendarCreateUCalendar
mov        rdi, rbx                       ; argument #1 for method ___CFCalendarCreateUCalendar
mov        rsi, rax                       ; argument #2 for method ___CFCalendarCreateUCalendar
call       ___CFCalendarCreateUCalendar   ; ___CFCalendarCreateUCalendar

... с использованием пустого идентификатора из CFLocaleGetIdentifier из CFLocaleGetSystem.

Когда вы проверяете в документации CFCalendarCreateWithIdentifier нет ни слова о текущем языковом стандарте, часовом поясе, ...

Что еще более интересно, так это разница (раздел Обсуждение) для этих двух методов:

Но нет никакой разницы, calendarWithIdentifier: просто вызывает alloc & initWithCalendarIdentifier:.

push       rbp
mov        rbp, rsp
push       r14
push       rbx
mov        rbx, rdx
mov        rsi, qword [0x3cb478]                       ; argument "selector" for method _objc_msgSend, @selector(alloc)
mov        r14, qword [_objc_msgSend_390220]           ; _objc_msgSend_390220
call       r14                                         ; Jumps to 0x553ae0 (_objc_msgSend), _objc_msgSend
mov        rsi, qword [0x3cc768]                       ; argument "selector" for method _objc_msgSend, @selector(initWithCalendarIdentifier:)
...

Я считаю, что это проблема с документацией, о которой следует сообщить в Apple (сделал это, FB7740798 ).

...