La traducción del artículo fue preparada específicamente para estudiantes del curso "Desarrollador iOS. Curso avanzado v 2.0 ".
La semana pasada comenzamos una nueva serie de
publicaciones sobre el marco SwiftUI. Hoy quiero continuar con este tema hablando sobre
Property Wrappers en SwiftUI. SwiftUI nos proporciona envoltorios para
@State
,
@Binding
,
@ObservedObject
,
@EnvironmentObject
y
@Environment
. Entonces, tratemos de entender la diferencia entre ellos y cuándo, por qué y cuál debemos usar.
Envoltorios de propiedad
Los envoltorios de propiedades (en lo sucesivo
denominados "envoltorios de propiedades") se describen en
SE-0258 . La idea principal es ajustar las propiedades con la lógica, que se puede extraer en una estructura separada para su reutilización en la base del código.
@State
es un contenedor que podemos usar para indicar el estado de una
View
. SwiftUI lo almacenará en una memoria interna especial fuera de la estructura de
View
. Solo una
View
vinculada puede acceder a ella. A
@State
cambia el valor de la propiedad
@State
, SwiftUI reconstruye la
View
para tener en cuenta los cambios de estado. Aquí hay un ejemplo simple.
struct ProductsView: View { let products: [Product] @State private var showFavorited: Bool = false var body: some View { List { Button( action: { self.showFavorited.toggle() }, label: { Text("Change filter") } ) ForEach(products) { product in if !self.showFavorited || product.isFavorited { Text(product.title) } } } } }
En el ejemplo anterior, tenemos una pantalla simple con un botón y una lista de productos. Tan pronto como hacemos clic en el botón, cambia el valor de la propiedad del estado y SwiftUI reconstruye la
View
.
@Binding
@Binding
proporciona acceso de referencia para el tipo de valor. A veces necesitamos hacer que el estado de nuestra
View
accesible para sus hijos. Pero no podemos simplemente tomar y pasar este valor, porque es un tipo de valor, y Swift pasará una copia de este valor. Aquí es donde la envoltura de la propiedad
@Binding
viene al rescate.
struct FilterView: View { @Binding var showFavorited: Bool var body: some View { Toggle(isOn: $showFavorited) { Text("Change filter") } } } struct ProductsView: View { let products: [Product] @State private var showFavorited: Bool = false var body: some View { List { FilterView(showFavorited: $showFavorited) ForEach(products) { product in if !self.showFavorited || product.isFavorited { Text(product.title) } } } } }
Usamos
@Binding
para marcar la propiedad
showFavorited
dentro de
FilterView
. También usamos el carácter especial
$
para pasar el enlace de anclaje, porque sin
$
Swift pasará una copia del valor en lugar de pasar el enlace de anclaje en sí.
FilterView
puede leer y escribir el valor de la propiedad
showFavorited
en
ProductsView
, pero no puede realizar un seguimiento de los cambios mediante este enlace. Tan pronto como
FilterView
cambie el valor de la propiedad showFavorited, SwiftUI vuelve a crear
ProductsView
y
FilterView
como elementos secundarios.
@ObservedObject
@ObservedObject
funciona de manera similar a
@State
, pero la principal diferencia es que podemos dividirlo entre varias
View
independientes, que pueden suscribirse y observar los cambios de este objeto, y tan pronto como aparecen los cambios,
SwiftUI
reconstruye todas las vistas asociadas con este objeto . Veamos un ejemplo.
import Combine final class PodcastPlayer: ObservableObject { @Published private(set) var isPlaying: Bool = false func play() { isPlaying = true } func pause() { isPlaying = false } }
Aquí tenemos la clase
PodcastPlayer
, que las pantallas de nuestra aplicación comparten entre sí. Cada pantalla debe mostrar un botón de pausa flotante cuando la aplicación está reproduciendo un episodio de podcast.
SwiftUI
rastrea los cambios en un
ObservableObject
utilizando el contenedor
@Published
, y tan pronto como la propiedad marcada como
@Published
cambios,
SwiftUI
reconstruye todas las
SwiftUI
asociadas con este
PodcastPlayer
. Aquí usamos el contenedor
@ObservedObject
para vincular nuestro
EpisodesView
a la clase
PodcastPlayer
struct EpisodesView: View { @ObservedObject var player: PodcastPlayer let episodes: [Episode] var body: some View { List { Button( action: { if self.player.isPlaying { self.player.pause() } else { self.player.play() } }, label: { Text(player.isPlaying ? "Pause": "Play") } ) ForEach(episodes) { episode in Text(episode.title) } } } }
@EnvironmentObject
En lugar de pasar un
ObservableObject
través del método init de nuestra
View
, podemos incorporarlo implícitamente en el
Environment
nuestra jerarquía de
View
. Al hacer esto, hacemos posible que todas las vistas secundarias del
Environment
actual accedan a este
ObservableObject
.
class SceneDelegate: UIResponder, UIWindowSceneDelegate { var window: UIWindow? func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { let window = UIWindow(frame: UIScreen.main.bounds) let episodes = [ Episode(id: 1, title: "First episode"), Episode(id: 2, title: "Second episode") ] let player = PodcastPlayer() window.rootViewController = UIHostingController( rootView: EpisodesView(episodes: episodes) .environmentObject(player) ) self.window = window window.makeKeyAndVisible() } } struct EpisodesView: View { @EnvironmentObject var player: PodcastPlayer let episodes: [Episode] var body: some View { List { Button( action: { if self.player.isPlaying { self.player.pause() } else { self.player.play() } }, label: { Text(player.isPlaying ? "Pause": "Play") } ) ForEach(episodes) { episode in Text(episode.title) } } } }
Como puede ver, debemos pasar el
PodcastPlayer
través del modificador
PodcastPlayer
de nuestra
View
. Al hacer esto, podemos acceder fácilmente al
PodcastPlayer
definiéndolo usando el contenedor
@EnvironmentObject
.
@EnvironmentObject
utiliza la función de búsqueda dinámica de miembros para encontrar una instancia de la clase
PodcastPlayer
en
Environment
, por lo que no necesita pasarla por el método de inicio
EpisodesView
. El entorno es la forma correcta de
inyectar dependencias en
SwiftUI .
@Environment
Como dijimos en el capítulo anterior, podemos transferir objetos personalizados a la jerarquía de
View
del
Environment
dentro de
SwiftUI . Pero
SwiftUI ya tiene un
Environment
lleno de configuraciones de todo el sistema. Podemos acceder fácilmente a ellos utilizando el contenedor
@Environment
.
struct CalendarView: View { @Environment(\.calendar) var calendar: Calendar @Environment(\.locale) var locale: Locale @Environment(\.colorScheme) var colorScheme: ColorScheme var body: some View { return Text(locale.identifier) } }
Al marcar nuestras propiedades con el contenedor
@Environment
, obtenemos acceso y nos suscribimos a los cambios en la configuración de todo el sistema. Tan pronto como
cambian los sistemas
Locale ,
Calendar o
ColorScheme ,
SwiftUI recrea nuestro
CalendarView
.
Conclusión
Hoy hablamos sobre los
Envoltorios de propiedades proporcionados por
SwiftUI .
@State
,
@Binding
,
@EnvironmentObject
y
@ObservedObject
juegan un papel muy importante en el desarrollo de
SwiftUI . Gracias por su atencion!