Search code examples
macoslistviewswiftuimodal-dialogframe

How can I correctly show a SwiftUI Button and List inside a sheet on macOS without a hardcoded frame size?


I'm having trouble putting a List inside a sheet on macOS. The sheet size doesn't expand for the List at all, and the whole view is only as large as the "Done" button. Is there any way to get this to work properly similar to the iOS screenshot?

Here is my minimal example code:

import SwiftUI

struct ContentView: View {
    @State var isPresented = false
    var stuffs = ["stuff 0", "stuff 1", "stuff 3"]

    var body: some View {
        VStack {
            Button("Show Sheet") {
                isPresented = true
            }
        }
        .padding()
        .sheet(isPresented: $isPresented) {
            Button("Done") {
                isPresented = false
            }
            List {
                ForEach(stuffs, id: \.self) { stuff in
                    Text(stuff)
                }
            }
            // Adding a hardcoded frame size fixes it, but I'd prefer not to do that if possible so that it adapts to window sizes better.
            // .frame(minWidth: 300, minHeight: 300)
        }
    }
}

How it looks on macOS 13.3.1

How it looks on iOS 16.4

I am hoping to not have to hard code a frame size. It does work better if I add a .frame(minWidth: 300, minHeight: 300) view modifier to the List, but I'd prefer not to do that if possible so that it adapts to window sizes better.

Commenting out the List view and just having the inner ForEach also works as expected, but in the real app I want to make use of List features.

I was expecting it to size itself automatically based on the content to something sensible.


Solution

  • This is something that comes with native macOS development compared to iOS or iPadOS.

    On iPhone and iPadOS, sheets have an intrinsic minimum frame size. List will expand to fill whatever it's given. However by default on macOS, sheets depend on their contents to determine how large they should be. So when List doesn't "push out" the frame by itself, you end up with a small view.

    So in this case, using .frame(minWidth:minHeight:) is exactly the right thing to do.

    If you're building a multiplatform target that also wants to serve iOS and/or iPadOS, you can make the frame conditional:

    .sheet(isPresented: $isPresented) {
      // I recommend providing a single view to `.sheet, with children
      // (it makes extracting your sheet to a separate view so much easier)
      VStack { 
        // ... contents here
      }
      #if os(macOS)
      .frame(minWidth: 300, minHeight: 300)
      #endif
    }