From da943bb5ae318d2aaec70a3a3ceb374fa6ecf68b Mon Sep 17 00:00:00 2001 From: Adrian Schoenig Date: Thu, 15 Oct 2026 10:51:19 +1100 Subject: [PATCH 1/2] fix: show the system sheet when pushed onto a navigation stack When the card controller gets pushed, `viewDidAppear` is called while the push transition is still under way, so the sheet couldn't be presented yet and the cards fell back to the classic look. Nothing retried until the next layout pass, e.g., after rotating the device. Now the cards stay hidden and the sheet gets presented right after the transition completes. Co-Authored-By: Claude Opus 5.5 --- .../TGCardViewController+SystemSheet.swift | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/Sources/TGCardViewController/TGCardViewController+SystemSheet.swift b/Sources/TGCardViewController/TGCardViewController+SystemSheet.swift index 36917d5..9d88a34 100644 --- a/Sources/TGCardViewController/TGCardViewController+SystemSheet.swift +++ b/Sources/TGCardViewController/TGCardViewController+SystemSheet.swift @@ -142,11 +142,23 @@ extension TGCardViewController { extension TGCardViewController { private func installSystemSheet() { + // Can't present while a transition is under way, e.g., when this got + // pushed onto a navigation stack, as `viewDidAppear` gets called before + // that transition fully completes. Keep the cards hidden and try again + // right after it, rather than showing them the classic way. + if let coordinator = transitionCoordinator { + coordinator.animate(alongsideTransition: nil) { [weak self] _ in + DispatchQueue.main.async { + self?.updateSystemSheetPresentation() + } + } + return + } + guard isVisible, view.window != nil, presentedViewController == nil, - transitionCoordinator == nil, let content = cardWrapperContent else { // We'll try again later, e.g., after the presented view controller got From 7ee87beeac57c0aaa2e8bdc643de0e8d6ee1638e Mon Sep 17 00:00:00 2001 From: Adrian Schoenig Date: Thu, 15 Oct 2026 11:02:19 +1100 Subject: [PATCH 2/2] feat: always use system sheets on iOS 27, deprecate classic On iOS 27 and later, cards now always show in a system sheet. Until the sheet can be presented, e.g., while something else is presented, the cards stay hidden rather than showing the classic way, so the classic cards never show on iOS 27. `presentationStyle` and its `.classic` option are deprecated on iOS 27, as they no longer have an effect, so that they can be removed once iOS 27 is the minimum. Co-Authored-By: Claude Opus 5.5 --- Example/ExampleCardViewController.swift | 6 ------ README.md | 4 ++-- .../TGCardViewController+SystemSheet.swift | 19 ++++++------------- .../TGCardViewController.swift | 17 +++++++---------- 4 files changed, 15 insertions(+), 31 deletions(-) diff --git a/Example/ExampleCardViewController.swift b/Example/ExampleCardViewController.swift index 6711810..fd19db6 100644 --- a/Example/ExampleCardViewController.swift +++ b/Example/ExampleCardViewController.swift @@ -28,12 +28,6 @@ class ExampleCardViewController: TGCardViewController { navigationButtonsAreSpringLoaded = true - // Pass `-classicCards` as a launch argument to compare with the classic - // presentation, where the cards aren't in a system sheet. - if ProcessInfo.processInfo.arguments.contains("-classicCards") { - presentationStyle = .classic - } - #if targetEnvironment(macCatalyst) mode = .sidebar #endif diff --git a/README.md b/README.md index afd85c9..e9f2c4e 100644 --- a/README.md +++ b/README.md @@ -74,7 +74,7 @@ Provides a card-based view controller for mapping applications where the card's ### System sheets on iOS 27+ -By default (`presentationStyle = .automatic`), on iOS 27 and later the cards are shown in a system sheet (`UISheetPresentationController`). The sheet adapts to the size classes by itself, like Maps': +On iOS 27 and later, the cards are shown in a system sheet (`UISheetPresentationController`). The sheet adapts to the size classes by itself, like Maps': | | Presentation | |---|---| @@ -84,7 +84,7 @@ By default (`presentationStyle = .automatic`), on iOS 27 and later the cards are | The card controller is itself presented, but not full screen | Classic | | iOS 26 and earlier, or built with Xcode 26 and earlier | Classic | -Set `presentationStyle` to `.classic` to opt out. Check `usesSystemSheet` for what's currently in use. +This can't be turned off: `presentationStyle` and its `.classic` option are deprecated on iOS 27, and will be removed once that's the minimum. Check `usesSystemSheet` for what's currently in use. Cards, map managers, headers, map buttons and paging work the same in both. Differences with a system sheet: diff --git a/Sources/TGCardViewController/TGCardViewController+SystemSheet.swift b/Sources/TGCardViewController/TGCardViewController+SystemSheet.swift index 9d88a34..8d24c43 100644 --- a/Sources/TGCardViewController/TGCardViewController+SystemSheet.swift +++ b/Sources/TGCardViewController/TGCardViewController+SystemSheet.swift @@ -27,6 +27,10 @@ extension TGCardViewController { /// The card controller positions the cards itself, and they're dragged using /// its own gestures. + /// + /// Has no effect on iOS 27 and later, where cards always show in a system + /// sheet. On earlier versions, ``automatic`` does the same. + @available(iOS, deprecated: 27.0, message: "Cards always show in a system sheet on iOS 27 and later; earlier versions use the classic presentation anyway.") case classic } @@ -72,7 +76,6 @@ extension TGCardViewController { #if compiler(>=6.4) && os(iOS) && !targetEnvironment(macCatalyst) // Xcode 27 proxy, for the sheet placement API guard #available(iOS 27.0, *), - presentationStyle == .automatic, mode == .floating else { return false } @@ -124,17 +127,9 @@ extension TGCardViewController { installSystemSheet() } else if !wantsSheet, sheetHost != nil { uninstallSystemSheet(animated: topCardView == nil && isVisible) - } else if !wantsSheet, isAwaitingSystemSheet { - revealClassicCards() } } - private func revealClassicCards() { - guard isAwaitingSystemSheet else { return } - isAwaitingSystemSheet = false - toggleCardWrappers(hide: topCardView == nil) - } - } // MARK: - Installing and removing the sheet @@ -162,9 +157,8 @@ extension TGCardViewController { let content = cardWrapperContent else { // We'll try again later, e.g., after the presented view controller got - // dismissed. Until then, show the cards in the classic way, as we don't - // know when that'll be. - revealClassicCards() + // dismissed. Until then, the cards stay hidden, as they never show the + // classic way when we want a sheet. return } @@ -203,7 +197,6 @@ extension TGCardViewController { // 2. Configure the sheet sheetHost = host - isAwaitingSystemSheet = false #if !os(visionOS) // No detents on visionOS, where cards never use a sheet if let sheet = host.sheetPresentationController { diff --git a/Sources/TGCardViewController/TGCardViewController.swift b/Sources/TGCardViewController/TGCardViewController.swift index a1bf30e..5d947a1 100644 --- a/Sources/TGCardViewController/TGCardViewController.swift +++ b/Sources/TGCardViewController/TGCardViewController.swift @@ -155,7 +155,9 @@ open class TGCardViewController: UIViewController { /// uses system sheets on iOS 27+. Check ``usesSystemSheet`` for what's /// currently used. /// - /// - Warning: Set before `viewDidLoad` is called. + /// - Note: Has no effect: cards always show in a system sheet on iOS 27 and + /// later, and the classic way before. + @available(iOS, deprecated: 27.0, message: "Cards always show in a system sheet on iOS 27 and later; earlier versions use the classic presentation anyway.") public var presentationStyle: PresentationStyle = .automatic // MARK: System sheet state, see `TGCardViewController+SystemSheet.swift` @@ -183,10 +185,6 @@ open class TGCardViewController: UIViewController { var sheetTargetPosition: TGCardPosition? var isSheetDraggingEnabled = true - /// Set while the initial cards wait for the sheet, so that they don't flash up - /// in the classic presentation first - var isAwaitingSystemSheet = false - /// Set while pushing or popping, which update the sheet in one go var isTransitioningCards = false @@ -728,9 +726,7 @@ open class TGCardViewController: UIViewController { didAddInitialCards = true // These get pushed the classic way and move into the sheet once we've - // appeared. Keep them hidden until then. - isAwaitingSystemSheet = wantsSystemSheet - + // appeared. They're hidden until then, see `toggleCardWrappers`. initialCards.forEach { push($0, animated: false, allowToNotify: $0 == initialCards.last, completionHandler: nil) } } @@ -900,8 +896,9 @@ open class TGCardViewController: UIViewController { } else { sidebarBackground.alpha = 0 } - // With a system sheet, the card wrapper is just an invisible placeholder - cardWrapperShadow.alpha = hide || usesSystemSheet || isAwaitingSystemSheet ? 0 : 1 + // With a system sheet, the card wrapper is just an invisible placeholder, + // and it stays invisible while waiting for the sheet to be presented + cardWrapperShadow.alpha = hide || usesSystemSheet || wantsSystemSheet ? 0 : 1 if !prepareOnly { if mode == .sidebar {