Skip to content

Recetas nativas completas ​

Estas pantallas ejecutables de Core NativeScript incluyen creación de vistas y limpieza. Usan APIs públicas sin depender de un framework; el preview Vue solo monta el Frame. El código se incluye directamente desde los archivos compilados.

Ejecute el demo con --recipe=gallery, --recipe=sheet o --recipe=lists. Android usa el extra recipe correspondiente. Se admite Core ListView; otros routers y listas necesitan su propio resolvedor. Las capturas validan el simulador, no el rendimiento físico.

Galería ​

Use claves estables y resuelva la celda actual al volver. Cargue imágenes mediante src para que cada vista conserve su propio wrapper; compartir un ImageSource mutable entre celdas recicladas puede borrar los píxeles de otra vista. El callback de Frame aplica las opciones de Jolt antes de volver. El visor mantiene la posición hasta aceptar o cancelar la navegación.

Simulador iPhone 17 · iOS 26.2. Toques y arrastres reales de XCTest; interfaz en inglés, velocidad original. Zoom, desplazamiento, cierre cancelado, reordenación del elemento seleccionado, retorno por clave y cierre por arrastre.
ts
import {
  GridLayout,
  Image,
  ListView,
  StackLayout,
  knownFolders,
  type Frame,
  type View,
} from '@nativescript/core'
import { zoomable, type Zoomable } from '@rafaelaugusto/jolt'
import {
  resolveListTarget,
  sharedNavigationEntry,
} from '@rafaelaugusto/jolt/shared-navigation'
import { button, label, page } from './ui'

/** Search only current native children; a recycled cell is never a saved identity. */
function currentImage(view: View, tag: string): Image | undefined {
  if (view instanceof Image && view.sharedTransitionTag === tag) return view
  let found: Image | undefined
  view.eachChildView((child) => {
    found = currentImage(child, tag)
    return !found
  })
  return found
}

export function galleryRecipe(frame: Frame) {
  const screen = page(),
    root = new GridLayout(),
    list = new ListView()
  root.rows = 'auto,*'
  screen.content = root
  const heading = label('A complete gallery recipe')
  root.addChild(heading)
  const pictures = ['cove', 'dunes'].map(
    (name) => `${knownFolders.currentApp().path}/assets/studio/${name}.png`,
  )
  let items = Array.from({ length: 60 }, (_, i) => ({
    key: `place-${i + 1}`,
    source: pictures[i % 2]!,
  }))
  list.row = 1
  list.rowHeight = 142
  list.items = items
  list.automationText = 'recipe-gallery-list'
  const tag = (key: string) => `recipe:${key}`
  list.on('itemLoading', (event) => {
    const photo = (event.view as Image) ?? new Image(),
      item = items[event.index]!
    Object.assign(photo, {
      src: item.source,
      height: 126,
      width: 260,
      margin: 8,
      stretch: 'aspectFill',
      sharedTransitionTag: tag(item.key),
      automationText: `recipe-${item.key}`,
      accessibilityLabel: `Open ${item.key}`,
    })
    event.view = photo
  })
  root.addChild(list)
  let opening = false
  list.on('itemTap', async (event) => {
    if (opening) return
    opening = true
    const item = items[event.index]!
    let viewer: Zoomable | undefined
    const detail = page(),
      host = new GridLayout(),
      viewport = new GridLayout()
    const backdrop = new GridLayout(),
      chrome = new StackLayout(),
      photo = new Image()
    host.rows = '*,auto'
    detail.content = host
    viewport.clipToBounds = true
    backdrop.rowSpan = 2
    backdrop.backgroundColor = '#102922'
    chrome.row = 1
    photo.src = item.source
    photo.stretch = 'aspectFit'
    photo.sharedTransitionTag = tag(item.key)
    photo.automationText = 'recipe-detail-photo'
    viewport.addChild(photo)
    host.addChild(backdrop)
    host.addChild(viewport)
    host.addChild(chrome)
    const entry = sharedNavigationEntry(frame, {
      tag: tag(item.key),
      canBegin: () => !viewer || (viewer.state.scale <= 1.001 && !viewer.state.pending),
      back: (options) => {
        if (!frame.canGoBack()) return false
        Object.assign(frame.currentEntry!, options)
        frame.goBack()
        return true
      },
      returnTarget: {
        key: item.key,
        fallbackFocus: () => heading,
        resolve: (context) =>
          resolveListTarget(context, {
            list,
            indexOf: (key) => items.findIndex((value) => value.key === key),
            viewForKey: (key) => currentImage(list, tag(key)),
          }),
      },
    })
    chrome.addChild(button('Zoom photo', () => viewer?.zoomTo(2.5)))
    chrome.addChild(button('Reset photo', () => viewer?.reset()))
    chrome.addChild(
      button('Move photo to end', () => {
        items = [...items.filter((value) => value.key !== item.key), item]
        list.items = items
      }),
    )
    chrome.addChild(button('Close photo', () => viewer?.requestDismiss()))
    detail.on('unloaded', () => viewer?.dispose())
    try {
      const opened = await entry.open((options) =>
        frame.navigate({ create: () => detail, ...options }),
      )
      if (!opened.navigated || frame.currentPage !== detail) {
        entry.dispose()
        return
      }
      viewer = zoomable(photo, {
        viewport,
        backdrop,
        chrome,
        dismiss: 'vertical',
        async onDismiss(intent) {
          const result = await entry.returnToSource({
            velocity: { x: 0, y: intent.velocity },
          })
          if (!result.navigated) intent.cancel()
        },
      })
    } catch (error) {
      entry.dispose()
      throw error
    } finally {
      opening = false
    }
  })
  return screen
}

Sheet ​

Un controlador posee el panel, handle, ScrollView anidado, seguimiento del teclado y retorno del foco. Se libera al descargar la raíz.

Simulador iPhone 17 · iOS 26.2. Toques y arrastres reales de XCTest; interfaz en inglés, velocidad original. Abrir, redimensionar, añadir contenido, escribir con el teclado real, cerrar/reabrir y desplazar.

La Activity Android usa android:windowSoftInputMode="adjustResize" para redimensionar el host sin desplazar toda la ventana. Aplica esta opción en el manifiesto de tu app.

Emulador Pixel 7 Android API 36 · interfaz en inglés, velocidad original. Toques, escritura y gesto reales: contenido dinámico, teclado con adjustResize, cerrar/reabrir y scroll interno. No es un dispositivo físico.
ts
import { GridLayout, ScrollView, StackLayout, TextField } from '@nativescript/core'
import {
  bottomSheet,
  GestureSurface,
  type BottomSheet,
} from '@rafaelaugusto/jolt/bottom-sheet'
import { button, label, page } from './ui'

export function sheetRecipe() {
  const screen = page(),
    root = new GridLayout(),
    base = new StackLayout()
  screen.content = root
  root.addChild(base)
  base.addChild(label('A complete sheet recipe'))
  let sheet: BottomSheet | undefined
  const open = button('Open sheet', () => sheet?.open('50%'))
  base.addChild(open)
  const backdrop = new GridLayout()
  backdrop.backgroundColor = '#102922'
  const panel = new GestureSurface()
  panel.rows = 'auto,*'
  panel.backgroundColor = '#F4F0E7'
  const handle = label('Drag to resize', 20)
  handle.automationText = 'recipe-sheet-handle'
  const scroll = new ScrollView(),
    content = new StackLayout()
  scroll.row = 1
  scroll.content = content
  scroll.automationText = 'recipe-sheet-scroll'
  content.addChild(
    button('Add content', () => content.addChild(label('One more useful row', 18))),
  )
  const field = new TextField()
  field.hint = 'Write a message'
  field.accessibilityLabel = 'Recipe message'
  field.margin = 16
  content.addChild(field)
  content.addChild(button('Close sheet', () => sheet?.dismiss()))
  for (let i = 1; i <= 16; i++) content.addChild(label(`Scrollable row ${i}`, 18))
  panel.addChild(handle)
  panel.addChild(scroll)
  root.addChild(backdrop)
  root.addChild(panel)
  root.on('loaded', () => {
    sheet ??= bottomSheet(panel, {
      handle,
      scrollView: scroll,
      backdrop,
      snap: ['content', '50%', '90%'],
      keyboard: 'follow',
      returnFocus: open,
    })
  })
  root.on('unloaded', () => {
    sheet?.dispose()
    sheet = undefined
  })
  return screen
}

Lista ​

Un vínculo animateItems por lista cargada, liberado al descargarla. itemLoading actualiza las etiquetas recicladas con los datos actuales; sustituir elementos no crea otro vínculo.

Simulador iPhone 17 · iOS 26.2. Toques y arrastres reales de XCTest; interfaz en inglés, velocidad original. Desplazar una Core ListView virtualizada y sustituir sus datos dos veces.

Con Core 9.1.3 en iOS, esta receta desactiva la precarga de celdas de UIKit: una celda precargada podía perder su contenido antes de mostrarse. Las alturas fijas y sus estimaciones mantienen el diseño predecible. Este ajuste se limita a la receta.

ts
import { GridLayout, Label, ListView } from '@nativescript/core'
import { animateItems } from '@rafaelaugusto/jolt'
import { button, label, page } from './ui'

export function listRecipe() {
  const screen = page(),
    root = new GridLayout(),
    list = new ListView()
  root.rows = 'auto,auto,*'
  screen.content = root
  root.addChild(label('A complete list recipe'))
  let generation = 0
  const rows = () =>
    Array.from({ length: 100 }, (_, i) => ({
      title: `Collection ${generation} · item ${i + 1}`,
    }))
  list.items = rows()
  list.row = 2
  list.rowHeight = 68
  list.iosEstimatedRowHeight = 68
  list.automationText = 'recipe-list'
  list.on('itemLoading', (event) => {
    const row = (event.view as GridLayout) ?? new GridLayout()
    row.height = 68
    if (!row.getChildrenCount()) row.addChild(label('', 18))
    ;(row.getChildAt(0) as Label).text = (list.items as { title: string }[])[
      event.index
    ]!.title
    event.view = row
  })
  const replace = button('Replace items', () => {
    generation++
    list.items = rows()
  })
  replace.row = 1
  root.addChild(replace)
  root.addChild(list)
  let unlink: (() => void) | undefined
  list.on('loaded', () => {
    // Core 9.1.3 can detach a prefetched UIKit cell before it is displayed.
    if (__IOS__) list.ios.prefetchingEnabled = false
    unlink?.()
    unlink = animateItems(list, 'fadeIn', { once: true, duration: 350 })
  })
  list.on('unloaded', () => {
    unlink?.()
    unlink = undefined
  })
  return screen
}

UI compartida ​

ts
import { Button, Color, Label, Page } from '@nativescript/core'

export function page() {
  const view = new Page()
  view.actionBarHidden = true
  view.backgroundColor = new Color('#F4F0E7')
  return view
}
export function label(text: string, size = 20) {
  const view = new Label()
  Object.assign(view, {
    text,
    fontSize: size,
    color: '#193D34',
    textWrap: true,
    margin: 16,
  })
  return view
}
export function button(text: string, action: () => void) {
  const view = new Button()
  Object.assign(view, {
    text,
    accessibilityLabel: text,
    height: 50,
    margin: 8,
    color: '#193D34',
    backgroundColor: '#D6E7B5',
  })
  view.on('tap', action)
  return view
}

Publicado bajo la licencia MIT.