2012-04-04 27 views
15

Me di cuenta de que the UIDatePicker doesn't work with NSHebrewCalendar en iOS 5.0 o 5.1. Decidí intentar y escribir el mío. Estoy confundido sobre cómo llenar los datos y cómo mantener las etiquetas para las fechas de una manera sana y eficiente de la memoria.¿Cómo reescribo el componente UIDatePicker?

¿Cuántas filas hay en realidad en cada componente? ¿Cuándo se "recargan" las filas con nuevas etiquetas?

voy a dar a este un tiro, y voy a publicar como me entero, pero por favor, puesto si sabe algo.

Respuesta

35

En primer lugar, gracias por enviar el fallo sobre UIDatePicker y el calendario hebreo. :)


EDITAR Ahora que iOS 6 ha sido puesto en libertad, usted encontrará que UIDatePicker ahora funciona correctamente con el calendario hebreo, por lo que el siguiente código innecesario. Sin embargo, lo dejaré para la posteridad.


Como usted ha descubierto, creando un selector de fecha de funcionamiento es un problema difícil, porque hay bajillions de los casos límite para cubrir extraños. El calendario hebreo es particularmente raro en este sentido, teniendo una intercalary month (Adar I), mientras que la mayor parte de la civilización occidental se utiliza para un calendario que sólo se agrega un día adicional alrededor de una vez cada 4 años.

Dicho esto, crear un selector de fecha hebreo mínimo no es demasiado complejo, suponiendo que esté dispuesto a renunciar a algunas de las sutilezas que ofrece UIDatePicker. Así que vamos a hacer que sea sencillo:

@interface HPDatePicker : UIPickerView 

@property (nonatomic, retain) NSDate *date; 

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

@end 

Estamos simplemente yendo a la subclase UIPickerView y agregar soporte para una propiedad date.Vamos a ignorar minimumDate, maximumDate, locale, calendar, timeZone, y todas las otras propiedades divertidas que ofrece UIDatePicker. Esto hará que nuestro trabajo sea mucho más simple.

La aplicación va a comenzar con una extensión de clase:

@interface HPDatePicker() <UIPickerViewDelegate, UIPickerViewDataSource> 

@end 

simplemente para ocultar que el HPDatePicker es su propia fuente de datos y delegado.

A continuación vamos a definir un par de constantes prácticos:

#define LARGE_NUMBER_OF_ROWS 10000 

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

Puede ver aquí que vamos a codificar el orden de las unidades de calendario. En otras palabras, este selector de fechas siempre mostrará las cosas como Mes-Día-Año, independientemente de las configuraciones personalizadas o locales que el usuario pueda tener. Por lo tanto, si está utilizando esto en una configuración regional donde el formato predeterminado sería "Día-Mes-Año", sería una pena. Para este simple ejemplo, esto será suficiente.

Ahora empezamos 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 reemplazando el inicializador designado para hacer un poco de configuración para nosotros. Configuramos el delegado y el origen de datos para ser nosotros mismos, mostrar el indicador de selección y crear un objeto de calendario hebreo. También creamos un NSDateFormatter y le decimos que debe formatear NSDates de acuerdo con el calendario hebreo. También sacamos un par de objetos NSRange y los almacenamos en la caché como ivars para que no tengamos que buscar constantemente. Finalmente, lo inicializamos con la fecha actual.

Estas son las implementaciones de los métodos expuestos:

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

-setDate: simplemente reenvía a otro método

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

recuperar un NSDateComponents representación de lo que esté seleccionado en ese momento, convertirlo en una NSDate, y devolver eso.

- (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 las cosas divertidas comienzan a suceder.

Primero, tomaremos la fecha que nos dieron y pediremos al calendario hebreo que lo descomponga en un objeto de componentes de fecha. Si le doy una NSDate que corresponde a la fecha gregoriana, de 4 Abr 2012, a continuación, el calendario hebreo me va a dar a un objeto NSDateComponents que corresponde al 12 Nisan 5772, que es el mismo día como el 4 2012 abr

En función de esta información, averiguo qué fila seleccionar en cada unidad y la selecciono. El caso del año es simple. Simplemente resta uno (las filas están basadas en cero, pero los años están basados ​​en 1).

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

Las implementaciones de los métodos 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 este momento 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 modificación para tener en cuenta la naturaleza repetitiva de las otras unidades de calendario.

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

Por último, necesito algunos hilos 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 complicado. Desafortunadamente para nosotros, NSDateFormatter solo puede formatear cosas cuando se le da un NSDate real. No puedo simplemente decir "aquí hay un 6" y espero volver "Adar I". Por lo tanto, tengo que construir una fecha 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 los componentes de la fecha 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 es bisiesto o no.

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

Una vez que hemos construido la fecha apropiada objeto componentes, que puede convertirse rápidamente en un NSDate con nuestro hebrewCalendar Ivar, establecer la cadena de formato en el formateador fecha que sólo se produjeran listas para la unidad que nos importa, y generar una cuerda.

Suponiendo que usted ha hecho todo esto correctamente, usted va a terminar con esto:

custom hebrew date picker


Algunas notas:

  • He dejado de lado la puesta en práctica de -pickerView:didSelectRow:inComponent:. Depende de usted averiguar cómo desea notificar que la fecha seleccionada ha cambiado.

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

  • UIDatePicker resaltará la fecha actual en azul. De nuevo, tendrías que devolver una vista personalizada en lugar de una cadena para hacer eso.

  • Su selector de fecha 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 se ajusten de forma más natural, deberá anular -pickerView:widthForComponent: para devolver un valor razonable para el componente apropiado. Esto podría implicar la codificación difícil de un valor o la generación de todas las cadenas, el tamaño de cada uno, y la elección de la más grande (además de algunos relleno).

  • Como se indicó anteriormente, esto siempre muestra las cosas en orden de Mes-Día-Año. Hacer esto dinámico a la configuración regional actual sería un poco más complicado. Tendría que obtener una cadena @"d MMMM y" localizada en la configuración regional actual (pista: mire los métodos de clase en NSDateFormatter para eso), y luego analizarla para averiguar cuál es el orden.

+10

Esta tiene que ser la mejor respuesta que he visto en desbordamiento de pila. +1 –

+0

Este código se vuelve aún más impresionante con ARC, por cierto. ;-) Jus 'diciendo. – Moshe

+3

@Dave DeLong es mi héroe. –

1

supongo que está utilizando un UIPickerView?

UIPickerView funciona como un UITableView en el que especifica que otra clase sea el origen de datos/delegado, y obtiene sus datos llamando a métodos de esa clase (que deben ajustarse al protocolo UIPickerViewDelegate/DataSource).

Entonces, lo que debe hacer es crear una nueva clase que sea una subclase de UIPickerView, luego en el método initWithFrame, configurarlo como su propio origen de datos/delegado y luego implementar los métodos.

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

Si ayuda, he escrito un UIPicker personalizada para seleccionar los países. Esto funciona en más o menos la misma manera que usted quiere hacer su clase, excepto que estoy usando una serie de nombres de países en lugar de fechas:

https://github.com/nicklockwood/CountryPicker

Con respecto a la cuestión de la memoria, la UIPickerView solo carga las etiquetas que son visibles en la pantalla y las recicla a medida que se desplazan por la parte inferior y vuelven a la parte superior, llamando de nuevo a su delegado cada vez que necesita mostrar una nueva fila. Esto es muy eficiente desde el punto de vista de la memoria, ya que solo habrá algunas etiquetas en pantalla a la vez.

+0

Hay fechas más posibles que posibles países, creo que esa es la verdadera pregunta aquí: ¿cuántas filas incluye en el selector de fechas? – jrturton

+1

Divide su selector en tres columnas, luego ponga días (31 filas), meses (12 filas) y años (un rango adecuado, por lo general 1900 - presente). UIPickerViewDelegate tiene un número de devolución de llamada de Columns que le permite establecer cuántas columnas desea. –

+0

Alternativamente, si prefiere tener una columna, simplemente establezca una propiedad de fecha mínima/máxima en su selector personalizado y luego genere una fila para cada fecha entre min y max. –

Cuestiones relacionadas