Skip to content

iOS: tint glass with the node's background; left-align fill_width rows - #78

Merged
GenericJam merged 2 commits into
masterfrom
fix/ios-glass-and-row-alignment
Aug 27, 2026
Merged

iOS: tint glass with the node's background; left-align fill_width rows#78
GenericJam merged 2 commits into
masterfrom
fix/ios-glass-and-row-alignment

Conversation

@GenericJam

Copy link
Copy Markdown
Owner

Two iOS renderer bugs, both reproduced and then verified fixed on a physical iPhone SE 3rd gen (iOS 26.5.2) and an iPhone 17 simulator, against a real 61-component app.

1. Glass discarded every Box background colour

mobBoxBackground(node:) never read node.backgroundColor on the glass path:

if node.useGlass {self.glassEffect(.clear, in: shape) // colour dropped
}else{self.background(node.backgroundColor.map{Color($0)}??Color.clear, in: shape)}

Under any theme with glass: true on iOS 26+, every Box background was discarded. Selected/active state vanished (a background: :primary chip rendered identically to :surface_raised) and semantic fills — highlight colours, avatar variants, pills — all collapsed to the same grey. Android was unaffected because it ignores glass: and keeps solid fills, so the same app stayed legible there and not here.

Fixed with glassEffect(.clear.tint(fill), in: shape). Glass.tint takes an Optional, so a Box with no background still gets plain clear glass. The pre-iOS-26 .ultraThinMaterial path puts the fill behind the material so it reads as frosted colour.

Measured on device with pixel sampling, same rects, same theme:

Buildactive (:primary)inactive (:surface_raised)
before0xFF20202E0xFF20202E — identical
after0xFF4000AF0xFF101023 — distinct

2. fill_width Row centred on iOS, left-aligned on Android

The row case applied .frame(maxWidth: .infinity) with no alignment:, so SwiftUI defaulted to .center, while the column case a few lines above explicitly passes .topLeading. Android's Row uses horizontalArrangement = Start.

Any fill_width row whose children are inflexible reported the sum of its children and centred that block — so checkbox and radio rows each centred independently and read as ragged. Now alignment: .leading.

Also: per-node glass opt-out

Mob.Renderer.inject_theme_flags/3 used Map.put/3, which clobbered an explicit glass: false on a node. Now Map.put_new/3, so a theme supplies a default and the node wins in either direction. mob_nif.m already decodes the prop, so no native surface is added.

This supersedes the stale material-liquid-glass branch, which proposed a parallel stringly-typed material: property; that branch also predates mobBoxBackground and would have ignored corner_radius. Recommend closing it.

Verification

  • mix compile --warnings-as-errors, mix format --check-formatted, mix credo --strict, mix test — all clean
  • Swift typechecked against the iOS 26.4 SDK; Glass.tint signature confirmed from SwiftUICore.swiftinterface rather than assumed
  • device-verified on iPhone SE 3rd gen + iPhone 17 sim; non-glass themes checked for regression

🤖 Generated with Claude Code

Two renderer bugs, both reproduced on an iPhone (iOS 26.5.2) and an
iPhone 17 simulator.
1. `mobBoxBackground(node:)` never read `node.backgroundColor` on the
glass path, so under any theme with `glass: true` every Box background
colour was discarded on iOS 26+. Selected/active states vanished (a
`background: :primary` chip rendered identically to `:surface_raised`)
and semantic fills all collapsed to the same grey. Android ignores
`glass:` and keeps solid fills, so the same app stayed legible there.
Fixed with `glassEffect(.clear.tint(fill), in: shape)` — `Glass.tint/1`
takes an Optional, so an unresolvable background still yields plain
clear glass. The pre-iOS-26 `.ultraThinMaterial` fallback puts the fill
behind the material so it reads as frosted colour.
2. The row case applied `.frame(maxWidth: .infinity)` with no `alignment:`,
so SwiftUI defaulted to `.center`. Compose's Row uses
`horizontalArrangement = Start`, so `fill_width` rows were left-aligned
on Android and centred on iOS — headers drifted, checkbox/radio rows
each centred independently and read as ragged. The column case already
passed `.topLeading`. Now `alignment: .leading`.
`Mob.Renderer.inject_theme_flags/3` uses `Map.put_new/3` instead of
`Map.put/3` so an explicit `glass:` prop on a Box overrides the theme in
either direction. `glass: false` is the escape hatch a glass theme needs
where translucency costs legibility; `glass: true` opts one Box in without
a glass theme. `mob_nif.m` already decodes the prop, so no native surface
is added — the theme's unconditional `put` was the only thing blocking it.
This supersedes the stale `material-liquid-glass` branch's per-node
`material: :glass` opt-in; see the decision record for why that branch is
closed rather than merged.
Swift rendering is not host-testable; the tint and the row alignment need
device verification. The Elixir contracts are covered in
test/mob/renderer_test.exs.
Resolves the lib/mob/theme.ex conflict, which was pure textual adjacency:
this branch adds an explanatory comment above `glass: false`, while master
appended `fonts: %{}` and `font_fallback: []` after it. Kept both.
Rebased-tree checks: format, compile --warnings-as-errors, credo --strict,
clang-format and swiftlint all clean; 1114 tests pass.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@GenericJam
GenericJam merged commit 1790961 into masterAug 27, 2026
3 checks passed
@GenericJam
GenericJam deleted the fix/ios-glass-and-row-alignment branch August 27, 2026 20:10
GenericJam added a commit that referenced this pull request Aug 28, 2026
…eadlock fix
Carries three merged changes since 0.7.31:
- #94 intrinsic Sheet detents ([:content] / [{:content, max_height: n}]) with
canonical validation through Mob.UI.normalize_sheet_detents!/1 re-applied at
the renderer boundary, iOS content measurement with capping, internal
scrolling and geometry re-clamping, composite Box accessibility (label,
explicit button role, disabled semantics), and Sheet in both platform tag
manifests.
- #74 the iOS launch deadlock behind an App Store rejection — safe_area()'s
unbounded dispatch_sync on the boot path could block the BEAM thread forever
so the app never finished launching.
- #78 iOS glass tint from the node background, and left-aligned fill_width rows.
The changelog records two known limitations rather than leaving them to be
discovered: a scrollable child inside a content detent expands instead of
scrolling independently (it reports intrinsic height), and a content sheet
presents at :medium for one frame before resizing, since content height is
only knowable after presentation.
Preflight: mix format, credo --strict, compile --warnings-as-errors, 1119
tests, git diff --check, and the native frame-registry harness all clean.
The Sheet work was additionally adversarially reviewed and device-verified on
an iPhone 17 Pro simulator (hug, cap, internal scroll, safe-area clearance)
and the Box/Android half on a physical Moto G.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@GenericJam