ReviewOS

craft-native/craft

feat(ios): the native tab bar is a SwiftUI recreation, so iOS 26 tab-bar behaviours are unavailable

#418
Closed glennmichael123 opened this 7 days ago ·
7 days ago

What happens

The native tab bar is not a tab bar. CraftTabBarView (CraftApp.swift:1181, content at :1192, body at :1204) is an HStack of Buttons that draws its own chrome:

HStack(spacing: 8) {
    bar
    if let accessory = chrome.tabAccessory {
        Button { CraftHybrid.shared.open(accessory.id) } label: {
            surface { Image(systemName: accessory.symbol) ... }
        }
    }
}

Selection is a tinted circle drawn by hand (Circle().fill(selectedColor.opacity(0.14)).padding(4)), the badge is Capsule().fill(Color.red) with its own minimum size, and the hit target is a contentShape(Rectangle()).

There is no UITabBarController, no UITab, and no SwiftUI TabView anywhere in the iOS templates.

To be clear about one thing, because it is the obvious first guess and it is wrong: the material is already correct. surface (:1276) uses the real iOS 26 API — GlassEffectContainer with .glassEffect(.regular.interactive(), in: .capsule) — and falls back to .ultraThinMaterial below 26. Nothing here needs glass applied to it. What is missing is the control, not the finish.

Why this bites

A recreation only has the behaviours someone wrote. The ones iOS 26 gives a real tab bar for free, and which do not appear anywhere in the templates:

  • tabBarMinimizeBehavior — the bar shrinking as content scrolls, which is what the system apps do. No reference in the repo.
  • scroll edge effects — no scrollEdgeEffect.
  • the interactive morph between the bar and its accessory.
  • selection animation and the haptic that goes with it; ours is a static tinted circle that swaps instantly.
  • the system badge, with its own metrics and Dynamic Type behaviour.

Put the app's tab bar beside the Phone app's on the same device and the difference reads immediately, before any individual detail is nameable. That is the cost: it is close enough to look intended and wrong enough to feel off.

This is also the root the tab-bar issues keep growing from. #386 — the selected-item highlight being a hardcoded Capsule with no way to change its shape — is not a styling oversight; it is what happens when a highlight is drawn by hand instead of being a UITab's. #347 and #359 are the same shape of problem for swiping. Each has been fixed as its own symptom.

What would help

Build the bar from UITabBarController (or SwiftUI TabView with UITab) and let the system own selection, the accessory, the badge, the morph and the minimize behaviour. The chrome contract would not have to change: tabs already arrive as a list of id/title/symbol with an optional accessory and badge, which maps onto UITab and the iOS 26 bottom accessory directly.

That is a large change and it would subsume several already-closed issues rather than adding another one beside them. If it is not the direction, the alternative worth stating out loud is that the bar is a deliberate custom control and the system behaviours above will keep being reimplemented one at a time as they are noticed.

Found on iOS 26 against a four-tab bar with an accessory pill.

7 days ago

Only note for whoever implements it:

Make sure that the web component "equivalent" for it still works nicely, too, so we have something to use for our non-native web apps that streamlines the native style

7 days ago

Implemented on main in 864d43a and 714c555 (two focused five-file commits).

Native and hybrid route shells now use UITabBarController while retaining their existing UINavigationController stacks. UIKit owns tab items, badges, selection presentation, iOS 26 scroll minimization, and the bottom accessory. Primary native scrollers are registered for both top and bottom chrome, and teardown clears those registrations. Lazy tabs retain explicit tab items so accessibility identifiers survive preparation.

The web-only shell remains unchanged, with a regression assertion guarding that boundary. The existing shared-WebView hybrid navigation CI job passed on 864d43a. Custom native selection shapes and repeating icon pulses are no longer forced into UIKit; the compatibility differences are documented in the native STX guide.

Verification: the initial simulator run passed 45 tests covering retained stacks, five-tab swipes, cancellation, accessory navigation and back-swipe state. The final iOS 26.5 run passed all 44 selected tests (41 unit tests plus Dynamic Type, scroll minimize/expand/navigation, and the held large-content viewer). The independently captured during-hold frame was also visually checked. SDK: 796 passed / 8 expected skips; builder and renderer contracts: 440 passed; release tests: 394 passed. Typecheck, package builds/verification, formatting and lint passed (0 lint errors).

Latest-commit CI and publication are still pending, so leaving this open rather than calling it shipped. v0.0.359 was tagged before these changes and does not contain this migration. Older-iOS accessory fallback compiles but has not been runtime-tested on an older simulator in this session.

6 days ago

Shipped in v0.0.360, release commit a5465af.

Verified exact-source CI (40374785563), Mobile E2E (40374785445, successful second attempt), and the complete publication workflow. This includes native asset verification, npm publication, and Pantry publication. Both craft-native@0.0.360 and @craft-native/ios@0.0.360 are available in the public npm registry; the published iOS archive was inspected for the implementation.

The published CraftHybrid.swift uses UITabBarController, iOS 26 on-scroll minimization, and UITabAccessory. Closing for native/hybrid iOS route tabs; the web-only shell remains unchanged. Older-iOS accessory fallback was compiled but not runtime-tested on an older simulator in this work, and custom selection shapes/repeating icon pulses are not imposed on UIKit.

Sign in to comment on this issue.