41

I'm using SwiftUI to develop a new macOS application and can't figure out how to define the window size. In the AppDelegate, the window size is defined as shown below:

// --- AppDelegate.swift ---

import Cocoa
import SwiftUI

@NSApplicationMain
class AppDelegate: NSObject, NSApplicationDelegate {

    var window: NSWindow!


    func applicationDidFinishLaunching(_ aNotification: Notification) {
        // Insert code here to initialize your application
        window = NSWindow(
            contentRect: NSRect(x: 0, y: 0, width: 480, height: 300),
            styleMask: [.titled, .closable, .miniaturizable, .resizable, .fullSizeContentView],
            backing: .buffered, defer: false)
        window.center()
        window.setFrameAutosaveName("Main Window")

        window.contentView = NSHostingView(rootView: ContentView())

        window.makeKeyAndOrderFront(nil)
    }
}

The ContentView is defined as follows:

// --- ContentView.swift ---

import SwiftUI

struct ContentView : View {
    var body: some View {
        VStack {
            Spacer()
            Text("Top Text")
            Spacer()
            Text("Bottom Text")
            Spacer()
        }
    }
}

When I build and run the application, the window appears but it is not the correct size. It appears as roughly the size of the two text labels which is not the 480x300 size that is defined in the AppDelegate.

How am I supposed to define window size for a Mac application when using SwiftUI?

wigging
  • 8,492
  • 12
  • 75
  • 117
  • 1
    Just trying to be helpful, but probably not since I haven't (yet) done a `macOS` app. Think about "intrinsic size. I think that's why it appears *"roughly the size of the two text labels"*. That's what it **should** be unless you add modifiers. So what happens if you add one - `.frame(width:480, height:300)`? –  Jul 02 '19 at 18:07
  • Second thing - don't use `AppDelegate` to define things like this. In iOS you now want `SceneDelegate` for most everything, since for iPads you finally have multi-threading.I'd guess that you nowadays need to consider the **entire** screen a "canvas" for a *single* instance of your app. so in your example, each "scene" or "instance' that you wish needs to be set to 480x300, and the best place IMHO is not at the scene level - but you haven't provided that context - but instead at the `View` level. –  Jul 02 '19 at 18:12
  • @dfd Where would you declare the `.frame`? On the VStack? Consider submitting your suggestion as an answer instead of a comment. – wigging Jul 02 '19 at 20:05
  • I really can't feel like this is an answer because - while I've worked with Windows apps since... wow, 1995 - I don't really know `AppKit`. So let me do my best... (1) Remember, this is still beta 2. Things will change. (2) Try and test. Modifiers placement *and* order matter. So yeah, I'd try the `Stack` second(!), and `ContentView` first. If either of these work, I'd suggest that *you* submit your own answer - no problem there - and I'd certainly upvote it. –  Jul 02 '19 at 22:17

4 Answers4

42

At times, the behaviour can be confusing. That is because once you run your app at least once, if you then manually resize and reposition your window, the sizes specified in the delegate will no longer matter.

Applications remember when a user has resized the window and will use the information stored in the UserDefaults instead, under the key "NSWindow Frame Main Window". If you would like to reset it, you need to wipe it out with the defaults command.

Now that that is out of the way, the reason why your window was so narrow is as follow:

With SwiftUI, not all views are created equal. For example: Text() is humble. It will only take as much space as needed. While other views, such as Spacer(), will expand as much as their parents offer (I call them greedy).

In your case, you have a VStack, with Spacer() in it. This means that the VStack fill expand to fill the height offered by its parent. In this case, the 300 pt from the delegate (or whatever is stored in the UserDefaults).

On the other hand, since you do not have any Spacer() inside a HStack, the ContentView will only expand horizontally to what it needs. That is, as wide as the widest Text() view. If you add HStack { Spacer() } inside the VStack, your content view will expand to occupy the 480 pt specified in the delegate (or whatever is stored in the UserDefaults). No need to set a frame().

The other approach (specifying a frame for the ContentView), is basically telling your ContentView to be 480x300, no matter what. In fact, if you do so, you will not be able to resize the window!

So now you know and I think it is clear... but, here's something that can be very useful to you:

There's another greedy view that may assist you in debugging your window sizes: GeometryReader. This view will always take as much as offered. Run this example, and you will know exactly how much space is offered at app launch:

struct ContentView : View {
    var body: some View {
        GeometryReader { geometry in
            VStack {
                Text("\(geometry.size.width) x \(geometry.size.height)")
            }.frame(width: geometry.size.width, height: geometry.size.height)
        }
    }
}

I have written an extensive article about GeometryReader, I recommend you check it out: https://swiftui-lab.com/geometryreader-to-the-rescue/

By the way, my AppDelegate looks like this:

func applicationDidFinishLaunching(_ aNotification: Notification) {
    // Insert code here to initialize your application
    window = NSWindow(
        contentRect: NSRect(x: 0, y: 0, width: 480, height: 300),
        styleMask: [.titled, .closable, .miniaturizable, .resizable, .fullSizeContentView],
        backing: .buffered, defer: false)
    window.center()
    window.setFrameAutosaveName("Main Window")
    window.contentView = NSHostingView(rootView: ContentView())
    window.makeKeyAndOrderFront(nil)
}

UPDATE (macOS Catalina - Beta 3)

Since the Beta3, the initial ContentView of a new project, uses maxWidth and maxHeight. A clever alternative.

struct ContentView : View {
    var body: some View {
        Text("Hello World")
            .frame(maxWidth: .infinity, maxHeight: .infinity)
    }
}
Erik Pragt
  • 13,513
  • 11
  • 58
  • 64
kontiki
  • 37,663
  • 13
  • 111
  • 125
  • Why does the default macOS project in Xcode define the window size in the AppDelegate when using SwiftUI? As you have shown in your answer, apparently you define the window size using the content view frame. – wigging Jul 03 '19 at 12:59
  • What does your AppDelegate code look like? Did you remove the NSWindow from the AppDelegate? – wigging Jul 03 '19 at 13:07
  • Ok, I just figured it out. There are several factors. I am updating my answer. Give me a few minutes. – kontiki Jul 03 '19 at 13:18
  • Ok, I updated my answer. I hope it is now clear. Let me know if you need further clarification. – kontiki Jul 03 '19 at 13:45
  • Thank you for the in-depth answer. – wigging Jul 03 '19 at 20:59
  • 1
    I added an update on beta3. New projects for macOS, use .frame(maxWidth: .infinity, maxHeight: .infinity), which is another cool way of achieving the same. – kontiki Jul 04 '19 at 14:59
  • Thank you so much for the Beta 3 update, adding that .frame line just helped me open up a WKWebView with maximized window size :) – esaruoho Jan 18 '20 at 19:56
  • Is there a simple way of setting the window title, too? – esaruoho Jan 18 '20 at 22:18
  • AFAIK not possible with SwiftUI, which only deals with Views. Check WWDC2019 session 240 (SwiftUI on All Devices). Jump to 29:30. You'll see that they specifically say that setting up a window needs to be done with AppKit, not with SwiftUI. – kontiki Jan 20 '20 at 08:43
  • Setting the size of the contentView is effectively the same as setting up the size of the window directly. However, I prefer to use idealWidth/idealHeight as well as minimum values; this way the app starts with a nice window size (and macOS still remembers the user's preferred size). – green_knight Apr 29 '22 at 19:14
16

I did do it this way. It got a startup size and resizable.

struct windowSize {
// changes let to static - read comments
static minWidth : CGFloat = 100
static minHeight : CGFloat = 200
static maxWidth : CGFloat = 200
static maxHeight : CGFloat = 250
}

struct ContentView : View {
  var body: some View {
    Group() {
        VStack {
            Text("Hot Stuff")
            .border(Color.red, width: 1)
            Text("Hot Chocolate")
            .border(Color.red, width: 1)
        }
    }
    .frame(minWidth: windowSize().minWidth, minHeight: windowSize().minHeight)
    .frame(maxWidth: windowSize().maxWidth, maxHeight: windowSize().maxHeight)
    .border(Color.blue, width: 1)
  }
}

A suggested edit place here after.

enum WindowSize {
    static let min = CGSize(width: 100, height: 200)
    static let max = CGSize(width: 200, height: 250)
}

struct ContentView: View {
    var body: some View {
        Group {
            VStack {
                Text("Hot Stuff")
                    .border(Color.red, width: 1)
                Text("Hot Chocolate")
                    .border(Color.red, width: 1)
            }
        }
        .frame(minWidth: WindowSize.min.width, minHeight: WindowSize.min.height)
        .frame(maxWidth: WindowSize.max.width, maxHeight: WindowSize.max.height)
        .border(Color.blue, width: 1)
    }
}
iPadawan
  • 898
  • 1
  • 12
  • 23
  • If you change the `windowSize` `let` to be `static`, you can save a lot of allocations. Right now four copies of the struct are initialized every time. – Barnyard Aug 18 '21 at 23:10
  • Of course. Thank you. Did change the code. – iPadawan Aug 20 '21 at 09:31
  • I believe that you are still allocating structs albeit empty. Also this will cause an error because the struct does not have a member variable minWidth, etc. anymore. I think you should use the following syntax: .frame(minWidth: windowSize.minWidth, minHeight: windowSize.minHeight) – Fred Appelman Jul 01 '22 at 07:32
9

SwiftUI

You could easily use ( compiler directives ) in the main Struct in my case, I used it to detect which screen app is running as I'm developing a multi-platform app ( iPhone, iPad , mac )

import SwiftUI

@main
struct DDStoreApp: App {
    var body: some Scene {
        WindowGroup {
            // use compiler directives #if to detect mac version or ios version
            #if os(macOS)
            // use min wi & he to make the start screen 800 & 1000 and make max wi & he to infinity to make screen expandable when user stretch the screen 
            ContentView().frame(minWidth: 800, maxWidth: .infinity, minHeight: 1000, maxHeight: .infinity, alignment: .center)
            #else
            ContentView()
            #endif
        }
    }
}
m4n0
  • 29,823
  • 27
  • 76
  • 89
belal medhat
  • 462
  • 1
  • 4
  • 18
9

Just give a size the content (or make it self-size) and tell the window group to restrict it's size to it's content:

@main
struct MySizingApp: App {
    var body: some Scene {
        WindowGroup {
            ContentView()
                .frame(minWidth: 350, minHeight: 350) // <- 1. Size here
        }
        .windowResizability(.contentSize) // <- 2. Add the restriction here
    }
}
Mojtaba Hosseini
  • 95,414
  • 31
  • 268
  • 278