Guida di riferimento per i progetti Swift gestiti da Tiknil e i suoi collaboratori.
L'obiettivo è darsi delle best practices sulla stesura del codice per agevolare il lavoro in team e velocizzare la comprensione del codice.
Linee guida da cui è stato preso spunto per scrivere questo documento:
In generale Tiknil adotta tutte le linee guida di RayWenderlich e in questo documento riportiamo quelle che consideriamo più importanti ed eventuali modifiche o integrazioni ad esse.
Per facilitare la lettura del codice seguiamo i soprattutto i seguenti principi:
- chiarezza > brevità
- usare
CamelCase
, maisnake_case
- preferire metodi e proprietà a funzioni
- evitare overload di metodi modificando solo il tipo ritornato
Usare la lingua Inglese per il codice, quella Italiana per i commenti e la documentazione del codice (ove non espressamente richiesta la lingua inglese)
👍
let myColor = UIColor.white
👎
let mioColore = UIColor.white
Tipi (classi) e protocolli in UpperCamelCase
Qualsiasi altra cosa in LowerCamelCase
👍
class MyAwesomeClass { ... }
struct MyAwesomeStruct { ... }
let constant = "http://www.tiknil.com"
var variable = 3
👎
class myAwesomeClass { ... }
struct my_awesome_struct { ... }
let _constant = "http://www.tiknil.com"
var Variable = 3
Ove possibile lasciare contestualizzare al compilatore per migliorare la leggibilità del codice.
👍
let selector = #selector(viewDidLoad)
view.backgroundColor = .red
let toView = context.view(forKey: .to)
let view = UIView(frame: .zero)
👎
let selector = #selector(ViewController.viewDidLoad)
view.backgroundColor = UIColor.red
let toView = context.view(forKey: UITransitionContextViewKey.to)
let view = UIView(frame: CGRect.zero)
Nelle classi utilizzare lo snippet di codice per la generazione dei MARK
in modo da separare uniformemente il codice in tutte le classi secondo la seguente struttura:
// MARK: - Properties
// MARK: Class
// MARK: Public
// MARK: Private
// MARK: - Methods
// MARK: Class
// MARK: Lifecycle
// MARK: Public
// MARK: Private
Implementare eventuali protocolli creando extension
della classe per separare logicamente il codice per contesto.
👍
class MyViewController: UIViewController {
// codice di classe
}
// MARK: - UITableViewDataSource
extension MyViewController: UITableViewDataSource {
// implementazione dei metodi di UITableViewDataSource
}
// MARK: - UIScrollViewDelegate
extension MyViewController: UIScrollViewDelegate {
// implementazione dei metodi di UIScrollViewDelegate
}
👎
class MyViewController: UIViewController, UITableViewDataSource, UIScrollViewDelegate {
// tutti i metodi
}
Sempre per migliorare la leggibilità, in generale, è meglio rimuovere:
- codice non più utilizzato o sostituito da altre parti di codice
- vecchio codice commentato
- metodi che chiamano semplicemente la
superclass
👍
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return elements.count
}
👎
// Questo metodo non aggiunge nessuna implementazione specifica quindi è meglio ometterlo
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
override func numberOfSections(in tableView: UITableView) -> Int {
// #warning Incomplete implementation, return the number of sections
return 1
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
// #warning Incomplete implementation, return the number of rows
return elements.count
}
- Indentare usando 2 spazi. Impostare Xcode nel modo seguente da
Preferences > Text Editing > Indentation
: - Le parentesi graffe
{
}
vanno sempre aperte sulla stessa riga e chiuse su un'altra riga.
CONSIGLIO: Si può indentare automaticamente premendo
⌘+A
(seleziona tutto) eControl+i
(indentazione automatica)
👍
if user.isHappy {
// Do something
} else {
// Do something else
}
👎
if user.isHappy
{
// Do something
}
else {
// Do something else
}
- I due punti
:
hanno sempre uno spazio a destra e zero a sinistra. Eccezioni: operatore ternario? :
, dizionario vuoto[:]
e#selector
per metodi con paramteri senza nome(_:)
.
👍
class TestDatabase: Database {
var data: [String: CGFloat] = ["A": 1.2, "B": 3.2]
}
👎
class TestDatabase : Database {
var data :[String:CGFloat] = ["A" : 1.2, "B":3.2]
}
Apple utilizza nel proprio SDK il design pattern MVC (Model-View-Controller).
Tale pattern normalmente promette di separare in 3 livelli il codice:
- Il Model è dove risiedono i dati dell'app. Persistenza, oggetti che rappresentano i dati, parser e networking sono normalmente in questo livello.
- Il livello View è la UI dell'app. Le sue classi rappresentano un elemento visibile dall'utente e dovrebbero essere tipicamente riusabili come ad esempio un pulsante.
- Il Controller ha il compito di prendere i dati dal Model e mostrarli nelle View ed elaborare le azioni dell'utente.
I legami tra i 3 layer sono rappresentabili così:
Per come è strutturato UIKit la realtà è che Apple utilizza un MVC leggermente modificato:
In pratica View e Controller risultano molto legati portando a scrivere la maggioranza del codice nei UIVIewController. In ambito iOS viene infatti spesso associata un'altra definizione all'acronimo MVC: Massive-View-Controller.
I problemi introdotti da questo pattern modificato sono:
- Il codice è difficilmente testabile
- Scarsa riusabilità delle view
Per risolvere tali problemi Tiknil ha deciso di utilizzare il pattern MVVM descritto di seguito.
Il design pattern MVVM (Model-View-ViewModel) applicato all'SDK di Apple risulta una naturale evoluzione di MVC:
Il livello View comprende quindi sia UIView che UIViewController:
L'obiettivo del ViewModel è di essere una rappresentazione astratta della view a cui è associato. Ad esempio se la view deve mostrare nome e cognome dell'utente il viewmodel avrà due proprietà di tipo string che contengono queste informazioni.
Ciò introduce i seguenti vantaggi:
- È possibile sostituire la view associata al viewmodel senza problemi.
- È possibile separare dalla view eventuali trasformazioni dei dati presenti nel model. Esempio: formattazione di una data in base alla lingua. Questa operazione viene normalmente chiamata Presentation logic.
- È possibile eseguire test funzionali direttamente sul viewmodel.
Per un'analisi più dettagliata dell'evoluzione MVC => MVVM leggere questo articolo di Ash Furrow.
L'Inversion of Control (IoC) è un software design pattern secondo il quale ogni componente dell'applicazione deve ricevere il controllo da un componente appartenente ad una libreria riusabile.
L'obiettivo è quello di rendere ogni componente il più indipendente possibile dagli altri in modo che ognuno sia modificabile singolarmente con conseguente maggior riusabilità e manutenibilità.
La Dependency Injection (DI) è una forma di IoC dove l'implementazione del pattern avviene stabilendo le dipendenze tra un componente e l'altro tramite delle interfacce (chiamate interface contracts).
A tali interfacce viene associata un'implementazione in fase di istanziazione del componente (nel costruttore) oppure in un secondo momento tramite setter.
In ogni caso è generalmente presente un oggetto container che si occupa di creare le istanze di ogni interfaccia; la configurazione di tale container può così influenzare le dipendenze tra i vari componenti.
L'utilizzo della DI è molto utile per la realizzazione di test automatici, infatti modificando il container è possibile mockare le dipendenze che non si desidera testare.
References:
Nelle seguenti sezioni definiamo le best practices di Tiknil per l'impostazione di un progetto iOS in Swift chiamato AwesomeApp.
La root del repository avrà la seguente struttura:
|-- .git # Working copy di git
|-- AwesomeApp # Codice sorgente dell'app
|-- AwesomeAppTests # Unit test automatici
|-- AwesomeAppUITests # Test automatici di UI
|-- Pods # Cartella contenente le librerie CocoaPods
|-- Podfile.lock # Gestione versioni dei pods. Gestito automaticamente da CocoaPods
|-- .gitignore # Specifica i file da escludere dal repo
|-- Podfile # Configurazione dei pod
|-- Readme.md # Readme con modifiche di versione
|-- AwesomeApp.xcodeproj # File di progetto. Non va utilizzato
|-- AwesomeApp.xcworkspace # File del workspace contenente configurazione dei CocoaPods.
Prima del primo commit sul repository git aggiungere il file .gitignore.
La cartella contenente il codice sorgente dell'app avrà la seguente struttura:
|-- Helpers # Classi di generico aiuto per tutto l'app. Es: Colors.swift
|-- Models # Tutti gli oggetti model
|-- ViewModels # Tutti i viewmodel eventualmente inseriti in sottocartelle di sezione
|-- Views # Tutti i viewcontroller eventualmente inseriti in sottocartelle di sezione
|-- Reusable # Tutte le view riutilizzabili in altre view. Es: navigation bar custom
|-- UI # Storyboards e xib.
|-- Reusable # Xib relativi alle view nella cartella Views/Reusable
|-- Services # Oggetti che forniscono servizi come networking e persistenza
|-- Libraries # Librerie create da Tiknil non importate come submodule
|-- Vendors # Librerie di terzi non importate con CocoaPods o submodule
|-- Resources # Assets, fonts, ecc
|-- Application # Info.plist, AppDelegate.swift ed eventual bridging-header.h
Le cartelle al primo livello le creiamo fisicamente nel file system e le importiamo in modo che creino il gruppo logico nel progetto Xcode, mentre quelle al secondo livello possiamo anche lasciarle solo come gruppi logici.
Al seguente link è disponibile il codice di un'applicazione di esempio che integra tutte le best practice definite in questo documento: