Struct in SwiftUI
La keyword struct
, per il linguaggio Swift, permette di definire nuovi Type
ovvero nuove fabbriche di oggetti. Puoi paragonare una struct
ad un contenitore che consente di raggruppare informazioni eterogenee come String
, Array
etc e, perché no, anche altre struct
.
Creare nuovi tipi di dato utilizzando la keyword struct
è alla base di un concetto informatico che prende il nome di encapsulation ed information hiding. Infatti, nascondere l'implementazione di logiche o oggetti complessi dietro una struct
permette a noi sviluppatori di poter creare applicazioni concentrandoci solamente sull'interazione di quest'ultimi.
Durante il corso SwiftUI Tour abbiamo incontrato diverse struct
. Quasi tutti gli oggetti di SwiftUI o del framework Foundation nascono da struct
: Text
, List
, String
, Date
etc. Ed in effetti, grazie al fatto che l'implementazione di questi oggetti è racchiusa dietro una struct
, ci ha permesso di utilizzare List
, per esempio, senza preoccuparci di come questo componente riesce a trasformare un Array
in una colonna di views.
Se da un lato struct
ci permetterà di nascondere implementazioni, dall'altro lato ci aiuterà a rappresentare oggetti che non sono codificati all'interno dei framework Apple e che saranno essenziali per dare forma alla logica delle tue applicazioni.
Infatti, immagina di voler creare un'app come Spotify dove una canzone è composta da diverse informazioni come autore, link, nome, descrizione etc.
Grazie alle struct
potrai creare un nuovo type, chiamato Song
, che al suo interno contiene delle proprietà (aka var/let) o metodi (aka func
) che ti permetterà di rappresentare canzoni:
struct Song {
let name: String
let author: Author
let link: URL
private(set) var listeningCount: Int = 0
mutating func play() {
print("Playing:", name)
// play music on device
// and increment listeningCount
listeningCount += 1
}
func fetchAllAlbums() -> [Album] {
// complex logic to get albums
return []
}
}
var songA = Song(name: "Ho Hey", author: .lumineers, link: URL(string: "https://www.spotify.com/..."))
var songB = Song(name: "Savior", author: .riseAgainst, link: URL(string: "https://www.spotify.com/..."))
songA.play()
songA.name
songB.fetchAllAlbums()
Quindi, capito quanto essenziali saranno le struct
durante il processo di creazione di un'app, vediamo insieme come utilizzarle!
A fine lezione, creeremo la seguente applicazione:
Come creare una struttura in Swift
Apri il Playground e partiamo dalla sintassi necessaria per creare nuove struct
con il linguaggio Swift. La sintassi richiede la keyword struct
seguita dal nome del nuovo type in lettera maiuscola ed un blocco di parentesi graffe:
struct Recipe {
}
In informatica il blocco di codice racchiuso all'interno delle parantesi graffe prende il nome di
scope
della struttura.
Dato che abbiamo creato un nuovo type, sappiamo dalla lezione sui tipi di dato che possiamo inizializzare degli oggetti utilizzando un costruttore o init
di default.
let carbonara = Recipe()
let hamburger = Recipe()
Aggiungere nuove proprietà ad una struct
Per proprietà di una struct
si intende una var
o let
definita all'interno del suo scope. Potrai aggiungere quante proprietà vorrai purché queste abbiano un senso logico ai fini del descrivere il tuo nuovo type.
Per esempio, al nostro type Recipe
potremmo aggiungere un name
ed un Array
di ingredients
:
struct Recipe {
let name: String
let ingredients: [String]
}
Se le proprietà non vengono inizializzate al momento della loro definizione, queste dovranno essere inizializzate quando l'oggetto che li contiene verrà costruito. Infatti Xcode ci segnala l'errore Missing arguments for parameters name, ingredients in call
:
Nel nostro caso sia name
che ingredients
non hanno un valore di partenza, di conseguenza Swift ci forza a passarglieli al momento della costruzione di un oggetto Recipe
:
let carbonara = Recipe(name: "Carbonara", ingredients: ["4 yolks", "250g pasta"])
let hamburger = Recipe(name: "Hamburger", ingredients: ["Bun", "Lettuce", "100g burger"])
Immutable vs Mutable object
In base a ciò che stai costruendo e rappresentando dovrai decidere quando utilizzare proprietà che sono mutabili var
o immutabili let
.
In linea di massima, se i tuoi oggetti non possono essere modificati da utente, per esempio tramite un Form
o da qualche logica, il consiglio generale è quello di utilizzare let
. Per esempio, se stai sviluppando un'app che scarica un feed di ricetta da un server e queste non possono essere modificate, tutte le proprietà all'interno potranno essere definite come let
.
Di conseguenza, un oggetto che contiene solamente let
viene chiamato immutable, mentre uno che ha almeno una proprietà var
viene chiamato mutable.
Definire init
in una struct
Fin ora abbiamo utilizzato il costruttore di default che Swift riesce a dedurre in base alle proprietà della struttura.
Nel caso in cui volessi manualmente definire il costruttore, potrai farlo aggiungendo all'interno dello scope della struct
la funzione speciale chiamata init
.
All'interno di un init
dovrai inizializzare, ovvero passare un valore, a tutte le proprietà che non hanno valori di default:
struct Point {
let x: Int
let y: Int
init() {
x = -1
y = -2
}
}
let pointA = Point() // or Point.init()
pointA.x // -1
pointA.y // -2
Una volta definito un init
potrai invocarlo con la sintassi che già conosci per creare nuovi oggetti: o scrivi il nome Type
seguito da parentesi tonde oppure Type
seguito da .init()
.
Nel caso in cui volessi passare dei valori dall'esterno, potrai aggiungere dei parametri all'interno delle tonde, divisi da virgole:
struct Point {
let x: Int
let y: Int
init() {
x = 0
y = 0
}
init(x: Int, y: Int) {
self.x = x
self.y = y
}
}
let pointB = Point(x: 10, y: 5)
let pointC = Point(x: 7, y: 23)
Per il linguaggio Swift non ci sono limitazioni al numero di
init
che potrai definire.
Esercizio 0. Crea un init
Come primo esercizio, prova a creare i seguenti init
alla struttura Point
:
init
che permette di inizializzarex
edy
con lo stesso valore.init
che accetta valori dix
edy
di tipoDouble
. Dovrai eseguire una conversione.
Cos'è self
?
self
è una keyword speciale del linguaggio Swift che, quando utilizzata all'interno dello scope di una struct
(verrà anche per altri costrutti), permette di far riferimento alle sue proprietà e metodi.
Di default, quando scrivi all'interno di un type e non ci sono ambiguità, la keyword self
viene automaticamente dedotta da Swift e non ti servirà specificarla manualmente.
Per esempio, nel caso dell'init
senza parametri, abbiamo inizializzato x
ed y
senza specificare il self
.
Quando invece ci sono parametri esterni che hanno lo stesso nome delle proprietà della struct
, allora dovrai inserire la keyword self
se vuoi far riferimento a quest'ultime. Nell'esempio di init(x: Int, y: Int)
abbiamo dovuto utilizzare il self
per far capire a Swift che stiamo inizializzando le proprietà della struttura.
In linea generale non ti servirà specificare il self
a meno di casi eccezionali ed uno di questi è l'inizializzazione di proprietà all'interno di un costruttore con nomi identici.
Computed property
Nella lezione delle variabili e state abbiamo visto come poter definire variabili di tipo get-only, ovvero che restituiscono un valore quando lette.
Nel caso delle struct
possiamo utilizzare get only property, per esempio, per combinare valori di altre proprietà tra loro:
struct Point {
var x: Int
var y: Int
var description: String {
"(x: \(x), y: \(y))"
}
}
var pointA = Point(x: 10, y: 15)
pointA.description // "(x: 10, y: 15)"
pointA.x = -5
pointA.description // "(x: -5, y: 15)"
Anche la
var body: some View
è una get-only computed property.
Esistono anche tipologie di computed-property che possono essere costruiti utilizzando gli operatori
get
,set
,didSet
ewillSet
. Esempio:var favourite: Bool = false { didSet { print("has been set to: \(favourite), before was: \(oldValue)") } } favourite = true // has been set to: true, before was: false
mutating func
Anche se già sai come si crea e definisce una funzione una menzione particolare va data alle funzioni che modificano le proprietà di una struttura.
Infatti, quando una func
modifica una var
interna ad una struttura, queste devono essere precedute dalla keyword mutating
:
struct Song {
let title: String
var isPlaying: Bool = false
mutating func play() {
isPlaying = true
}
mutating func stop() {
isPlaying = false
}
}
var songA = Song(title: "Sweet Home Alabama")
songA.play()
songA.isPlaying // true
Struct in SwiftUI, creiamo un ricettario
Adesso che abbiamo visto creare strutture all'interno del Playground, vediamo come possono esserci d'aiuto in un progetto SwiftUI.
Immaginiamo di voler creare un app per ricette, quest'app è composta da una TabBar
con due view principali: una per l'inserimento di una nuova ricetta e l'altra per la visualizzazione di tutte le ricette inserite.
Cominciamo dal creare la struttura che rappresenterà una ricetta. Per semplicità ipotizziamo che una ricetta sia composta dai seguenti attributi di base:
struct Recipe {
var title: String
var description: String
/// time in seconds
var time: TimeInterval
}
TimeInterval
è il type generalmente utilizzato per rappresentare secondi.Il commento con triplo slash
///
permette di attaccare quella descrizione alla variabile. Se usi il Quick Help noterai che il commento viene visualizzato al suo interno.
Mancano ovviamente pezzi fondamentali come ingredienti e gli steps da eseguire. Potremmo utilizzare una nuova struct
per rappresentare entrambi i concetti.
struct Ingredient {
var name: String
var quantity: String
}
struct RecipeStep {
var description: String
}
Per semplificare il progetto la
quantity
è rappresentata da unaString
. Se vuoi complicare il progetto potresti utilizzare il typeDouble
ed aggiungere unaquantityDescription: String
per specificare l'unità di misura.Se invece vuoi rendere l'app professionale e pronta ad essere utilizzata in più paesi, puoi utilizzare il type
Measurement
che permette di rappresentare unità di misura in maniera strutturata.
Adesso che abbiamo i due type Ingredient
e RecipeStep
possiamo aggiungerli all'interno della struct Recipe
sotto forma di array di entrambi i type.
Infine aggiungiamo due init
, uno che ci permette di specificare tutti i parametri e l'altro che crea un oggetto con tutti i parametri con valore di default.
struct Recipe {
var title: String
var description: String
/// time in seconds
var time: TimeInterval
var ingredients: [Ingredient]
var steps: [RecipeStep]
init(
title: String,
description: String,
time: TimeInterval,
ingredients: [Ingredient],
steps: [RecipeStep]
) {
self.title = title
self.description = description
self.time = time
self.steps = steps
self.ingredients = ingredients
self.steps = steps
}
init() {
self.title = ""
self.description = ""
self.time = 0
self.ingredients = []
self.steps = []
}
}
La cartella Models
Dove definiamo i type all'interno di un progetto?
Ci sono diverse scuole di pensiero, la più intuitiva ed utile da utilizzare in un piccolo progetto è quella di creare una cartella chiamata Models
in cui inserire tutte le definizioni dei nostri custom type.
Per creare una cartella, ti basta fare tap destro sulla cartella principale, seleziona New Group
, rinomina la cartella in Models
. Tap destro su Models
e seleziona New file...
chiama il file con lo stesso nome del type che vuoi definire, nel nostro caso Recipe
(non è obbligatorio ma ti aiuterà a ricordarti che type c'è dentro quel file).
Incolla all'intero del file Recipe
tutte le definizioni viste poco fa: Recipe
, Ingredients
e RecipeStep
:
Adesso i type definiti in quel file fanno ufficialmente parte del vocabolario dei type utilizzabili all'interno del progetto.
Passiamo alla creazione delle user interface.
TabView
Il componente TabView
si comporta da contenitore dove ogni view che utilizza il modifier tabView
viene considerata una tab:
TabView {
VStack {
Text("Home")
}
.tabItem {
Label("Home", systemImage: "house.fill")
}
VStack {
Text("Account")
}
.tabItem {
Label("Account", systemImage: "person.crop.circle")
}
}
I nomi delle icone puoi prenderle dall'app SF Symbol che ho spiegato nel tutorial sulle Image in SwiftUI.
Dato che ogni singola tab avrà al suo interno views e state, ti consiglio di dividere il layout in componenti riutilizzabili per massimizzare la leggibilità e mantenibilità del codice.
Nel caso di una TabView
il consiglio è di dividere ogni tab in un suo file e definizione.
All'interno del progetto, crea una nuova cartella chiamata Views
ed al suo interno aggiungi una sotto cartella Recipes
ed EditRecipe
. Al loro interno crea un nuovo SwiftUI View
file con lo stesso nome seguito dal suffisso View
.
Se hai avuto qualche problema, dai uno sguardo alla struttura del mio progetto:
Infine, passa le due nuove view all'interno della TabView
contenuta nel file ContentView
:
TabView {
RecipesView()
.tabItem {
Label("Home", systemImage: "house.fill")
}
EditRecipeView()
.tabItem {
Label("New Recipe", systemImage: "plus.circle.fill")
}
}
All'interno del file EditRecipeView
definire un Form
per la creazione di nuove ricette, mentre in RecipesView
mostreremo tutte le ricette create.
EditRecipeView
Partiamo dalla funzionalità di edit di una ricetta. Creeremo un semplice Form
con all'interno delle sezioni che ci permetteranno di editare i vari elementi di un oggetto Recipe
.
State e private
La prima cosa da fare è definire lo @State
che ci permetterà di modificare/creare la ricetta. Se non ti ricordi cos'è uno state in SwiftUI lo spiegato in questa lezione.
Dato che EditRecipeView
può essere utilizzata in altre view, nel nostro caso ContentView
, è importante che i suoi State
vengano definiti come private
ovvero inaccessibili dall'esterno della view:
struct EditRecipeView: View {
@State
private var recipe = Recipe()
La keyword private
è un modificatore d'accesso, in pratica sarà accessibili solamente all'interno dello scope in cui è stata definita. Grazie a private
Swift non sintetizzerà un costruttore di default con il parametro recipe
.
Infatti, senza il private
, potresti creare la RecipesView
passando un oggetto Recipe
ovvero RecipesView(recipe: Recipe())
.
Nel contesto di SwiftUI questo è sconsigliato, in quanto la gestione di uno State
è legato alla view che lo definisce e, la modifica dall'esterno, potrebbe modificare o resettare il valore gestito dallo State
in maniera inaspettata.
Ad ogni modo, non entriamo troppo in questi dettagli, più in là farò un tutorial dedicato alla gestione degli state in SwiftUI.
In linea di massima, il consiglio è di definire tutto come private
e lasciare solamente variabili di tipo Binding
ed il var body
come non-private.
Le sezioni di partenza
Partiamo definendo il backbone della view e cominciamo aggiungendo quattro Section
:
infoSection
definiremo i componenti per la modifica delrecipe.title
erecipe.time
.descriptionSection
aggiungeremo unTextEditor
per la scrittura di una descrizione multi linee.ingredientsListSection
mostreremo tutti gli ingredienti ed aggiungeremo la possibilità di modificare gliIngredient
.stepsListSection
simile allaingredientsListSection
la useremo per modificare gli step della ricetta.
Di conseguenza, questo sarà il layout di partenza:
struct EditRecipeView: View {
@State private var recipe = Recipe()
var body: some View {
Form {
infoSection
descriptionSection
ingredientsListSection
stepsListSection
}
}
private var infoSection: some View {
Section {
TextField("Title", text: $recipe.title)
} header: {
Text("Info")
}
}
private var descriptionSection: some View {
Section {
TextEditor(text: $recipe.description)
.frame(minHeight: 80, maxHeight: 150)
} header: {
Text("Description")
}
}
private var ingredientsListSection: some View {
Section {
} header: {
Text("Ingredients")
}
}
private var stepsListSection: some View {
Section {
} header: {
Text("Steps")
}
}
}
Aggiungere un nuovo ingrediente
Cominciamo riempendo la sezione degli ingredienti ed aggiungiamo un Button
suggerisce l'utente di aggiungere il primo ingrediente. Quindi, in base al recipe.ingredients.isEmpty
mostreremo questo bottone:
// dentro la section ingredients
if recipe.ingredients.isEmpty {
Button("Add your first ingredient", action: addEmptyIngredient)
}
Come action
ho passato il riferimento ad una funzione, questo perché non voglio mischiare codice UI con la logica dell'app. Quindi, sotto la Section
o meglio sotto tutte le definizioni delle tue view, crea una funzione senza parametri chiamata addEmptyIngredient
:
private func addEmptyIngredient() {
}
Useremo questa funzione per creare un ingrediente vuoto che l'utente dovrà riempire. Vediamo come.
Innanzitutto creiamo un nuovo State
chiamato editIngredient: Ingredient?
. A questo state diamo type opzionale perché verrà creato solamente quando l'utente utilizzerà la funzione addEmptyIngredient
.
@State private var editIngredient: Ingredient?
Ho spiegato gli opzionali in Swift in questa lezione. In breve, permettono di rappresentare l'assenza di valori.
Adesso, all'interno della funzione addEmptyIngredient
creiamo un nuovo oggetto Ingredient
ed assegniamolo allo state.
All'interno della Section
invece, utilizziamo un if editIngredient != nil
per mostrare due TextField
che ci permetteranno di modificare l'ingrediente:
if {
TextField("Name", text: Binding(
get: { self.editIngredient?.name ?? "" },
set: { self.editIngredient?.name = $0 }
))
TextField("Quantity", text: Binding(
get: { self.editIngredient?.quantity ?? "" },
set: { self.editIngredient?.quantity = $0 }
))
}
Nota come ho creato un Binding
manualmente definendo le sue funzioni get
e set
. Purtroppo in SwiftUI, non possiamo creare un binding di uno State
opzionale utilizzando il simbolo $
.
Se adesso provi ad aggiungere un nuovo ingrediente dovresti vedere i due field comparire subito sotto:
Adesso non ci rimane che aggiungere il nuovo ingrediente alla lista degli ingredienti della ricetta che stiamo creando.
Partiamo dall'aggiungere un Button
dopo i Fields che invoca una funzione saveNewIngredient
al tap ed è disattivato quando il nome dell'ingrediente è isEmpty
:
Button("Add", action: saveNewIngredient)
.disabled(editIngredient?.name.isEmpty ?? true)
La funzione saveNewIngredient
invece aggiungerà l'editIngredient
alla ricetta e resetterà la variabile subito dopo:
private func saveNewIngredient() {
// unwrap `editIngrediet` se esiste, altrimenti esce dalla funzione
guard let editIngredient else { return }
recipe.ingredients.append(editIngredient)
// resetta editIngredient così da poterne aggiungere uno nuovo
self.editIngredient = nil
}
Nota come ho utilizzato self.editIngredient
, infatti senza il self
la parola editIngredient
fa riferimento alla let editIngredient
definita nel guard
che non può essere modificata. Quindi, utilizzando il self
stiamo facendo riferimento alla variabile definita nello scope esterno.
Infine, modifichiamo il bottone Add your first ingredient
facendo si che questo scompaia quando l'utente sta aggiungendo un nuovo ingrediente e cambi il suo testo in Add ingredient
quando la ricetta contiene ingredienti:
if editIngredient == nil {
let title = recipe.ingredients.isEmpty
? "Add your first ingredient"
: "Add ingredient"
Button(title, action: addEmptyIngredient)
}
Infine, utilizziamo un ForEach
per mostrare la lista degli ingredienti:
ForEach(recipe.ingredients, id: \.self) { ingredient in
HStack {
Text(ingredient.name)
Spacer()
Text(ingredient.quantity)
}
}
Pe utilizzare il ForEach
sugli ingredienti il type Ingredient
dovrà implementare il protocollo Hashable
, ti basterà aggiungere questo protocollo alla definizione del type Ingredient
:
struct Ingredient: Hashable
Eliminare un ingrediente
Per aggiungere la gesture di eliminazione di un ingrediente possiamo aggiungere il modifier onDelete
al componente ForEach
.
Questo modifier passa come parametro un IndexSet
, ovvero l'indice o indici degli elementi da eliminare, che potremo passare al metodo remove(atOffsets:
dell'array ingredients
per eliminare l'elemento selezionato dall'utente.
Definiamo questa logica all'interno di una nuova funzione chiamata deleteIngredient
:
private func deleteIngredient(at offset: IndexSet) {
recipe.ingredients.remove(atOffsets: offset)
}
Ed infine, passiamo questa funzione al metodo onDelete
del componente ForEach
:
ForEach(recipe.ingredients, id: \.self) { ingredient in
}
.onDelete(perform: deleteIngredient(at:))
Il modifier
onDelete
aggiunge in automatico tutto il supporto alla gesture classica di eliminazione di una riga da una lista in SwiftUI. Documentazione qui.
Esercizio 1. Aggiungere la sezione steps
Come esercizio lascio a te il compito di inserire la sezione per la definizione degli step della lezione. Utilizza lo stesso look and feel della sezione ingredienti come riferimento.
Se dovessi avere qualche problema, al solito, scrivimi sotto nei commenti.
Salvare ricetta e passare oggetti da una view all'altra
Una volta riempiti tutti i campi di una nuova ricetta, dobbiamo salvare e passare quest'ultima dalla EditRecipeView
ad uno storage condiviso che tutte le altre view possono accedere.
Ci sono diverse strategie che si possono utilizzare per passare modifiche ed eventi da una view all'altra. Per esempio, potremmo passare un Binding
di tutte le ricette, potremmo creare un Environment
o potremmo definire una closure che permetta alle view esterna di intercettare certi eventi.
Closure e propagazione di eventi a view esterne
Non c'è una regola generale e tutto dipende da caso a caso. In questo esempio, utilizzerò una closure per passare un evento onEdit
alle view esterne.
In questo modo, evitiamo di creare dipendenze dirette con gli state della nostra applicazione e possiamo testare la view in maniera isolata.
Ho spiegato cos'è una closure nella lezione delle funzioni in Swift. Una closure in Swift è una funzione che può essere passata da un punto all'altro all'interno del tuo codice.
Torniamo alla EditRecipeView
ed aggiungiamo una closure che prende in input una Recipe
chiamata onEdit
:
struct EditRecipeView: View {
private(set) var onEdit: (Recipe) -> ()
Questa onEdit
non ha valore di default e dovrà essere definita al momento della creazione della EditRecipeView
. In questo momento la view è utilizzata in Preview
e nella ContentView
, di conseguenza modifichiamo le due istanze:
// Preview
#Preview {
EditRecipeView { recipe in
}
}
// ContentView
EditRecipeView(
onEdit: onEditRecipe(_:)
)
private func onEditRecipe(_ recipe: Recipe) {
}
Adesso quando l'utente salverà la ricetta, all'interno della EditRecipeView
, potremmo invocare la funzione onEdit
che permetterà la view esterne di intercettare la Recipe
modificata.
Vediamo come fare!
Aggiungiamo una NavigationStack
come root della EditRecipeView
e sul form utilizziamo il modifier toolbar
per aggiungere un bottone di salvataggio:
struct EditRecipeView: View {
// states e var....
var body: some View {
NavigationStack {
Form {
infoSection
descriptionSection
ingredientsListSection
stepsListSection
}
.navigationTitle("New Recipe")
.toolbar {
Button("Save", action: saveRecipe)
}
}
}
// var e funcs..
private func saveRecipe() {
onEdit(recipe)
recipe = Recipe()
}
La NavigationStack
oltre ad aggiungere le funzionalità di navigazione orizzontale all'interno della view ti permetterà di definire una Toolbar
, ovvero una barra di navigazione, in cui potrai aggiungere bottoni e definire un titolo.
Quando l'utente tapperà il bottone Save
la funzione saveRecipe
passerà la recipe
alla closure onEdit
. Tutte le view che hanno definito questa closure riceveranno la ricetta:
Definire State centrali e propagazione a view interne
Infine, dobbiamo salvare questa ricetta in uno State
centrale in modo da poterle poi propagare alla RecipesView
per la visualizzazione.
Dato che sia EditRecipeView
che RecipesView
si trovano all'interno della ContentView
, quest'ultima è la view ideale per la definizione di una variabile State
di tipo [Recipe]
.
struct ContentView: View {
@State private var recipes: [Recipe] = []
Adesso, il metodo onEditRecipe
prenderà la nuova ricetta e la appenderà a questo state:
private func onEditRecipe(_ recipe: Recipe) {
recipes.append(recipe)
}
Ed una volta che lo State
viene correttamente modificato, dobbiamo passare le recipes
alla RecipesView
.
In questo caso, dato che la RecipesView
visualizzerà le ricette, possiamo aggiungere una let recipes
al suo interno:
struct RecipesView: View {
let recipes: [Recipe]
Ed in ContentView
passeremo lo state al parametro recipes
:
struct ContentView: View {
@State private var recipes: [Recipe] = []
var body: some View {
TabView {
RecipesView(recipes: recipes)
In questo modo, ogni qual volta lo State recipes
cambia la view RecipesView
verrà ricalcolata correttamente.
Extension, static func e mock data
Adesso che abbiamo sincronizzato l'operazione di aggiunta con il passaggio delle ricette alla RecipesView
, passiamo a costruire la lista che ci permetterà di visualizzarle.
Innanzitutto dobbiamo rendere la struct Recipe
conforme al protocollo Hashable
. Questo ci permetterà di utilizzare l'array in un ForEach
:
struct Recipe: Hashable {
Nel caso in cui questo protocollo dovesse generare l'errore:
Type 'Recipe' does not conform to protocol 'Equatable'
Dovrai aggiungere la conformità al protocollo Hashable
o Equatable
al type RecipeStep
. Infatti, l'unica proprietà della struct Recipe
che non è Equatable
è var steps: [RecipeStep]
.
Ti consiglio di aggiungere Hashable
al type RecipeStep
dato che ti permetterà di usare l'array in un ForEach
.
Prima di passare alla view, per velocizzare il testing della lista senza dover ogni volta creare una nuova ricetta, potremo creare degli oggetti di testing o mock
che ci permetteranno di settare la Preview
con degli elementi di partenza.
Per farlo potremo creare una funzione statica, ovvero invocabile direttamente dal type su cui viene definita, che ci restituisce una lista di oggetti di testing.
Crea un nuovo file nella cartella Models
chiamato Recipe+Mock
ed al suo interno creiamo una extension Recipe
in cui definiamo una static func mocks() -> [Recipe]
:
extension Recipe {
static func mocks() -> [Recipe] {
[
Recipe(
title: "Pasta alla Carbonara",
description: "Autentico piatto di pasta italiano fatto con uova, formaggio, guanciale e pepe nero.",
time: 900, // 15 minuti
ingredients: [
Ingredient(name: "Spaghetti", quantity: "400g"),
Ingredient(name: "Uova", quantity: "4"),
Ingredient(name: "Guanciale", quantity: "200g"),
Ingredient(name: "Parmigiano", quantity: "100g"),
Ingredient(name: "Pepe Nero", quantity: "A piacere"),
Ingredient(name: "Sale", quantity: "A piacere")
],
steps: [
RecipeStep(description: "Portare a ebollizione una grande pentola d'acqua salata."),
RecipeStep(description: "Cuocere gli spaghetti secondo le istruzioni della confezione fino a che sono al dente."),
RecipeStep(description: "In una padella separata, cuocere la guanciale fino a renderla croccante. Togliere dal fuoco e mettere da parte."),
RecipeStep(description: "In una ciotola, sbattere insieme le uova, il formaggio Parmigiano grattugiato e il pepe nero."),
RecipeStep(description: "Una volta cotta la pasta, scolarla e rimetterla nella pentola mentre è ancora calda."),
RecipeStep(description: "Versare immediatamente il composto di uova sulla pasta calda e mescolare rapidamente per rivestirla. Il calore della pasta cuocerà le uova, creando una salsa cremosa."),
RecipeStep(description: "Aggiungere la guanciale cotta alla pasta e mescolare per combinare."),
RecipeStep(description: "Condire con sale e altro pepe nero a piacere."),
RecipeStep(description: "Servire caldo, guarnendo con ulteriore formaggio Parmigiano e pepe nero se desiderato.")
]
)
]
}
}
Una
extension
ti permette di estendere una definizione di un type aggiungendo nuove funzioni o computed properties. Diventa utilissima quando vuoi evitare di riempire i tuoi type con diverse definizioni o vuoi estendere type esistenti.
Adesso, torna al file RecipesView
e modifica la Preview
passando al campo recipes
il valore Recipe.mocks()
. Infine, crea un'altra Preview
per testare il caso empty:
#Preview("Mock data") {
RecipesView(recipes: Recipe.mocks())
}
#Preview("Empty state") {
RecipesView(recipes: [])
}
Lista delle ricette
Finiamo questa lezione definendo la lista delle ricette, creiamo una semplice List
con un ForEach
che itera sull'array recipe
ed un semplice Text(recipe.title)
. Utilizziamo anche qui un NavigationStack
ed un navigationTitle
.
Infine, utilizziamo un ContentUnavailableView
nel caso in cui la lista sia empty
:
struct RecipesView: View {
let recipes: [Recipe]
var body: some View {
NavigationStack {
List {
if recipes.isEmpty {
ContentUnavailableView {
Label("No recipes", systemImage: "fork.knife.circle")
} description: {
Text("Your recipes will appear here.")
}
}
ForEach(recipes, id: \.self) { recipe in
Text(recipe.title)
}
}
.navigationTitle("Recipes")
}
}
}
Se tutto è andato per il verso giusto, all'aggiunta di una ricetta dovremmo vederla comparire all'interno della lista:
Esercizio 2. Disattiva il bottone Save
quando la ricetta non è pronta per essere salvata
Come esercizio, definisci la logica per disattivare il bottone quando la ricetta non ha i campi title
, description
, steps
e ingredients
.
Un buon sistema potrebbe essere quello di aggiungere una computed-property all'interno della struct Recipe
chiamata canBeSaved
che restituisce true/false
in base a delle condizioni. Poi, potresti utilizzare canBeSaved
per disattivare o meno il bottone:
Button("Save", action: saveRecipe)
.disabled(!recipe.canBeSaved)
Esercizio 3. Crea la RecipeDetailsView
Utilizzando un NavigationLink
ed il modifier navigationDestination(for: Recipe.self)
crea la navigazione e view dei dettagli per le tue ricette. Prova a creare qualcosa di simile a questo:
Conclusione e download progetto
In questa lezione abbiamo visto come una struct
ci può aiutare a rappresentare concetti complessi che racchiudono a loro interno diverse proprietà e funzionalità.
Da qui in avanti, il tuo compito sarà quello di cercare di rappresentare il più possibile funzionalità e componenti della tua applicazione tramite nuovi oggetti.
Nel caso in cui volessi dare un'occhiata al progetto finale, lo trovi qui: https://github.com/peppe-app/SimpleRecipe
Buona programmazione!