---
title: "Un gioco TicTacToe in SwiftUI"
url: https://www.sofapps.it/it/blog/tictactoe-swiftui/
date: 2019-06-18
author: Costantino Pistagna
language: it
translated_from: en
tags: [SwiftUI, Swift, UIKit, Tutorial]
---

# Un gioco TicTacToe in SwiftUI

> Il post costruisce in SwiftUI beta un TicTacToe Uomo contro Macchina in meno di 160 righe. Il model Square pubblica i cambiamenti tramite un PassthroughSubject di Combine, la SquareView li osserva con @ObjectBinding e un flag @State governa l'alert di fine partita in ContentView.

Oggi proviamo a costruire un gioco TicTacToe in meno di 160 righe con SwiftUI, il nuovo framework dichiarativo di Apple che promette di sostituire nel prossimo futuro il suo fratello UIKit. Apriamo Xcode.

>In queste ore sono uscite la beta 2 di Xcode e quella di macOS Catalina. Vi consiglio caldamente di installarle entrambe: abbiamo notato un netto miglioramento generale delle prestazioni e una maggiore reattività di Xcode sotto ogni aspetto.

Una volta aperto Xcode, creiamo un nuovo progetto chiamato TicTacToe e controlliamo che la spunta su SwiftUI sia attiva. Premiamo next e scegliamo una cartella in cui salvare il progetto.

![Finestra di nuovo progetto di Xcode con il nome prodotto TicTacToe e la casella Use SwiftUI attiva](https://www.sofapps.it/blog/tictactoe-swiftui/fig-1.jpg)

Terminata l'inizializzazione, Xcode mostra lo scheletro del progetto pre-generato. Passiamo a ContentView.swift e iniziamo subito a dare forma alla logica del gioco.

Nel TicTacToe due avversari si sfidano su una scacchiera 3x3, ciascuno con il proprio segno, X oppure O. Vince chi per primo allinea tre segni uguali in orizzontale, in verticale o in diagonale.

>La prima cosa da fare, quindi, è creare un model che rifletta quanto appena detto.

Scriviamo un enum che rappresenti lo stato di una casella. Può essere vuota, occupata da noi o dal computer.

```swift
enum SquareStatus {
    case empty
    case visitor
    case home
}
```

Per giocare ci serve una scacchiera 3x3. Proviamo quindi a definire il model di una singola casella:

```swift
class Square: BindableObject {
    let didChange = PassthroughSubject<Void, Never>()
    
    var status: SquareStatus {
        didSet {
            didChange.send(())
        }
    }
    
    init(status: SquareStatus) {
        self.status = status
    }
}
```

La classe Square adotta il protocollo **BindableObject**. Gli oggetti che adottano questo protocollo notificano i subscriber nel framework SwiftUI quando una o più delle loro proprietà cambiano.

Per farlo bisogna adottare correttamente il protocollo, aggiungendo un'istanza che pubblica un evento quando l'oggetto cambia. Questa istanza si chiama didChange:

```swift
let didChange = PassthroughSubject <Void, Never> ()
```

PassthroughtSubject è una classe messa a disposizione dal framework Combine che ci permette di pubblicare una notifica a tutti i richiedenti. La notifica ha due parametri: l'output e un eventuale errore.

Per ora vogliamo solo avvisare tutti che qualcosa è cambiato nell'oggetto Square. Poiché nessuno dei due parametri ci interessa, passiamo Void come primo e Never come secondo. Il primo indica che non passiamo alcun output, il secondo che non c'è possibilità di errore.

Definiamo poi la proprietà status della nostra casella e, al suo interno, aggiungiamo un meccanismo che invia una notifica quando ne cambiamo il valore:

```swift
var status: SquareStatus {
    didSet {
        didChange.send(())
    }
}
```

Possiamo passare adesso al Model vero e proprio che descrive il gioco e la scacchiera. Lo chiameremo ModelBoard:

```swift
class ModelBoard {
    var squares = [Square]()

    init() {
        for _ in 0...8 {
            squares.append(Square(status: .empty))
        }
    }
```

Cominciamo dichiarando la variabile array che conterrà le singole caselle della scacchiera. Nella funzione init aggiungiamo 9 elementi di tipo Square, tutti con status .empty

A fine partita ci servirà un metodo per riportare il gioco allo stato iniziale:

```swift
func resetGame() {
    for i in 0...8 {
        squares[i].status = .empty
    }
}
```

Ora serve un modo per controllare se la partita è finita.

>Una partita finisce quando non ci sono più caselle libere oppure quando uno dei due giocatori allinea tre simboli prima dell'altro.

```swift
var gameOver: (SquareStatus, Bool) {
    get {
        if thereIsAWinner != .empty {
            return (thereIsAWinner, true)
        } else {
            for i in 0...8 {
                if squares[i].status == .empty {
                    return (.empty, false)
                }
            }
            return (.empty, true)
        }
    }
}
```

La variabile getter thereIsAWinner controlla semplicemente tutte le combinazioni vincenti:

```swift
private var thereIsAWinner:SquareStatus {
    get {
        if let check = self.checkIndexes([0, 1, 2]) {
            return check
        } else  if let check = self.checkIndexes([3, 4, 5]) {
            return check
        }  else  if let check = self.checkIndexes([6, 7, 8]) {
            return check
        }  else  if let check = self.checkIndexes([0, 3, 6]) {
            return check
        }  else  if let check = self.checkIndexes([1, 4, 7]) {
            return check
        }  else  if let check = self.checkIndexes([2, 5, 8]) {
            return check
        }  else  if let check = self.checkIndexes([0, 4, 8]) {
            return check
        }  else  if let check = self.checkIndexes([2, 4, 6]) {
            return check
        }
        return .empty
    }
}
```

La funzione checkIndex prende in input un array di interi (gli indici) e restituisce un SquareStatus opzionale: .home o .visitor se c'è un vincitore, nil altrimenti:

```swift
private func checkIndexes(_ indexes: [Int]) -> SquareStatus? {
    var homeCounter:Int = 0
    var visitorCounter:Int = 0
    for anIndex in indexes {
        let aSquare = squares[anIndex]
        if aSquare.status == .home {
            homeCounter = homeCounter + 1
        } else if aSquare.status == .visitor {
            visitorCounter = visitorCounter + 1
        }
    }
    if homeCounter == 3 {
        return .home
    } else if visitorCounter == 3 {
        return .visitor
    }
    return nil
}
```

Ci serve un modo per fare una mossa. Per il giocatore umano controlliamo se la casella selezionata è vuota. Se lo è, ne cambiamo lo status in .home

Dato che implementiamo solo una partita Uomo contro Macchina, alla fine dobbiamo anche far partire la mossa del computer.

```swift
func makeMove(index: Int, player:SquareStatus) -> Bool {
    if squares[index].status == .empty {
        squares[index].status = player
        if player == .home { aiMove() }
        return true
    }
    return false
}
```

La mossa dell'intelligenza artificiale è davvero semplice: si estrae un indice a caso, si controlla se la casella è vuota e, in caso affermativo, la si riempie con il flag .visitor.

```swift
private func aiMove() {
    var anIndex = Int.random(in: 0 ... 8)
    while (makeMove(index: anIndex, player: .visitor) == false && gameOver.1 == false) {
        anIndex = Int.random(in: 0 ... 8)
    }
}
```

Ora che il model è pronto, dobbiamo interagire con SwiftUI per costruire una View che rispetti il model Square. Chiamiamola SquareView:

```swift
struct SquareView: View {
    @ObjectBinding var dataSource:Square
    var action: () -> Void
    var body: some View {
        Button(action: {
            self.action()
        }) {
            Text((dataSource.status != .empty) ?
                (dataSource.status != .visitor) ? "X" : "0"
                : " ")
                .frame(minWidth: 60, minHeight: 60)
                .background(Color.gray)
                .padding(EdgeInsets(top: 4, leading: 4, bottom: 4, trailing: 4))
        }
    }
}
```

Prima di tutto aggiungiamo il property wrapper @ObjectBinding davanti al datasource Square.

In questo modo diciamo a SwiftUI che la SquareView è interessata alle notifiche di cambiamento del model. Quando ne arriverà una, SwiftUI farà calcolare automaticamente un nuovo body alla SquareView.

Dentro il body controlliamo se lo status è diverso da .empty. In tal caso scriviamo X oppure O a seconda del giocatore.

.frame, .background e .padding sono solo ritocchi estetici della casella.

Infine dobbiamo modificare la ContentView per ospitare la scacchiera, creando a questo scopo una variabile private che chiamiamo checker:

```swift
struct ContentView : View {
    private var checker = ModelBoard()
    @State private var isGameOver = false
```

Definiamo anche una proprietà @State per sapere se la partita è finita: deve essere @State perché vogliamo aggiornare il body della ContentView una volta terminato il gioco.

Poiché ogni SquareView è identica e il suo comportamento cambia solo in base all'indice, definiamo anche un metodo buttonAction:

```swift
func buttonAction(_ index: Int) {
    _ = self.checker.makeMove(index: index, player: .home)
    self.isGameOver = self.checker.gameOver.1
}
```

In questo modo ogni SquareView invoca il metodo con il proprio indice e viene eseguito il codice relativo alla posizione indicata.

Infine possiamo costruire il body completo, che riflette le 9 SquareView (3 righe da 3):

```swift
var body: some View {
    VStack {
        HStack {
            SquareView(dataSource: checker.squares[0]) { self.buttonAction(0) }
            SquareView(dataSource: checker.squares[1]) { self.buttonAction(1) }
            SquareView(dataSource: checker.squares[2]) { self.buttonAction(2) }
        }
        HStack {
            SquareView(dataSource: checker.squares[3]) { self.buttonAction(3) }
            SquareView(dataSource: checker.squares[4]) { self.buttonAction(4) }
            SquareView(dataSource: checker.squares[5]) { self.buttonAction(5) }
        }
        HStack {
            SquareView(dataSource: checker.squares[6]) { self.buttonAction(6) }
            SquareView(dataSource: checker.squares[7]) { self.buttonAction(7) }
            SquareView(dataSource: checker.squares[8]) { self.buttonAction(8) }
        }
    }
```

Un'ultima cosa riguarda il feedback per l'utente: vogliamo avvisarlo con un popup quando la partita finisce o quando qualcuno vince.

Per farlo possiamo usare il View modifier .presentation. Nella sua forma più semplice accetta:

- Un booleano condizionale che decide se mostrarlo o no
- Un parametro message con un Text per il corpo dell'alert
- Un dismissButton: con un Alert.Button

```swift
.presentation($isGameOver) { () -> Alert in
    Alert(title: Text("Game Over"),
          message: Text(self.checker.gameOver.0 != .empty ?
            (self.checker.gameOver.0 == .home) ? "You Win!" : "iPhone Wins!"
            : "Parity"), dismissButton: Alert.Button.destructive(Text("Ok"), onTrigger: {
                self.checker.resetGame()
            }) )
}
```

Ed è tutto! Come promesso, in meno di 160 righe di codice abbiamo un TicTacToe perfettamente funzionante. Se facciamo Build and Run, la nostra applicazione apparirà così:

![L'app TicTacToe in esecuzione su iPhone simulator, con una griglia di nove caselle grigie tre per tre](https://www.sofapps.it/blog/tictactoe-swiftui/fig-2.gif)
