Skip to content

UIKit UITabBarController migration + pull-to-dismiss polish

jedmund requested to merge jedmund/sao-paulo-v9 into main

Created by: jedmund

Summary

Replaces the signed-in root's SwiftUI TabView with a UIKit UITabBarController so tab-bar visibility can be driven by the first-party setTabBarHidden(_:animated:) API instead of SwiftUI's .toolbar(.hidden, for: .tabBar), which ran a multi-frame internal layout reconciliation that couldn't be suppressed from the outside. Bundled alongside the migration are several pull-to-dismiss visual/interaction fixes that share the IllustDetailViewController surface and benefit from the new tab-controller hooks.

Tab-bar migration

  • New PixillateTabBarController subclass owns three UITabs (Home, Search, Bookmarks) with viewControllerProvider closures that lazily build PixillateNavigationControllers via a new PixillateNavCoordinator (the per-tab nav builder lifted out of the retired NavigationControllerHost Representable).
  • New MainTabBarHost (UIViewControllerRepresentable) presented from ContentView.SignedInRoot. Owns the shared CellRectRegistry, PixivImageSizeRegistry, BookmarksChromeState, and a new TabBarHostState observable that mirrors tab selection + visibility so the SwiftUI iPad Bookmarks chrome overlay can stay in SwiftUI.
  • PixillateNavigationController.navigationController(_:willShow:animated:) delegate hook calls setTabBarContentHidden on every push/pop boundary. Non-root VC → hide; root → show. Authoritative control point, replacing per-VC viewWillAppear calls that were proving unreliable.
  • setTabBarContentHidden(_:animated:) forwards to super.setTabBarHidden — the one iPadOS 26 lever that actually hides the top strip. self.tabBar turns out to be a zero-sized compatibility shadow at the bottom of the screen on iPadOS 26; the visible top strip is rendered via private views we can't reach, but the UITabBarController API handles it correctly.
  • Deletes TabBarVisibility.swift, MainTabView.swift, the NavigationControllerHost struct + its nested Coordinator, and the tabBarVisibility: init param on PixillateNavigationController.
  • Extracts BookmarksChromeState and BookmarksTabBarRowChrome into their own files.

Pull-to-dismiss polish

  • pedestalRect(in:) now converts via CALayer.convert instead of UIView.convert — the latter is documented as undefined when any view in the chain has a non-identity transform, and the pedestal's pull-to-dismiss transform was causing the un-transformed rect to be returned, making the hero snapshot pop to "top-middle" on release.
  • Pedestal scale anchors around the finger's pan.began position inside the pedestal's bounds, so the grabbed pixel tracks the finger through the scale-down.
  • Trigger threshold 220→110pt; phase B release always commits (no velocity branch, no cancel-after-trigger). Tab bar is force-revealed at trigger fire.
  • pedestal.backgroundColor permanently .clear — detail VC's own background still provides the at-rest dark backdrop, but phase A drag no longer paints a moving grey rectangle over the metadata card.
  • NukeUIImageView.placeholderColor default → .clear, plus removed per-pedestal-page palette assignment. No more light-grey backdrop peeking through during spring animations.
  • Hero snapshot's colored backdrop dropped in makeHeroLayer — only the aspect-fit image flies during dismiss.
  • contentStack.bringSubviewToFront(pedestal) so a scaled/translated pedestal renders above the below-pedestal metadata card.
  • Diagnostic logs added to IllustFeedViewModel + IllustFeedView (kept for next-round debugging).

Test plan

  • Tab bar hides cleanly on push into detail, restores cleanly on pop. No flash/flicker.
  • Home hero push animates from the tapped cell (not from top-left).
  • Pull-to-dismiss: phase A drag does NOT paint a grey pedestal rectangle over the metadata card. Pedestal follows finger with scale anchored at the touch point.
  • Pull-to-dismiss commit: snapshot flies from wherever-the-finger-was to the source cell, not from top-middle.
  • Profile pushed from inside a detail: tab bar stays hidden; popping profile back to detail doesn't reveal the strip (refcount/delegate chaining correct).
  • iPhone compact: tab bar at bottom works; Bookmarks chrome renders inline in ProfileView.
  • iPad regular: top strip + Bookmarks overlay chrome works.

🤖 Generated with Claude Code

Merge request reports

Loading