iosswiftiphoneswiftui

Different results for different iOS versions when calculating height using GeometryReader SwiftUI


My goal is to make a presented sheet fit to it's content on iOS 16 and up, using the presentationDetent .height and passing in the height calculated from geometry reader.

I don't want the second bottom sheet to be draggable, it should remain it's size calculated from the content height.

The issue is that I'm getting a different height back from the geometry proxy which is very annoying, and ending up with different results depending on the versions.

When running below code on simulator iPhone 15 on iOS 17.4 the height printed will be: Height: 40.666666666666664

When running on iPhone 15 using iOS 18 the height printed will be: Height: 0.0

I've tried using a preference key, but then when the sheet is dragged, the sheets height will of course be updated. I've also tried using .background instead of overlay, task instead of onAppear, etc.

Anyone has an idea how to fix this, or another approach I could try?

This is the minimum reproducible example code:

import SwiftUI

@main
struct experimentingApp: App {
    var body: some Scene {
        WindowGroup {
            ContentView(viewModel: .init())
        }
    }
}

enum ActiveSheet: Identifiable {
    
    case sheetOne, sheetTwo
    
    var id: String {
        switch self {
        case .sheetOne:
            "sheet-one"
        case .sheetTwo:
            "sheet-two"
        }
    }
}
final class ViewModel: ObservableObject {
    @Published var activeSheet: ActiveSheet? = .sheetOne
    
}

struct ContentView: View {
    @ObservedObject var viewModel: ViewModel
    
    @State var sheetHeight: CGFloat = .zero
    var body: some View {
        Text("Parent view")
            .sheet(isPresented: isPresented(for: .sheetOne), content: {
                VStack {
                    Text("Sheet one")
                    Button(action: { viewModel.activeSheet = .sheetTwo}, label: { Text("Next")})
                }
            })
            .sheet(isPresented: isPresented(for: .sheetTwo), content: {
                VStack(spacing: 0) {
                        Text("Sheet two")
                        Text("Sheet two")
                    }
                    .overlay(content: {
                        GeometryReader { proxy in
                            Color.clear
                                .ignoresSafeArea(edges: .all)
                                .onAppear {
                                    print("Height: \(proxy.size.height)")
                                    sheetHeight = proxy.size.height
                                }
                        }
                    })
                    .presentationDetents([.height(sheetHeight)])
                
            })
       
    }
}
extension ContentView {
    func isPresented(for sheet: ActiveSheet) -> Binding<Bool> {
        Binding(
            get: { self.viewModel.activeSheet == sheet },
            set: { if !$0 { self.viewModel.activeSheet = nil } }
        )
    }
}



Solution

  • An alternative way to measure the content height is to use .onGeometryChange. This will be called on initial show and every time the height changes. This modifier was added in Xcode 16 (I think) and is backwards compatible with iOS 16.

    With this approach, the first call has a height of 0, which corresponds to what you were seeing before with your .onAppear callback. However, the next call delivers a useful height. So this probably resolves the problem.

    .sheet(isPresented: isPresented(for: .sheetTwo)) {
        VStack(spacing: 0) {
            Text("Sheet two")
            Text("Sheet two")
        }
        .onGeometryChange(for: CGFloat.self) { proxy in
            proxy.size.height
        } action: { height in
            if height > sheetHeight {
                print("Height: \(height)")
                sheetHeight = height
            }
        }
        .presentationDetents([.height(sheetHeight)])
    }