Native views

<ns-view> shows a native view an app builds in TypeScript: a UIView on iOS, an android.view.View on Android. The template places and sizes the element like any other view; the UIView fills it.

import { Component, signal } from '@angular/core';
import { Pressable, Text } from '@ng-native/components';
import { NsView, nsView } from '@ng-native/nativescript';

const badge = nsView<{ title: string }, UILabel>({
  create(props) {
    'worklet';
    const label = UILabel.new();
    label.text = props.title;
    label.textAlignment = NSTextAlignment.Center;
    label.backgroundColor = UIColor.systemYellowColor;
    return label;
  },
  update(label, props) {
    'worklet';
    label.text = props.title;
  },
});

@Component({
  selector: 'app-counter',
  imports: [NsView, Pressable, Text],
  template: `
    <ns-view class="h-12" [view]="badge" [props]="{ title: 'Count ' + count() }" />
    <pressable (press)="count.set(count() + 1)"><text>Add one</text></pressable>
  `,
})
export class Counter {
  protected readonly badge = badge;
  protected readonly count = signal(0);
}

nsView() declares the view: create builds it from the props, and update brings it up to date each time they change. Both are worklets, and run on the main thread, where UIKit can be touched; see Calling native APIs for what a worklet can read. Declare the view at the top of a file, as badge is above: an object there is left alone by the module's hot reloading, where a function on its own would lose what makes it a worklet.

Props

[props] is passed to create, then to update whenever the object changes. It is copied to the main thread, so it holds plain data: numbers, strings, booleans, and objects and arrays of them. A function in it arrives as one the worklet cannot call.

An object literal in the template, as above, is a new object only when a value in it changes, so update runs when the title does and not on every change detection.

update is optional. Without it a view shows what create built, whatever the props become.

Views and their lifetime

A different [view] builds a new view in place of the one shown, and the old one is released. So is the view of an element that goes, from an @if or an @for. The view is held on the main thread from create until then.

UIKit holds a delegate or a control's target weakly, so one made in create is released as soon as create returns unless something holds it. Hold it on the view, as a property of its own: a native object takes JavaScript properties beside its native ones, and keeps them as long as it is held.

const field = nsView<{ placeholder: string }, UITextField>({
  create(props) {
    'worklet';
    const ReturnKey = NSObject.extend(
      { textFieldShouldReturn: (view: UITextField) => view.resignFirstResponder() },
      { protocols: [UITextFieldDelegate] },
    );
    const view = UITextField.new() as UITextField & { heldDelegate?: NSObject };
    view.placeholder = props.placeholder;
    const delegate = ReturnKey.new();
    view.delegate = delegate;
    view.heldDelegate = delegate;
    return view;
  },
});

heldDelegate is a name of the app's own; any name the class does not use works. It is not in the class's types, hence the cast.

create returns a UIView. What it throws is reported to Angular's ErrorHandler, and so is a value that is not a UIView, null included; either way the element is left empty, and a view it showed before a new [view] is let go.

Each view is a separate create on the main thread: a hundred <ns-view> elements build theirs in about a tenth of a second on the iOS simulator.

Sizing

The element is laid out by its styles, as a <view> is, and the UIView is sized to it. The view's own size is not measured: a UILabel does not make its element as tall as its text. Give the element a size, or a size from its container, as class="h-12" does above.

A view with a fixed size of its own, such as a UISwitch, stays that size, at the top left of the element.

Touches

The UIView takes the touches that land on it, as a view inside any UIKit app does. A drag that starts on a view with gestures of its own, a map or a SceneKit view with allowsCameraControl, pans that view rather than scrolling a <scroll-view> it is in.

Children

<ns-view> shows its UIView and nothing else: elements written inside it are not rendered. Put them beside it, or over it with position: absolute.

On Android

On Android create returns an android.view.View, made with androidContext(), the current Activity, and both functions run on the main thread:

import { androidContext, nsView } from '@ng-native/nativescript';

const badge = nsView<{ title: string }, android.widget.TextView>({
  create(props) {
    'worklet';
    const label = new android.widget.TextView(androidContext() as android.content.Context);
    label.setText(props.title);
    return label;
  },
  update(label, props) {
    'worklet';
    label.setText(props.title);
  },
});

The view fills the element, as on iOS. A spec is for one platform: a template that shows a native view on both picks one spec or the other, by NativeScript's platform.

Behind it

@nativescript/react-native ships a Fabric view, NativeScriptUIView, that shows the UIView whose pointer it is given. <ns-view> commits as that view, runs create on the main thread, keeps the view there under the element's id, and binds its pointer to the element. The package's own defineUIKitView does the same for React components, which Angular Native does not render.

Angular components as native iOS and Android views, built and shipped with Expo.

An alpha. MIT licensed. Sponsor its development.

An independent project, not affiliated with or endorsed by Google, the Angular team or Expo. Angular is a trademark of Google LLC.