Skip to content

Instantly share code, notes, and snippets.

@Chronos2500
Created August 14, 2025 13:13
Show Gist options
  • Save Chronos2500/2bc3b385f5147170eac8698bb35ea3e0 to your computer and use it in GitHub Desktop.
Save Chronos2500/2bc3b385f5147170eac8698bb35ea3e0 to your computer and use it in GitHub Desktop.
FullDetentSheet

SheetをDismiss後Bottom Accessoryに吸い込まれるようなApple Musicの効果はおそらくAPI化されていないので再現していません🙇

The Apple Music effect where the sheet gets absorbed into the bottom accessory after dismissal probably isn’t API-ized, so I haven’t reproduced it 🙇

import SwiftUI
struct FullSheet: View {
@State private var isPresented: Bool = false
@Namespace private var namespace
var body: some View {
TabView {
Tab("Home", systemImage: "house") {
NavigationStack {
list
.navigationTitle("Home")
}
}
Tab("Search", systemImage: "magnifyingglass") {
}
Tab("Profile", systemImage: "person.circle") {
}
Tab(role: .search) {
}
}
.tabViewBottomAccessory {
Button("Show Full Sheet") {
isPresented.toggle()
}
// この効果を使うとZoom効果が適用されますが、現状のAppleMusicでは使われていないので消しました
// .matchedTransitionSource(id: "sheet", in: namespace)
// AppleMusicに寄せるには追加でカスタムVCのTransitionを書く必要があります
}
.sheet(isPresented: $isPresented) {
VStack {
Text("This is a full sheet")
.font(.title)
.padding()
Button("Dismiss") {
isPresented = false
}
.buttonStyle(.borderedProminent)
}
.foregroundColor(.white)
// .navigationTransition(.zoom(sourceID: "sheet", in: namespace))
.presentationBackground(.pink.gradient)
.fullDetentSheet()
}
}
var list: some View {
List {
ForEach(0..<20) { index in
Text("Item \(index)")
}
}
}
}
extension View {
public func fullDetentSheet() -> some View {
modifier( FullDetentSheetModifier() )
}
}
fileprivate struct FullDetentSheetModifier: ViewModifier {
func body(content: Content) -> some View {
content.background{ VCWrapper() }
}
}
fileprivate struct VCWrapper: UIViewControllerRepresentable {
@MainActor final class DummyVC: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .clear
view.isUserInteractionEnabled = false
}
override func didMove(toParent parent: UIViewController?) {
super.didMove(toParent: parent)
parent?.sheetPresentationController?.detents = [ .full() ]
}
}
func makeUIViewController(context: Context) -> DummyVC {
.init()
}
func updateUIViewController(_ uiViewController: DummyVC, context: Context) {
}
}
fileprivate extension UISheetPresentationController.Detent {
static func full() -> UISheetPresentationController.Detent {
value(forKey: "_\("full")Detent") as! UISheetPresentationController.Detent
}
}
#Preview {
FullSheet()
}
@Chronos2500
Copy link
Author

2025-08-14.22.04.05.mov

@LeoNatan
Copy link

LeoNatan commented Sep 7, 2025

You can fix that jump from full screen to below status bar setting:

@interface UISheetPresentationController ()

@property (assign,setter=_setWantsFullScreen:,nonatomic) bool _wantsFullScreen NS_SWIFT_NAME(wantsFullScreen);
@property (assign,setter=_setAllowsInteractiveDismissWhenFullScreen:,nonatomic) bool _allowsInteractiveDismissWhenFullScreen NS_SWIFT_NAME(allowsInteractiveDismissWhenFullScreen); 

@end
sheetPresentationController?.wantsFullScreen = true
sheetPresentationController?.allowsInteractiveDismissWhenFullScreen = true
Simulator.Screen.Recording.-.iPhone.16.Pro.Max.-.2025-09-07.at.08.07.15.mp4

Unfortunately, that disables other detents, so some juggling needs to be done to enable and disable wantsFullScreen if you want both detents and smooth interaction. Apple's Music doesn't any detents other than full screen, so they set both to true, as above.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment