• Programmazione Android
  • CORSI ONLINE
  • Web Agency

Logo

Corsi di programmazione web e mobile online
Navigation
  • Home
  • CORSI ONLINE
  • Tutorial Pratici
  • GUIDE COMPLETE
    • Corso completo di C
    • Corso videogame con Cocos2d
    • Programmazione Cocoa Touch
  • Sezioni
    • Libri e manuali
    • Tips & Tricks
    • Risorse utili
    • Strumenti di Sviluppo
    • Materiale OpenSource
    • Framework
    • Guide Teoriche
    • Guide varie
    • Grafica e Design
    • iPad
    • News
    • Video Tutorial
    • Windows Phone
  • Pubblicità
  • About
    • Chi siamo
    • Pubblicazioni
    • Collabora
    • Sostieni devAPP

T#082 – Guida all’uso di Core Data (Parte 2)

By doppioslash | on 23 Dicembre 2010 | 9 Comments
Senza categoria

t081-usare-core-data-iphone-ipad-00 Eccoci alla seconda puntata della nostra Guida all’uso di Core Data nelle nostre applicazioni iPhone e iPad.
Ne approfitto per augurare Buon Natale a tutti 🙂

 

In questo episodio:

  • Core Data vs Database: le differenze
  • Aggiungiamo al nostro programma la possibilità di modificare ed eliminare i record
  • Sfruttiamo la procedura automatica per creare le nostre classi Model, che ereditano da NSManagedObject, per non dover ricorrere continuamente al KVC.

1. Core Data vs Database

Come abbiamo detto nella scorsa puntata, Core Data non è un database.
Quest’affermazione a prima vista potrebbe dare la stessa sensazione di dissonanza cognitiva di “Ceci n’est pas une pipe”, quindi è il caso di approfondire.

Il primo istinto, una volta data un’occhiata ai metodi disponibili, è quello di usarlo come un database, cioé fetch/manipola i dati/salvataggio, che poi è quello che facciamo in questo tutorial. Questo perché il nostro Model è molto semplice. Ma Core Data brilla soprattutto nella gestione di oggetti e rapporti fra oggetti anche molto complicati.

Il concetto fondamentale è che Core Data può avere SQLite come backend, ma non è semplicemente un wrapper.
Passando attraverso Core Data otteniamo delle funzionalità di gestione dei rapporti fra le Entità che, come vedremo più avanti in questo tutorial, sono autentici oggetti objective-C, sottoclassi di NSManagedObject: ciò significa che dobbiamo istanziarli prima di poter agire su di loro.

Core Data funzionerebbe anche senza backend, anche se in questo caso rinunceremmo alla persistenza dei dati: possiamo operare completamente in ram, se istanziamo tutti i record e li colleghiamo fra di loro possiamo arrivare a tutti i record senza usare ricerche e senza accedere al disco.
Manipolare oggetti in ram è più rapido che aggiornare un database, quando salviamo i dati del nostro contesto allora Core Data userà il database e quindi i tempi di salvataggio di Core Data si sommeranno a quelli del database.

Riassumiamo in una lista le caratteristiche dei database e di Core Data.

Database:

  • può aggiornare un attributo senza caricare l’intero oggetto in memoria
  • è lento nel creare nuovi record
  • agisce su dati residenti sul disco rigido

Core Data:

  • tiene aggiornate le connessioni fra gli oggetti automaticamente
  • quando un oggetto viene cancellato è possibile cancellare automaticamente quelli che gli sono collegati (come vedremo in un prossimo tutorial)
  • agisce su dati residenti in ram
  • istanziare nuovi record è rapido
  • le modifiche agli oggetti sono osservabili con il Key-Value Observing

Il compromesso:

in Core Data istanziare gli oggetti è rapido, perché sono in RAM piuttosto che sull’HD, ma per modificare anche un solo attributo o per cancellare un’oggetto è necessario caricarlo prima in RAM.
Quindi, se vogliamo sviluppare un’app, in cui modifichiamo molto spesso poche proprietà in più migliaia di record (esempio: lettore rss, letto, non letto) allora SQLite sarà più rapido di Core Data.

2. Creiamo le nostre classi Model

Torniamo all’app che abbiamo sviluppato nella scorsa puntata.
Finora abbiamo usato setValue:forKey: per modificare e ottenere i valori delle proprietà nelle istanze della nostra Entità.
Il Key-Value Coding ci permette di accedere ad ogni proprietà, di qualunque classe, per cui è disponibile un metodo setter e un metodo getter, che seguano questa naming convention:

getter
nomeVariabile (quindi non getNomeVariabile, che viene usato in un'altro caso)
setter
setNomeVariabile (sempre usando il Camel Case)

Quindi è una soluzione generica che funziona, ma che comporta un notevole spreco di caratteri, che scriviamo senza l’aiuto dell’autocompletamento di Xcode e soprattutto senza il type checking, per cui abbiamo una notevole gamma di errori possibili che non verranno segnalati al compile-time.
Per ovviare a questi inconvenienti possiamo creare una sottoclasse di NSManagedObject per la nostra Entità.
C’è un modo rapido di sottoclassare NSManagedObject avendo già definito gli attributi dell’Entità nell’editor grafico di Core Data: apriamo il nostro xcdatamodel nell’editor e selezioniamo l’Entità per cui vogliamo creare una classe, nel nostro caso Contatto.
A questo punto scegliamo dal menu File > New File…, sotto Cocoa Touch Class avremo accesso ad un nuovo template: Managed Object Class.


Selezioniamo l'Entità...


Scegliamo il template...

Andiamo avanti nello wizard e assicuriamoci che Add To Project: punti al nostro progetto e che nei targets sia selezionato CoreDataPart2, e nella schermata successiva che l’Entità sia selezionata (è possibile generare Classi per più Entità contemporaneamente, se il nostro file xcdatamodel contenesse più di un’Entità sarebbero tutte elencate qui), come anche Generate Accessors e Generate Obj-C 2.0 properties e poi confermiamo premendo Finish.


Dentro il wizard - 1


Dentro il wizard - 2

A questo punto i file .h e .m della nostra classe sono stati aggiunti al progetto, e nel file xcdatamodel la classe della nostra Entità, NSManagedObject è stata sostituita con la nuova classe, Contatto. E necessario salvare il file xcdatamodel per finalizzare il cambiamento.
Diamo un’occhiata all’interfaccia della classe Contatto.

#import 

@interface Contatto :  NSManagedObject  
{
}

@property (nonatomic, retain) NSString * nick;
@property (nonatomic, retain) NSString * cognome;
@property (nonatomic, retain) NSString * nome;

@end

Vediamo che eredita da NSManagedObject e che anche se gli attributi dell’Entità non sono dichiarati come variabili d’istanza poi vengono comunque generate le proprietà.
Per capire questa particolarità dobbiamo vedere anche l’implementazione.

#import "Contatto.h"

@implementation Contatto 

@dynamic nick;
@dynamic cognome;
@dynamic nome;

@end

Le proprietà non vengono sintetizzate usando la direttiva @syntesize, ma con la direttiva @dynamic, cioé promettiamo al compilatore che saranno presenti al runtime, generate e gestite da Core Data. Naturalmente se dovessero mancare a causa di errori di battitura o altro, l’app crasherebbe.

3. Aggiorniamo i metodi che utilizzano il KVC

Adesso che abbiamo la nostra classe Model dobbiamo rimaneggiare tutto il codice nell’applicazione dove abbiamo usato il KVC.

Per prima cosa importiamo l’header in CoreDataPart2AppDelegate.m;

#import "CoreDataPart2AppDelegate.h"
#import "Contatto.h"

e poi modifichiamo i metodi interessati: addContatto, cellForRowAtIndexPath.

addContatto:

//Otteniamo il puntatore al NSManagedContext
NSManagedObjectContext *context = [self managedObjectContext];

//Parte1: Creiamo un'istanza di NSManagedObject per l'Entità che ci interessa
//Parte2: Creiamo un'istanza della classe Model dell'Entità che ci interessa, usando comunque NSEntityDescription
Contatto *contatto = [NSEntityDescription
                                   insertNewObjectForEntityForName:@"Contatto" 
                                   inManagedObjectContext:context];
		
//Parte1: Usando il Key-Value Coding inseriamo i dati presi dall'interfaccia nell'istanza dell'Entità appena creata
//Parte2: Inseriamo i dati usando le proprietà della classe Contatto
contatto.cognome = surnameField.text;
contatto.nome = nameField.text;
contatto.nick = nickField.text;

cellForRowAtIndexPath:

// Set up the cell...
	
//Parte1: istanziamo NSManagedObject perché gli oggetti dentro l'array sono di quel tipo
//Parte2: istanziamo invece Contatto
Contatto *contatto = [contattiList objectAtIndex:indexPath.row];
	
//Parte1: accediamo ai dati contenuti dal'oggetto utilizzando il Key-Value Coding
//Parte2: accediamo ai dati utilizzando le proprietà della classe Contatto
cell.textLabel.text = [NSString stringWithFormat:@"%@ %@", contatto.nome, contatto.cognome];
cell.detailTextLabel.text = contatto.nick;
	
return cell;

Et voilà, proviamo la nostra app e vediamo che funziona come prima, ma usando la classe Model che abbiamo generato.

4. Aggiungiamo altre funzionalità: Modifica ed Elimina i record già inseriti

Per prima cosa aggiungiamo al nostro file .h due variabili d’istanza, due metodi e un protocollo.
Le variabili d’istanza sono currentRecord e addButton, un’intero e un’outlet.
La funzione di currentRecord è tenere traccia su che record agiranno addContatto e deleteContatto, se abbiamo selezionato un record nella tabella sarà uguale al suo indexPath.row, mentre quando non c’è record selezionato sarà uguale a -1.
La funzione di addButton è darci l’accesso al pulsante aggiungi, per cambiare il suo testo da “Aggiungi” in “Modifica” e viceversa.
Il protocollo da aggiungere a quelli supportati è UITableViewDelegate, di cui useremo didSelectRowAtIndexPath:.
I metodi sono deleteContatto e clearFields. Dovremo pulire i campi in due metodi diversi: addContatto e deleteContatto, quindi è meglio estrarre la funzionalità da addContatto ed inserirla in un metodo apposito: clearFields.
In deleteContatto, com’è facilmente deducibile dal nome, agiremo sul record selezionato in didSelectRowAtIndexPath: eliminandolo.
showContatti non verrà più invocato schiacciando il pulsante mostra, quindi sostituiamo IBAction con void.

#import 
#import 

@interface CoreDataPart2AppDelegate : NSObject  {
    
    UIWindow *window;
	
	IBOutlet UITextField *nameField;
	IBOutlet UITextField *surnameField;
	IBOutlet UITextField *nickField;
	IBOutlet UITableView *contattiTable;
	
	IBOutlet UIButton *addButton;
	NSInteger currentRecord;
	
	NSArray *contattiList;
    
@private
    NSManagedObjectContext *managedObjectContext_;
    NSManagedObjectModel *managedObjectModel_;
    NSPersistentStoreCoordinator *persistentStoreCoordinator_;
}

@property (nonatomic, retain) IBOutlet UIWindow *window;

@property (nonatomic, retain, readonly) NSManagedObjectContext *managedObjectContext;
@property (nonatomic, retain, readonly) NSManagedObjectModel *managedObjectModel;
@property (nonatomic, retain, readonly) NSPersistentStoreCoordinator *persistentStoreCoordinator;

- (NSString *)applicationDocumentsDirectory;

- (void) showContatti;
- (void) clearFields;
- (IBAction) addContatto;
- (IBAction)deleteContatto;

@end

Apriamo MainWindow.xib e colleghiamo l’outlet per addButton.


Colleghiamo l'outlet per addButton.

Adesso passiamo a CoreDataPart2AppDelegate.m e inseriamo i nuovi metodi, modificando quelli vecchi dove necessario.
Per prima cosa sostituiamo anche qui void ad IBAction per showContatti, poi aggiungiamo un’invocazione a showContatti in didFinishLaunchingWithOptions:, per mostrare eventuali record già presenti all’avvio dell’app e settiamo currentRecord = -1 come valore iniziale.

- (void) showContatti {
	//Otteniamo il puntatore al NSManagedContext
	NSManagedObjectContext *context = [self managedObjectContext];
	
	//istanziamo la classe NSFetchRequest di cui abbiamo parlato in precedenza
	NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
	
	//istanziamo l'Entità da passare alla Fetch Request
	NSEntityDescription *entity = [NSEntityDescription 
								   entityForName:@"Contatto" inManagedObjectContext:context];
	//Settiamo la proprietà Entity della Fetch Request
	[fetchRequest setEntity:entity];
	
	//Eseguiamo la Fetch Request e salviamo il risultato in un array, per visualizzarlo nella tabella
	NSError *error;
	NSArray *fo = [context executeFetchRequest:fetchRequest error:&error];
	contattiList = [fo retain];
	
	[fetchRequest release];
	
	[contattiTable reloadData];
}
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {    
	
    // Override point for customization after application launch.
	
    [window makeKeyAndVisible];
	
	//aggiorniamo la tabella
	[self showContatti];
	
	//settiamo il valore iniziale di currentRecord
	currentRecord = -1;
	
	return YES;
}

5. Inseriamo i nuovi metodi

Estraiamo le righe che si occupano di pulire i campi da addContatto e inseriamole nel nuovo metodo clearFields

- (void)clearFields {
	//per pulire i campi
	NSString *clear = [NSString stringWithFormat:@""];
	
	[nameField setText:clear];
	[surnameField setText:clear];
	[nickField setText:clear];
}

E’ il momento di aggiungere i due metodi più importanti di questa fase: deleteContatto e didSelectRowAtIndexPath:

- (IBAction)deleteContatto {
	//Otteniamo il puntatore al NSManagedContext
	NSManagedObjectContext *context = [self managedObjectContext];
	
	//usiamo currentRecord per agire sul record selezionato
	[context deleteObject:[contattiList objectAtIndex:currentRecord]];
	
	//Effettuiamo il salvataggio gestendo eventuali errori
	NSError *error;
	if (![context save:&error]) {
		NSLog(@"Errore durante il salvataggio: %@", [error localizedDescription]);
	}
	
	//resettiamo surrentRecord
	currentRecord = -1;
	
	//puliamo i campi
	[self clearFields];
	
	//aggiorniamo la tabella
	[self showContatti];
}

Infine modifichiamo addContatto per fargli gestire sia il caso della modifica sia quello dell’aggiunta, pulire i campi col nuovo metodo, aggiornare la tabella e resettare currentRecord:

- (IBAction) addContatto {
	//apriamo alertView se i campi non sono riempiti
	if (nameField.text.length == 0 || surnameField.text.length == 0 || nickField.text.length == 0) {
		
		//Costruiamo l'alert che ci impedisce di inserire un record senza tutti i campi
		NSString *titolo = @"Problema";
		NSString *messaggio = @"Devi inserire tutti i campi!";
		
		UIAlertView *alert = [[UIAlertView alloc] initWithTitle:titolo
														message:messaggio
													   delegate:self
											  cancelButtonTitle:@"OK, non lo farò più."
											  otherButtonTitles:nil];
		
;
; } else { //per far sparire la tastiera if ([nameField isFirstResponder]) { [nameField resignFirstResponder]; } else if ([surnameField isFirstResponder]) { [surnameField resignFirstResponder]; } else if ([nickField isFirstResponder]){ [nickField resignFirstResponder]; } //Otteniamo il puntatore al NSManagedContext NSManagedObjectContext *context = [self managedObjectContext]; //dichiariamo contatto senza istanziarlo, lo faremo all'interno dell'if. Contatto *contatto; //se è selezionato un contatto agiremo su quello altrimenti ne aggiungeremo uno nuovo if (currentRecord >= 0) { contatto = [contattiList objectAtIndex:currentRecord]; } else { //Parte1: Creiamo un'istanza di NSManagedObject per l'Entità che ci interessa //Parte2: Creiamo un'istanza della classe Model dell'Entità che ci interessa, usando comunque NSEntityDescription contatto = [NSEntityDescription insertNewObjectForEntityForName:@"Contatto" inManagedObjectContext:context]; } //Parte1: Usando il Key-Value Coding inseriamo i dati presi dall'interfaccia nell'istanza dell'Entità appena creata //Parte2: Inseriamo i dati usando le proprietà della classe Contatto contatto.cognome = surnameField.text; contatto.nome = nameField.text; contatto.nick = nickField.text; //Effettuiamo il salvataggio gestendo eventuali errori NSError *error; if (![context save:&error]) { NSLog(@"Errore durante il salvataggio: %@", [error localizedDescription]); } //puliamo i campi nell'interfaccia [self clearFields]; //aggiorniamo il testo del pulsante [addButton setTitle:@"Aggiungi" forState:UIControlStateNormal]; } //resettiamo currentRecord currentRecord = -1; //aggiorniamo la tabella [self showContatti]; }

6. Tocchi finali

Per amor di evitare crash aggiungiamo un check in deleteContatto.
Controlliamo che currentRecord sia maggiore o uguale a zero prima di cancellare l’oggetto.

- (IBAction)deleteContatto {
	if (currentRecord >= 0) {
		//Otteniamo il puntatore al NSManagedContext
		NSManagedObjectContext *context = [self managedObjectContext];
	
		//usiamo currentRecord per agire sul record selezionato
		[context deleteObject:[contattiList objectAtIndex:currentRecord]];
	
		//Effettuiamo il salvataggio gestendo eventuali errori
		NSError *error;
		if (![context save:&error]) {
			NSLog(@"Errore durante il salvataggio: %@", [error localizedDescription]);
		}
	
		//resettiamo surrentRecord
		currentRecord = -1;
	
		//puliamo i campi
		[self clearFields];
	
		//aggiorniamo la tabella
		[self showContatti];
	}
}

7. Abbiamo finito

Adesso è il momento di testare il programma, aggiungendo, modificando e cancellando record.
Il codice come al solito è sul mio github.

Riassunto

Abbiamo elencato le differenze fra Core Data e i database, abbiamo creato una classe Model con la procedura automatica, l’abbiamo utilizzata nell’app, abbiamo aggiunto la possibilità di modificare e cancellare i record.

Nella prossima puntata

Come inserire dati da un semplice file txt a Core Data.

Share this story:
  • tweet

Tags: core dataNSEntityDescriptionNSFetchRequestNSManagedObjectNSManagedObjectContextNSManagedObjectModelNSPersistentStoreCoordinatorNSPredicateSQLiteTutorial PraticiXML iPhone

Recent Posts

  • Parte il percorso programmatori iOS in Swift su devACADEMY.it

    20 Dicembre 2017 - 0 Comment
  • Android, crittografare dati velocemente con Encryption

    24 Settembre 2018 - 0 Comment
  • Sql2o, accesso immediato ai database tramite Java

    3 Settembre 2018 - 0 Comment
  • Okio, libreria per ottimizzare l’input/output in Java

    27 Agosto 2018 - 0 Comment

Related Posts

  • Android e SQLite: comandi precompilati con SqliteStatement

    18 Dicembre 2017 - 0 Comment
  • Android app con database interno: guida passo passo

    27 Novembre 2017 - 2 Comments
  • Android e database: operazioni rapide con la classe DatabaseUtils

    21 Novembre 2017 - 0 Comment

Author Description

9 Responses to “T#082 – Guida all’uso di Core Data (Parte 2)”

  1. 23 Dicembre 2010

    Tweets that mention Guida all'uso di Core Data (Parte 2) iPhone e iPad | devAPP -- Topsy.com

    […] This post was mentioned on Twitter by Rynox. Rynox said: RT @iPhone_devAPP: Guida all’uso di Core Data (Parte 2) http://www.devapp.it/wordpress/t082-guida-alluso-di-core-data-parte-2.html […]

  2. 23 Dicembre 2010

    Fra

    Salve ottimo tutorial 🙂
    Vorrei chiedere una cosa: come faccio a rilevare il testo in un UILabel ad esempio abilitare un bottone solo se c’è un testo in una UILabel? Grazie

  3. 23 Dicembre 2010

    doppioslash

    Grazie 🙂

    Devi definire due IBOutlet nel file.h del tuo viewcontroller: una per l’UILabel e una per l’UIButton, collegarle alla label e al bottone in IB, sintetizzare le proprietà, e nel file .m, nel metodo dove vuoi effettuare il check, invocare il metodo di NSString isEqualToString sulla proprietà text della UILabel, con argomento il testo da rilevare:

    if ([label2.text isEqualToString:@”Testo da rilevare”]) {
    NSLog(@”E’ uguale”);
    [button1 setEnabled:YES];
    } else {
    NSLog(@”E’ diverso”);
    [button1 setEnabled:NO];
    }

    Nella comparazione fra oggetti isEqualToString:, (e gli altri metodi equivalenti, isEqualToArray:, etc) è da preferire a == (Cocoa Fundamentals Guide, Cocoa Objects, Object Comparison)

  4. 30 Dicembre 2010

    SoleLuna

    Rieccomi 🙂

    Sto facendo alcuni test prendendo spunto da questo tutorial, come dicevo nei commenti della prima parte, sto realizzando un app con più view (view controller + xib) ho passato senza problemi il riferimento NSManagedContex alla view che utilizzera coredata.

    Ora all’interno di questa view ho la necessita di aprirne una seconda, una subview,
    su questa non ho necessita di accedere a coredata quindi non ho passato nessun riferimento a NSManagedContex, devo pero visualizzare dei valori, con riferimento al tutorial, dovrei visualizzare il valore di “contatto.nome”, ho passato il riferimento alla classe “contatto” da una view all’altra, tutto funziona correttamente, ed ho ottenuto ciò che volevo, ora pero per curiosità ho provato a modificare il valore di contatto.nome dalla subview e con stupore ho notato che la modifica è persistente, non avendo passato nessun riferimento a coredata, è forse merito della classe contatto?

    grazie ancora e buon 2011 a tutti 🙂

  5. 29 Gennaio 2011

    le0n

    Ciao, ottimo articolo, aspetto i successivi 😉
    mi permetto di aggiungere una piccola modifica, all’interno di showContatti, prima dell’istruzione “contattiList = [fo retain];” sarebbe meglio fare un release di contattiList 😉

  6. 22 Marzo 2011

    Paolo

    Ciao, sono nuovo della programmazione iphone. Mi sono impantanato quando dici di collegare la tableview. Non so come procedere nel settare una tableview “stand alone” correttamente … mi puoi segnalare se esiste un tutorial per colmare questo gap? Grazie

  7. 3 Luglio 2011

    Pipesif

    Ciao,
    Complimenti ottimo tutorial….. volevo però chiere.. a quanto la parte 3???

  8. 31 Agosto 2011

    NDS

    Complimenti per gli articoli sul Core Data.
    Aspetto anche io la parte 3.
    🙂

  9. 5 Maggio 2012

    frankp2138

    Ciao ho trovato la dimostrazione molto utile, ma devo dire per me che ho iniziato da poco ci sono delle poco chiare, ho eseguito le tue istruzioni ma l’applicazione va in crash, mi d a error nella parte di scrittura nel data base, e come se non lo trovasse oppure non ho eseguito dei passaggi che non erano indicati.
    ti sarei molto grato se potresti aiutarmi.
    grazie

Leave a Reply

Your email address will not be published. Required fields are marked *


*
*

Corso online di programmazione android e java

SEZIONI

  • Android
  • Comunicazioni
  • Contest
  • Corsi ed Eventi
  • Corso completo di C
  • Corso programmazione videogiochi
  • Framework
  • Grafica e Design
  • Guida rapida alla programmazione Cocoa Touch
  • Guide Teoriche
  • Guide varie
  • iPad
  • Le nostre applicazioni
  • Libri e manuali
  • Materiale OpenSource
  • News
  • Pillole di C++
  • Progetti completi
  • Risorse utili
  • Strumenti di Sviluppo
  • Swift
  • Tips & Tricks
  • Tutorial Pratici
  • Video Tutorial
  • Windows Phone

Siti Amici

  • Adrirobot
  • Allmobileworld
  • Apple Notizie
  • Apple Tribù
  • Avvocato360
  • Blog informatico 360°
  • bubi devs
  • fotogriPhone
  • GiovaTech
  • iApp-Mac
  • iOS Developer Program
  • iPodMania
  • MelaRumors
  • Meritocracy
  • SoloTablet
  • TecnoUser
  • Privacy & Cookie Policy
©2009-2018 devAPP - All Rights Reserved | Contattaci
devAPP.it è un progetto di DEVAPP S.R.L. - Web & Mobile Agency di Torino
Str. Volpiano, 54 - 10040 Leini (TO) - C.F. e P.IVA 11263180017 - REA TO1199665 - Cap. Soc. € 10.000,00 i.v.

devACADEMY.it

Vuoi imparare a programmare?

Iscriviti e accedi a TUTTI i corsi con un’unica iscrizione.
Oltre 70 corsi e migliaia di videolezioni online e in italiano a tua disposizione.

ISCRIVITI SUBITO