Как переписать компонент UIDatePicker?

Я заметил, что UIDatePicker не работает с NSHebrewCalendar в iOS 5.0 или 5.1. Я решил попробовать написать свою собственную. Я смущен тем, как заполнять данные и как поддерживать ярлыки для дат в разумной и эффективной памяти.

Сколько строк есть в каждом компоненте? Когда строки "перезагружаются" новыми ярлыками?

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

Ответы

Ответ 1

Прежде всего, спасибо за подачу ошибки о UIDatePicker и еврейском календаре.:)


ИЗМЕНИТЬ Теперь, когда iOS 6 был выпущен, вы обнаружите, что UIDatePicker теперь корректно работает с еврейским календарем, делая код ниже ненужным. Однако я оставлю это для потомков.


Как вы обнаружили, создание подходящего выбора даты - сложная проблема, потому что для прикрытия используются байкилии странных краевых случаев. Еврейский календарь особенно странный в этом отношении, имеющий промежуточный месяц (Adar I), тогда как большая часть западной цивилизации используется для календаря, который только добавляет дополнительный день примерно раз в 4 года.

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

@interface HPDatePicker : UIPickerView

@property (nonatomic, retain) NSDate *date;

- (void)setDate:(NSDate *)date animated:(BOOL)animated;

@end

Мы просто перейдем к подклассу UIPickerView и добавим поддержку свойства date. Мы проигнорируем minimumDate, maximumDate, locale, calendar, timeZone и все другие забавные свойства, которые предоставляет UIDatePicker. Это упростит нашу работу.

Реализация начнется с расширения класса:

@interface HPDatePicker () <UIPickerViewDelegate, UIPickerViewDataSource>

@end

Просто чтобы скрыть, что HPDatePicker является его собственным делегатом и источником данных.

Далее мы определим пару удобных констант:

#define LARGE_NUMBER_OF_ROWS 10000

#define MONTH_COMPONENT 0
#define DAY_COMPONENT 1
#define YEAR_COMPONENT 2

Здесь вы можете видеть, что мы собираемся упорядочить порядок блоков календаря. Другими словами, это средство выбора даты всегда будет отображать вещи как Месяц-Год-Год, независимо от настроек или настроек языка, которые могут быть у пользователя. Поэтому, если вы используете это в локали, где формат по умолчанию будет нужен "Day-Month-Year", то это слишком плохо. Для этого простого примера этого будет достаточно.

Теперь мы начинаем реализацию:

@implementation HPDatePicker {
    NSCalendar *hebrewCalendar;
    NSDateFormatter *formatter;

    NSRange maxDayRange;
    NSRange maxMonthRange;
}

- (id)initWithFrame:(CGRect)frame
{
    self = [super initWithFrame:frame];
    if (self) {
        // Initialization code
        [self setDelegate:self];
        [self setDataSource:self];

        [self setShowsSelectionIndicator:YES];

        hebrewCalendar = [[NSCalendar alloc] initWithCalendarIdentifier:NSHebrewCalendar];
        formatter = [[NSDateFormatter alloc] init];
        [formatter setCalendar:hebrewCalendar];

        maxDayRange = [hebrewCalendar maximumRangeOfUnit:NSDayCalendarUnit];
        maxMonthRange = [hebrewCalendar maximumRangeOfUnit:NSMonthCalendarUnit];

        [self setDate:[NSDate date]];
    }
    return self;
}

- (void)dealloc {
    [hebrewCalendar release];
    [formatter release];
    [super dealloc];
}

Мы переопределяем назначенный инициализатор, чтобы выполнить некоторые настройки для нас. Мы устанавливаем делегат и источник данных сами, показываем индикатор выбора и создаем объект календаря на иврите. Мы также создаем NSDateFormatter и говорим, что он должен отформатировать NSDates в соответствии с еврейским календарем. Мы также вытаскиваем пару объектов NSRange и кешируем их как ivars, поэтому нам не нужно постоянно искать вещи. Наконец, мы инициализируем его с текущей датой.

Ниже приведены реализации открытых методов:

- (void)setDate:(NSDate *)date {
    [self setDate:date animated:NO];
}

-setDate: просто переходит к другому методу

- (NSDate *)date {
    NSDateComponents *c = [self selectedDateComponents];
    return [hebrewCalendar dateFromComponents:c];
}

Извлеките NSDateComponents, представляющий все, что выбрано в данный момент, превратите его в NSDate и верните это.

- (void)setDate:(NSDate *)date animated:(BOOL)animated {
    NSInteger units = NSYearCalendarUnit | NSMonthCalendarUnit | NSDayCalendarUnit;
    NSDateComponents *components = [hebrewCalendar components:units fromDate:date];

    {
        NSInteger yearRow = [components year] - 1;
        [self selectRow:yearRow inComponent:YEAR_COMPONENT animated:animated];
    }

    {
        NSInteger middle = floor([self pickerView:self numberOfRowsInComponent:MONTH_COMPONENT] / 2);
        NSInteger startOfPhase = middle - (middle % maxMonthRange.length) - maxMonthRange.location;
        NSInteger monthRow = startOfPhase + [components month];
        [self selectRow:monthRow inComponent:MONTH_COMPONENT animated:animated];
    }

    {
        NSInteger middle = floor([self pickerView:self numberOfRowsInComponent:DAY_COMPONENT] / 2);
        NSInteger startOfPhase = middle - (middle % maxDayRange.length) - maxDayRange.location;
        NSInteger dayRow = startOfPhase + [components day];
        [self selectRow:dayRow inComponent:DAY_COMPONENT animated:animated];
    }
}

И здесь начинается забавное вещание.

Сначала мы возьмем дату, которую нам дали, и попросим календарь на иврите разбить его на объект компонентов даты. Если я дам ему NSDate, который соответствует дате gregorian от 4 апреля 2012 года, тогда ивритский календарь даст мне объект NSDateComponents, который соответствует 12 Nisan 5772, что в тот же день, что и 4 апреля 2012 года.

Основываясь на этой информации, я определяю, какую строку выбрать в каждом модуле, и выберите ее. Дело года просто. Я просто вычитаю один (строки основаны на нуле, но годы основаны на 1).

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

Реализации базовых методов <UIPickerViewDataSource> довольно тривиальны. Мы показываем 3 компонента, каждый из которых имеет 10 000 строк.

- (NSInteger)numberOfComponentsInPickerView:(UIPickerView *)pickerView {
    return 3;
}

- (NSInteger)pickerView:(UIPickerView *)pickerView numberOfRowsInComponent:(NSInteger)component {
    return LARGE_NUMBER_OF_ROWS;
}

Получение того, что выбрано в настоящий момент, довольно просто. Я получаю выбранную строку в каждом компоненте и добавляю 1 (в случае NSYearCalendarUnit) или выполняю небольшую операцию mod для учета повторяющегося характера других блоков календаря.

- (NSDateComponents *)selectedDateComponents {
    NSDateComponents *c = [[NSDateComponents alloc] init];

    [c setYear:[self selectedRowInComponent:YEAR_COMPONENT] + 1];

    NSInteger monthRow = [self selectedRowInComponent:MONTH_COMPONENT];
    [c setMonth:(monthRow % maxMonthRange.length) + maxMonthRange.location];

    NSInteger dayRow = [self selectedRowInComponent:DAY_COMPONENT];
    [c setDay:(dayRow % maxDayRange.length) + maxDayRange.location];

    return [c autorelease];
}

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

- (NSString *)pickerView:(UIPickerView *)pickerView titleForRow:(NSInteger)row forComponent:(NSInteger)component {
    NSString *format = nil;
    NSDateComponents *c = [[NSDateComponents alloc] init];

    if (component == YEAR_COMPONENT) {
        format = @"y";
        [c setYear:row+1];
        [c setMonth:1];
        [c setDay:1];        
    } else if (component == MONTH_COMPONENT) {
        format = @"MMMM";
        [c setYear:5774];
        [c setMonth:(row % maxMonthRange.length) + maxMonthRange.location];
        [c setDay:1];
    } else if (component == DAY_COMPONENT) {
        format = @"d";
        [c setYear:5774];
        [c setMonth:1];
        [c setDay:(row % maxDayRange.length) + maxDayRange.location];
    }

    NSDate *d = [hebrewCalendar dateFromComponents:c];
    [c release];

    [formatter setDateFormat:format];

    NSString *title = [formatter stringFromDate:d];

    return title;
}

@end

Здесь ситуация немного сложнее. К сожалению для нас NSDateFormatter может форматировать данные только при фактическом NSDate. Я не могу просто сказать "здесь 6" и надеюсь вернуться "Адар I". Таким образом, я должен построить искусственную дату, которая имеет значение, которое я хочу в блоке, о котором я забочусь.

В случае лет, это довольно просто. Просто создайте компоненты даты для этого года на Tishri 1, и я хорошо.

В течение нескольких месяцев я должен убедиться, что год - високосный год. Поступая таким образом, я могу гарантировать, что имена месяцев всегда будут "Adar I" и "Adar II", независимо от того, будет ли текущий год високосным годом или нет.

В течение нескольких дней я выбрал произвольный год, потому что у каждого Тишри 30 дней (и ни один месяц в еврейском календаре не превышает 30 дней).

Как только мы построим соответствующий объект компонентов даты, мы можем быстро превратить его в NSDate с нашим hebrewCalendar ivar, установить строку формата в форматировании даты только для создания строк для единицы, которая нас интересует, и сгенерировать строку.

Предполагая, что вы сделали все это правильно, вы получите следующее:

custom hebrew date picker


Некоторые примечания:

  • Я отказался от реализации -pickerView:didSelectRow:inComponent:. Вам решать, как вы хотите сообщить, что выбранная дата изменилась.

  • Это не обрабатывает недействительные даты. Например, вы можете захотеть посеять "Adar I", если выбранный год не является високосным годом. Для этого потребуется использовать -pickerView:viewForRow:inComponent:reusingView: вместо метода titleForRow:.

  • UIDatePicker выделит текущую дату синим цветом. Опять же, вам нужно будет вернуть пользовательский вид вместо строки, чтобы сделать это.

  • Ваш выборщик будет иметь черную рамку, потому что это UIPickerView. Только UIDatePickers получает синий цвет.

  • Компоненты представления выбора будут охватывать всю его ширину. Если вы хотите, чтобы все было более естественно, вам нужно переопределить -pickerView:widthForComponent:, чтобы вернуть нормальное значение для соответствующего компонента. Это может включать жесткое кодирование значения или генерирование всех строк, их размер и выбор самого большого (плюс некоторое дополнение).

  • Как отмечалось ранее, это всегда отображает вещи в порядке Month-Day-Year. Придание этой динамике текущему языку было бы немного сложнее. Вам нужно будет получить строку @"d MMMM y", локализованную в текущей локали (подсказка: посмотрите на методы класса на NSDateFormatter для этого), а затем проанализируйте ее, чтобы выяснить, что такое заказ.

Ответ 2

Я предполагаю, что вы используете UIPickerView?

UIPickerView работает как UITableView, поскольку вы указываете другой класс как dataSource/delegate, и он получает его данные, вызывая методы этого класса (которые должны соответствовать протоколу UIPickerViewDelegate/DataSource).

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

Если вы раньше не использовали UITableView, вам следует ознакомиться с шаблоном datasource/delegate, потому что это немного сложно, но вы поняли, что он очень прост в использовании.

Если это помогает, я написал пользовательский UIPicker для выбора стран. Это работает примерно так же, как вы хотите сделать свой класс, за исключением того, что я использую массив имен стран вместо дат:

https://github.com/nicklockwood/CountryPicker

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