Buongiorno a tutti! Il corso di C mi ha tenuto parecchio impegnato ed è da un pò che non scrivo tutorial per iOS, ma oggi ho una piccola chicca da proporvi.
L’obbiettivo di oggi è quello di realizzare un contatore meccanico, simile a quello che si vede in molti giochi, con tanto di animazione per passare da un numero ad un altro.
In questa immagine si vede l’effetto finale da “fermo” e durante un’animazione (a fondo articolo trovate anche un video con il contatore all’opera):


Come sempre cercheremo di creare del codice che sia riutilizzabile in diverse occasioni quindi una volta realizzato questo tutorial non vi resterà altro da fare che copiare i file nel vostro progetto ed interagire con il contatore come si fa con qualsiasi altro oggetto dell’UIKit.
Per iniziare abbiamo bisogno delle immagini per il nostro contatore, potete realizzarle con photoshop, potete comprarle online oppure ancora potete anche usare le mie se vi piacciono. Nel progetto allegato le troverete già separate, non impazzite a tagliare queste:

Adesso qui si svela il primo trucco, le immagini che serviranno saranno 20 e non 10 come si potrebbe pensare all’inizio, perché ci serviranno delle imagini per l’animazione.

Io ho cercato con photoshop di dare l’effetto “movimento” inserendo due numeri per volta con una sfocatura verticale. Ad essere sincero questa idea non è tutta farina del mio sacco, ma l’idea mi è venuta giocando a Flight Control, infatti come potete vedere in questo screen sul loro gioco utilizzano proprio questa tecnica:

Un’aspetto da non sottovalutare è la naming convention, ossia i nomi da dare ai file, sarà importante chiamare le immagini con dei nomi come “immagine_0.png”, “”immagine_1.png” etc.
io ho usato da “mc_0.png” fino a “mc_9.png” per le immagini delle singole cifre, e da “mc_10.png” fino a “mc_20” per le altre 10 immagini.
Realizzate quindi le immagini non ci resta che passare ad xcode, create quindi un nuovo progetto ed importate le immagini che avete creato.
Io per semplicità ho creato un progetto di tipo “ViewBased application” e nel metodo “viewDidLoad” del viewcontroller principale ho inserito questo codice per generare lo sfondo ed il pulsante:
- (void)viewDidLoad
{
[super viewDidLoad];
/* Creazione dello sfondo
*/
UIImageView *bg = [[UIImageView alloc] initWithImage:[UIImage imageNamed:@"sfondo_devapp.png"]];
[bg setFrame:[[UIScreen mainScreen] bounds]];
[self.view addSubview:bg];
[bg release];
/* Creazione del pulsante
*/
UIButton *btn = [UIButton buttonWithType:UIButtonTypeCustom];
[btn setFrame:CGRectMake(160 - 150 / 2, 230 - 60 / 2, 150, 60)];
[btn setBackgroundImage:[UIImage imageNamed:@"button.png"] forState:UIControlStateNormal];
[btn setTitle:@"TAP ME!" forState:UIControlStateNormal];
[btn addTarget:self action:@selector(buttonPressed:) forControlEvents:UIControlEventTouchUpInside];
[self.view addSubview:btn];
Questo codice è stato inserito solo per completezza e non ha direttamente a che vedere con il nostro contatore.
Arriviamo quindi al cuore del progetto, realizziamo la nostra classe “MechanicalCounter” creando una nuova classe che derivi direttamente da uiview.
La dichiarazione della classe è la seguente:
#import
@interface MechanicalCounter : UIView {
NSMutableArray *ciphers;
int currentValue;
int *values;
}
@property (nonatomic, retain) NSMutableArray *ciphers;
@property (nonatomic, assign) int currentValue;
- (id)initWithFrame:(CGRect)frame andCiphers:(int)nchipers;
- (void)setValue:(int)newValue;
- (void)showNextValue;
@end
Ho dichiarato e variabili di istanza, la prima è un NSMutableArray, conterrà le UIImageView del nostro contatore, la seconda variabile è un intero che contiene il valore attualmente visualizzato sul contatore mentre l’ultima variabile è un puntatore ad intero, verrà usata per costruire un semplice array in puro stile C. Questo array conterrà in ogni posizione il valore della cifra corrispondente visualizzata nel contatore.
Facciamo un piccolo esempio: supponiamo che il contatore visualizzi 001234, l’array conterrà 6 uiimageview, la variabile currentValue conterrà il valore 1234, mentre l’array values sarà così:
values[0] = 0;
values[1] = 0;
values[2] = 1;
values[3] = 2;
values[4] = 3
values[5] = 4;
Perchè uso un array C invece di un NSArray? Perché negli NSArray non si possono inserire variabili di tipo int, quindi dovrei passare da nsnumber e lo ritengo un’inutile spreco di spazio.
Guardiamo quindi il primo metodo:
-(id)initWithFrame:(CGRect)frame andCiphers:(int)nciphers {
self = [super initWithFrame:frame];
if (self) {
self.ciphers = [NSMutableArray arrayWithCapacity:nciphers];
values = calloc(nciphers, sizeof(int));
/* Calcolo la dimensione in larghezza
*/
UIImage *imgtest = [UIImage imageNamed:@"mc_0.png"];
float width;
if (imgtest.size.width * nciphers > 320) {
width = 320 / nciphers;
}
else {
width = frame.size.width / nciphers;
}
/* Calcolo l'array delle immagini per l'animazione
*/
NSMutableArray *arr = [NSMutableArray arrayWithCapacity:10];
for (int i = 10; i <= 19; i++) {
NSString *str = [NSString stringWithFormat:@"mc_%d.png",i];
[arr addObject:[UIImage imageNamed:str]];
}
/* Istanzio le singole view
*/
for (int i = 0; i < nciphers; i++) {
UIImageView *v = [[UIImageView alloc] initWithFrame:CGRectMake(i * width, 0, width, frame.size.height)];
[v setImage:imgtest];
v.contentMode = UIViewContentModeScaleAspectFit;
[v setAnimationImages:arr];
[v setAnimationRepeatCount:1];
[v setAnimationDuration:0.5];
[ciphers addObject:v];
[self addSubview:v];
[v release];
}
}
return self;
}
Questo metodo è il costruttore principale della nostra classe, lo invochiamo passando il frame in cui vogliamo visualizzare il contatore ed il numero di cifre che devono essere visualizzate.
self.ciphers = [NSMutableArray arrayWithCapacity:nciphers];
values = calloc(nciphers, sizeof(int));
Iniziamo quindi con il richiamare il costruttore della classe super e se tutto è andato per il verso giusto proseguiamo inizializzando l'array ciphers e l'array values.
UIImage *imgtest = [UIImage imageNamed:@"mc_0.png"];
float width;
if (imgtest.size.width * nciphers > 320) {
width = 320 / nciphers;
}
else {
width = frame.size.width / nciphers;
}
Segue un piccolo calcolo per ottenere la larghezza di ogni singola cifra, notare che se il numero delle cifre moltiplicato per la larghezza della singola immagine supera la dimensione dello schermo, tutto viene ridimensionato per far rientrare tutto il contatore entro i 320px dell'iphone.
/* Calcolo l'array delle immagini per l'animazione
*/
NSMutableArray *arr = [NSMutableArray arrayWithCapacity:10];
for (int i = 10; i <= 19; i++) {
NSString *str = [NSString stringWithFormat:@"mc_%d.png",i];
[arr addObject:[UIImage imageNamed:str]];
}
Creo un array con le uiimage che comporranno la mia animazione, lo creo solo una volta e lo associo a tutte le uiimageview.
/* Istanzio le singole view
*/
for (int i = 0; i < nciphers; i++) {
UIImageView *v = [[UIImageView alloc] initWithFrame:CGRectMake(i * width, 0, width, frame.size.height)];
[v setImage:imgtest];
v.contentMode = UIViewContentModeScaleAspectFit;
[v setAnimationImages:arr];
[v setAnimationRepeatCount:1];
[v setAnimationDuration:0.5];
[ciphers addObject:v];
[self addSubview:v];
[v release];
}
}
Creo infine tutte le UIView che mi serviranno, e per ciascuna di esse imposto tutti i parametri necessari per l'animazione.
A questo punto l'inizializzazione è terminata, questo oggetto può essere gestito come tutti gli altri oggetti dell'UIKit, infatti tornando al viewcontroller principale, sempre nel metodo viewdidiload possiamo aggiungere:
//mc è una variabile di tipo MechanicalCounter dichiarata nel file .h
mc = [[MechanicalCounter alloc] initWithFrame:CGRectMake(0, 0, 320, 124) andCiphers:10];
[self.view addSubview:mc];
per vedere questo risultato:

Ma ovviamente non è solo questo quello che ci aspettiamo, ci serve un contatore che "conti" :D, quindi rimbocchiamoci le maniche ed implementiamo i restanti due metodi dichirati nel file .h
- (void)setValue:(int)newValue;
- (void)showNextValue;
Partirei dal secondo, la cui implementazione è banale:
-(void)showNextValue {
[self setValue:++self.currentValue];
}
Quello che fa questo metodo è richiamare un ulteriore metodo, che non è dichiarato nel file .h e quindi si potrebbe definire "privato" passando come parametro il valore della variabile currentValue incrementato di 1.
Anche il primo metodo se vogliamo è piuttosto semplice perché la sua implementazione è questa:
- (void)setCurrentValue:(int)acurrentValue {
currentValue = acurrentValue;
[self setValue:self.currentValue];
}
Si occupa di impostare correttamente la variabiel currentValue e richiamare lo stesso metodo privato del precedente.
A questo punto allora bisogna capire cosa fa questo metodo setValue: !!
Il codice è questo, c'è un pò di matematica, ma niente paura:
- (void)setValue:(int)newValue {
if (newValue > (pow(10, [ciphers count]) - 1)) {
if (newValue == (pow(10, [ciphers count]))) {
newValue = 0;
currentValue = 0;
}
else {
NSLog(@"Error: value too high");
return;
}
}
int cifra;
for (int i = [ciphers count] - 1; i >= 0; i--) {
cifra = newValue / pow(10, i);
if (cifra) {
newValue = newValue - cifra * pow(10, i);
}
[self setCipher:[ciphers count] - i - 1 value:cifra];
}
}
Con
if (newValue > (pow(10, [ciphers count]) - 1)) {
Verifico se il valore da settare è maggiore del più grande numero rappresentabile con quel dato numero di cifre. Ricordo che [chipers count] ritorna il numero di cifre del contatore, supponiamo sia 5, allora il maggiore numero rappresentabile è 10^5 - 1 = 99999.
if (newValue == (pow(10, [ciphers count]))) {
Questo secondo controllo l'ho inserito perché voglio ottenere un contatore che quando arriva al suo massimo valore, se incrementato di 1 si riazzera, un pò come i contachilometri delle auto.
In questo caso se il valore da settare è proprio 100000 allora riporto a zero il contatore ed il currentvalue, viceversa scrivo un messaggio d'errore sulla console.
int cifra;
for (int i = [ciphers count] - 1; i >= 0; i--) {
cifra = newValue / pow(10, i);
if (cifra) {
newValue = newValue - cifra * pow(10, i);
}
[self setCipher:[ciphers count] - i - 1 value:cifra];
}
Qui c'è un attimino di matematica, è il primo modo che mi è venuto in mente, magari si può ottimizzare, quello che mi serve è recuperare le singole cifre da un numero intero.
Quello che faccio è questo: con un ciclo for sulla variabile i parto dalla cifra più significativa e calcolo la divisione tra il nuovo valore da settare e 10^i Se il risultato è maggiore di zero allora sottraggo dal valore da settare il prodotto del risultato per 10^i infine imposto il valore della singola cifra con il metodo setCipher: value:
Vediamo come al solito un esempio per chiarire meglio.
Supponiamo di avere un contatore a 4 cifre e di voler visualizzare il valore 234.
La prima iterazione del ciclo for calcolerà 234 / 10^3 = 0 (divisione intera, viene arrotondata) qiundi non entra nell'if e richiama direttamente il metodo per impostare a 0 la cifra in posizione 0 dell'array.
Alla seconda iterazione verrà calcolato 234 / 10^2 = 2 quindi entro nell'if e porto il nuovo valore a 234 - 200 = 34, infine imposto a 2 la cifra in posizione 1 dell'array.
Alla terza iterazione verrà calcolato 34 / 10^1 = 3 quindi entro nell if e porto il nuovo valore a 34 - 30 = 4, ed imposto a 3 la cifra in posizione 2 dell'array.
All'ultima iterazione verrà calcolato 4 / 10^0 = 4 e quindi entro nell'if e porto il nuovo valore a 4 - 4 = 0, ed imposto a 4 la cifra in posizione 4 dell'array.
Ecco che quindi nell'array ho impostato 0234, proprio il numero che volevo. Difficile? no, dai!!
Non ci resta che vedere il metodo che modifica il valore della singola cifra:
- (void)setCipher:(int)acipher value:(int)value {
if (value != values[acipher]) {
NSString *str2 = [NSString stringWithFormat:@"mc_%d.png",value];
[[ciphers objectAtIndex:acipher] setImage:[UIImage imageNamed:str2]];
[[ciphers objectAtIndex:acipher] startAnimating];
values[acipher] = value;
}
}
Anche questo metodo è piuttosto auto-esplicativo, l'unica particolarità è che faccio il controllo per verificare se effettivamente il nuovo valore è diverso dal precedente, in questo modo, quando viene incrementato il contatore non si animano tutte le cifre, ma solo quelle che effettivamente devono cambiare.
Qui potete vedere anche il video del contatore in azione:
Non mi resta che lasciarvi il progetto in allegato ed attendere i vostri commenti.
Ciao!
IgnazioC
Se avete problemi con il tutorial, questo è il nostro file di progetto.
14 Responses to “T#094 – Creare un contatore meccanico animato per iPhone o iPad”
3 Giugno 2011
FraTutorial carino 🙂 Dov’è il progetto?
3 Giugno 2011
Francesco BianchettiDov’ è il progetto! 😉
3 Giugno 2011
Rino PicardiOPS.. scusate.. l’ho perso per strada.. 😛 lo recupero e lo metto su..
3 Giugno 2011
Ragazzettocome sempre ……..
……. tutorial spettacolare !!!!
Bravo Ignazio !!
e naturalmente grazie !
4 Giugno 2011
CesareCome potrei impostarlo per far si che “conti” fino ad una determinata data con la sola unità di misura dei secondi?
Grazie
4 Giugno 2011
C.I.R.stupendo!
4 Giugno 2011
IgnaziocQuel contatore “conta” e basta 🙂 se poi vuoi aggiungervi della logica lo devi fare tu in qualche modo.
4 Giugno 2011
Francesco Bianchettima il progetto!?
5 Giugno 2011
Rino PicardiProgetto online 😉
7 Giugno 2011
Gabrielepuoi rispondere ad una mia domanda nel tutorial 059?? è importante
7 Giugno 2011
Ignazioc“è importante”? c’è il forum per le richieste e poi bisogna sempre tenere in mente questo link: http://www.catb.org/~esr/faqs/smart-questions.html
cmq vai a leggere, ti ho dato la risposta.
10 Giugno 2011
cursaoDavvero bello, anche se per completezza manca il metodo buttonPressed, anche se la presenza del progetto colma tutti i gap! Grandi continuate così
16 Settembre 2011
milonetUna domanda.. se io volessi fare in modo che ogni secondo si tappa da solo? 😀 tipo per fare in timer.. dove lo richiamo il meoto?
grandissimo cmq ;D
19 Dicembre 2012
Francescobravo ignazio… complimenti!!