Skip to content

Instantly share code, notes, and snippets.

@Mcrich23
Created September 4, 2025 05:20
Show Gist options
  • Save Mcrich23/c69a5151f18e50155358c36430a80b77 to your computer and use it in GitHub Desktop.
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
//
// 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