Target · iOS

GEA on iOS

Your code becomes a UIKit project that Xcode builds and signs like any other iOS app. The view tree talks to the system directly, with no webview around it and no JavaScript bridge in the way.

npx create-geastack
A GEA companion app running natively on iOS through UIKit
Surfaces

The same components, rendered through UIKit

The same components that run on a microcontroller, on macOS or on a Linux panel reconcile to native UIKit views here. You keep one codebase, and each platform renders it with its own widgets.

A GEA iOS app showing a live MapKit map and AVFoundation camera preview
On iOSDevice APIs
A 3D Metal-rendered arcade game running natively on iOS
On iOSMetal 3D
A GEA watch companion app on iOS
AppWatch companion
A GEA iOS app home screen
AppHome
A GEA iOS lock-screen UI
AppLock screen
A GEA iOS controls UI
AppControls
A GEA iOS voice-control UI
AppVoice control
A GEA iOS accessory pairing sheet
AppPairing sheet

Drag or scroll to explore

Same code

The UI is ordinary TypeScript

These are the same GEA components you run on every other target. geatsc reads each one at build time and lowers it to C++, which compiles into the Xcode target next to the Objective-C++ glue. The app that ships carries no WKWebView, no JSContext and no JavaScriptCore.

import { ReactiveComponent } from 'gea-embedded'

// One component, one reactive field. geatsc lowers count to a typed Signal;
// each tap runs a typed method whose write re-renders only the dependent node.
export class Counter extends ReactiveComponent {
  count = 0

  template() {
    return (
      <div class="counter">
        <span class="count">{this.count}</span>
        <button onClick={() => this.count++}>+</button>
      </div>
    )
  }
}
How it renders

Component nodes map to UIKit views

Each node in your component tree becomes a native UIKit view. The renderer keeps one view per node, keyed by node id, and picks the concrete class from the node's type: a Text node is a UILabel, a Button is a UIButton, an Image is a UIImageView, a scrolling list is a UIScrollView, a plain View is a UIView. The view is reused across frames and rebuilt only when the node's type changes.

// one native UIKit view per node, chosen by node type
if (isScrollableNode(node)) return [[GeaNativeScrollContainer alloc] initWithFrame:CGRectZero]; // UIScrollView
switch (type) {
  case NodeType::Text:   return [[GeaNativeLabel alloc] initWithFrame:CGRectZero];   // UILabel
  case NodeType::Image:  return [[UIImageView alloc] initWithFrame:CGRectZero];       // UIImageView
  case NodeType::Canvas: return [[GeaCanvasView alloc] initWithFrame:CGRectZero];      // CoreGraphics
  case NodeType::Button: return [GeaNativeButton buttonWithType:UIButtonTypeCustom];   // UIButton
  default:               return [[UIView alloc] initWithFrame:CGRectZero];             // View / list
}

On each frame GEA runs the component update, recomputes layout, and reconciles the tree onto those views — frame, background, corner radius and transform set directly on the UIView, scrolling driven by a real UIScrollView. The build goes through the GEA CLI: npx gea build --target ios runs a normal Xcode build (xcodebuild) and produces a standard .app bundle you install on the simulator or a device.

Bindings

System frameworks, in TypeScript

The system frameworks are available from TypeScript under Apple's own names. The camera (AVFoundation), maps and location (MapKit and CoreLocation), and the GPU (Metal) are the actual iOS classes you would find in the documentation — AVCaptureSession, MKMapView, CLLocationManager, MTLDevice — with the same properties and selectors. You write against iOS as it is documented.

import { UIView, UIColor } from '@geastack/apple/UIKit'

const card  = new UIView()
const inner = new UIView()
inner.backgroundColor = UIColor.systemBackgroundColor()
card.addSubview(inner)

Each call lowers straight to the Objective-C message it names — a constructor to alloc/init, a method to its selector, a property to a property access on the real object — with no generated C wrapper in between:

// card.addSubview(inner) lowers to a direct Objective-C++ message send
[((__bridge ::UIView *)gea::apple::objc::object(card.handle)) addSubview:
   ((__bridge ::UIView *)gea::apple::objc::object(inner.handle))];
Native JSX

Native views, written as JSX

Alongside the cross-platform GEA component model, you can write JSX directly against the real UIKit classes, inline in your .tsx. The tags are the same documented iOS classes the bindings expose — UIVisualEffectView, UILabel — with their real properties and selectors. It reads like SwiftUI, but there is no SwiftUI underneath: it builds ordinary UIKit views.

A Vite plugin, appleNativeJsxPlugin from @geastack/vite-plugin-apple-native, lowers each element to imperative construction — new Tag(), property assignments and addSubview — so the declarative source and the native view tree stay one and the same. The ios-native-showcase example is built this way.

import { CGRectMake } from '@geastack/apple/CoreGraphics'
import { UIBlurEffect, UIColor, UILabel, UIVisualEffectView, installRootView } from '@geastack/apple/UIKit'

// JSX over real UIKit classes → new UIVisualEffectView() + addSubview
const card = (
  <UIVisualEffectView frame={CGRectMake(20, 150, 320, 180)} effect={UIBlurEffect.effectWithStyle(4)}>
    <UILabel text="Native UIKit, written in TSX" textColor={UIColor.whiteColor()} />
  </UIVisualEffectView>
) as UIVisualEffectView

installRootView(card)

See it running