¿Cómo reescribo el componente UIDatePicker?

Me he dado count de que el UIDatePicker no funciona con NSHebrewCalendar en iOS 5.0 o 5.1. He decidido probar y escribir el mío. Estoy confundido en cuanto a cómo poblar los datos y cómo mantener las tags para las dates de una manera sana y con memory.

¿Cuántas filas hay en realidad en cada componente? ¿Cuándo se vuelven a cargar las filas con nuevas tags?

Voy a darle una oportunidad, y publicaré como lo descubro, pero por favor publica si sabes algo.

Primero, gracias por archivar el error sobre UIDatePicker y el calendar hebreo. 🙂


EDITAR Ahora que se ha lanzado iOS 6, encontrará que UIDatePicker ahora funciona correctamente con el calendar hebreo, por lo que el código siguiente es innecesario. Sin embargo, lo dejaré para la posteridad.


Como ha descubierto, crear un selector de date de funcionamiento es un problema difícil, porque hay miles de millones de casos de borde extraños que cubrir. El calendar hebreo es particularmente extraño en este aspecto, teniendo un mes intercalado (Adar I), mientras que la mayoría de la civilización occidental se utiliza para un calendar que solo agrega un día adicional aproximadamente una vez cada 4 años.

Dicho eso, crear un selector de date hebreo mínimo no es demasiado complejo, suponiendo que estás dispuesto a renunciar a algunas de las sutilezas que ofrece UIDatePicker . Así que hagámoslo simple:

 @interface HPDatePicker : UIPickerView @property (nonatomic, retain) NSDate *date; - (void)setDate:(NSDate *)date animated:(BOOL)animated; @end 

Simplemente vamos a subclass UIPickerView y agregamos soporte para una propiedad de date . Vamos a ignorar minimumDate , maximumDate , locale , calendar , timeZone , y todas las demás properties divertidas que proporciona UIDatePicker . Esto hará que nuestro trabajo sea mucho más simple.

La implementación comenzará con una extensión de class:

 @interface HPDatePicker () <UIPickerViewDelegate, UIPickerViewDataSource> @end 

Simplemente para ocultar que HPDatePicker es su propio delegado y HPDatePicker datos.

A continuación definiremos un par de constantes útiles:

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

Puedes ver aquí que vamos a codificar en forma dura el order de las unidades de calendar. En otras palabras, este selector de date siempre mostrará las cosas como Mes-Día-Año, independientemente de cualquier configuration de personalización o configuration regional que el usuario pueda tener. Entonces, si está usando esto en una configuration regional donde el formatting pnetworkingeterminado querría "Día-Mes-Año", entonces muy mal. Para este sencillo ejemplo, esto será suficiente.

Ahora comenzamos la implementación:

 @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]; } 

Estamos anulando el inicializador designado para hacer alguna configuration para nosotros. Establecimos el delegado y la fuente de datos para ser nosotros mismos, mostramos el indicador de selección y creamos un object de calendar hebreo. También creamos un NSDateFormatter y le decimos que debería formatear NSDates acuerdo con el calendar hebreo. También NSRange un par de objects NSRange y los guardamos como ivars para que no tengamos que search constantemente. Finalmente, lo inicializamos con la date actual.

Aquí están las implementaciones de los methods expuestos:

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

-setDate: solo avanza al otro método

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

Recupere un NSDateComponents represente lo que se select en este momento, NSDate en un NSDate y devuélvalo.

 - (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]; } } 

Y aquí es donde empiezan a pasar cosas divertidas.

Primero, tomaremos la date en que nos dieron y pediremos al calendar hebreo que lo descomponga en un object de componentes de date. Si le doy un NSDate que corresponda a la date gregoriana del 4 de abril de 2012, entonces el calendar hebreo me dará un object NSDateComponents que corresponde a 12 Nisan 5772, que es el mismo día que el 4 de abril de 2012.

Basado en esta información, descubro qué fila seleccionar en cada unidad y la seleccioné. El caso del año es simple. Simplemente rest una (las filas tienen base cero, pero los años están basadas en 1).

Durante meses, escojo la columna del medio de las filas, descubro dónde comienza esa secuencia y agrego el número de mes. Lo mismo con los días.

Las implementaciones de los methods base <UIPickerViewDataSource> son bastante triviales. Estamos mostrando 3 componentes, y cada uno tiene 10,000 filas.

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

Obtener lo que se selecciona en el momento actual es bastante simple. Obtengo la fila seleccionada en cada componente y agrego 1 (en el caso de NSYearCalendarUnit ), o hago una pequeña operación de mod para tener en count la naturaleza repetitiva de las otras unidades de calendar.

 - (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]; } 

Finalmente, necesito algunas cadenas para mostrar en la interfaz de usuario:

 - (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 

Aquí es donde las cosas son un poco más complicadas. Desafortunadamente para nosotros, NSDateFormatter solo puede formatear las cosas cuando se les da un NSDate real. No puedo decir "aquí hay un 6" y espero volver "Adar I". Por lo tanto, tengo que build una date artificial que tenga el valor que quiero en la unidad que me importa.

En el caso de los años, eso es bastante simple. Solo crea un componente de date para ese año en Tishri 1 y estoy bien.

Durante meses, tengo que asegurarme de que el año sea un año bisiesto. Al hacerlo, puedo garantizar que los nombres de los meses siempre serán "Adar I" y "Adar II", independientemente de si el año actual pasa a ser un año bisiesto o no.

Durante días, elegí un año arbitrario, porque cada Tishri tiene 30 días (y ningún mes en el calendar hebreo tiene más de 30 días).

Una vez que hemos creado el object de componentes de date apropiado, podemos convertirlo rápidamente en un NSDate con nuestro hebrewCalendar ivar, establecer la cadena de formatting en el formateador de date para producir cadenas solamente para la unidad que nos interesa y generar una cadena.

Suponiendo que hayas hecho todo esto correctamente, terminarás con esto:

selector de fecha hebreo personalizado


Algunas notas:

  • He -pickerView:didSelectRow:inComponent: la implementación de -pickerView:didSelectRow:inComponent: Depende de usted averiguar cómo desea notificar que la date seleccionada cambió.

  • Esto no controla las dates no válidas. Por ejemplo, es posible que desee considerar la posibilidad de "Adar I" si el año seleccionado actualmente no es un año bisiesto. Esto requeriría usar -pickerView:viewForRow:inComponent:reusingView: lugar del método -pickerView:viewForRow:inComponent:reusingView:

  • UIDatePicker resaltará la date actual en azul. Una vez más, tendría que devolver una vista personalizada en lugar de una cadena para hacerlo.

  • Su selector de date tendrá un bisel negruzco, porque es un UIPickerView . Solo UIDatePickers obtiene el azulado.

  • Los componentes de la vista del selector abarcarán todo su ancho. Si desea que las cosas encajen más naturalmente, tendrá que anular -pickerView:widthForComponent: para devolver un valor sensato para el componente apropiado. Esto podría implicar codificar un valor o generar todas las cadenas, dimensionarlas cada una y elegir la más grande (más un poco de relleno).

  • Como se señaló anteriormente, esto siempre muestra las cosas en el order Month-Day-Year. Hacer esta dinámica a la configuration actual sería un poco más complicada. Tendrías que get una cadena @"d MMMM y" localizada en la configuration regional actual (sugerencia: mira los methods de class en NSDateFormatter para eso), y luego NSDateFormatter para averiguar cuál es el order.

¿Supongo que estás usando una UIPickerView?

UIPickerView funciona como un UITableView en el sentido de que especifica otra class para ser dataSource / delegate, y obtiene sus datos llamando a methods en esa class (que deben cumplir con el protocolo UIPickerViewDelegate / DataSource).

Entonces, lo que debe hacer es crear una nueva class que sea una subclass de UIPickerView, luego en el método initWithFrame, establecerlo como su propio origen de datos / delegado y luego implementar los methods.

Si no ha utilizado un UITableView anteriormente, debe familiarizarse con el patrón de fuente de datos / delegado porque es un poco complicado dar la vuelta, pero una vez que lo comprende es muy fácil de usar.

Si ayuda, he escrito un UIPicker personalizado para seleccionar países. Esto funciona casi de la misma manera en que querrás hacer tu class, excepto que estoy usando una variedad de nombres de países en lugar de dates:

https://github.com/nicklockwood/CountryPicker

Con respecto a la cuestión de la memory, UIPickerView solo carga las tags que están visibles en la pantalla y las recicla a medida que se desplazan por la parte inferior y vuelven a la parte superior, llamando a su delegado nuevamente cada vez que necesita mostrar una nueva fila. Esto es muy eficiente de memory ya que solo habrá unas pocas tags en la pantalla a la vez.