📱 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.
Tabla de contenido
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
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.