📱 Aprende a CREAR la APP CALCULADORA de iOS en SWIFTUI en Español

📱 Aprende a CREAR la APP CALCULADORA de iOS en SWIFTUI en Español

Crear la calculadora de iOS en SwiftUI es muy simple. Vamos a crear vistas en SwiftUI, para ser exactos dos LazyVGrid con varios modificadores en SwiftUI. Al acabar el post habrás aprendido a crear un clon de la calculadora de iOS en SwiftUI.

SwiftBeta
Aprende a crear la app calculadora iOS en SwiftUI

Hoy lo que vamos a ver en SwiftBeta es a como crear la calculadora que tenemos en iOS y obviamente lo vamos hacer en SwiftUI. Vamos a usar vistas que hemos visto en anteriores posts, así que nada de esto te pillará por sorpresa, lo interesante es que de una manera muy sencilla podemos crear una app que sea una calculadora.

Sobretodo nos centraremos en la parte visual y añadiremos muy poca lógica.

Crear una calculadora en SwiftUI

Lo primero de todo que vamos hacer es crear el modelo de datos que queremos usar para representar cada tecla de nuestro teclado de la calculadora. En nuestro caso vamos a crear un modelo muy simple:

struct KeyboardButton: Hashable {
    let title: String
    let textColor: Color
    let backgroundColor: Color
    let isDoubleWidth: Bool
    let type: ButtonType
}

Vamos a ver por qué hemos creado estas propiedades:
- title es el texto de la tecla, desde los tipos de operaciones A/C, +/-, /, +, -, etc hasta los números 1,2,3,4..etc.
- textColor, cada tecla tiene un color en el texto
- backgroundColor, cada tecla también tiene un color de fondo
- isDoubleWidth nos dirá que teclas tienen el doble de ancho, en este caso solo será la tecla 0.
- type, es un enum que nos proporciona mucha información, como si esa tecla es de valor númerico, si es una operacion, si es para mostrar el resultado de una operación, etc.

A continuación vamos a crear el ButtonType:

enum ButtonType: Hashable {
    case number(Int)
    case operation(OperationType)
    case result
    case reset
}

Como hemos dicho antes, el ButtonType nos dirá qué realiza esa tecla al ser pulsada, si es solo un número, si es para calcular alguna operación, si es para mostrar un resultado, etc las acciones las vamos a definir en cada case del enum.

y de OperationType, vamos a añadir solo dos para acotar el post de hoy, vamos hacer que solo se pueda sumar y multiplicar.

enum OperationType: Hashable {
    case sum
    case multiplication
}

Una vez hemos modelado los datos que queremos, vamos a añadir 3 colores para nuestra calculadora, añadimos el siguiente código:

let customOrange = Color(red: 254/255,
                         green: 159/255,
                         blue: 6/255,
                         opacity: 1.0)

let customLightGray = Color(red: 165/255,
                            green: 165/255,
                            blue: 165/255,
                            opacity: 1.0)

let customDarkGray = Color(red: 51/255,
                           green: 51/255,
                           blue: 51/255,
                           opacity: 1.0)

Ahora que ya tenemos todo lo necesario, crearemos un modelo de nuestra calculadora, para ello creamos una struct y la llamamos Matrix, y dentro de ella creamos dos propiedades donde alojaremos en la primera propiedad todas las filas con el mismo tamaño en los botones de nuestra calculadora y otra sección para añadir la última sección, la que aparece el 0.

struct Matrix {
    static let firstSectionData: [KeyboardButton] = [
        .init(title: "AC", textColor: .black, backgroundColor: customLightGray, isDoubleWidth: false, type: .reset),
        .init(title: "+/-", textColor: .black, backgroundColor: customLightGray, isDoubleWidth: false, type: .reset),
        .init(title: "%", textColor: .black, backgroundColor: customLightGray, isDoubleWidth: false, type: .reset),
        .init(title: "/", textColor: .white, backgroundColor: customOrange, isDoubleWidth: false, type: .reset),
        .init(title: "7", textColor: .white, backgroundColor: customDarkGray, isDoubleWidth: false, type: .number(7)),
        .init(title: "8", textColor: .white, backgroundColor: customDarkGray, isDoubleWidth: false, type: .number(8)),
        .init(title: "9", textColor: .white, backgroundColor: customDarkGray, isDoubleWidth: false, type: .number(9)),
        .init(title: "X", textColor: .white, backgroundColor: customOrange, isDoubleWidth: false, type: .operation(.multiplication)),
        .init(title: "4", textColor: .white, backgroundColor: customDarkGray, isDoubleWidth: false, type: .number(4)),
        .init(title: "5", textColor: .white, backgroundColor: customDarkGray, isDoubleWidth: false, type: .number(5)),
        .init(title: "6", textColor: .white, backgroundColor: customDarkGray, isDoubleWidth: false, type: .number(6)),
        .init(title: "-", textColor: .white, backgroundColor: customOrange, isDoubleWidth: false, type: .reset),
        .init(title: "1", textColor: .white, backgroundColor: customDarkGray, isDoubleWidth: false, type: .number(1)),
        .init(title: "2", textColor: .white, backgroundColor: customDarkGray, isDoubleWidth: false, type: .number(2)),
        .init(title: "3", textColor: .white, backgroundColor: customDarkGray, isDoubleWidth: false, type: .number(3)),
        .init(title: "+", textColor: .white, backgroundColor: customOrange, isDoubleWidth: false, type: .operation(.sum)),
    ]
    
    static let secondSectionData: [KeyboardButton] = [
        .init(title: "0", textColor: .white, backgroundColor: customDarkGray, isDoubleWidth: true, type: .number(0)),
        .init(title: ",", textColor: .white, backgroundColor: customDarkGray, isDoubleWidth: false, type: .reset),
        .init(title: "=", textColor: .white, backgroundColor: customOrange, isDoubleWidth: false, type: .result)
    ]
}

Una vez tenemos el modelo de cada tecla de nuestra calculadora, necesitamos tener algo para representar estos datos en SwiftUI y para ello usaremos un LazyVGrid, y como ya vimos en nuestro video de LazyVgrid necesitamos GridItem para crear los grids en SwiftUI. Por eso vamos a aprovechar y dentro de la struct Matrix vamos a crear un grid para cada sección:

    static let firstSectionGrid: (CGFloat) -> [GridItem] = { width in
        return Array(repeating: GridItem(.flexible(minimum: width), spacing: 0), count: 4)
    }
    
    static let secondSectionGrid: (CGFloat) -> [GridItem] = { width in
        return [GridItem(.flexible(minimum: width * 2), spacing: 0),
                GridItem(.flexible(minimum: width), spacing: 0),
                GridItem(.flexible(minimum: width), spacing: 0)]
        }

Una vez tenemos esto, ya podemos empezar a crear nuestra vista.

Vamos a crear una clase llamada VerticalButtonStack y vamos a crear el siguiente código, vamos a explicarlo paso por paso:

struct VerticalButtonStack: View {
        
    let data: [KeyboardButton]
    let columns: [GridItem]
    let width: CGFloat
    
    init(data: [KeyboardButton],
         columns: [GridItem],
         width: CGFloat) {
        self.data = data
        self.columns = columns
        self.width = width
    }
            
    var body: some View {
        LazyVGrid(columns: columns,
                  spacing: 12) {
            ForEach(data, id: \.self) { model in
                Button(action: {
                    // TODO
                }, label: {
                    if model.isDoubleWidth {
                        Rectangle()
                            .foregroundColor(model.backgroundColor)
                            .overlay(Text(model.title)
                                        .font(.largeTitle)
                                        .offset(x: 0 - (width * 0.22 * 0.5)))
                            .frame(width: width * 2 * 0.22,
                                   height: width * 0.22)
                    } else {
                        Text(model.title)
                            .font(.largeTitle)
                            .frame(width: width * 0.22,
                                   height: width * 0.22)
                    }
                })
                .foregroundColor(model.textColor)
                .background(model.backgroundColor)
                .cornerRadius(width * 0.25)
            }
        }
        .frame(width: width)
    }
}

y para crear las previews vamos a poner el siguiente código en VerticalButtonStack_Previews

struct VerticalButtonStack_Previews: PreviewProvider {
    static var previews: some View {
        Group {
            VerticalButtonStack(
                data: Matrix.firstSectionData,
                columns: Matrix.firstSectionGrid(390 * 0.25),
                width: 390)
            .previewLayout(.sizeThatFits)

            VerticalButtonStack(
                data: Matrix.secondSectionData,
                columns: Matrix.secondSectionGrid(390 * 0.25),
                width: 390)
            .previewLayout(.sizeThatFits)
        }
    }
}

y como resultado podemos ver en el canvas estas dos secciones:

Perfecto, que tenemos estas vistas, necesitamos una vista padre que las componga. Nos vamos al ContentView y creamos el siguiente código:

struct ContentView: View {
        
    var body: some View {
        ZStack {
            Color.black
                .ignoresSafeArea()
            GeometryReader { proxy in
                VStack {
                    VStack {
                        Spacer()
                        HStack {
                            Spacer()
                            Text("0")
                                .foregroundColor(.white)
                                .font(.system(size: 100, weight: .regular))
                                .frame(height: 100)
                                .padding(.trailing, 20)
                        }
                    }
                    VerticalButtonStack(
                        data: Matrix.firstSectionData,
                        columns: Matrix.firstSectionGrid(proxy.size.width * 0.25),
                        width: proxy.size.width)
                    VerticalButtonStack(
                        data: Matrix.secondSectionData,
                        columns: Matrix.secondSectionGrid(proxy.size.width * 0.25),
                        width: proxy.size.width)
                }
            }
            .background(Color.black)
        }
    }
}

Y el resultado es el siguiente 👇. Podríamos llegar a pensar que es la calculadora de nuestro iPhone

Calculadora en SwiftUI

Si pulsamos cualquier tecla de nuestra calculadora no pasa nada, ahora vamos a centrarnos en añadir algo de lógica para que pueda realizar las dos operaciones que hemos añadido en el OperationType que son sumar y multiplicar

Añadiendo lógica en nuestra calculadora en SwiftUI

Vamos a crear un ViewModel que va a conformar a ObservableObject (como ya vimos en varios de nuestros videos). Y crearemos 1 propiedad @Published, para que cualquier cambio que hagamos en esta propiedad haga que se actualice la vista que esté escuchando este valor.

final class ViewModel: ObservableObject {
    @Published var textFieldValue: String = "0"
    
    var textFieldSavedValue: String = "0"
    var currentOperationToExecute: OperationType?
    var shouldRunOperation: Bool = false

    func logic(key: KeyboardButton) {
        switch key.type {
        case .number(let value):
            if shouldRunOperation {
                textFieldValue = "0"
            }
            textFieldValue = textFieldValue == "0" ? "\(value)" : textFieldValue + "\(value)"

        case .reset:
            textFieldValue = "0"
            textFieldSavedValue = "0"
            currentOperationToExecute = nil
            shouldRunOperation = false
        case .result:
            guard let operation = currentOperationToExecute else {
                return
            }
            switch operation {
            case .multiplication:
                textFieldValue = "\(Int(textFieldValue)! * Int(textFieldSavedValue)!)"
            case .sum:
                textFieldValue = "\(Int(textFieldValue)! + Int(textFieldSavedValue)!)"
            }
        case .operation(let type):
            textFieldSavedValue = textFieldValue
            currentOperationToExecute = type
            shouldRunOperation = true
        }
    }
}

La lógica aplicada es muy sencilla, hay una función y según la tecla pulsada hacemos una lógica o otra.

Este ViewModel lo vamos a instanciar en el ContentView y quedaría de la siguiente manera:

struct ContentView: View {
    
    @StateObject var viewModel: ViewModel = ViewModel()
    
    var body: some View {
        ZStack {
            Color.black
                .ignoresSafeArea()
            GeometryReader { proxy in
                VStack {
                    VStack {
                        Spacer()
                        HStack {
                            Spacer()
                            Text(viewModel.textFieldValue)
                                .foregroundColor(.white)
                                .font(.system(size: 100, weight: .regular))
                                .frame(height: 100)
                                .padding(.trailing, 20)
                        }
                    }
                    VerticalButtonStack(
                        viewModel: viewModel,
                        data: Matrix.firstSectionData,
                        columns: Matrix.firstSectionGrid(proxy.size.width * 0.25),
                        width: proxy.size.width)
                    VerticalButtonStack(
                        viewModel: viewModel,
                        data: Matrix.secondSectionData,
                        columns: Matrix.secondSectionGrid(proxy.size.width * 0.25),
                        width: proxy.size.width)
                }
            }
            .background(Color.black)
        }
    }
}

Fíjate que al VerticalButtonStack le estamos pasando este ViewModel, ¿por qué? para que cuando pulsemos una tecla, la acción de la tecla llame al método de nuestro ViewModel y haga lo que tenga que hacer (llamará al método logic)

La única línea que hemos modificado ha sido que cuando se pulsa una tecla, se envía la acción al ViewModel. Es decir, nuestro VerticalButtonStack quedará de la siguiente manera:

struct VerticalButtonStack: View {
    
    @ObservedObject var viewModel: ViewModel
    
    let data: [KeyboardButton]
    let columns: [GridItem]
    let width: CGFloat
    
    init(viewModel: ViewModel,
         data:[KeyboardButton],
         columns: [GridItem],
         width: CGFloat) {
        self.viewModel = viewModel
        self.data = data
        self.columns = columns
        self.width = width
    }
            
    var body: some View {
        LazyVGrid(columns: columns,
                  spacing: 12) {
            ForEach(data, id: \.self) { model in
                Button(action: {
                    viewModel.logic(key: model)
                }, label: {
                    if model.isDoubleWidth {
                        Rectangle()
                            .foregroundColor(model.backgroundColor)
                            .overlay(Text(model.title)
                                        .font(.largeTitle)
                                        .offset(x: 0 - (width * 0.22 * 0.5)))
                            .frame(width: width * 2 * 0.22,
                                   height: width * 0.22)
                    } else {
                        Text(model.title)
                            .font(.largeTitle)
                            .frame(width: width * 0.22,
                                   height: width * 0.22)
                    }
                })
                .foregroundColor(model.textColor)
                .background(model.backgroundColor)
                .cornerRadius(width * 0.25)
            }
        }
        .frame(width: width)
    }
}

Antes de compilar debemos arreglar las Previews, ya que al añadir el parámetro nuevo del ViewModel tendremos varios errores. Una vez se muestra todo correctamente en el Canvas podemos compilar nuestra app y probarla!

Si pulsamos 2 + 2 y le damos al = el resultado es 4
Si pulsamos 2 * 2 y le damos al = el resultado es 4
etc

¡Podemos hacer varias pruebas y ver que nuestra calculadora ya funciones!

En el post de hoy hemos aprendido a crear una calculadora usando SwiftUI, como ves ha sido realmente sencillo.

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


SwiftUI desde cero