DEV Community

GoyesDev
GoyesDev

Posted on

SwiftUI #25: Estado (@State)

El paradigma declarativo no solo se trata de cómo organizar las vistas, sino de que cada vez que cambia el estado de la aplicación, las vistas deben actualizarse y reflejarlo.

@State

El "property-wrapper" @State envuelve un valor de tipo SomeType en una estructura de tipo State<SomeType> y notifica al sistema cuando ese valor cambia, para que la vista se actualice automáticamente.

La propiedad marcada con @State debería ser private puesto que representa el estado de esa vista y no debería ser modificado por sus clientes.

struct ContentView: View {
  @State private var title: String = "Título inicial"
  var body: some View {
    VStack {
      Text(title)
      Button {
        title = "Otro título"
      } label: {
        Text("Modificar")
      }
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

En el ejemplo anterior, cuando title cambia, @State notifica el cambio al sistema y luego el contenido de body es actualizado.

Binding

Un Binding es una referencia a un valor almacenado por un State.

Para que una vista pueda modificar internamente el valor almacenado por un State, se necesita pasarle una referencia al State por medio de su projectedValue, que puede ser accedido agregando el prefijo $ al nombre de la variable.

struct ContentView: View {

  @State private var tituloFinal: String = "Título inicial"
  @State private var title: String = ""

  var body: some View {
    VStack {
      Text(tituloFinal)
        .padding(10)
      TextField("Ingrese título", text: $title)
      Button {
        tituloFinal = title
      } label: {
        Text("Actualizar título")
      }
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Por otro lado, a la hora de construir una vista que modifique el valor envuelto por un State externo, se debe almacenar la referencia con una variable de instancia de tipo Binding, usando el "property-wrapper" @Binding.

struct ContentSubview: View {
  var title: String
  @Binding var titleInput: String

  var body: some View {
    VStack {
      Text(title)
        .padding(10)
      TextField("Ingrese título", text: $titleInput)
    }
  }
}
Enter fullscreen mode Exit fullscreen mode
struct ContentView: View {

  @State private var tituloFinal: String = "Título inicial"
  @State private var title: String = ""

  var body: some View {
    VStack {
      ContentSubview(title: tituloFinal, titleInput: $title)
      Button {
        tituloFinal = title
      } label: {
        Text("Actualizar título")
      }
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Una propiedad @Binding siempre recibe su valor de una propiedad @State, así que no es necesario asignar un valor por defecto.

State y Binding como estructura

Swift permite acceder a la estructura del "property-wrapper" usando un guión bajo como prefijo del nombre de la variable. Por ejemplo, si se marca una variable nombre con @State, se puede acceder a la estructura asociada con _nombre. A continuación se listan las propiedades de esta estructura:

  • wrappedValue: El valor manejado por @State (referenciado por @Binding). Se accede a este valor usando directamente el nombre de la propiedad (sin _)
  • projectedValue: Un Binding que funciona como referencia hacia State. Se accede a él a través de $ en lugar de _.
struct ContentSubview: View {
  var title: String
  @Binding var titleInput: String

  var body: some View {
    VStack {
      Text(title)
        .padding(10)
      TextField("Ingrese título", text: _titleInput.projectedValue)
        .textFieldStyle(.roundedBorder)
    }
  }
}

struct ContentView: View {

  @State private var tituloFinal: String = "Título inicial"
  @State private var title: String = ""

  var body: some View {
    VStack {
      ContentSubview(title: _tituloFinal.wrappedValue, titleInput: _title.projectedValue)
      Button {
        _tituloFinal.wrappedValue = _title.wrappedValue
      } label: {
        Text("Actualizar título")
      }
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Binding manual

Generalmente se usa un Binding en conjunto con un State. Sin embargo, también es posible usar solo el Binding por medio del constructor init(get:set:) que recibe dos closures: get y set para retornar y modificar un valor.

Crear un Binding manualmente puede servir para crear una previsualización de un componente que espera recibir un Binding.

struct ContentSubview: View {
  var title: String
  @Binding var titleInput: String

  var body: some View {
    VStack {
      Text(title)
        .padding(10)
      TextField("Ingrese título", text: _titleInput.projectedValue)
        .textFieldStyle(.roundedBorder)
    }
  }
}
Enter fullscreen mode Exit fullscreen mode
#Preview("Manual binding") {
  var someTitle = ""
  var titleBinding = Binding<String> {
    someTitle
  } set: { newValue in
    someTitle = newValue
  }

  ContentSubview(title: "Título inicial", titleInput: titleBinding)
}
Enter fullscreen mode Exit fullscreen mode

A veces conviene solo pasar de referencia al Binding un valor inmutable. En este caso, se usa el método estático: .constant(_:).

#Preview("Manual binding") {
  ContentSubview(title: "Título inicial", titleInput: .constant("Algún título"))
}
Enter fullscreen mode Exit fullscreen mode

Creando State desde #Preview

Aunque se puede crear manualmente el Binding, es mucho más fácil usar el macro @Previewable permite usar un @State dentro de un #Preview.

#Preview("@Previewable") {
  @Previewable @State var title = ""
  ContentSubview(title: "Título inicial", titleInput: $title)
}
Enter fullscreen mode Exit fullscreen mode

Top comments (0)