segunda-feira, 28 de maio de 2012

iOS: UIActionSheet + UIDatePicker

Olá povo,

Hoje pela manhã estava fazendo uma telinha no iOS que tinha 2 campos de data. E como o iOS tem um componente padrão para datas, o UIDatePicker, resolvi usá-lo. Entretanto ele ocupa muito espaço e fica inviável colocar dois na tela. Foi aí que me surgiu a ideia de utilizar uma UIActionSheet personalizada. Onde eu poderia selecionar a data, e em seguida a ActionSheet desapareceria da tela.

Para exemplificar isso, crie um novo projeto no Xcode, se tiver dúvidas, dê uma olhada aqui. Na tela da sua aplicação, arraste dois Labels e dois Buttons e deixe-os conforme abaixo. Nos botões, mudei a propriedade Type para Detail Disclosure e a propriedade Tag para 1 e 2 respectivamente.

Crie os Outlets para os dois Labels e utilize o método changeDateClick no Touch Up Inside dos dois botões. Nós saberemos qual botão foi clicado através da propriedade Tag.
#import <UIKit/UIKit.h>

@interface NGDetailViewController : UIViewController 
  <UIActionSheetDelegate>

- (IBAction)changeDateClick:(id)sender;

@property (weak, nonatomic) 
  IBOutlet UILabel *txtData1;
@property (weak, nonatomic) 
  IBOutlet UILabel *txtData2;

@end
Note que nossa classe implementa o protocolo UIActionSheetDelegate, necessário para sabermos qual botão o usuário clicou na ActionSheet. Deixe a implementação da classe conforme abaixo:
#import "NGDetailViewController.h"

@implementation NGDetailViewController

@synthesize txtData1;
@synthesize txtData2;

- (id)initWithNibName:(NSString *)nibNameOrNil 
  bundle:(NSBundle *)nibBundleOrNil {

  self = [super initWithNibName:
    nibNameOrNil bundle:nibBundleOrNil];
  if (self) {
      // Custom initialization
  }
  return self;
}

- (void)viewDidLoad {
  [super viewDidLoad];
}

- (void)viewDidUnload {
    [self setTxtData1:nil];
    [self setTxtData2:nil];
    [super viewDidUnload];
}

- (BOOL)shouldAutorotateToInterfaceOrientation:
  (UIInterfaceOrientation)interfaceOrientation {
  return (interfaceOrientation != 
    UIInterfaceOrientationPortraitUpsideDown);
}

- (IBAction)changeDateClick:(id)sender {
  UIButton *button = sender;
    
  UIDeviceOrientation orientation = 
    [[UIDevice currentDevice] orientation];

  int topMargin = 
    (orientation == UIDeviceOrientationPortrait) ? 
      40 : 60;
    
  UIActionSheet *dateActionSheet = 
    [[UIActionSheet alloc] initWithTitle:
      @"Selecione a data" 
      delegate:self 
      cancelButtonTitle:@"Cancelar" 
      destructiveButtonTitle:nil 
      otherButtonTitles:@"OK", nil];
  [dateActionSheet setTag:button.tag];
  [dateActionSheet setActionSheetStyle:
    UIActionSheetStyleBlackTranslucent];
  [dateActionSheet showInView:self.view];    
  [dateActionSheet setFrame:CGRectMake(
    0, topMargin, self.view.frame.size.width, 400)];
}

- (void)willPresentActionSheet:
  (UIActionSheet *)actionSheet {

  // Inicializa o picker e adiciona ao actionSheet
  UIDatePicker *pickerView = 
    [[UIDatePicker alloc] init];
    
  [pickerView setDatePickerMode:UIDatePickerModeDate];
  [actionSheet insertSubview:pickerView atIndex:1];
  // A actionSheet tem 3 subviews agora,
  // o picker e os dois botões  
  UIView *titleView   = 
    [actionSheet.subviews objectAtIndex:0];
  UIButton *btnOk     =  
    [actionSheet.subviews objectAtIndex:2];
  UIButton *btnCancel = 
    [actionSheet.subviews objectAtIndex:3];
    
  UIDeviceOrientation orientation = 
    [[UIDevice currentDevice] orientation];
    
  if (orientation == UIDeviceOrientationPortrait) {
    // Mudou o Y do picker para ficar abaixo do título
    pickerView.frame  = CGRectMake(
      pickerView.frame.origin.x, 
      titleView.frame.size.height + 20, 
      pickerView.frame.size.width, 
      pickerView.frame.size.height);

    // Aqui também... só o Y, ficando abaixo do picker
    btnOk.frame = CGRectMake(
      btnOk.frame.origin.x, 
      pickerView.frame.origin.y + 
        pickerView.frame.size.height + 10, 
      btnOk.frame.size.width, 
      btnOk.frame.size.height);
    
    // De novo só o Y, ficando abaixo do btnOk
    btnCancel.frame = CGRectMake(
      btnCancel.frame.origin.x, 
      btnOk.frame.origin.y + 
        btnOk.frame.size.height + 10, 
      btnCancel.frame.size.width, 
      btnCancel.frame.size.height);

  } else {
    // Aqui dei um espaço de 10 em X,
    // em Y ficou abaixo do título 20px,
    // na largura, o picker ocupa 2/3 da tela,
    // e altura não mudou
    pickerView.frame  = CGRectMake(
      10, 
      titleView.frame.size.height + 20, 
      ((pickerView.frame.size.width) / 3 ) * 2, 
      pickerView.frame.size.height);

   // No eixo X, o botão fica a direita do picker,
   // em Y, fica alinhado com o topo do picker,
   // a largura do botão é área entre o fim do 
   // picker e a margem direita da tela,
   // a altura não mudou
   btnOk.frame = CGRectMake(
     pickerView.frame.origin.x + 
       pickerView.frame.size.width + 10, 
     pickerView.frame.origin.y, 
     (actionSheet.frame.size.width - 
       pickerView.frame.size.width -  
       pickerView.frame.origin.x - 20), 
     btnOk.frame.size.height);
        
   // Aqui é similar ao anterior, a diferença é 
   // que em Y, o botão está abaixo do btnOk.
   btnCancel.frame = CGRectMake(
     pickerView.frame.origin.x + 
       pickerView.frame.size.width + 10, 
     btnOk.frame.origin.y + 
       btnOk.frame.size.height + 10,
     (actionSheet.frame.size.width - 
       pickerView.frame.size.width - 
       pickerView.frame.origin.x - 20), 
     btnCancel.frame.size.height);
  }
}

- (void)actionSheet:(UIActionSheet *)actionSheet 
  clickedButtonAtIndex:(NSInteger)buttonIndex {
  if (buttonIndex == 0){
    UIDatePicker *picker = 
      [actionSheet.subviews objectAtIndex:1];
        
    NSString *dataStr = [NSDateFormatter 
      localizedStringFromDate:picker.date 
      dateStyle:NSDateFormatterShortStyle 
      timeStyle:NSDateFormatterNoStyle];
        
    if (actionSheet.tag == 1){
      txtData1.text = [NSString 
        stringWithFormat:@"Data: %@", dataStr];
    } else {
      txtData2.text = [NSString 
        stringWithFormat:@"Data: %@", dataStr];
    }
  }
}

@end

O método shouldAutorotateToInterfaceOrientation define que nossa aplicação apenas não suporta a orientação de Portrait invertido (ou seja, o aparelho de cabeça pra baixo).
A implementação do método changeDateClick inicia pegando a referência do botão que foi clicado. Em seguida pegamos a orientação do aparelho para definir a distância da ActionSheet para margem superior do aparelho. Depois criamos a ActionSheet passando as informações necessárias: título, botões, e o mais importante, o delegate (ou listener) que será chamado quando clicar em algum dos botões. Como nossa classe está implementando esse protocolo, passamos a instância da prórpria classe (self).
Na linha seguinte, setamos a tag da actionSheet para podermos identificar qual dos botões foi clicado para alterar a caixa de texto correspondente. Por fim, alteramos a área (frame) da actionSheet.

O método willPresentActionSheet irá personalizar a actionSheet, adicionando o UIDateTimePicker e reposicionando os botões de acordo com a orientação do aparelho. O código deste método está comentado. Então não vou entrar em detalhes.

O último método, assim como o anterior, é definido no protocolo UIActionSheetDelegate, ele vai ver se clicamos no primeiro botão (OK) e em caso positivo, pega a referência do UIDatePicker e obtém a data selecionada no componente. Em seguida, converte a data em uma NSString usando a classe NSDateFormatter. Feito isso, verifica a tag da actionSheet e altera o Label correspondente.

Se você executar a aplicação e clicar em um dos botões, a actionSheet é exibida abaixo.
Aproveite e teste a aplicação também em landscape.
É isso pessoal. Qualquer dúvida, deixem seus comentários.

4br4ç05,
nglauber

Um comentário:

Unknown disse...

Show de bola, no maior estilo MacGyver! :D