Skip to main content

Triki UI navigation

This layer connects motion input to a focus-driven SwiftUI UI used by menus, calibration, and quiz-style selection screens.

Use it when you want players to navigate UI with the Triki controller instead of touch.

Core pieces

ComponentRole
MotionInputProvider (TrikiInputAdapter)Produces GameInput/liveInput from BLE frames
TrikiUINavigatorHolds UI navigation state (focusIndex, hold progress, activation callback)
TrikiFocusGateStabilizes slot focus before selection
TrikiHoldTrackerConverts focused dwell time into activation
.trikiUIScreen(...)View modifier that wires screen lifecycle + HUD + ticking
TrikiFocusRowReusable row that reflects focus and hold state

.trikiUIScreen behavior

trikiUIScreen is the main integration point for a screen with N selectable items.

VStack(spacing: 12) {
TrikiFocusRow(index: 0, title: "Start")
TrikiFocusRow(index: 1, title: "Settings")
TrikiFocusRow(index: 2, title: "Quit")
}
.trikiUIScreen(itemCount: 3, isActive: true, showsPhoneHUD: true) { index in
handleSelection(index)
}

What happens in lifecycle

When the modifier becomes active:

  1. GameManager.applyUIMode(to:) applies UI-friendly motion tuning.
  2. Navigator clock resets.
  3. Navigator is configured with itemCount and onActivate.
  4. Every game/UI tick, navigator consumes MotionInputProvider state.
  5. Optional phone HUD appears (TrikiUIHUD) when Triki control is available.

When deactivated, navigator state is cleared so stale focus does not leak between screens.

Input integration model

The flow is:

BLE / parser -> MotionInputProvider.liveInput.posX -> focused slot -> hold or click -> onActivate(index)
  • Focus source: horizontal position (posX) mapped to discrete slots.
  • Activation source: cap button edge or hold-complete (depending on current screen logic).
  • Fallback: touch buttons still work because TrikiFocusRow is a normal SwiftUI Button.
  1. Render rows/items with deterministic indices (0..<count).
  2. Attach .trikiUIScreen(itemCount:isActive:showsPhoneHUD:onActivate:) at the container level.
  3. Keep onActivate side effects idempotent (navigation, submit, continue).
  4. Toggle isActive when overlays/modals should temporarily own focus.
  5. Optionally hide HUD (showsPhoneHUD: false) for TV-first screens.

Side effects and gotchas

  • Reconfiguring itemCount while active resets navigation mapping.
  • Leaving isActive = true on hidden screens can steal focus updates.
  • If your menu feels jittery, tune MotionConfig deadzones and smoothing in UI mode.

Calibration and simple menu

Triki UI lives in the sample app only. You need:

  1. MotionInputProvider + TrikiUINavigator as @EnvironmentObject (see gametrikiApp.swift).
  2. BLE: motion.connect() (Main menu → POŁĄCZ BLE or ConnectView).
  3. Calibration — neutral pose while holding the cap:
// Same as TrikiCalibrationView — sets SDK neutral center
motion.performCalibration() // → MotionSDK.calibrateNeutralPose()

After the first frames, the app may show TrikiCalibrationView automatically (motion.showsCalibrationPrompt).

  1. UI mode for horizontal menu selection (Quiz category picker uses this):
GameManager.applyUIMode(to: motion) // .paddle tuning for posX slots + hold
  1. Menu screen — mirror QuizFlowView category pick:
import SwiftUI
import VeltoKit

struct SimpleTrikiMenuView: View {
@EnvironmentObject private var motion: MotionInputProvider
@EnvironmentObject private var trikiUI: TrikiUINavigator

private let items = ["Start", "Settings", "Quit"]
@State private var lastChoice: String?

var body: some View {
VStack(spacing: 12) {
Text("Triki: turn = focus · hold or button = OK")
.font(.caption.monospaced())

ForEach(Array(items.enumerated()), id: \.offset) { i, title in
TrikiFocusRow(index: i, title: title, accent: .cyan, icon: "circle.fill")
}

if let lastChoice {
Text("Selected: \(lastChoice)")
}
Spacer()
}
.padding()
.trikiUIScreen(itemCount: items.count, isActive: true) { index in
guard items.indices.contains(index) else { return }
lastChoice = items[index]
}
.onAppear {
if !motion.isConnected { motion.connect() }
GameManager.applyUIMode(to: motion)
}
}
}

Reference implementation: app/UI/Quiz/QuizFlowView.swifttrikiNavigationActive only in .categoryPick, rows use TrikiFocusRow, activation in handleTrikiActivate.

StepQuiz equivalent
Calibrate onceGlobal TrikiCalibrationView on main menu after BLE
Menu with Triki.categoryPick + .trikiUIScreen
Touch fallbackTrikiFocusRow is a Button

Where to look in the sample app

  • app/UI/TrikiCalibrationView.swift — calibration UX
  • app/UI/MainMenu.swift — BLE + calibration sheet wiring
  • app/UI/TrikiUI/TrikiUIComponents.swift
  • app/UI/TrikiUI/TrikiUINavigator.swift
  • app/UI/TrikiUI/TrikiFocusGate.swift
  • app/UI/TrikiUI/TrikiHoldTracker.swift
  • app/UI/Quiz/QuizFlowView.swift
  • app/UI/GameCalibrationView.swift

GameInput · MotionSDK · Architecture