I want to implement a CustomPresentationDetent
that has a different height depending on some external values. For a concrete example, let’s say I want my detent to be x
points below the maxDetentValue
, i.e.
struct MyDetent: CustomPresentationDetent {
static func height(in context: Context) -> CGFloat? {
context.maxDetentValue - x
}
}
Basically, I want to pass this x
from the outside.
CustomPresentationDetent.height(in:)
is a static method so I cannot write an init
that takes those values. Even if I could, I do not even get a chance to call the initialiser, since at the use site, I can only pass the meta type of my custom detent. That is, I must do .custom(MyDetent.self)
, and not .custom(MyDetent(x: 100))
.
Then I thought of passing the values through the environment. The context
parameter of height(in:)
apparently can be used to access EnvironmentValues
by using this dynamic member subscript.
However, the documentation of that subscript says something very confusing:
This uses the environment from where the sheet is shown, not the environment where the presentation modifier is applied.
Hence the question is:
Where exactly to put a .environment
modifier so that I modify the environment of “where the sheet is shown”?
I tried adding .environment
everywhere I can think of, passing each a different value, but I still see the default value in the height(in:)
method. Here is a minimal reproducible example:
struct ContentView: View {
var body: some View {
SheetPresenter()
.environment(.myValue, 1)
}
}
struct SheetPresenter: View {
@State private var shown = false
var body: some View {
Button("Show Sheet") {
shown = true
}
.environment(.myValue, 2)
.sheet(isPresented: $shown) {
Text("Sheet")
.environment(.myValue, 3)
.presentationDetents([.custom(MyDetent.self)])
.environment(.myValue, 4)
}
.environment(.myValue, 5)
}
}
struct MyEnvKey: EnvironmentKey {
static let defaultValue: Int = 0
}
extension EnvironmentValues {
var myValue: Int {
get { self[MyEnvKey.self] }
set { self[MyEnvKey.self] = newValue }
}
}
struct MyDetent: CustomPresentationDetent {
static func height(in context: Context) -> CGFloat? {
print(context.myValue)
return 100
}
}
On iOS 17.4, simulator, the print(context.myValue)
statement always prints 0, the default value.
12
I am adding another answer that more directly addresses your question and code, but which at the same time shows the problems with your approach.
There were several issues with your code, as follows:
- Your environment was set to use values of type Int, whereas a detent needs CGFloat
- Regardless, they were never used because MyDetent was returning a constant of 100 of the correct type, CGFloat, instead of returning the actual
context.myValue
- You were setting the env value in many places, but you were never reading anywhere for actually passing it further to a view
- This is a problem since it means you can’t manipulate the value
- Even if you did read and adjusted the value, it wouldn’t change anything, because MyDetent would likely show the defaultValue
- To fix this and actually use the adjusted values, you’d need to read it again in the SheetPresenter, BUT use the
.height
parameter instead, which makes using a custom detent pointless (not that it made sense to use one in the first place).
So the conclusion is that for what you’re trying to achieve, you need the .height parameter anyway, and some kind of global value. And unless you have a specific reason for that to exist and be passed via environment, it would make much more sense for it to be a property of an observable object instead.
import SwiftUI
struct SheetCustomDetentRootView: View {
@Environment(.myValue) private var myValue: CGFloat // <- Here, read the myValue from the environment
var body: some View {
SheetPresenterView()
.environment(.myValue, myValue)
// .environment(.myValue, myValue + 350) // <- Here, uncomment this (and comment out the line above) to pass a custom value via environment
}
}
struct SheetPresenterView: View {
@Environment(.myValue) private var myValue: CGFloat // <- Here, read the myValue from the environment
@State private var shown = false
var body: some View {
Button("Show Sheet") {
shown = true
}
.sheet(isPresented: $shown) {
Text("My value from environment is: (Int(myValue))")
.presentationDetents([.custom(MyDetent.self)])
// .presentationDetents([.height(myValue)]) // <- Here, uncomment this (and comment out the line above) to use the value passed via environment
}
}
}
struct MyEnvKey: EnvironmentKey {
static let defaultValue: CGFloat = 100
}
extension EnvironmentValues {
var myValue: CGFloat {
get { self[MyEnvKey.self] }
set { self[MyEnvKey.self] = newValue }
}
}
struct MyDetent: CustomPresentationDetent {
static func height(in context: Context) -> CGFloat? {
print(context.myValue)
return context.myValue
}
}
#Preview {
SheetCustomDetentRootView()
}
5
Because I can’t wrap my mind around the need for a custom detent or environment values gymnastics, here’s a simpler example that uses:
- An observable class with a singleton to hold the max detent value
- A view that accepts optional parameters for adjusting the sheet height relative to the max detent value
- An internal state that allows further control if needed over the adjustment value
As such, you can:
- Define a general max detent value in the singleton class
- Update the max detent value as needed from anywhere (shown here as the textfield and stepper input of SomeTopView() )
- Call the view that has the sheet with an adjustment value or without one (in which case the sheet will have the max detent value height)
- Adjust the sheet height internally, if needed, in the same view as the sheet
- Lose less hair due to not having to worry about environment values or custom detents.
import SwiftUI
struct SheetDetentView: View {
//State values
@State private var settings = SomeObserverClass.settings
var body: some View {
VStack {
SomeTopView()
.background(.background.secondary)
.frame(height: 100)
SheetPresenter(adjustHeight: 20) //or simply SheetPresenter() if no adjustment needed
}
}
}
struct SomeTopView: View {
//Bindings
@Bindable private var settings = SomeObserverClass.settings
var body: some View {
Form {
Section("Top View"){
HStack {
Text("Set max height")
.fixedSize()
TextField("Height", value: $settings.maxDetentValue, formatter: NumberFormatter() )
.multilineTextAlignment(.center)
.background(.background.secondary)
Stepper("", value: $settings.maxDetentValue, in: 100...450, step: 50)
}
}
}
}
}
struct SheetPresenter: View {
//Parameters
var adjustHeight: CGFloat? = 0
//Bindings
@Bindable private var settings = SomeObserverClass.settings
//State values
@State private var adjustValue: CGFloat = 0
@State private var shown = true
var body: some View {
Form {
Section("Bottom Sheet Presenter View") {
Text("Max height is: (settings.maxDetentValue, format: .number.precision(.fractionLength(0)) )")
//Inputs
HStack {
Text("Adjust height")
.fixedSize()
TextField("Height", value: $adjustValue, formatter: NumberFormatter() )
.multilineTextAlignment(.center)
.background(.background.secondary)
Stepper("", value: $adjustValue, in: 0...settings.maxDetentValue, step: 10)
}
Button("Toggle Sheet") {
shown.toggle()
}
.sheet(isPresented: $shown) {
let maxHeight = settings.maxDetentValue
let adjustedHeight = maxHeight - adjustValue
Text("Adjust parameter is: (adjustValue, format: .number.precision(.fractionLength(0)) )")
.foregroundStyle(.secondary)
Text("Adjusted height is: (adjustedHeight, format: .number.precision(.fractionLength(0)) )")
Text("((Int(adjustValue)) below max detent value of (Int(maxHeight)) )")
.foregroundStyle(.secondary)
.padding(.top)
.interactiveDismissDisabled()
.presentationDragIndicator(.hidden)
.presentationBackgroundInteraction(.enabled)
.presentationDetents([.height(adjustedHeight), .large])
}
}
}
.onAppear {
//if a parameter is passed to the view, set the state value to the parameter value
if let value = adjustHeight {
adjustValue = value
}
}
}
}
@Observable
class SomeObserverClass {
var maxDetentValue: CGFloat = 350
static let settings = SomeObserverClass() //singleton
private init() {}
}
#Preview {
SheetDetentView()
}
If you run the code above, you’ll get the following, which allows you to play around with the values and notice how sheet height changes thanks to the observable singleton:
3