From 68bde9eed59f8301e5d910420fab4d77bb33557d Mon Sep 17 00:00:00 2001 From: Adrian Schoenig Date: Fri, 19 Jun 2026 20:42:31 +0200 Subject: [PATCH] Fix card-position breakage from always-on floatingCardToolBarItems constraints MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `floatingCardToolBarItems` (2.5.0) added `cardFloatingView` to the controller's root view in `viewDidLoad` and *permanently* pinned it to `cardWrapperContent` — the card's content wrapper — including a bottom constraint to the wrapper's bottom edge. Those constraints were active for **every** card, even ones that provide no floating items. Because the wrapper animates (its top/height constraints change) on push/pop, coupling a root-level view to it via that bottom constraint disrupted the card's position layout: after pushing then popping a card, the wrapper could be left visually extended while `cardWrapperDesiredTopConstraint` (and therefore `cardPosition`) read collapsed — so the content faded out, the map buttons showed, and the card couldn't be dragged. It reproduced on plain cards (e.g. a timetable card) that don't use the affordance at all. Two changes: - Install `cardFloatingView` and its constraints lazily — only while a card actually has `floatingCardToolBarItems` — and remove them again otherwise. Cards without items now carry none of this layout, exactly as before 2.5.0. - Pin the bottom to the screen's safe area instead of `cardWrapperContent`. The card always reaches at or below the screen edge, so it's the same place visually, but it no longer couples to the wrapper as it animates. Horizontal alignment stays relative to the card (its x/width don't move on a vertical transition). Co-Authored-By: Claude Opus 4.8 --- .../TGCardViewController.swift | 44 +++++++++++-------- 1 file changed, 26 insertions(+), 18 deletions(-) diff --git a/Sources/TGCardViewController/TGCardViewController.swift b/Sources/TGCardViewController/TGCardViewController.swift index eb18cd7..bdd6799 100644 --- a/Sources/TGCardViewController/TGCardViewController.swift +++ b/Sources/TGCardViewController/TGCardViewController.swift @@ -409,24 +409,27 @@ open class TGCardViewController: UIViewController { cardWrapperShadow.layer.shadowOpacity = 0.16 } - setUpCardFloatingView() - monitorVoiceOverStatus() } - /// Pins `cardFloatingView` to the bottom of the card's visible area, clamped - /// to the safe area: it prefers the card's bottom but never drops below the - /// screen, so it stays visible whatever the card's drag position. - private func setUpCardFloatingView() { + /// Lazily adds `cardFloatingView` and its constraints the first time a card + /// actually provides `floatingCardToolBarItems`, and they're torn down again + /// when a card without items becomes top (see `updateCardFloatingViewContent`). + /// So cards that don't use the affordance carry none of its layout and behave + /// exactly as before it existed. + /// + /// The bottom is pinned to the screen's safe area, **not** the card's bottom. + /// The card always reaches at or below the screen edge, so this is the same + /// place visually, but it avoids coupling this root-level view to the card + /// wrapper while the wrapper animates during push/pop — that coupling + /// corrupted the card's position layout for *every* card. Horizontal alignment + /// stays relative to the card (its x/width don't move during a vertical card + /// transition); the leading/trailing inequality clamps keep a wide row from + /// overflowing the card for any alignment. + private func installCardFloatingViewIfNeeded() { + guard cardFloatingView.superview == nil else { return } view.addSubview(cardFloatingView) - let preferCardBottom = cardFloatingView.bottomAnchor.constraint(equalTo: cardWrapperContent.bottomAnchor, constant: -16) - preferCardBottom.priority = .defaultHigh - - // The horizontal position is governed by these three equality constraints, - // toggled per card by `applyCardFloatingAlignment(_:)`. The leading/trailing - // *inequality* clamps below stay active for every alignment, so a wide row - // never overflows the card. cardFloatingCenterXConstraint = cardFloatingView.centerXAnchor.constraint(equalTo: cardWrapperContent.centerXAnchor) cardFloatingLeadingConstraint = cardFloatingView.leadingAnchor.constraint(equalTo: cardWrapperContent.leadingAnchor, constant: 16) cardFloatingTrailingConstraint = cardFloatingView.trailingAnchor.constraint(equalTo: cardWrapperContent.trailingAnchor, constant: -16) @@ -434,8 +437,7 @@ open class TGCardViewController: UIViewController { NSLayoutConstraint.activate([ cardFloatingView.leadingAnchor.constraint(greaterThanOrEqualTo: cardWrapperContent.leadingAnchor, constant: 16), cardFloatingView.trailingAnchor.constraint(lessThanOrEqualTo: cardWrapperContent.trailingAnchor, constant: -16), - cardFloatingView.bottomAnchor.constraint(lessThanOrEqualTo: view.safeAreaLayoutGuide.bottomAnchor, constant: -16), - preferCardBottom, + cardFloatingView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor, constant: -16), ]) applyCardFloatingAlignment(.center) @@ -2005,11 +2007,14 @@ extension TGCardViewController { } /// (Re)populates the card-attached floating view from `card`'s - /// `floatingCardToolBarItems`. Safe to call repeatedly; a nil or empty list - /// hides it. Called both during the full floating-view refresh and right - /// after `didBuild`, since cards commonly configure their items there. + /// `floatingCardToolBarItems`. Safe to call repeatedly. The view and its + /// constraints are installed only when a card actually has items, and torn + /// down again otherwise — so a card without items carries none of the + /// affordance's layout. Called both during the full floating-view refresh and + /// right after `didBuild`, since cards commonly configure their items there. private func updateCardFloatingViewContent(card: TGCard?) { if let cardItems = card?.floatingCardToolBarItems, !cardItems.isEmpty { + installCardFloatingViewIfNeeded() populateFloatingView(cardFloatingView, with: cardItems) applyCardFloatingAlignment(card?.floatingCardToolBarAlignment ?? .center) cardFloatingView.isHidden = false @@ -2017,6 +2022,9 @@ extension TGCardViewController { } else { cleanUpFloatingView(cardFloatingView) cardFloatingView.isHidden = true + // Drop the view and its constraints entirely when unused, so a card that + // doesn't adopt the affordance never has it coupled into the layout. + cardFloatingView.removeFromSuperview() } }