Bienvenido, les saluda Miguel y hoy les traigo un nuevo tutorial.
Usando SwiftUI con arrastrar y soltar
Este artículo se basa en iOS 11.4 y una versión de Swift 5 lanzada a finales de marzo de 2020. Tenga cuidado: si no ha actualizado su iDevice y / o Xcode, obviamente no funcionará.
En este artículo, revisaré el nuevo protocolo de arrastrar y soltar con varios ejemplos. El objetivo final es crear un mini tablero de Sudoku que luego puedas poblar. Nuestro objetivo es simplemente crear un tablero en el que puedas arrastrar piezas. Por supuesto, podría ser cualquier tablero para cualquier juego de mesa que quieras probar y construir.
Inicialmente tenía la intención de escribir solo dos partes, pero sentí que el trabajo estaba inconcluso, así que aquí está la tercera.
¿A dónde vamos después de la parte 2? Estoy seguro de que casi puedes adivinar la palabra en la que estoy pensando: gamificación.
Reduzcamos el tamaño del tablero y agreguemos un temporizador que registre cuánto tiempo se tarda en completar, correctamente, por supuesto. El bit «correcto» será el desafío aquí tanto en las pruebas como en la codificación.
Empiece por reducir el número de valores en el textValue
cadena y el número de colores. También puedes hacer todo un poco más pequeño, cambiar la fuente y la altura a 48 y el ancho a 42. A tener en cuenta: Estoy probando esto en un iPhone 8. Si estás en un iPad, puedes usar diferentes dimensiones.
Compílelo, ejecútelo y verifique si todavía funciona. Ahora agregue algunos estados más al ciclo principal. Vamos a necesitar estos:
@State private var timerText = 0 @State private var startStop = false
A continuación, agregue un ciclo de temporizador, cuya sintaxis es irreconocible en SwiftUI:
let timer = Timer.publish(every: 0.1, on: .main, in: .common).autoconnect()
A continuación, agreguemos algo de código a nuestra interfaz para que funcione:
Text("\(timerText)") .font(Fonts.futuraCondensedMedium(size: fontSize/2)) .onReceive(timer) { input in if self.startStop { self.timerText = self.timerText + 1 } } .onReceive(timePublisher) { ( _ ) in self.startStop = false }
¿Qué tenemos aquí? Una etiqueta de texto, una variable y un editor combinado. Sí, uno de esos de nuevo. Obviamente, obtendrá un error cuando corte y pegue esto en su proyecto. Para aclararlo, debe agregar esto al principio del archivo:
import Combine let timePublisher = PassthroughSubject<Void, Never>()
¡Pero espera! Aunque se compilará, no funcionará. Aún debe iniciar y detener el temporizador. Para iniciarlo, configure la variable startStop
a true
en el dropUpdated
método, que es parte de tu dropDelegate
. Aquí hay un fragmento de código para ayudarlo a encontrarlo. Sí, metí una variable extra aquí. Volveremos a eso más tarde:
func dropUpdated(info: DropInfo) -> DropProposal? { let item = info.hasItemsConforming(to: ["public.utf8-plain-text"]) let dp = DropProposal(operation: .move) self.startStop = true // self.textValue = "" runOnce = false return dp }
Y para detenerlo, debe verificar que se hayan llenado todas las casillas. Podemos hacer eso con un método simple como este, nuevamente para ser llamado en el dropDelegate
:
func boardFull(textColors:[Color],figures:Int) -> Bool { for loop in 0 ..< figures { if textColors[loop] == Color.clear { return false } } return true }
Agregue este método a una llamada dentro del dropDelegate
método performDrop
. Quieres comprobar cada vez que alguien agrega un mosaico:
if boardFull(textColors: self.textColors, figures: self.rect.count) timePublisher.send() }
Justo antes de ejecutarlo, asegúrese de que la variable impar que mencioné anteriormente esté definida y configurada en true
:
var runOnce = true
Bien, deberías estar listo para navegar de nuevo. Compila todo y pruébalo. El temporizador debería comenzar a funcionar tan pronto como coloques tu primer mosaico y detenerse cuando coloques el último.
Sin embargo, es posible que se pregunte qué runOnce
es sobre. Bueno, después de agregar el cheque, descubrí que la matriz con las posiciones y tamaños de los cuadrados de hecho estaba creciendo cada vez que borraba un elemento con un toque. No importaba hasta que usé el rect
variable para decidir si terminó el tablero. puse runOnce
en su lugar para asegurar que el rect
no creció una vez elaborado. Necesitaba hacer un cambio nano en el InsideView
método también para hacer referencia a nuestro runOnce
bool:
.onAppear { if runOnce { self.rect.append (geometry.frame (in: .global)) } }
Todo está bien, pero hay otra pieza de lógica que agregar aquí: una pieza fundamental. Recuerda la regla: puedes colocar un número en cualquier mosaico, pero no debe ser otro mosaico con el mismo número en la misma línea, horizontal o verticalmente. Estrictamente hablando, también debemos incluir las diagonales. Escribí el código. Pero después de intentarlo varias veces, decidí dejar las diagonales. Con ellos en su lugar, fue demasiado difícil de completar.
Necesitamos ocho secuencias en el ejemplo que se muestra, con los mosaicos de izquierda a derecha (por ejemplo, mosaico 15 a 12, mosaico 11 a 8, etc.) y de arriba a abajo (p. Ej., Mosaico 15 a 3, mosaico 14 a 2, etc.) .
Necesitamos revisar los mosaicos en cada grupo para asegurarnos de que el mismo número / color no aparezca dos veces.
Es la aplicación perfecta para conjuntos. Selecciono los elementos colocados en cada línea en las ocho secuencias y los agrego a ocho conjuntos. Luego miro la cantidad de elementos que agregué a cada conjunto. Si tengo menos de cuatro elementos en un conjunto, según el ejemplo anterior, sé que debo haber agregado el mismo elemento dos veces. Entonces obtuve un duplicado.
Aquí está el código. Quizás tenga más sentido que mi explicación. Dejé la verificación inicial para asegurarme de que el tablero esté lleno antes de buscar duplicados:
func confirmColours(textColors:[Color],figures:Int) -> Bool? { for loop in 0 ..< figures { if textColors[loop] == Color.clear { return false } } var tfigures = figures - 1 let rfigure = Int(Double(figures).squareRoot()) for _ in 0..<rfigure { var superSet = Set<String>() for loop in stride(from: tfigures, to: tfigures - rfigure, by: -1) { superSet.insert(textColors[loop].description) print("loop ",loop) } tfigures = tfigures - rfigure print("superSet ",superSet,superSet.count) if superSet.count != rfigure { alertPublisher.send() } } tfigures = figures - 1 for _ in 0..<rfigure { var superSet = Set<String>() for loop in stride(from: tfigures, to: -1, by: -rfigure) { superSet.insert(textColors[loop].description) print("loop2 ",loop) } tfigures = tfigures - 1 print("superSet ",superSet,superSet.count) if superSet.count != rfigure { alertPublisher.send() } } return true }
También dejé bastantes declaraciones impresas allí para que pueda ver lo que está sucediendo cuando lo ejecuta.
Para completar la imagen, debe modificar performDrop
dentro de dropDelegate
método. Aquí está el código en cuestión:
func performDrop(info: DropInfo) -> Bool { textID = dropTarget(info: info) if textID == nil { return false } if let item = info.itemProviders(for: ["public.utf8-plain-text"]).first { item.loadItem(forTypeIdentifier: "public.utf8-plain-text", options: nil) { (urlData, error) in DispatchQueue.main.async { if let urlData = urlData as? Data { let text = String(decoding: urlData, as: UTF8.self) self.textText[self.textID!] = text // we need to subtract 1 cause array starts at zero self.textColors[self.textID!] = backgrounds[Int(text)! - 1] if boardFull(textColors: self.textColors, figures: self.rect.count) { if confirmColours(textColors: self.textColors, figures: self.rect.count) { timePublisher.send() } } } } } return true } else { return false } }
Nuestro método de validación también usa Combine para publicar un mensaje en la pantalla para advertir al usuario. Podemos agregar la etiqueta para el cuadro de alerta y el onReceive
justo debajo para que funcione:
.alert(isPresented: $showingAlert) { Alert(title: Text("Sorry You Failed"), message: Text("Sudoku Snaz"), dismissButton: .default(Text("Got it!"))) } .onReceive(alertPublisher) { (_) in self.showingAlert = true }
Necesitas agregar el showingAlert
variable de estado y la alertPublisher
a la mezcla también, por supuesto:
@State private var showingAlert = false
Y eso es. Estás listo para compilar y ejecutar de nuevo. Pruébelo y vea qué tan rápido puede completar una matriz correctamente. Intente cambiar el tamaño. Obviamente, cuanto mayor sea el conjunto, más difícil. Encontré los conjuntos impares más difíciles que los pares.
Jugué con él durante demasiado tiempo, de verdad, y me sorprendió que te envíe un mensaje para decir que fallaste pero ninguno para decir que lo lograste. Agreguemos eso también al código base. Podemos usar la plantilla para el error de nuestro mensaje de paso. Es una victoria rápida:
let winPublisher = PassthroughSubject<Void, Never>()
Y, por supuesto, el código SwiftUI que lo acompaña:
Spacer().onReceive(winPublisher, perform: { (_) in self.showingWin = true }).alert(isPresented: $showingWin) { Alert(title: Text("Success, You did it"), message: Text("Well Done"), dismissButton: .default(Text("Shake To Try Again!"))) }
Necesitas agregar el showingWin
estado a sus variables de estado. Copie y pegue el código y pruebe nuevamente. El desafío de la prueba esta vez es ganar el juego.
Necesitábamos pulir un poco nuestra aplicación. ¿Qué pasa cuando terminas y te dice que pasaste / reprobaste?
Nada. Lo dijiste … nada.
Necesitamos un método de reinicio. Usemos un buen gesto de agitar a la antigua para activar un reinicio. También deberá agregar un cuadro de confirmación, en caso de que accidentalmente comience a agitar su teléfono durante el juego.
Este código nos iniciará. Detectará un batido y usará otro. PassThroughSubject
para activar un elemento de interfaz:
extension UIWindow { open override func motionEnded(_ motion: UIEvent.EventSubtype, with event: UIEvent?) { if motion == .motionShake { // print("Device shaken") resetPublisher.send() } } }
Por supuesto, necesitamos declarar otro editor y el mensaje de alerta correspondiente y el código de restablecimiento:
let resetPublisher = PassthroughSubject<Void, Never>()
.onReceive(resetPublisher, perform: { (_) in self.showingReset = true }) Spacer().alert(isPresented:$showingReset) { Alert(title: Text("Reset Sure?"), message: Text("Zudoku Reset?"), primaryButton: .destructive(Text("Reset")) { for loop in 0 ..< self.textValue.count * self.textValue.count { self.textText[loop] = "" self.textColors[loop] = Color.clear } self.timerText = 0 }, secondaryButton: .cancel()) }
Probé algunas pruebas en el pasillo con mi hijo de 10 años. Le gustó, pero hizo dos solicitudes de mejora:
- Agrega un arrastre a la placa principal.
- Dame un gesto de empujar para sumar números.
Le di una oportunidad. No estoy seguro de la interfaz de usuario con poke. Se siente un poco desequilibrado dado que el golpe en el tablero lo borra, pero puede decidirlo usted mismo. Aquí, el código cambia. En primer lugar, debe agregar un poke
estado a través de un onTap
a la vista de texto:
HStack(alignment: .center, spacing: 8) { ForEach((0 ..< textValue.count), id: \.self) { column in Text(self.textValue[column]) .font(Fonts.futuraCondensedMedium(size: fontSize)) .frame(width: minWidith, height: minHeight, alignment: .center) .background(backgrounds[column]) .cornerRadius(minHeight/2) .onTapGesture { self.poke = self.textValue[column] } .onDrag { return NSItemProvider(object: self.textValue[column] as NSItemProviderWriting) } } }
Y luego necesitas agregar la funcionalidad a la matriz principal, que confieso que se ha convertido en un monstruo:
VStack(alignment: .center, spacing: 8) { ForEach((0 ..< self.textValue.count).reversed(), id: \.self) { row in HStack(alignment: .center, spacing: 8) { ForEach((0 ..< self.textValue.count).reversed(), id: \.self) { column in return VStack { if self.textColors[fCalc(c: column, r: row, x: self.textValue.count)] == Color.clear { Text(self.textText[fCalc(c: column, r: row, x: self.textValue.count)]) .font(Fonts.futuraCondensedMedium(size:fontSize - 12)) .frame(width: minWidith, height: minHeight, alignment: .center) .background(InsideView(rect: self.$rect)) .onDrop(of: ["public.utf8-plain-text"], delegate: dropDelegate) .onTapGesture { if self.poke != "" { self.textText[fCalc(c: column, r: row, x: self.textValue.count)] = self.poke self.textColors[fCalc(c: column, r: row, x: self.textValue.count)] = backgrounds[Int(self.poke)! - 1] self.poke = "" } } // .onAppear { // self.textText[fCalc(c: column, r: row, x: self.textValue.count)] = String(fCalc(c: column, r: row, x: self.textValue.count)) // } } else { Text(self.textText[fCalc(c: column, r: row, x: self.textValue.count)]) .onTapGesture { if self.poke != "" { self.textText[fCalc(c: column, r: row, x: self.textValue.count)] = self.poke self.textColors[fCalc(c: column, r: row, x: self.textValue.count)] = backgrounds[Int(self.poke)! - 1] self.poke = "" } else { self.textText[fCalc(c: column, r: row, x: self.textValue.count)] = "" self.textColors[fCalc(c: column, r: row, x: self.textValue.count)] = Color.clear } } .font(Fonts.futuraCondensedMedium(size:fontSize)) .frame(width: minWidith, height: minHeight, alignment: .center) .background(self.textColors[fCalc(c: column, r: row, x: self.textValue.count)]) .onDrop(of: ["public.utf8-plain-text"], delegate: dropDelegate) .onDrag { self.textColors[fCalc(c: column, r: row, x: self.textValue.count)] = Color.clear let copyCell = self.textText[fCalc(c: column, r: row, x: self.textValue.count)] self.textText[fCalc(c: column, r: row, x: self.textValue.count)] = "" return NSItemProvider(object: copyCell as NSItemProviderWriting) } } } } } } }
Lo probé de nuevo, pero me di cuenta de que el nuevo comportamiento de empuje había introducido nuevos errores. No solo no pudo iniciar el temporizador, sino que no revisó el tablero cuando terminé. Solo un poco más de ajustes. Necesitaba agregar este código al onTap
Acabo de publicar en la esencia de arriba:
self.startStop = true runOnce = false if boardFull(textColors: self.textColors, figures: self.rect.count) { if confirmColours(textColors: self.textColors, figures: self.rect.count) { timePublisher.send() } }
Y eso es. Por fin, se sentirá aliviado al leer que hemos terminado. Creé un icono, lo agregué a los activos en el proyecto y lo cargué en Apple. yo lo llamó Zudoku.
Feliz codificación.
Añadir comentario