Concurrencia en Swift
En cualquier programa escrito es Swift al iniciarse se crea un hilo de ejecución principal "@main". Este hilo de ejecución realiza cíclicamente un sondeo para detectar eventos de entradas, ejecutar código del usuario y actualizar la interfaz gráfica. Este sondeo es lo suficientemente rápido para dar la impresión que la aplicación reacciona al instante. Para lograr ello, el código síncrono, es decir el lineal, debe ser lo suficientemente optimizado. Pero esta capacidad de respuesta inmediata se ve afectada si se intenta ejecutar una operación pesada o que dependa de factores externos como velocidad y acceso a servicios en redes. Entonces se experimenta lo que conocemos como laps o retardos e incluso puede llegar a bloquearse por completo la aplicación.
La solución a este problema es el código concurrente. Debido a que Swift ejecuta el código asíncrono o concurrente fuera del hilo principal y pararelo a este, el hilo principal puede seguir ejecutándose y responder a los eventos del usuario, e incluso puede seguir ejecutando otro código sobre demanda.
Algo importante a tener en cuenta es que el código concurrente solo puede ser llamado desde otro código concurrente. Esta restricción esta implentada intencionalmente en Swift debido a que resulta muy inseguro llamar código concurrente desde código síncrono.
Una función o método asíncrono llava la palabra reservada async al final de la lista de parámetros y antes del valor de retorno, o de throws si es una función de lanzamiento. Ejemplo:
func listPhotos() async ->[Image]{
}
Para llamar a una función asincrónica debe utilizar la palabra reservada await que indica una posible pausa en la ejecución del código actual hasta el retorno de una función o método asíncrono. Ejemplo:
let result = await listPhotos()
Para llamara a una función asíncrona desde SwiftUI utilice Task para crear un contexto asíncrono en donde poder llamar la función:
Button("Tap me"){
Task{
await listPhotos()
}
}
Otro ejemplo, un poco más complejo, lo tenemos aqui: Una función que acepta dos parámetros, un String y como segundo un closure asíncrono con el valor del primer parámetro. Debido a que la función retorna un closure asíncrono, el parámetro de closure debe ser asíncrono también.
func caller(value: String, action: @escaping (String)async ->()) async ->(){
return await action(value)
}
Para llamar a esta función desde SwiftUI u otra función tendríamos:
Button("Tap me"){
Task{
await caller(value: "Yorj") {str in
print(str)
}
}
}
Operaciones asincrónicas en paralelo:
Aunque las operaciones asíncronas se realizan en paralelo, cada operación debe esperar a terminar para dar paso a la siguiente. Esto produce un código secuencial que no es óptimo para realizar varias tareas a la vez.
Para lograr que el código asíncrono se ejecute de forma paralela al código circundante utilice la notación: async let = nombreFunciónAsincrónica:
async let result1 = test1()
async let result2 = test2()
async let result3= test3()
Después cuando quiera recuperar el valor de estas funciones escriba await delante de cada constante:
let resultTotal = await [result1, result2, result3]
De esta forma se logra que las funciones sincrónicas se ejecuten en paralelo, sin haber esperas entre ellas.
Actores:
Debido a que las funciones asíncronas están asialadas entre si, es seguro ejecutarlas al mismo tiempo. Sin embargo a veces es útil compartir información entre funciones asíncronas. La solución a ello son los actores. Un actor es un tipo por referencia, como las clases, que permite compartir información entre código concurrente. Los actores garantizan que su estado mutable (sus propiedades) sean accedidas y modificadas por un código concurrente a la vez. Esto resuelve los posibles conflictos de acceso superpuesto sobre una misma propiedad. Un ejemplo de actor seria:
actor temperaturas {
var arrayTemp : [Double] = []
var tempMax : Double
init(tempMax : Double){
self.tempMax = tempMax
}
func addValor(valor : Double){
arrayTemp.append(valor)
}
}
Para acceder a este actor debe primero crear una instancia del mismo. Luego para leer algunas de sus propiedades se debe utilizar la palabra reservada await para indicar un posible punto de suspención:
let actor = temperaturas(tempMax: 14)
print( await actor.tempMax)
Para modificar el estado mutable de un actor debe crear un método dentro del propio actor para ello. Por ejemplo:
extensión temperaturas {
func setTempMax(value : Double){
self.tempMax = value
}
}
Propiedades Calculadas Async
Las propiedades calculadas también pueden ser asíncronas. La única regla es que deben ser de solo lectura. Utilice await para acceder a su valor:
struct yy {
var contents: Int { get async {
45
}
}
}
let tt= await yy().contentsCrear modificador personalizado
Por ejemplo. Esto crea una función de tipo modificador que añade la funcionalidad de poder ocultar una vista según un valor true/false dado.
En cualquier parte de nuestro código, o dentro de un archivo swift, con alcance global:
extension View {
@ViewBuilder func isHidden(_ isHidden: Bool) -> some View {
if isHidden {
self.hidden()
} else {
self
}
}
}
Un ejemplo de su uso sería:
@State private var isHidden = true
Button("OK"){ //code }.isHidden(isHidden)
Otra forma de crear nuestros modificadores personalizados es utilizando struct que conformen el protocolo ViewModifier. La manera de aplicar este tipo de modificador es utilizando el modificador especial .modifier("NombreModificador") en una vista: Button, Text, etc... Por ejemplo, el siguiente modificador crea un botón con degradado de colores:
//Buttom Style
struct GradientButtonStyle: ViewModifier {
var alto : CGFloat = 20
var ancho : CGFloat = 100
var colors : [Color] = [.red, .orange]
func body(content: Content) -> some View { content
.frame(width: ancho, height: alto)
.foregroundColor(Color.white)
.padding()
.background(LinearGradient(gradient: Gradient(colors: colors), startPoint: .top, endPoint: .bottom))
.cornerRadius(15.0)
}
}
Un ejemplo de uso seria:
Button{
showGame = true
}label: {
bloqueA("gamecontroller", "Evaluación")
}.modifier(GradientButtonStyle(ancho: sizeWigth, colors: colorGradientButton))
Como se puede apreciar es una capacidad muy útil para personalizar nuestro código o construir nuestras propias bibliotecas de código prefrabricado para nuestros proyectos.
Espero que les haya resultado útil. Feliz codificación!
Mostrar/Ocultar Barra de Progreso
Actualmente no existe un modificador que permita ocultar una vista dado un valor. Una solución es hacer una extensión de View que incluya este comportamiento:
extension View {
@ViewBuilder func isHidden(_ isHidden: Bool) -> some View {
if isHidden {
self.hidden()
} else {
self
}
}
}
Ahora podríamos aplicarla, por ejemplo, para mostrar/ocultar una barra de progreso mientras se realiza una tarea asincrónica:
Primero:
Definir la función asincrónica que realizará la tarea en segundo plano. Para hacerla lo más completa posible vamos a hacer una función que acepte un parámetro y devuelva el valor de ese parámetro en un closure:
func test( name : String, action: @escaping (String)->()) async {
let tt = name.lowercased()
sleep(3) //Hace una pausa de 3 segundo para simular trabajo…
return action(tt)
}
Segundo: dentro de nuestra Vista:
Nota: en este ejemplo, se muestra una barra de progreso circular hasta que se completa la tarea asincrónica. Después de lo cual se oculta.
@State private var show = true
VStack {
Text(text)
.padding(25)
.overlay { //Mostrar la barra de progreso
ProgressView(label: {
Text("Cargando...")
})
.frame(width: 200) //Para que se vea todo el texto
.isHidden(isHidden)
}
Button("OK"){
text = ""
isHidden = false
//las funciones async deben de llamarse dentro de un contexto asincrónico y siempre precedidas por await
Task{
await test(name: "Yorjandis") { str in
text = str
isHidden = true
}
}
}
}
Swift 5.9 Novedades
Ha sido publicada la versión 5.9 de Swift, el lenguaje de programación preferido por la comunidad Apple Developer para el desarrollo de software en sus distintas plataformas: iOS/ipadOS, macOS, watchOS, tvOs y visionOS. Esta nueva iteración de Swift, soportada en Xcode 15.1, viene con numerosas características que favorecen la expresividad, claridad y limpieza del código a la vez que elimina en su mayor parte el código repetitivo sumando tiempo útil al desarrollador. Además introduce nuevos conceptos como tipos no copiables y una forma útil de administrar, programáticamente, el ciclo de vida de una variable.
Entre sus novedades están:
* If/switch como expresiones* Un sistema de generación de macros expresivos
*Empaquetamiento de parámetros en funciones
*Nuevo concepto de struct o enum no copiable: ~Copyable
*Nuevo operador consume para controlar el alcance o ciclo de vida de una variable o tipo.
*Soporte para interoperabilidad bidireccional con C++
*Nueva Macro para marcar clases como ObservableObject
Veámoslo con un poco más de detalles.
If/switch como expresiones(SE-0380):
Ahora podemos utilizar las estructuras condicionales if/switch para obtener el valor directamente de una variable. Es decir, pueden actual como expresiones asignables a una variable. Veamos un ejemplo:
import SwiftUI
let color : Color = if myGenere == "M" {.blue} else {.pink}
let color : Color = switch myGenere {
case "M": .blue
case "H": .pink
default: .black
}
Esta nueva técnica cobra relevancia en las variables computadas donde sabemos que la palabra return puede ignorarse:
@State private var color : Color {
switch myGenere {
case "M": .blue
case "H": .pink
default: .black
}
Sistema de Macros expresivos(SE-0382, SE-0389, SE-0397):
Las macros son un recurso que nos permite reducir el texto repetitivo a la vez que nos ayudan a crear bibliotecas de código más expresivas. Son similares a funciones, pero no pertenecen a su código. Las macros utilizan la biblioteca SwiftSyntax para generar código que luego será insertado en su código de producción. De esta forma ayudan a la generación de código reutilizable a la vez que contribuyen a mantener el enfoque claro y expresivo de Swift.
Una macro se compone de un nombre precedido del símbolo @. Su uso en el código resulta natural e intuitivo. Veamos un ejemplo tomado del sitio web de Swift.org:
// Move storage from the stored properties into a dictionary,
// turning the stored properties into computed properties.
@DictionaryStorage
struct Point {
var x: Int = 1
var y: Int = 2
}
Otras de las ventajas de las macros es lo relativo a la observación de cambios en objetos: Antiguamente se tenia que ajustar la clase obervada al protocolo @ObservableObject y marcar las propiedades a verificar con @Published. Esto generaba mucho código repetitivo. Ahora, con la macro @Observable todo este proceso se simplifica. Veamos un ejemplo:
@Observable final class User{
var name : String
var edad : Int
init(name: String, edad: Int) {
self.name = name
self.edad = 10
}
}
Luego para hacer uso de esta clase:
struct vista : View {
private var nombre : User = User(name: "Yorjandis", edad: 25)
var body: some View {
VStack{
Text(nombre.name)
Button("OK"){
nombre.name = "Juancito"
}
}
}
}
Las Macros en Swift se escriben con un enfoque potente y flexible. Similar a una función pueden aceptar parámetros para modificar su comportamiento de una manera dinámica.
Actualmente, esta versión de Swift admite macros en macOS y Linux, y próximamente Windows.
Empaquetado de parámetros en funciones:
Esta nueva opción nos permite escribir tipos y funciones genéricas con una cantidad arbitraria de parámetros, en una forma concisa y elegante. Esto viene a solucionar el problema del limite superior de parámetros posibles. Por ejemplo, para definir una función genérica que compruebe un número albitrario de parámetros como nil, tendríamos que hacer uso de sobrecarga para abarcar todas las longitudes posibles de parámetros. Esto nos lleva a establecer un límite lógico superior. Ahora con la nueva notación de parámetros empaquetados podemos eliminar este inconveniente:
Func ComprobarValorNulo<each setValores>(setValoresPosibles : repeat (each setValores)?)->(repeat [each setValores?]){
Return (repeat [each setValores?])
}
Nuevo concepto de struct o enum no copiable: ~Copyable (SE-0390)
Swift 5.9 introduce el concepto de tipos no copiables que significa que solo puede haber una instancia en memoria. Se puede cambiar de propietario de la instancia, pero solo habrá una sola referencia a dicha instancia. Esto permite el acceso a una única instancia desde muchos lugares. Se ha intriducido una nueva sintaxis para definir este comportamiento: ~Copyable. Por ejemplo:
struct User: ~Copyable {
var name: String
}
Let user1 = User(name: “juan”)
Let userCopy = user1
Print(user1.name) //!ERROR el compilador nos dirá que este tipo ya ha sido consumido.
Al hacer la copia de la instancia de user1 a userCopy se ha cambiado el propietario de la instancia. Ya user1 es nil porque su valor ha sido “Consumido”.
Esta restricción también se aplica a los parámetros de tipo no copiables adicinando dos nuevas palabras de lenguajes: consuming y borrowing. La primera le indica a la función que el tipo no copiable pasado como parámetros será consumido al finalizar la función. La segunda palabra “borrowing” o borrador le indica a la función que debe tomar prestado una copia de solo lectura del valor. Por ejemplo:
struct Persona : ~Copyable{
let name : String
}
func test(){
let p1 = Persona(name: "juan")
consumir(a: p1)
print(p1.name) //Error!! El valor de P1 ha sido consumido al finalizar la func consumir(a : )…
}
func test2(){
let p1 = Persona(name: "juan")
NoConsumir (a: p1)
print(p1.name) //el valor aún puede ser leído porque no se ha consumido
}
func consumir(a : consuming Persona){
print(a.name)
}
func NoConsumir(a : borrowing Persona){
print(a.name)
}
Soporte para interoperabilidad bidireccional con C++
Swift 5.9 introduce una amplia interoperabilidad con el lenguaje C++ y ObjectiveC++ permitiendo el uso código nativo C++ en Swift y viceversa con muy poco esfuerzo.
La interoperabilidad de C++ está evolucionando activamente, con algunos aspectos sujetos a cambios en futuras versiones a medida que la comunidad recopile comentarios sobre la adopción en el mundo real en bases de código mixtas de Swift y C++.
Para obtener información sobre cómo habilitar la interoperabilidad de C++ y el subconjunto de idiomas admitidos, consulte la documentación.
Nueva Macro para marcar clases como ObservableObject
Con el nuevo sistema de Macros expresivas en Swift 5.9 ahora podemos marcar clases como conformen al protocolo Observable con solo una sola palabra: @Observable. Esto hace automáticamente todas las propiedades de la clases como públicas pudiendo reaccionar luego a los cambios de estado. Veamos un ejemplo sencillo:
import SwiftUI
final class Yorjan : ObservableObject {
var name : String
var edad : Int
init(name: String, edad: Int) {
self.name = name
self.edad = 10
}
}
struct vista : View {
private var nombre : Yorjan = Yorjan(name: "Yorjandis", edad: 25)
var body: some View {
VStack{
Text(nombre.name)
Button("OK"){
nombre.name = "juancito"
}
}
}
}
Muchas otras mejoras se han añadido como el soporte para una mejor detección de errores en Xcode, optimización para operaciones de concurrencia, mejoras en el Swift Package Manager, etc. Podeis ver la lista completa aquí.