Crea la app Wordle en SwiftUI
Crea la app Wordle en SwiftUI

Cómo crear WORDLE en SWIFTUI con la arquitectura MVVM

Aprende a crear el juego Wordle en SwiftUI. Te guío paso a paso para crear el modelo de datos, vistas y ViewModel en Xcode. En tan solo 30 minutos crearás un juego que fue vendido por más de 1 millón de euros. Arquitectura MVVM en SwiftUI

SwiftBeta

Tabla de contenido

Crea la app de Wordle en SwiftUI

Hoy en SwiftBeta vamos a aprender a cómo crear la app Wordle. Vamos a centrarnos en el juego, es decir, en cómo está construido y vamos a replicarlo. Vas a ver que en tan solo unos minutos vas a poder crear tu propio Wordle usando Xcode, SwiftUI y Swift.

Wordle en SwiftUI

Creamos el Modelo

Lo primero de todo que vamos hacer es crear dos vistas. La primera vista es la parte del juego (las celdas distribuidas en un Grid de 5x6), donde el usuario añade las palabras. Y la segunda parte es el teclado.

Para poder representar la información que se muestra en estas vistas, vamos a crear un fichero llamado Model.
Dentro de Model vamos a crear un enum llamado Status, este enum va a tener distintos estados para poder representar que:

  • La letra que hemos añadido aparece en la palabra
  • La letra que hemos añadido aparece y está en la posición correcta
  • La letra que hemos añadido no aparece en la palabra
  • Color de las celdas del juego y teclas del teclado

El código quedaría de la siguiente manera:

enum Status {
    case normal
    case match
    case dontAppear
    case appear
}
Creamos el enum con todos los estados posibles de las letras

A continuación creamos una struct llamada LetterModel para añadir la letra que se mostrará en cada celda del grid y también en las teclas de nuestro teclado. Estas celdas y teclas tienen que saber que estado tienen, es por eso que vamos a crear una propiedad con el tipo Status que acabamos de crear, es decir:

struct LetterModel: Hashable {
    let id: String = UUID().uuidString
    let name: String
    var status: Status
          
    init(_ name: String) {
        self.name = name
        self.status = .normal
    }
}
Creamos el tipo LetterModel en Swift

Ahora vamos a crear dos propiedades computadas que nos ayudaran a saber qué color poner en el texto y en el background de la celda y la tecla. Nuestra struct LetterModel quedaría de la siguiente manera:

struct LetterModel: Hashable {
    let id: String = UUID().uuidString
    let name: String
    var status: Status
        
    var backgroundColor: Color {
        switch status {
        case .normal:
            return Color(red: 213.0/255, green: 216.0/255, blue: 222.0/255)
        case .match:
            return Color(red: 109.0/255, green: 169.0/255, blue: 103.0/255)
        case .dontAppear:
            return Color(red: 120.0/255, green: 124.0/255, blue: 127.0/255)
        case .appear:
            return Color(red: 201.0/255, green: 180.0/255, blue: 87.0/255)
        }
    }
    
    var foregroundColor: Color {
        switch status {
        case .normal:
            return .black
        case .match, .dontAppear, .appear:
            return .white
        }
    }
    
    init(_ name: String) {
        self.name = name
        self.status = .normal
    }
}
Añadimos dos propiedades: backgroundColor y foregroundColor

Ahora solo nos queda construir un Array de LetterModel que representará los datos necesarios para crear nuestro teclado.

var keyboardData: [LetterModel] = [
    .init("Q"), .init("W"), .init("E"), .init("R"), .init("T"), .init("Y"), .init("U"), .init("I"), .init("O"), .init("P"),
    .init("A"), .init("S"), .init("D"), .init("F"), .init("G"), .init("H"), .init("J"), .init("K"), .init("L"), .init("Ñ"),
    .init("🚀"), .init(""), .init("Z"), .init("X"), .init("C"), .init("B"), .init("N"), .init("M"), .init(""), .init("🗑")
]
Creamos nuestro modelo de teclado

Esto es solo el modelo de datos, la información que necesitamos para representar de alguna manera nuestra vista. Es decir, este array que contiene la variable keyboardData lo podríamos representar visualmente de muchas maneras, pero nosotros usaremos un Grid para darle el aspecto de un teclado.

Creamos la vista KeyboardView

Lo siguiente que vamos hacer es usar un LazyVGrid en SwiftUI para crear el Grid que queremos y simular el aspecto de un teclado:

struct KeyboardView: View {
    let columns: [GridItem] = Array(repeating: GridItem(.flexible(minimum: 20), spacing: 0), count: 10)
    
    var body: some View {
        LazyVGrid(columns: columns,
                  spacing: 12) {
            ForEach(keyboardData, id: \.id) { model in
                Button(action: {
                    // TODO: Send letter ViewModel
                }, label: {
                    Text(model.name)
                        .font(.body)
                })
                    .frame(width: 34, height: 50)
                    .foregroundColor(model.foregroundColor)
                    .background(model.backgroundColor)
                    .cornerRadius(8)
            }
        }
                  .padding(.horizontal, 8)
    }
}
Creamos la vista KeyboardView

Podemos ver en el Canvas cómo queda nuestro teclado. Hemos creado 10 columnas con un GridItem flexible de mínimo 20.

Ahora ya tenemos la vista inferior. Más tarde volveremos a ella para conectar con nuestro ViewModel (el que crearemos más adelante).

Creamos la vista GameView

Ahora vamos a centrarnos en la vista superior del juego, en la que el user añade las letras en un Grid. Para hacerlo vamos a seguir el mismo proceso que antes, vamos a crear una variable llamada gameData, y esta variable va a representar las filas de nuestro Grid (de nuestro tablero).

var gameData: [[LetterModel]] =
[
    [.init(""), .init(""), .init(""), .init(""), .init("")],
    [.init(""), .init(""), .init(""), .init(""), .init("")],
    [.init(""), .init(""), .init(""), .init(""), .init("")],
    [.init(""), .init(""), .init(""), .init(""), .init("")],
    [.init(""), .init(""), .init(""), .init(""), .init("")],
    [.init(""), .init(""), .init(""), .init(""), .init("")]
]
Reprensentamos los datos de nuestro tablero. De momento estará vacío

A continuación vamos a crear un LazyVGrid, tal y como hemos creado para nuestro teclado:

struct GameView: View {
    private let columns: [GridItem] = Array(repeating: GridItem(.flexible(minimum: 20), spacing: 0), count: 5)
    
    var body: some View {
        LazyVGrid(columns: columns,
                  spacing: 8) {
            ForEach(0...5, id: \.self) { index in
                ForEach(gameData[index], id: \.id) { model in
                    Button(action: {
                        // TODO
                    }, label: {
                        Text(model.name)
                            .font(.system(size: 40, weight: .bold))
                        
                    })
                        .frame(width: 60, height: 60)
                        .foregroundColor(model.foregroundColor)
                        .background(model.backgroundColor)
                }
            }
        }
                  .padding(.horizontal, 20)
    }
}
Creamos la vista GameView

Dentro del LazyVGrid creamos un ForEach para saber en qué columna estamos y así poner la posición correcta los datos de la variable gameData.

Una vez hemos creado el Modelo y las vistas, ahora vamos a unir la vista GameView y KeyboardView en nuestro ContentView.

Unir GameView y KeyboardView en ContentView

Para mostrar las dos vistas en ContentView, vamos a utilizar un VStack, tan sencillo como hacer lo siguiente:

import SwiftUI

struct ContentView: View {
    var body: some View {
        VStack(spacing: 40) {
            GameView()
            KeyboardView()
        }
    }
}
Juntamos la vista GameView y KeyboardView en un VStack

Una vez hemos creado nuestras vistas, lo siguiente que vamos a crear es el ViewModel. El responsable de crear toda la lógica de nuestro juego.

Creamos el ViewModel

Creamos nuestro ViewModel y añadimos las siguiente propiedades:

final class ViewModel: ObservableObject {
    var numOfRow: Int = 0
    @Published var word: [LetterModel] = []
    @Published var gameData: [[LetterModel]] =
    [
        [.init(""), .init(""), .init(""), .init(""), .init("")],
        [.init(""), .init(""), .init(""), .init(""), .init("")],
        [.init(""), .init(""), .init(""), .init(""), .init("")],
        [.init(""), .init(""), .init(""), .init(""), .init("")],
        [.init(""), .init(""), .init(""), .init(""), .init("")],
        [.init(""), .init(""), .init(""), .init(""), .init("")]
    ]
}
Creamos la clase ViewModel donde irá toda la lógica de nuestra juego
  • numOfRow nos servirá para saber en qué fila estamos de nuestro Grid
  • word nos servirá para hacer comprobaciones a la palabra que está insertando un user
  • gameData, representación de nuestro Grid, de nuestro tablero de juego. Al inicializar el juego este tablero está vacío, pero a medida que un user añada palabras se irá rellenando.

Fíjate que ya teníamos una variable llamada gameData que habíamos creado en la vista de GameView. PERO ya no la necesitamos, es decir la puedes borrar, ya que el ViewModel será el responsable de tener esta información (y de modificarla!).

Lo siguiente que vamos hacer es añadir un método llamado addNewLetter y aceptará un parámetro que será de tipo LetterModel:

    func addNewLetter(letterModel: LetterModel) {
        if letterModel.name == "🚀" {
            tapOnSend()
            return
        }
        if letterModel.name == "🗑" {
            tapOnRemove()
            return
        }
        if word.count < 5 {
            let letter = LetterModel(letterModel.name)
            word.append(letter)
            game[numOfRow][word.count-1] = letter
        }
    }
Creamos el método que se llamará cada vez que un user pulse una tecla de KeyboardView

Este método como bien has deducido lo vamos a conectar con nuestro KeyboardView:

  • Si el button es un cohete llamará a un método que aplicará cierta lógica
  • Si llama a la cesta de la basura llamará a otra lógica completamente diferente
  • Si no es ninguna de las teclas anteriores, lo que hará será almacenar la nueva letra en la casilla correcta de nuestro Grid.

Para que el compilador no se queje, vamos a crear los dos métodos:

  • tapOnSend()
  • tapOnRemove()
    private func tapOnSend() {
        print("Tap on send")
    }
    
    private func tapOnRemove() {
        print("Tap on remove")
    }
Creamos los métodos de comprobar palabra y borrar letra

Y ahora vamos a conectar este método addNewLetter con nuestra vista KeyboardView cada vez que se pulse una tecla, lo único que tenemos que hacer es crear una instancia de ViewModel y llamar al método addNewLetter cada vez que se pulse una tecla:

struct KeyboardView: View {
    @ObservedObject var viewModel: ViewModel
    
    let columns: [GridItem] = Array(repeating: GridItem(.flexible(minimum: 20), spacing: 0), count: 10)
    
    var body: some View {
        LazyVGrid(columns: columns,
                  spacing: 12) {
            ForEach(keyboardData, id: \.id) { model in
                Button(action: {
                    viewModel.addNewLetter(letterModel: model)
                }, label: {
                    Text(model.name)
                        .font(.body)
                })
                    .frame(width: 34, height: 50)
                    .foregroundColor(model.foregroundColor)
                    .background(model.backgroundColor)
                    .cornerRadius(8)
            }
        }
                  .padding(.horizontal, 8)
    }
}
Conectamos la vista KeyboardView con nuestro ViewModel

Arreglamos también la preview de nuestra vista:

struct KeyboardView_Previews: PreviewProvider {
    static var previews: some View {
        KeyboardView(viewModel: ViewModel())
    }
}
Arreglamos la preview en SwiftUI

Ahora, si intentamos compilar nuestra app fallará, ya que KeyboardView espera una instancia de ViewModel. Vamos a arreglarlo creando e instanciando una propiedad de ViewModel en ContentView:

import SwiftUI

struct ContentView: View {
    @StateObject var viewModel = ViewModel()
    
    var body: some View {
        VStack(spacing: 40) {
            GameView()
            KeyboardView(viewModel: viewModel)
        }
    }
}
Creamos la instancia de nuestro ViewModel y se la pasamos a las vistas

Vamos a compilar nuestra app y vamos a ver qué hemos conseguido. De momento tenemos la vista bien estructurada, y cuando pulsamos una tecla de nuestro teclado se printa su valor por consola, diferenciando 3 casos:

  • Se ha pulsado la tecla de 🚀
  • Se ha pulsado la tecla de 🗑
  • Se ha pulsado cualquier otra tecla.

Ahora vamos a continuar añadiendo código en nuestro ViewModel. Lo siguiente que vamos hacer es crear una propiedad con la palabra que estamos buscando, en nuestro caso vamos a asignarle el valor de "REINA".

    @Published var result: String = "REINA"
Propiedad que contendrá la palabra a buscar

y también vamos a añadir un método que nos va a indicar si una palabra existe o no.

    private func wordIsReal(word: String) -> Bool {
        UIReferenceLibraryViewController.dictionaryHasDefinition(forTerm: word)
    }
Método que nos devuelve si una palabra existe o no
Si intentamos compilar ahora fallará, debemos importar en nuestro ViewModel el framework UIKit

Lo siguiente que vamos hacer es rellenar el método de tapOnSend(), dentro de este método vamos a aplicar la lógica más importante de nuestra app.

private func tapOnSend() {
        guard word.count == 5 else {
            print("¡Añade más letras!")
            return
        }
        
        let finalStringWord = word.map { $0.name }.joined()
        
        if wordIsReal(word: finalStringWord) {
            print("Correct word")
            
            for (index, _) in word.enumerated() {
                let currentCharacter = word[index].name
                var status: Status
                
                if result.contains(where: { String($0) == currentCharacter }) {
                    status = .appear
                    print("\(currentCharacter) Appear")
                    
                    if currentCharacter == String(result[result.index(result.startIndex, offsetBy: index)]) {
                        status = .match
                        print("\(word[index].name) MATCH ✅")
                    }
                } else {
                    status = .dontAppear
                    print("\(word[index].name) DONT Appear")
                }

                // Update GameView
                var updateGameBoardCell = game[numOfRow][index]
                updateGameBoardCell.status = status
                game[numOfRow][index] = updateGameBoardCell
                
                // Update KeyboardView
                let indexToUpdate = keyboardData.firstIndex(where: { $0.name == word[index].name })
                var keyboardKey = keyboardData[indexToUpdate!]
                if keyboardKey.status != .match {
                    keyboardKey.status = status
                    keyboardData[indexToUpdate!] = keyboardKey
                }
            }
            
            // Clean word and move to the next row
            word = []
            numOfRow += 1
        } else {
            print("Incorrect word")
        }
    }
    
Lógica para actualizar las letras de nuestra palabra en la vista GameView y KeyboardView

Para poder ver los cambios en nuestra vista GameView debemos crear una propiedad de tipo ViewModel. Tal y como hemos hecho con KeyboardView.

import SwiftUI

struct GameView: View {
    @ObservedObject var viewModel: ViewModel
    private let columns: [GridItem] = Array(repeating: GridItem(.flexible(minimum: 20), spacing: 0), count: 5)
    
    var body: some View {
        LazyVGrid(columns: columns,
                  spacing: 8) {
            ForEach(0...columns.count, id: \.self) { index in
                ForEach(viewModel.game[index], id: \.id) { model in
                    Button(action: {
                        // TODO
                    }, label: {
                        Text(model.name)
                            .font(.system(size: 40, weight: .bold))
                        
                    })
                        .frame(width: 60, height: 60)
                        .foregroundColor(model.foregroundColor)
                        .background(model.backgroundColor)
                }
            }
        }
                  .padding(.horizontal, 20)
    }
}

struct GameView_Previews: PreviewProvider {
    static var previews: some View {
        GameView(viewModel: ViewModel())
    }
}
Actualizamos la vista GameView

También, en nuestra vista ContentView debemos pasarle la instancia que hemos creado de ViewModel y pasarsela a GameView.

struct ContentView: View {
    @StateObject var viewModel = ViewModel()
    
    var body: some View {
        VStack(spacing: 40) {
            GameView(viewModel: viewModel)
            KeyboardView(viewModel: viewModel)
        }
    }
}
Pasamos instancia de ViewModel a GameView

Vamos a compilar, y vamos a probar nuestra app.

¡Perfecto! vemos como al pulsar una tecla se modifica nuestro Grid, nuestro tablero. Ahora vamos a rellenar un método para poder eliminar letras de una palabra. Para ello, volvemos a nuestro ViewModel y rellenamos el método tapOnRemove()

    private func tapOnRemove() {
        guard word.count > 0 else {
            return
        }
        game[numOfRow][word.count-1] = .init("")
        word.removeLast()
    }
Creamos método para borrar letras en nuestra palabra

Creamos la pantalla de error o victoria

En nuestro anterior código printabamos un error por consola, lo que haremos a continuación es mostrar un banner, es decir, una vista indicando qué error ha ocurrido y también marcaremos en rojo las celdas del Grid, para que sea más fácil de visualizar dónde tenemos el error.
También vamos a aprovechar y vamos a añadir el caso de que un user gane la partida.

Para hacerlo, nos vamos a nuestro ViewModel y creamos el siguiente enum:

enum BannerType {
    case error(String)
    case success
}
Creamos un enum con dos cases, error y success

Y dentro de la clase ViewModel vamos a crear una propiedad nueva de tipo BannerType

    @Published var bannerType: BannerType? = nil
Creamos la propiedad en nuestro ViewModel

Ahora, cada vez que tengamos un error, vamos a asigna a esta propiedad el case error con un mensaje. Es decir, el método tapOnSend() nos quedaría de la siguiente manera:

    private func tapOnSend() {
        guard word.count == 5 else {
            print("¡Añade más letras!")
            bannerType = .error("¡Añade más letras!")
            return
        }
        
        let finalStringWord = word.map { $0.name }.joined()
        
        if wordIsReal(word: finalStringWord) {
            print("Correct word")
            
            for (index, _) in word.enumerated() {
                let currentCharacter = word[index].name
                var status: Status
                
                if result.contains(where: { String($0) == currentCharacter }) {
                    status = .appear
                    print("\(currentCharacter) Appear")
                    
                    if currentCharacter == String(result[result.index(result.startIndex, offsetBy: index)]) {
                        status = .match
                        print("\(currentCharacter) MATCH ✅")
                    }
                } else {
                    status = .dontAppear
                    print("\(currentCharacter) DONT Appear")
                }

                // Update GameView
                var updateGameBoardCell = game[numOfRow][index]
                updateGameBoardCell.status = status
                game[numOfRow][index] = updateGameBoardCell
                
                // Update KeyboardView
                let indexToUpdate = keyboardData.firstIndex(where: { $0.name == word[index].name })
                var keyboardKey = keyboardData[indexToUpdate!]
                if keyboardKey.status != .match {
                    keyboardKey.status = status
                    keyboardData[indexToUpdate!] = keyboardKey
                }
            }
            
            // Clean word and move to the next row
			word = []
            numOfRow += 1
        } else {
            print("Incorrect word")
            bannerType = .error("¡Palabra incorrecta, no existe!")
        }
    }
Asignamos un valor a la propiedad BannerType en los caminos de error

Ahora solo nos falta crear un método nuevo para saber cuando tenemos un error, y este nuevo método lo llamaremos desde la vista GameView.

    func hasError(index: Int) -> Bool {
        guard let bannerType = bannerType else {
            return false
        }

        switch bannerType {
        case .error(_):
            return index == numOfRow
        case .success:
            return false
        }
    }
Creamos un método que se llamará desde GameView para saber qué hay un error y así actualizar las vistas correctas

Antes de irnos a la vista, vamos a añadir la siguiente línea del código justo al principio del método addNewLetter:

bannerType = nil
Reseteamos el error

de esta manera si se pulsa una tecla nueva desmarcamos todas las celdas del Grid que estaban en rojo.

Y ahora si, vamos a llamar al método hasError desde GameView, así cuando obtengamos un error cambiaremos el color del texto y del background en el Grid, es decir:

import SwiftUI

struct GameView: View {
    @ObservedObject var viewModel: ViewModel
    private let columns: [GridItem] = Array(repeating: GridItem(.flexible(minimum: 20), spacing: 0), count: 5)
    
    var body: some View {
        LazyVGrid(columns: columns,
                  spacing: 8) {
            ForEach(0...columns.count, id: \.self) { index in
                ForEach(viewModel.game[index], id: \.id) { model in
                    Button(action: {
                        // TODO
                    }, label: {
                        Text(model.name)
                            .font(.system(size: 40, weight: .bold))
                        
                    })
                        .frame(width: 60, height: 60)
                        .foregroundColor(viewModel.hasError(index: index) ? .white : model.foregroundColor)
                        .background(viewModel.hasError(index: index) ? .red : model.backgroundColor)
                }
            }
        }
                  .padding(.horizontal, 20)
    }
}
Llamamos al método hasError de nuestro ViewModel para modificar el color de nuestras celdas del Grid

Fíjate que hemos añadido una condición en el modificador .foregroundColor y .background.

Ahora lo único que nos falta es mostrar una vista para indicar a un user que error está obteniendo. Primero debemos crear la vista:

import Foundation
import SwiftUI

struct BannerView: View {
    private let bannerType: BannerType
    @State private var isOnScreen: Bool = false
    
    init(bannerType: BannerType) {
        self.bannerType = bannerType
    }
    
    var body: some View {
        VStack {
            Spacer()
            switch bannerType {
            case .error(let errorMessage):
                Text(errorMessage)
                    .foregroundColor(.white)
                    .padding()
                    .background(.red)
                    .cornerRadius(12)
            case .success:
                Text("¡HAS GANADO!")
                    .foregroundColor(.white)
                    .padding()
                    .background(.blue)
                    .cornerRadius(12)
            }
            
            Spacer()
        }
        .padding(.horizontal, 12)
        .frame(height: 40)
        .animation(.easeInOut(duration: 0.3), value: isOnScreen)
        .offset(y: isOnScreen ? -350 : -500)
        .onAppear {
            isOnScreen = true
        }
    }
}
Vista que se mostrará para dar feedback al user del error o de que ha acertado la palabra a buscar

Y ahora vamos a llamarla desde ContentView y para hacerlo vamos a añadir un ZStack:

import SwiftUI

struct ContentView: View {
    @StateObject var viewModel = ViewModel()
    
    var body: some View {
        ZStack {
            VStack(spacing: 40) {
                GameView(viewModel: viewModel)
                KeyboardView(viewModel: viewModel)
            }
            if viewModel.bannerType != nil {
                BannerView(bannerType: viewModel.bannerType!)
            }
        }
    }
}
Modificamos ContentView para mostrar el BannerView que acabamos de crear

¡Perfecto! Ahora cada vez que tenemos un error estamos marcando la fila del Grid en rojo y también estamos mostrando un banner con el error específico. Ahora vamos a hacer un cambio muy simple para indicar que un user ha ganado.

Dentro de nuestro ViewModel, justo después de actualizar el teclado, añadimos las siguientes líneas que nos van a indicar si un user ha ganado o no, nuestro métodod tapOnSend debería quedar de la siguiente manera:

    private func tapOnSend() {
        guard word.count == 5 else {
            print("¡Añade más letras!")
            bannerType = .error("¡Añade más letras!")
            return
        }
        
        let finalStringWord = word.map { $0.name }.joined()
        
        if wordIsReal(word: finalStringWord) {
            print("Correct word")
            
            for (index, _) in word.enumerated() {
                let currentCharacter = word[index].name
                var status: Status
                
                if result.contains(where: { String($0) == currentCharacter }) {
                    status = .appear
                    print("\(currentCharacter) Appear")
                    
                    if currentCharacter == String(result[result.index(result.startIndex, offsetBy: index)]) {
                        status = .match
                        print("\(currentCharacter) MATCH ✅")
                    }
                } else {
                    status = .dontAppear
                    print("\(currentCharacter) DONT Appear")
                }

                // Update GameView
                var updateGameBoardCell = game[numOfRow][index]
                updateGameBoardCell.status = status
                game[numOfRow][index] = updateGameBoardCell
                
                // Update KeyboardView
                let indexToUpdate = keyboardData.firstIndex(where: { $0.name == word[index].name })
                var keyboardKey = keyboardData[indexToUpdate!]
                if keyboardKey.status != .match {
                    keyboardKey.status = status
                    keyboardData[indexToUpdate!] = keyboardKey
                }
            }
            
            let isUserWinner = game[numOfRow].reduce(0) { partialResult, letterModel in
                if letterModel.status == .match {
                    return partialResult + 1
                }
                return 0
            }
            
            if isUserWinner == 5 {
                bannerType = .success
            } else {
                // Clean word and move to the next row
                word = []
                numOfRow += 1
            }
        } else {
            print("Incorrect word")
            bannerType = .error("¡Palabra incorrecta, no existe!")
        }
    }
Actualizamos este método para saber cuándo un usuario ha ganado la partida

Si ahora compilamos nuestra app y la probamos, vemos como si acertamos la palabra, poniendo REINA, vemos como aparece el banner en la parte superior indicando que hemos ganado.

Conclusión

Hoy hemos aprendido a cómo crear el juego Wordle (que tanto está de moda) en SwiftUI. Hemos ido paso por paso, creando el modelo, las vistas y luego el ViewModel. En tan solo 30 minutos hemos podido replicar este juego.

Si quieres seguir aprendiendo sobre SwiftUI, Swift, Xcode, o cualquier tema relacionado con el ecosistema Apple