DEV Community

GoyesDev
GoyesDev

Posted on • Edited on

[SUI] Sheets

Para presentar una vista de forma modal:

  • sheet(isPresented:onDismiss:content:): isPresented es un Binding<Bool>. onDismiss es un closure que se ejecuta cuando se cierra el modal. content es el contenido del modal.
  • sheet(item:onDismiss:content:): content pinta el contenido del modal, recibiendo un argumento de tipo Item (requerido). item es un Binding<Item?> opcional que sirve como argumento de content: si es nil se cierra el modal, si es distinto de nil se pinta el modal, y si se cambia el valor de item entonces se cierra y vuelve a abrir el modal. onDismiss es una acción a ejecutar cuando se cierra el modal.

Para presentar una vista de pantalla completa se usa:

Tener en cuenta que las vistas presentadas con sheet o fullScreenCover no hacen parte del stack del NavigationStack.

struct DetailView: View {
  let person: Person

  var body: some View {
    VStack {
      Text("Esta es la vista detalle para el siguiente personaje:")
      Text("\(person.name) \(person.lastname)")
    }
    .navigationTitle("Detalle")
    .navigationBarTitleDisplayMode(.inline)
  }
}
Enter fullscreen mode Exit fullscreen mode
struct PersonForm: View {
  // ⚠️ Se recibe la "base de datos" por Binding
  @Binding var people: [Person]
  @State private var name: String = ""
  @State private var lastname: String = ""
  @Environment(\.dismiss) private var dismissAction

  var body: some View {
    Form {
      TextField("Nombre", text: $name)
      TextField("Nombre", text: $lastname)
      Button("Guardar") {
        // ⚠️ Se crea el nuevo Person
        let newPerson = Person(name: name, lastname: lastname)
        // ⚠️ Se agrega en la "base de datos"
        people.append(newPerson)
        // ⚠️ Se cierra el formulario con dismissAction()
        dismissAction()
      }
    }
  }
}
Enter fullscreen mode Exit fullscreen mode
struct ContentView: View {

  @State private var searchText: String = ""
  @State private var navigationPath = NavigationPath()
  @State private var _people = people
  @State private var formVisible = false

  private var filteredPeople: [Person] {
    if searchText.isEmpty {
      _people
    } else {
      _people.filter {
        $0.name.localizedStandardContains(searchText) }
    }
  }

  var body: some View {
    NavigationStack(path: $navigationPath) {
      List(filteredPeople) { person in
        NavigationLink(value: person) {
          Label("\(person.name) \(person.lastname)", systemImage: "person")
        }
      }
      .navigationDestination(for: Person.self, destination: { person in
        DetailView(person: person)
      })
      .navigationTitle("Personas")
      .searchable(text: $searchText, placement: .navigationBarDrawer(displayMode: .automatic), prompt: "¿A quién busca?")
      .toolbar {
        ToolbarItem(placement: .topBarTrailing) {
          Button("Nuevo", systemImage: "plus") {
            // ⚠️ Se pone formVisible=true para mostrar el sheet
            formVisible = true
          }
        }
      }
      // ⚠️ el sheet (PersonForm) es visible si formVisible=true
      .sheet(isPresented: $formVisible) {
        // Se crea la instancia de PersonForm y se pasa la
        // "base de datos"
        PersonForm(people: $_people)
      }
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Arrastrar para cerrar el Sheet

Por defecto, se puede arrastrar la vista hacia abajo para cerrar el sheet. En caso de que este comportamiento no sea deseado, se puede desactivar aplicando el modificador interactiveDismissDisabled(_:) sobre la vista presentada de forma modal.

En caso de desactivar la función de arrastre por defecto, conviene agregar un botón adicional que le permita al usuario cerrar el sheet.

struct PersonForm: View {
  @Binding var people: [Person]
  @State private var name: String = ""
  @State private var lastname: String = ""
  @Environment(\.dismiss) private var dismissAction

  var body: some View {
    NavigationStack {
      Form {
        TextField("Nombre", text: $name)
        TextField("Nombre", text: $lastname)
        Button("Guardar") {
          let newPerson = Person(name: name, lastname: lastname)
          people.append(newPerson)
          dismissAction()
        }
      }
      .toolbar {
        // ⚠️ Con este botón el usuario puede cerrar el sheet
        Button("Cerrar", systemImage: "x.circle") {
          dismissAction()
        }
      }
    }
  }
}
Enter fullscreen mode Exit fullscreen mode
struct ContentView: View {

  @State private var searchText: String = ""
  @State private var navigationPath = NavigationPath()
  @Namespace var personNamespace
  @State private var _people = people
  @State private var formVisible = false

  private var filteredPeople: [Person] {
    if searchText.isEmpty {
      _people
    } else {
      _people.filter {
        $0.name.localizedStandardContains(searchText) }
    }
  }

  var body: some View {
    NavigationStack(path: $navigationPath) {
      List(filteredPeople) { person in
        NavigationLink(value: person) {
          Label("\(person.name) \(person.lastname)", systemImage: "person")
            .matchedTransitionSource(id: person.id, in: personNamespace)
        }
      }
      .navigationDestination(for: Person.self, destination: { person in
        DetailView(person: person)
          .navigationTransition(.zoom(sourceID: person.id, in: personNamespace))
      })
      .navigationTitle("Personas")
      .searchable(text: $searchText, placement: .navigationBarDrawer(displayMode: .automatic), prompt: "¿A quién busca?")
      .toolbar {
        ToolbarItem(placement: .topBarTrailing) {
          Button("Nuevo", systemImage: "plus") {
            formVisible = true
          }
        }
      }
      .sheet(isPresented: $formVisible) {
        // ⚠️ Se aplica interactiveDismissDisabled para quitar
        // la función de arrastre por defecto
        PersonForm(people: $_people)
          .interactiveDismissDisabled()
      }
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Configurar el tope ("detent") del sheet

presentationDetents(_:) define los topes (PresentationDetent) de un sheet con los siguientes valores:

Cuando hay varios topes disponibles, el sistema muestra un indicador que se puede ocultar con presentationDragIndicator(_:).

Notar en el siguiente ejemplo que se aplican los modificadores a la vista a presentar como sheet.

struct ContentView: View {

  @State private var searchText: String = ""
  @State private var navigationPath = NavigationPath()
  @Namespace var personNamespace
  @State private var _people = people
  @State private var formVisible = false

  private var filteredPeople: [Person] {
    if searchText.isEmpty {
      _people
    } else {
      _people.filter {
        $0.name.localizedStandardContains(searchText) }
    }
  }

  var body: some View {
    NavigationStack(path: $navigationPath) {
      List(filteredPeople) { person in
        NavigationLink(value: person) {
          Label("\(person.name) \(person.lastname)", systemImage: "person")
            .matchedTransitionSource(id: person.id, in: personNamespace)
        }
      }
      .navigationDestination(for: Person.self, destination: { person in
        DetailView(person: person)
          .navigationTransition(.zoom(sourceID: person.id, in: personNamespace))
      })
      .navigationTitle("Personas")
      .searchable(text: $searchText, placement: .navigationBarDrawer(displayMode: .automatic), prompt: "¿A quién busca?")
      .toolbar {
        ToolbarItem(placement: .topBarTrailing) {
          Button("Nuevo", systemImage: "plus") {
            formVisible = true
          }
        }
      }
      .sheet(isPresented: $formVisible) {
        PersonForm(people: $_people)
          // ⚠️ El sheet tendrá topes .medium y .large
          .presentationDetents([.medium, .large])
          // ⚠️ Quitar el indicador de arrastre con .hidden
          .presentationDragIndicator(.automatic)
          .interactiveDismissDisabled()
      }
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Cambiar la forma del sheet

  • presentationBackground(_:) define el fondo de un sheet de tipo ShapeStyle como un Color o Material. Antes de iOS 26 tenía sentido aplicar un Material para hacer translúcido el sheet, sin embargo, a partir de iOS 26 cualquier material lo hace ver opaco, mientras que el comportamiento por defecto tiene el efecto vidrio.
.sheet(isPresented: $formVisible) {
  PersonForm(people: $_people)
    .presentationBackground(.ultraThinMaterial)
    .presentationDetents([.medium])
    .interactiveDismissDisabled()
}
Enter fullscreen mode Exit fullscreen mode

Fondo por defecto:

Con Material.ultraThinMaterial:

Con Color.red:

Interacción con el fondo con el sheet visible

.sheet(isPresented: $formVisible) {
  PersonForm(people: $_people)
    .presentationBackground(.ultraThickMaterial)
    .presentationDetents([.height(120), .medium, .large])
    .presentationBackgroundInteraction(.enabled(upThrough: .height(120)))
    .interactiveDismissDisabled()
}
Enter fullscreen mode Exit fullscreen mode

En modo .medium no se puede interactuar con el fondo:

En .height(120), sí se puede interactuar con el fondo:

Fijar el radio de las esquinas de un sheet

.sheet(isPresented: $formVisible) {
  PersonForm(people: $_people)
    .presentationBackground(.ultraThickMaterial)
    .presentationDetents([.height(120), .medium, .large])
    .presentationCornerRadius(10)
    .interactiveDismissDisabled()
}
Enter fullscreen mode Exit fullscreen mode

Parece casi recto (y antinatural):


Bibliografía

Top comments (0)