Blocchi e celle di visualizzazione tabella su iOS


Una cella di visualizzazione tabella non conosce la vista tabella a cui appartiene e va bene. In effetti, è così che dovrebbe essere. Tuttavia, le persone che sono nuove a questo concetto sono spesso confuse da esso. Ad esempio, se l'utente tocca un pulsante in una cella di visualizzazione tabella, come si ottiene il percorso dell'indice della cella in modo da poter recuperare il modello corrispondente? In questo tutorial, ti mostrerò come non farlo, come è fatto di solito e come farlo con stile ed eleganza.

1. Introduzione

Quando l'utente tocca una cella di visualizzazione tabella, la vista tabella viene richiamata tableView: didSelectRowAtIndexPath: del UITableViewDelegate protocollo sul delegato della vista tabella. Questo metodo accetta due argomenti, la vista tabella e il percorso dell'indice della cella selezionata.

Il problema che affronteremo in questo tutorial, tuttavia, è un po 'più complesso. Supponiamo di avere una vista tabella con celle, con ogni cella contenente un pulsante. Toccando il pulsante, viene attivata un'azione. Nell'azione, dobbiamo recuperare il modello che corrisponde alla posizione della cella nella vista tabella. In altre parole, dobbiamo conoscere il percorso dell'indice della cella. Come si deduce il percorso dell'indice della cella se si ottiene solo un riferimento al pulsante che è stato toccato? Questo è il problema che risolveremo in questo tutorial.

2. Impostazione del progetto

Passaggio 1: Crea progetto

Crea un nuovo progetto in Xcode selezionando il Applicazione vista singola modello dalla lista di Applicazione iOS modelli. Assegna un nome al progetto Blocchi e celle, impostato dispositivi a i phone, e fare clic Il prossimo. Dillo a Xcode dove desideri archiviare il progetto e premi Creare.

Passaggio 2: aggiornamento obiettivo di distribuzione

Apri il Project Navigator a sinistra, seleziona il progetto nel Progetto sezione e impostare il Obiettivo di distribuzione a iOS 6. Lo facciamo per assicurarci di poter eseguire l'applicazione su iOS 6 e iOS 7. La ragione di ciò risulterà chiara più avanti in questo tutorial.

Passaggio 3: Crea UITableViewCell sottoclasse

Selezionare Nuovo> File ... dal File menu e scegliere Classe Objective-C dalla lista di Cocoa Touch modelli. Dai un nome alla classe TPSButtonCell e assicurarsi che erediti da UITableViewCell.

Aprire il file di intestazione della classe e dichiarare due punti vendita, a UILabel istanza chiamata titleLabel e a UIButton istanza chiamata ActionButton.

 #importare  @interface TPSButtonCell: UITableViewCell @property (weak, nonatomic) IBOutlet UILabel * titleLabel; @property (weak, nonatomic) IBOutlet UIButton * actionButton; @fine

Passaggio 4: Aggiorna visualizzazione controller

Apri il file di intestazione del file TPSViewController classe e creare uno sbocco chiamato tableView di tipo UITableView. Il TPSViewController ha anche bisogno di adottare il UITableViewDataSource e UITableViewDelegate protocolli.

 #importare  @interface TPSViewController: UIViewController  @property (weak, nonatomic) IBOutlet UITableView * tableView; @fine

Dobbiamo anche dare una breve occhiata al file di implementazione del controller della vista. Aprire TPSViewController.m e dichiarare una variabile statica di tipo NSString che useremo come identificatore di riutilizzo per le celle nella vista tabella.

 #import "TPSViewController.h" @implementation TPSViewController static NSString * CellIdentifier = @ "CellIdentifier"; //… // @fine

Passaggio 5: Interfaccia utente

Apri lo storyboard principale del progetto, Main.Storyboard e trascina una vista tabella nella vista del controller della vista. Seleziona la vista tabella e collegala fonte di dati e delegare punti vendita con l'istanza del controller di visualizzazione. Con la vista tabella ancora selezionata, apri il Ispettore degli attributi e impostare il numero di Prototipo di cellule a 1. Il Soddisfare attributo deve essere impostato su Prototipi dinamici. Ora dovresti vedere una cella prototipo nella vista tabella.

Seleziona la cella prototipo e impostala Classe a TPSButtonCell nel Identity Inspector. Con la cella ancora selezionata, apri il Ispettore degli attributi e impostare il Stile attribuire a costume e il Identifier a CellIdentifier.

Trascina a UILabel istanza dal Libreria di oggetti alla visualizzazione del contenuto della cella e ripetere questo passaggio per a UIButton esempio. Seleziona la cella, apri il Connections Inspector, e connetti il titleLabel e ActionButton prese con le loro controparti nella cella prototipo.

Prima di tornare al codice, dobbiamo creare un'altra connessione. Seleziona il controller di visualizzazione, apri il Connections Inspector ancora una volta e collegare il controller della vista tableView presa con la vista tabella nello storyboard. Questo è tutto per l'interfaccia utente.

3. Compilazione della vista tabella

Passaggio 1: creare un'origine dati

Inseriamo la vista tabella con alcuni film importanti che sono stati rilasciati nel 2013. Nel TPSViewController classe, dichiara una proprietà di tipo NSArray e nominalo fonte di dati. La variabile di istanza corrispondente manterrà i film che mostreremo nella vista tabella. Popolare fonte di dati con una dozzina circa di film nel controller della vista viewDidLoad metodo.

 #import "TPSViewController.h" @interface TPSViewController () @property (strong, nonatomic) NSArray * dataSource; @fine
 - (void) viewDidLoad [super viewDidLoad]; // Imposta origine dati self.dataSource = @ [@ @ "titolo": @ "Gravity", @ "anno": @ (2013), @ @ "titolo": @ "12 anni schiavo", @ "anno": @ (2013), @ @ "titolo": @ "Prima di mezzanotte", @ "anno": @ (2013), @ @ "titolo": @ "American Hustle", @ "anno ": @ (2013), @ @" titolo ": @" Blackfish ", @" anno ": @ (2013), @ @" titolo ": @" Capitan Phillips ", @" anno ": @ (2013), @ @ "titolo": @ "Nebraska", @ "anno": @ (2013), @ @ "titolo": @ "Rush", @ "anno": @ (2013) , @ @ "title": @ "Frozen", @ "year": @ (2013), @ @ "title": @ "Star Trek Into Darkness", @ "anno": @ (2013), @ @ "title": @ "The Conjuring", @ "year": @ (2013), @ @ "title": @ "Effetti collaterali", @ "anno": @ (2013), @  @ "title": @ "The Attack", @ "year": @ (2013), @ @ "title": @ "Lo Hobbit", @ "anno": @ (2013), @ @ " title ": @" We Are What We Are ", @" year ": @ (2013), @ @" title ": @" Something in the Air ", @" year ": @ (2013)]; 

Passaggio 2: Implementare il UITableViewDataSource Protocollo

L'implementazione del UITableViewDataSource il protocollo è molto semplice. Dobbiamo solo implementare numberOfSectionsInTableView:, tableView: numberOfRowsInSection:, e tableView: cellForRowAtIndexPath:.

 - (NSInteger) numberOfSectionsInTableView: (UITableView *) tableView return self.dataSource? 1: 0;  - (NSInteger) tableView: (UITableView *) tableView numberOfRowsInSection: (NSInteger) section return self.dataSource? self.dataSource.count: 0;  - (UITableViewCell *) tableView: (UITableView *) tableView cellForRowAtIndexPath: (NSIndexPath *) indexPath TPSButtonCell * cell = (TPSButtonCell *) [tableView dequeueReusableCellWithIdentifier: CellIdentifier forIndexPath: indexPath]; // Fetch Item NSDictionary * item = [self.dataSource objectAtIndex: indexPath.row]; // Configura cella Visualizza tabella [cell.titleLabel setText: [NSString stringWithFormat: @ "% @ (% @)", elemento [@ "titolo"], elemento [@ "anno"]]]; [cell.actionButton addTarget: self action: @selector (didTapButton :) forControlEvents: UIControlEventTouchUpInside]; cella di ritorno; 

Nel tableView: cellForRowAtIndexPath:, usiamo lo stesso identificatore che abbiamo impostato nello storyboard principale, CellIdentifier, che abbiamo dichiarato in precedenza nel tutorial. Lanciamo la cella su un'istanza di TPSButtonCell, recupera l'elemento corrispondente dall'origine dati e aggiorna l'etichetta del titolo della cella. Aggiungiamo anche un obiettivo e un'azione per il UIControlEventTouchUpInside evento del pulsante.

Non dimenticare di aggiungere una dichiarazione di importazione per TPSButtonCell classe nella parte superiore di TPSViewController.m.

 #import "TPSButtonCell.h"

Per evitare l'arresto anomalo dell'applicazione quando viene toccato un pulsante, attuare didTapButton: come mostrato di seguito.

 - (void) didTapButton: (id) sender NSLog (@ "% s", __PRETTY_FUNCTION__); 

Costruisci il progetto ed eseguilo in iOS Simulator per vedere cosa abbiamo ottenuto finora. Dovresti visualizzare un elenco di film e toccare il pulsante a destra per registrare un messaggio sulla console Xcode. Grande. È tempo per la carne del tutorial.

4. Come non farlo

Quando l'utente tocca il pulsante a destra, invierà un messaggio di didTapButton: al controller della vista. È quasi sempre necessario conoscere il percorso dell'indice della cella di visualizzazione tabella in cui si trova il pulsante. Ma come si ottiene il percorso dell'indice? Come ho detto, ci sono tre approcci che puoi adottare. Diamo prima un'occhiata a come non farlo.

Dai un'occhiata all'implementazione di didTapButton: e prova a scoprire cosa c'è che non va. Hai notato il pericolo? Lascia che ti aiuti. Esegui l'applicazione prima su iOS 7 e poi su iOS 6. Dai un'occhiata a quali uscite Xcode alla console.

 - (void) didTapButton: (id) sender // Trova tabella Visualizza cella UITableViewCell * cell = (UITableViewCell *) [[[superstender sender] superview] superview]; // Infer Index Path NSIndexPath * indexPath = [self.tableView indexPathForCell: cell]; // Fetch Item NSDictionary * item = [self.dataSource objectAtIndex: indexPath.row]; // Accedi a Console NSLog (@ "% @", elemento [@ "titolo"]); 

Il problema con questo approccio è che è soggetto a errori. Su iOS 7, questo approccio funziona bene. Su iOS 6, tuttavia, non funziona. Per farlo funzionare su iOS 6, devi implementare il metodo come mostrato di seguito. La gerarchia delle viste di un numero di comuni UIView sottoclassi, come UITableView, è cambiato in iOS 7 e il risultato è che l'approccio di cui sopra non produce un risultato coerente.

 - (void) didTapButton: (id) sender // Trova la tabella Visualizza la cella UITableViewCell * cell = (UITableViewCell *) [[supender superview] superview]; // Infer Index Path NSIndexPath * indexPath = [self.tableView indexPathForCell: cell]; // Fetch Item NSDictionary * item = [self.dataSource objectAtIndex: indexPath.row]; // Accedi a Console NSLog (@ "% @", elemento [@ "titolo"]); 

Non possiamo controllare se il dispositivo ha iOS 7? Questa è un'ottima idea. Tuttavia, cosa farai quando iOS 8 modifica la gerarchia della vista interna di UITableView ancora una volta? Hai intenzione di applicare la patch all'applicazione ogni volta che viene introdotta una versione principale di iOS? E che dire di tutti quegli utenti che non aggiornano alla versione più recente (con patch) della tua applicazione? Spero sia chiaro che abbiamo bisogno di una soluzione migliore.

5. Una soluzione migliore

Un approccio migliore consiste nel dedurre il percorso dell'indice della cella nella vista tabella in base alla posizione del file mittente, il UIButton esempio, nella vista tabella. Noi usiamo convertPoint: toview: per realizzare questo. Questo metodo converte il centro del pulsante dal sistema di coordinate del pulsante al sistema di coordinate della vista tabella. Diventa quindi molto facile. Noi chiamiamo indexPathForRowAtPoint: sul tavolo guarda e passa pointInSuperview ad esso. Questo ci fornisce un percorso dell'indice che possiamo usare per recuperare l'elemento corretto dall'origine dati.

 - (void) didTapButton: (id) sender // Cast Sender in UIButton UIButton * button = (UIButton *) mittente; // Trova punto in Superview CGPoint pointInSuperview = [button.superview convertPoint: button.center toView: self.tableView]; // Infer Index Path NSIndexPath * indexPath = [self.tableView indexPathForRowAtPoint: pointInSuperview]; // Fetch Item NSDictionary * item = [self.dataSource objectAtIndex: indexPath.row]; // Accedi a Console NSLog (@ "% @", elemento [@ "titolo"]); 

Questo approccio può sembrare complicato, ma in realtà non lo è. È un approccio che non è influenzato dalle modifiche nella gerarchia di visualizzazione di UITableView e può essere utilizzato in molti scenari, anche nelle viste di raccolta.

6. La soluzione elegante

C'è un'altra soluzione per risolvere il problema e richiede un po 'più di lavoro. Il risultato, tuttavia, è una visualizzazione del moderno Objective-C. Inizia rivisitando il file di intestazione del file TPSButtonCell e dichiara un metodo pubblico chiamato setDidTapButtonBlock: che accetta un blocco.

 #importare  @interface TPSButtonCell: UITableViewCell @property (weak, nonatomic) IBOutlet UILabel * titleLabel; @property (weak, nonatomic) IBOutlet UIButton * actionButton; - (void) setDidTapButtonBlock: (void (^) (id sender)) didTapButtonBlock; @fine

Nel file di implementazione di TPSButtonCell creare una proprietà privata denominata didTapButtonBlock come mostrato di seguito. Si noti che la proprietà attribuita è impostata su copia, perché i blocchi devono essere copiati per tenere traccia del loro stato acquisito al di fuori dell'ambito originale.

 #import "TPSButtonCell.h" @interface TPSButtonCell () @property (copy, nonatomic) void (^ didTapButtonBlock) (id sender); @fine

Invece di aggiungere un obiettivo e un'azione per il UIControlEventTouchUpInside evento nel controller della vista tableView: cellForRowAtIndexPath:, aggiungiamo un obiettivo e un'azione in awakeFromNib nel TPSButtonCell classe stessa.

 - (void) awakeFromNib [super awakeFromNib]; [self.actionButton addTarget: self action: @selector (didTapButton :) forControlEvents: UIControlEventTouchUpInside]; 

L'implementazione di didTapButton: è banale.

 - (void) didTapButton: (id) sender if (self.didTapButtonBlock) self.didTapButtonBlock (sender); 

Questo può sembrare un sacco di lavoro per un semplice pulsante, ma tieni i cavalli fino a quando non li abbiamo rifattorizzati tableView: cellForRowAtIndexPath: nel TPSViewController classe. Invece di aggiungere un obiettivo e un'azione al pulsante della cella, impostiamo la cella didTapButtonBlock. Ottenere un riferimento all'elemento corrispondente dell'origine dati diventa molto, molto facile. Questa soluzione è di gran lunga la soluzione più elegante a questo problema.

 - (UITableViewCell *) tableView: (UITableView *) tableView cellForRowAtIndexPath: (NSIndexPath *) indexPath TPSButtonCell * cell = (TPSButtonCell *) [tableView dequeueReusableCellWithIdentifier: CellIdentifier forIndexPath: indexPath]; // Fetch Item NSDictionary * item = [self.dataSource objectAtIndex: indexPath.row]; // Configura cella Visualizza tabella [cell.titleLabel setText: [NSString stringWithFormat: @ "% @ (% @)", elemento [@ "titolo"], elemento [@ "anno"]]]; [cell setDidTapButtonBlock: ^ (id sender) NSLog (@ "% @", elemento [@ "titolo"]); ]; cella di ritorno; 

Conclusione

Anche se il concetto di blocchi esiste da decenni, gli sviluppatori di Cocoa hanno dovuto attendere fino al 2011. I blocchi possono rendere più semplici i problemi complessi e semplificare il codice complesso. Dall'introduzione dei blocchi, Apple ha iniziato a farne un ampio uso nelle proprie API, quindi ti incoraggio a seguire la guida di Apple sfruttando i blocchi nei tuoi progetti.