Skip to content

Receitas nativas completas ​

Estas telas executáveis de Core NativeScript mostram a integração inteira, incluindo criação das views e limpeza. Usam APIs públicas e funcionam sem framework. O preview Vue apenas monta o Frame. Cada código abaixo vem do arquivo compilado pelo demo.

Execute o demo compilado com --recipe=gallery, --recipe=sheet ou --recipe=lists. No Android, use o extra de intenção recipe com o valor correspondente. A lista suportada é Core ListView; outros roteadores e plugins de virtualização precisam de seu próprio resolvedor. As gravações demonstram comportamento em simulador, não desempenho de aparelho físico.

Galeria ​

Use chaves estáveis e resolva a célula atual da lista na volta. Carregue a imagem pelo src de cada view: compartilhar o mesmo wrapper mutável de ImageSource entre células recicladas pode limpar os pixels de outra view. O callback do Frame aplica as opções de navegação do Jolt antes de voltar. O visualizador mantém a posição de descarte até a navegação aceitar ou cancelar.

Simulador de iPhone 17 · iOS 26.2. Toques e arrastos reais de XCTest; interface em inglês, velocidade original. Zoom, pan, descarte cancelado, item selecionado movido ao fim, retorno à miniatura pela chave e novo arrasto para fechar.
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 ​

Um controlador cuida do painel, handle, ScrollView aninhado, teclado e retorno de foco. O unload da raiz descarta o controlador.

Simulador de iPhone 17 · iOS 26.2. Toques e arrastos reais de XCTest; interface em inglês, velocidade original. Abrir, redimensionar, adicionar conteúdo, digitar com o teclado virtual real, fechar/reabrir e rolar.

A Activity do demo Android usa android:windowSoftInputMode="adjustResize": o teclado redimensiona o host sem deslocar a janela inteira. Aplique essa configuração no manifesto do seu app ao usar esta receita.

Emulador Pixel 7 Android API 36 · interface em inglês, velocidade original. Toques, digitação e gesto reais via ADB: conteúdo dinâmico, teclado com adjustResize, fechar/reabrir e scroll interno. Não é aparelho 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 ​

Crie um vínculo animateItems por lista carregada e libere-o no unload. itemLoading atualiza os labels reciclados com os dados atuais. Trocar os itens não exige outro vínculo.

Simulador de iPhone 17 · iOS 26.2. Toques e arrastos reais de XCTest; interface em inglês, velocidade original. Rolar uma Core ListView virtualizada e trocar seus dados duas vezes.

No Core 9.1.3 usado no iOS, esta receita desativa o prefetch de células do UIKit: uma célula preparada antecipadamente podia perder seu conteúdo antes de aparecer. Alturas fixas e estimativas correspondentes mantêm o layout previsível. A configuração vale apenas para esta receita.

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 compartilhada ​

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 sob a licença MIT.