Created
September 4, 2025 05:20
-
-
Save Mcrich23/c69a5151f18e50155358c36430a80b77 to your computer and use it in GitHub Desktop.
Using iOS 26 Private APIs to always have a clear background for the sheet
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // | |
| // ClearSheetWrapper.swift | |
| // Radiance | |
| // | |
| // Created by Morris Richman on 8/24/25. | |
| // | |
| import SwiftUI | |
| import UIKit | |
| import PrivateObfuscationMacro | |
| /// A SwiftUI wrapper that presents a clear-background sheet using `UISheetPresentationController`. | |
| /// | |
| /// This wrapper allows presenting a custom SwiftUI `sheet` over `content` with a transparent background | |
| /// and a height that dynamically adapts to the sheet content. | |
| struct ClearSheetWrapper<Content: View, Sheet: View>: UIViewControllerRepresentable { | |
| /// A binding controlling whether the sheet is presented. | |
| @Binding var isSheetPresented: Bool | |
| /// The main content view to display beneath the sheet. | |
| @ViewBuilder let content: Content | |
| /// The sheet content view. | |
| @ViewBuilder let sheet: Sheet | |
| /// Creates the underlying `ClearSheetViewController`. | |
| func makeUIViewController(context: Context) -> ClearSheetViewController<Content, Sheet> { | |
| let vc = ClearSheetViewController<Content, Sheet>(rootView: content) | |
| vc.sheet = sheet | |
| vc.onSheetDismiss = { | |
| isSheetPresented = false | |
| } | |
| return vc | |
| } | |
| /// Updates the underlying view controller when SwiftUI state changes. | |
| func updateUIViewController(_ uiViewController: ClearSheetViewController<Content, Sheet>, context: Context) { | |
| if isSheetPresented { | |
| uiViewController.presentSheet() | |
| } else { | |
| uiViewController.sheetController?.dismiss(animated: true) | |
| } | |
| guard let sheetController = uiViewController.sheetController else { return } | |
| sheetController.sheetPresentationController?.detents = [.custom(for: sheetController)] | |
| } | |
| } | |
| /// A hosting controller that manages presenting a clear-background sheet. | |
| class ClearSheetViewController<Content: View, Sheet: View>: UIHostingController<Content>, UISheetPresentationControllerDelegate { | |
| /// The SwiftUI sheet content. | |
| var sheet: Sheet? = nil | |
| /// The hosting controller used for the sheet. | |
| var sheetController: ClearSheetHostingController<Sheet>? = nil | |
| /// The code run when the sheet is dismissed | |
| var onSheetDismiss: () -> Void = { } | |
| /// Updates the preferred content size and ensures the background is clear. | |
| override func viewDidLayoutSubviews() { | |
| super.viewDidLayoutSubviews() | |
| self.preferredContentSize = view.intrinsicContentSize | |
| view.backgroundColor = .clear | |
| } | |
| /// Presents the sheet over the current content. | |
| func presentSheet() { | |
| guard let sheet else { return } | |
| sheetController = ClearSheetHostingController(rootView: sheet) | |
| guard let sheetController else { return } | |
| sheetController.onDismiss = { | |
| self.sheetController = nil | |
| self.onSheetDismiss() | |
| } | |
| sheetController.view.backgroundColor = .clear | |
| if #available(iOS 26, *) { | |
| sheetController.sheetPresentationController?.perform(NSSelectorFromString(#base64Encoded("_setLargeBackground:")!), with: UIColor.clear) | |
| sheetController.sheetPresentationController?.perform(NSSelectorFromString(#base64Encoded("_setNonLargeBackground:")!), with: UIColor.clear) | |
| } | |
| sheetController.sheetPresentationController?.detents = [.custom(for: sheetController)] | |
| present(sheetController, animated: true) | |
| } | |
| } | |
| /// A hosting controller that automatically sizes itself to its SwiftUI content. | |
| class ClearSheetHostingController<Content: View>: UIHostingController<Content> { | |
| /// Closure called when the sheet is dismissed. | |
| var onDismiss: (() -> Void)? | |
| /// Updates the preferred content size based on intrinsic content. | |
| override func viewDidLayoutSubviews() { | |
| super.viewDidLayoutSubviews() | |
| let targetSize = CGSize(width: view.bounds.width, height: UIView.layoutFittingCompressedSize.height) | |
| self.preferredContentSize = view.systemLayoutSizeFitting(targetSize) | |
| } | |
| /// Calls the `onDismiss` closure when the view is about to disappear. | |
| override func viewWillDisappear(_ animated: Bool) { | |
| super.viewWillDisappear(animated) | |
| onDismiss?() | |
| } | |
| } | |
| private extension UISheetPresentationController.Detent { | |
| /// Creates a custom detent for a given `UIViewController` based on its preferred content size. | |
| /// - Parameter uiViewController: The view controller for which the detent should be sized. | |
| /// - Returns: A custom detent matching the view controller's height. | |
| static func custom(for uiViewController: UIViewController) -> UISheetPresentationController.Detent { | |
| .custom { _ in | |
| if #available(iOS 26, *) { | |
| return CGFloat(uiViewController.preferredContentSize.height-25) | |
| } else { | |
| return CGFloat(uiViewController.preferredContentSize.height-35) | |
| } | |
| } | |
| } | |
| } | |
| extension View { | |
| /// Presents a clear-background sheet with dynamic height. | |
| /// | |
| /// - Parameters: | |
| /// - isPresented: Binding to control whether the sheet is shown. | |
| /// - sheet: A closure that returns the sheet's SwiftUI content. | |
| /// - Returns: A view that can present a clear-background sheet over itself. | |
| @ViewBuilder | |
| func clearSheet<Sheet: View>( | |
| isPresented: Binding<Bool>, | |
| @ViewBuilder sheet: () -> Sheet | |
| ) -> some View { | |
| ClearSheetWrapper(isSheetPresented: isPresented) { | |
| self | |
| } sheet: { | |
| sheet() | |
| .fixedSize(horizontal: false, vertical: true) | |
| } | |
| } | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment