Cocoa Touch è un framework completo è funzionale ma per quanto riguarda la User Interface, e quindi UIKit, sembra non sia stata posta grande attenzione ad un aspetto: la creazione e gestione di form per il data entry.
Creare form per il data entry è un’esigenza comune a molte applicazioni e quando si affronta il problema ci si rende conto che non è un’operazione banale come idealmente ci si aspetterebbe. In questo articolo analizzeremo un approccio progettuale che ci permetterà di utilizzare una UITableView in modalità “Grouped” per questo scopo. Non è ovviamente l’unico modo ma lo ritengo funzionale per varie ragioni: è semplice per l’utente, è un modello già usato in varie parti dell’iOS ed è pratico per lo sviluppatore.
Quello che si vuole ottenere è qualcosa di simile alla figura qui sopra, ovvero l’utilizzo della UITableView sulla falsa riga di come viene usata nei Settings di sistema.
Prerequisiti
Per non fare un articolo chilometrico do per scontato che il lettore abbia già una certa dimestichezza con lo sviluppo per iPhone, conosca il sistema MVC e il funzionamento delle UITableView (e relativi delegate e data source).
Iniziamo
Alla base di tutto c’è la creazione di alcune UITableViewCell custom da inserire nella tabella, avremo vari tipi di cella in funzione del tipo di input che vogliamo acquisire (testo semplice, data, opzioni mutuamente esclusive…). Ma la sola creazione e aggiunta “by code” delle celle non sopperirebbe al requisito di rapidità che ci siamo posti, per questo introdurremo un meccanismo che utilizza i file plist come struttura della form.
Procediamo per gradi: creiamo innanzitutto una semplice app composta da una UITableView e relativo controller.
Il layout del nostro progetto è in questo momento molto semplice come riportato nella figura seguente.
Dobbiamo configurare la UITableView in modalità “grouped” da Interface Builder, di default è in modalità “plain” che non è utile al nostro fine.
Possiamo ora iniziare a creare una semplice cella custom da utilizzare per richiedere in input del testo, l’idea di base è realizzare il classico layout con un etichetta a sinistra ed il TextFiled a destra. Creeremo prima una base class dalla quale far derivare tutti i tipi di cella che vogliamo gestire, capiremo man mano il motivo di questa scelta.
@interface BaseDataEntryCell : UITableViewCell {
}
@property (nonatomic, retain) NSString *dataKey;
-(void) setControlValue:(id)value;
-(id) getControlValue;
-(void)postEndEditingNotification;
@end
@implementation BaseDataEntryCell
@synthesize dataKey;
- (id)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier {
if ((self = [super initWithStyle:style reuseIdentifier:reuseIdentifier])) {
self.selectionStyle = UITableViewCellSelectionStyleNone;
self.textLabel.font = [UIFont boldSystemFontOfSize:14] ;
}
return self;
}
-(void) setControlValue:(id)value
{
}
-(id) getControlValue
{
return nil;
}
-(void)postEndEditingNotification
{
[[NSNotificationCenter defaultCenter]
postNotificationName:CELL_ENDEDIT_NOTIFICATION_NAME
object:[(UITableView *)self.superview indexPathForCell: self]];
}
- (void)dealloc {
[super dealloc];
}
@end
Creata la base class iniziamo ad implementare il primo tipo di cella per il dataentry.
@interface TextDataEntryCell : BaseDataEntryCell {
}
@property (nonatomic, retain) UITextField *textField;
@end
@implementation TextDataEntryCell
@synthesize textField;
- (id)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier {
if (self = [super initWithStyle:style reuseIdentifier:reuseIdentifier]) {
// Configuro il textfield secondo la necessità
self.textField = [[UITextField alloc] initWithFrame:CGRectZero];
self.textField.clearsOnBeginEditing = NO;
self.textField.textAlignment = UITextAlignmentLeft;
self.textField.returnKeyType = UIReturnKeyDone;
self.textField.font = [UIFont systemFontOfSize:14];
self.textField.autocorrectionType = UITextAutocorrectionTypeNo;
self.textField.autocapitalizationType = UITextAutocapitalizationTypeNone;
[self.contentView addSubview:self.textField];
}
return self;
}
- (void)layoutSubviews {
[super layoutSubviews];
CGRect labelRect = CGRectMake(self.textLabel.frame.origin.x,
self.textLabel.frame.origin.y,
self.contentView.frame.size.width * .35,
self.textLabel.frame.size.height);
[self.textLabel setFrame:labelRect];
// Rect area del textbox
CGRect rect = CGRectMake(self.textLabel.frame.origin.x + self.textLabel.frame.size.width + LABEL_CONTROL_PADDING,
12.0,
self.contentView.frame.size.width-(self.textLabel.frame.size.width + LABEL_CONTROL_PADDING + self.textLabel.frame.origin.x)-RIGHT_PADDING,
25.0);
[textField setFrame:rect];
}
@end
Quel che facciamo è semplicemente creare un UITextField e posizionarlo opportunamente, per l’etichetta “ricicliamo” la textLabel preesistente nella UITableViewCell.
In questa prima versione la TextDataEntryCell è ancora molto semplice e non soddisfa ancora tutte le necessità, ma procediamo per gradi e vediamo come usarla nella nostra UITableView.
Come noto le celle vengono configurate nel metodo cellForRowAtIndexPath appartenere alla UITableViewDataSource, se volessimo utilizzare la TextDataEntryCell appena creata avremmo un’implementazione tipo quella riportata di seguito.
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
static NSString *CellIdentifier = @"Cell";
BaseDataEntryCell *cell = (BaseDataEntryCell *)[tableView dequeueReusableCellWithIdentifier:CellIdentifier];
if (cell == nil) {
cell = [[[TextDataEntryCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier] autorelease];
}
cell.textLabel.text = @"Nome";
return cell;
}
Vogliamo però rendere dinamico il tipo di cella che andiamo a creare in funzione del tipo dati da acquisire, al momento abbiamo solo una tipologia di cella per accogliere dati testuali ma nel caso reale avremo celle per le date, e-mail, numero di telefono e così via.
Un aspetto cruciale del ragionamento che stiamo facendo è che l’allocazione della cella nel codice precedente potrei anche scriverla così:
[[[NSClassFromString(@"TextDataEntryCell") alloc] initWithStyle:....
Questo meccanismo è molto importante perché ci permette di creare una cella in funzione di una stringa che ne contiene il tipo, questo principio ci permetterà di avvantaggiarci dei file plist per la struttura.
Presupponendo di avere già creato una serie di tipi di celle per testo libero, data ed e-mail il sistema che dovrei usare per strutturare la mia form sarebbe totalmente delegato al codice. Mi troverei a dover scrivere una serie di strutture condizionali (tipo if o switch ) per discriminare riga per riga quale tipo di cella istanziare. Se fosse possibile assolvere allo stesso compito creando un semplice file descrittore avrei un notevole vantaggio sia in termini temporali sia per la manutenibilità del codice prodotto.
I plist (abbreviazione di Property List) sono file che contengono lo stato di oggetti serializzati in XML, hanno il grande vantaggio di essere facilmente gestibili sia da XCode che da tool presenti in Mac OS X. Questo tipo di file è molto usato in tutti i sistemi operativi ad oggi prodotti da Apple.
L’idea è usare un file plist per creare un array contenente le informazioni necessarie per creare ogni riga, partiamo da un esempio.
Label
Nome
CellType
TextDataEntryCell
Label
Cognome
CellType
TextDataEntryCell
Label
Email
CellType
TextDataEntryCell
Label
Sesso
CellType
TextDataEntryCell
Label
Data di nascita
CellType
TextDataEntryCell
Con l’XML sopra sto strutturando una UITableView di 5 righe, per ogni riga uso un Dictionary (
Creato il file bisogna deserializzarlo in un NSArray, faremo questo nel classico metodo viewDidLoad come segure:
- (void)viewDidLoad {
[super viewDidLoad];
NSString *plisStructure = [[NSBundle mainBundle] pathForResource:@"table-structure.plist" ofType:nil];
tableStructure = [NSArray arrayWithContentsOfFile:plisStructure];
}
Letto il file andiamo a vedere come modificare cellForRowAtIndexPath in modo da usare l’array tableStructure.
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
static NSString *CellIdentifier = @"Cell";
NSDictionary *cellData = [tableStructure objectAtIndex:indexPath.row];
NSString *cellType = [cellData objectForKey:@"CellType"];
BaseDataEntryCell *cell = (BaseDataEntryCell *)[tableView dequeueReusableCellWithIdentifier:CellIdentifier];
if (cell == nil) {
cell = [[[NSClassFromString(cellType) alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier] autorelease];
}
cell.textLabel.text = [cellData objectForKey:@"Label"];
return cell;
}
Come visibile dal codice l’idea è molto semplice: estraggo dall’array il Dictionary con le varie proprietà e le utilizzo per costruire la cella. La proprietà principale è CellType che contiene il nome della classe da istanziare ma abbiamo anche Label che definisce l’etichetta per la cella. Il primo passo è fatto: siamo in grado di costruire una UITableView semplicemente strutturando un file plist e creando le varie celle in modo dichiarativo.
Arrivati a questo punto siamo in grado di creare la user interface che desideriamo (ammesso ovviamente di costruirsi tutte le tipologie di celle del caso) ma non abbiamo visto come leggere i dati inseriti dall’utente.
Anche in questo caso si possono adottare vari sistemi: uno potrebbe essere quello di definire un delegate conforme ad un nostro protocollo in modo che la cella invochi un metodo quando l’editing è completo. Ho preferito adottare una Notification allo stesso scopo perché la sua natura disaccoppiata permette un maggiore dinamismo (anche se pone la necessità di documentazione per l’utilizzatore).
Tornando al sorgente della base class delle celle notiamo un metodo che fin ora abbiamo tralasciato:
-(void)postEndEditingNotification
{
[[NSNotificationCenter defaultCenter]
postNotificationName:CELL_ENDEDIT_NOTIFICATION_NAME
object:[(UITableView *)self.superview indexPathForCell: self]]; }
Il metodo invia semplicemente una notifica, l’idea è far si che quando l’editing di una cella è completo questa invii una notifica custom in modo tale che gli observer possano prendere delle azioni.
L’invio della notifica è ovviamente diverso a seconda del tipo di cella che stiamo implementando, la TextDataEntryCell dovrà inviarla nel momento in cui l’editing del TextField è finito. Se avessimo una cella che gestisce il classico check a due stati (selezionato/non selezionato) l’invio andrebbe fatto al cambio di stato e così via per ogni tipologia.
Concentrandoci sulla nostra TextDataEntryCell sfrutteremo la possibilità dell’UITextField di comunicare l’evento di fine editing mediante il classico pattern del delegate.
Per prima cosa conformiamo la classe al protocollo UITextFieldDelegate modificando la definizione dell’interfaccia in questo modo
@interface TextDataEntryCell : BaseDataEntryCell
A livello di implementazione dovremo impostare il delegate del UITextField (self.textField.delegate = self;) ed “intercettare” l’evento di fine editing per postare la nostra notifica.
#pragma mark UITextFieldDelegate
- (void)textFieldDidEndEditing:(UITextField *)txtField
{
[self postEndEditingNotification];
}
Resta un ultimo sforzo per arrivare ad una prima implementazione funzionante: intercettare la notifica nel controller principale e gestire il dato. Per intercettare una notifica dobbiamo registrarci come observer, aggiungeremo il pezzo di codice seguente nel metodo viewDidLoad del controller della UITableView.
[[NSNotificationCenter defaultCenter]
addObserver:self
selector:@selector(cellControlDidEndEditing:)
name:CELL_ENDEDIT_NOTIFICATION_NAME
object:nil];
Senza entrare nel dettaglio del funzionamento delle notifiche, per il quale esistono ottimi articoli, diciamo solamente che “agganciamo” un metodo chiamato cellControlDieEndEditing alla ricezione della notifica.
In cellControlDidEndEditing è dove andremo a gestire la ricezione del dato inserito, quel che viene fatto in questo metodo dipende ovviamente dal contesto dell’applicazione potremmo dover scrivere il dato in un file, inviarlo ad un servizio web o qualsiasi altra attività. Ai fini dell’esempio lo logghiamo semplicemente nella console di debug.
-(void)cellControlDidEndEditing:(NSNotification *)notification
{
NSIndexPath *cellIndex = (NSIndexPath *)[notification object];
BaseDataEntryCell *cell = (BaseDataEntryCell *)[self.tableView cellForRowAtIndexPath:cellIndex];
if(cell != nil)
{
NSLog(@"L'utente ha digitato %@", [cell getControlValue]);
}
}
Ai più attenti sarà saltato all’occhio un problema: come faccio a discriminare quale cella mi sta notificando la fine dell’editing? Supponiamo di avere una semplice form composta da 4 celle : nome, cognome, indirizzo e città. Ogni cella scatenerà la notifica di “end editing” ma in questo momento non ho modo di capire quale cella l’ha fatto. Se torniamo ad analizzare la classe base delle celle noteremo una property che fin ora abbiamo trascurato : dataKey. Questa property esiste proprio per gestire il nostro problema, per utilizzarla dobbiamo innanzitutto riprendere il nostro file plist e aggiungere ad ogni riga un property DataKey valorizzata con il discriminante che vogliamo usare.
DataKey
firstName
Label
Nome
CellType
TextDataEntryCell
Il metodo cellForRowAtIndex dovrà a questo punto leggere dal plist anche la DataKey ed impostarla nell’omonima property della cella, nel seguente modo:
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
// Nota: non va usato un discriminante unico perché abbiamo (potenzialmente) più tipi di cella
static NSString *CellIdentifier = @"Cell";
NSDictionary *cellData = [tableStructure objectAtIndex:indexPath.row];
NSString *dataKey = [cellData objectForKey:@"DataKey"];
NSString *cellType = [cellData objectForKey:@"CellType"];
BaseDataEntryCell *cell = (BaseDataEntryCell *)[tableView dequeueReusableCellWithIdentifier:cellType];
if (cell == nil) {
cell = [[[NSClassFromString(cellType) alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:cellType] autorelease];
}
// Impostiamo la datakey della cella
cell.dataKey = dataKey;
cell.textLabel.text = [cellData objectForKey:@"Label"];
return cell;
}
A questo punto il giro è completo, tornado al nostro cellControlDidEndEditing siamo in grado di dedurre il contesto dell’editing ricevuto dalla dataKey della cella.
-(void)cellControlDidEndEditing:(NSNotification *)notification
{
NSIndexPath *cellIndex = (NSIndexPath *)[notification object];
BaseDataEntryCell *cell = (BaseDataEntryCell *)[self.tableView cellForRowAtIndexPath:cellIndex];
if(cell != nil)
{
NSLog(@"L'utente ha digitato %@ per la datakey %@", [cell getControlValue], cell.);
}
}
Abbiamo realizzato un sistema per creare form in modo quasi visuale, il sistema ha ampi spazi di miglioramento ma già in questa versione permette di gestire le casistiche tipiche di data entry.
Lascio al lettore l’onere (e l’onore) di costruire tutti i tipi di cella necessari in un’applicazione reale.
In figura uno screenshot del lavoro ultimato:

Se avete problemi con il tutorial, questo è il nostro file di progetto.
14 Responses to “T#075 – Creiamo form per gestire DataEntry con UITableView e plist”
6 Ottobre 2010
ErrivaraRaga, come si fa a fare una vibrazione virtuale del iPhomne simulator… O almeno metterlo in lanscape?
Grazie mille:-)
6 Ottobre 2010
ErrivaraRaga, come si fa a fare una vibrazione virtuale del iPhomne simulator… O almeno metterlo in lanscape?
Grazie mille:-) 😉
6 Ottobre 2010
NicolaPer il landscape dovrebbe essere [ + freccia sinistra]
8 Ottobre 2010
stefanoCome faccio ad inviare i dati ad esempio ad una pagina che me li prende in $_POST e poi elaborarli da questa?
8 Ottobre 2010
Fabrizio Guglielmino@stefano: la parte di invio dei dati non è trattata dall’articolo che si focalizza più che altro sulla creazione della struttura per la form.
In ogni caso potresti creare un NSDictionary nel quale inserire i dati man mano che vengono inseriti (usando la datakey come chiave) e poi crearti lo strato che comunica con il web server inviando il dictionary in post.
8 Ottobre 2010
SamIo e un mio amico stiamo realizzando un’applicazione, ma stiamo utilizzando ancora l’sdk 3.x ci chiedevamo se per importarla poi nell’sdk 4.x basta copiare i files nel nuovo ambiente di lavoro? Scusate la domanda un po’ banale ma siamo ancora alle prime armi 🙂
17 Ottobre 2010
XfightUhmmm
tableStructure = [NSArray arrayWithContentsOfFile:plisStructure];
Una cosa del genere non ha un gran senso se si vuole riusarla… da vari manuali è riportato che se un oggetto è istanziato da metodi diversi da [[alloc] init] o new è settato come autorelease e prima o poi verrà rilasciato senza preavviso.
Ed in effetti a me il codice crasha dando un EXC_BAD_ACCESS appena tento di scrollare su e giù la tabella perché implica un refresh (parziale) della tabella.
Si fixa facilmente con
tableStructure = [[NSArray arrayWithContentsOfFile:plisStructure] retain];
ed una aggiunta in dealloc per rilasciarlo
– (void)dealloc {
[tableStructure release];
[super dealloc];
}
Spero che questa correzione sia giusta ^^.
Ciao !
17 Ottobre 2010
Fabrizio GuglielminoQuel che scrivi è corretto, in realtà l’errore è che ho omesso la gestione della tableStructure con una property (che è come normalmente uso questo codice).
Di fatto avresti:
@property (nonatomic, retain) NSArray *tableStructure;
dichiarato nell’interfaccia che essendo di tipo retain fa quello che tu hai fatto a mano con [[NSArray arrayWithContentsOfFile:plisStructure] retain]. Ovviamente vale il discorso di settare a nil la property su unload per ottenerne il release.
ciao
19 Maggio 2011
CIRda una tableview mi apro una view con un form simile…come faccio salvare nella table view il nome salvato nel form?
19 Maggio 2011
Fabrizio GuglielminoDipende da cosa intendi con “salvare nella tableview”.
La tableview, come saprai, usa il pattern DataSource per valorizzare il contenuto delle row. Questo implica che lo storage dei dati possa essere gestito in vari modi (CoreData, un NSArray, altro…), a seconda di come gestisci lo storage il meccanismi per riportare il nome letto dalla form di data entry possono essere diversi.
25 Settembre 2011
lucaho i seguenti errori:
‘CELL_ENDEDIT_NOTIFICATION_NAME’ undeclared (first use in this function)
‘LABEL_CONTROL_PADDING’ undeclared (first use in this function)
‘RIGHT_PADDING’ undeclared (first use in this function)
‘CELL_ENDEDIT_NOTIFICATION_NAME’ undeclared (first use in this function)
sapete dirmi qualcosa?
29 Novembre 2011
luigiciao a tutti… ho seguito il tutorial e a questo ho aggiunto un bottone che invia i dati presenti nelle celle ad un web server..
il problema è che se io inserisco i valori nelle varie celle, una volta che schiaccio il bottone , l’ultima cella che è stata modificata è come se non contenesse niente..
è come se non arrivasse al metodo : cellControlDidEndEditing la notifica dell’avvenuta modifica del’ultima cella modificata..
AVETE SUGGERIMENTI?????
AIUTOOOOOOOOOOOOOOOOOOOOOOO
14 Ottobre 2013
FrancoMolto bello questo tutorial, mi da errore con ios7
Nella notification non trova l’indice della cella nella riga
…
object:[(UITableView *)self.superview indexPathForCell: self]];
…
Potete aiutarmi ?
5 Agosto 2014
MicheleL’errore che segnala Franco riguarda il postEndEditingNotification: che dovrebbe essere modificato come segue:
NSDictionary *userInfo = [[NSDictionary alloc] initWithObjectsAndKeys:[(UITableView *)self.superview.superview indexPathForCell:self], @”indexPath”, nil];
[[NSNotificationCenter defaultCenter] postNotificationName:@”CELL_END_EDIT” object:nil userInfo:userInfo];
ovvero con 2 superview consecutivi, poiché in iOS7 la gerarchia delle viste della tableView è differente.
Inoltre ho preferito mettere il valore dell’indexPath nello userInfo della notifica.
Infine faccio notare che si può evitare di utilizzare la property dataKey, in quanto con l’indexPath possiamo risalire alla label della tableViewCell.