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
| Component | Role |
|---|---|
MotionInputProvider (TrikiInputAdapter) | Produces GameInput/liveInput from BLE frames |
TrikiUINavigator | Holds UI navigation state (focusIndex, hold progress, activation callback) |
TrikiFocusGate | Stabilizes slot focus before selection |
TrikiHoldTracker | Converts focused dwell time into activation |
.trikiUIScreen(...) | View modifier that wires screen lifecycle + HUD + ticking |
TrikiFocusRow | Reusable 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:
GameManager.applyUIMode(to:)applies UI-friendly motion tuning.- Navigator clock resets.
- Navigator is configured with
itemCountandonActivate. - Every game/UI tick, navigator consumes
MotionInputProviderstate. - 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
TrikiFocusRowis a normal SwiftUIButton.
Recommended screen pattern
- Render rows/items with deterministic indices (
0..<count). - Attach
.trikiUIScreen(itemCount:isActive:showsPhoneHUD:onActivate:)at the container level. - Keep
onActivateside effects idempotent (navigation, submit, continue). - Toggle
isActivewhen overlays/modals should temporarily own focus. - Optionally hide HUD (
showsPhoneHUD: false) for TV-first screens.
Side effects and gotchas
- Reconfiguring
itemCountwhile active resets navigation mapping. - Leaving
isActive = trueon hidden screens can steal focus updates. - If your menu feels jittery, tune
MotionConfigdeadzones and smoothing in UI mode.
Calibration and simple menu
Triki UI lives in the sample app only. You need:
MotionInputProvider+TrikiUINavigatoras@EnvironmentObject(seegametrikiApp.swift).- BLE:
motion.connect()(Main menu → POŁĄCZ BLE orConnectView). - 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).
- UI mode for horizontal menu selection (Quiz category picker uses this):
GameManager.applyUIMode(to: motion) // .paddle tuning for posX slots + hold
- Menu screen — mirror
QuizFlowViewcategory 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.swift — trikiNavigationActive only in .categoryPick, rows use TrikiFocusRow, activation in handleTrikiActivate.
| Step | Quiz equivalent |
|---|---|
| Calibrate once | Global TrikiCalibrationView on main menu after BLE |
| Menu with Triki | .categoryPick + .trikiUIScreen |
| Touch fallback | TrikiFocusRow is a Button |
Where to look in the sample app
app/UI/TrikiCalibrationView.swift— calibration UXapp/UI/MainMenu.swift— BLE + calibration sheet wiringapp/UI/TrikiUI/TrikiUIComponents.swiftapp/UI/TrikiUI/TrikiUINavigator.swiftapp/UI/TrikiUI/TrikiFocusGate.swiftapp/UI/TrikiUI/TrikiHoldTracker.swiftapp/UI/Quiz/QuizFlowView.swiftapp/UI/GameCalibrationView.swift