使用SwiftUI定义macOS窗口大小

42

我正在使用SwiftUI开发一个新的macOS应用程序,但是无法确定如何定义窗口大小。在AppDelegate中,窗口大小定义如下:

// --- 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)
    }
}

ContentView的定义如下:

// --- ContentView.swift ---

import SwiftUI

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

当我构建并运行该应用程序时,窗口出现了,但它的大小不正确。它的大小大约是两个文本标签的大小,并不是在AppDelegate中定义的480x300大小。

当使用SwiftUI时,我应该如何定义Mac应用程序的窗口大小?


1
只是试图提供帮助,但可能不太好因为我还没有(至今)做过macOS应用程序。考虑“内在大小”。我认为这就是为什么它看起来大约是“两个文本标签的大小”。除非您添加修饰符,否则它应该是这样。那么如果您添加一个修饰符——.frame(width:480, height:300)会发生什么? - user7014451
第二件事 - 不要使用AppDelegate来定义这样的东西。在iOS中,您现在希望对大多数内容使用SceneDelegate,因为对于iPad,您终于拥有了多线程。我猜想,现在您需要将整个屏幕视为应用程序的单个实例的“画布”。因此,在您的示例中,您希望设置每个“场景”或“实例”为480x300,并且在我的看法中,最好的位置不是在场景级别 - 但是您没有提供该上下文 - 而是在View级别。 - user7014451
@dfd 你会在哪里声明.frame? 在VStack上吗? 考虑将您的建议提交为答案而不是评论。 - wigging
我真的觉得这不是一个答案,因为虽然我从1995年开始就一直在使用Windows应用程序,但我并不真正了解AppKit。所以让我尽力而为... (1) 记住,这仍然是beta 2版本,事情会改变的。(2) 尝试和测试。修饰符的位置和顺序都很重要。所以是的,我会先尝试ContentView,再尝试Stack,如果其中任何一个起作用,我建议提交自己的答案 - 没有问题 - 我肯定会点赞的。 - user7014451
4个回答

43
有时候,行为可能会让人感到困惑。这是因为一旦您至少运行了应用程序一次,如果您手动调整窗口大小和重定位,则委托中指定的大小将不再起作用。
应用程序会记住用户何时调整了窗口大小,并使用存储在UserDefaults中的信息,键为"NSWindow Frame Main Window"。如果您想重置它,需要使用defaults命令清除它。
现在解释完毕,您的窗口过窄的原因如下:
使用SwiftUI时,并非所有视图都是相等的。例如:Text()是谦虚的。它只会占用所需的空间。而其他视图,如Spacer(),会扩展父视图提供的空间(我称之为贪婪)。
在您的情况下,您有一个VStack,在其中有Spacer()。这意味着VStack将扩展以填充其父视图提供的高度。在这种情况下,即委托中的300 pt(或存储在UserDefaults中的任何内容)。
另一方面,由于您在HStack内没有任何Spacer(),因此ContentView仅会水平扩展到所需的宽度。也就是说,与最宽的Text()视图一样宽。如果在VStack中添加,则您的内容视图将扩展以占用委托中指定的480 pt(或存储在UserDefaults中的任何内容)。不需要设置frame()。
另一种方法(为ContentView指定框架)基本上告诉您的ContentView是480x300,无论如何。实际上,如果您这样做,您将无法调整窗口大小!
所以现在你明白了,我认为很清楚...但是,这里有一些对您非常有用的东西:
还有一个贪婪的视图可能会帮助您调试窗口大小:GeometryReader。该视图将始终占用提供的空间。运行此示例,您将确切地知道启动应用程序时提供了多少空间。
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)
        }
    }
}

我写了一篇关于GeometryReader的详细文章,建议你去看看: https://swiftui-lab.com/geometryreader-to-the-rescue/

顺便提一下,我的AppDelegate长这样:

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)
}

更新(macOS Catalina - Beta 3)

从Beta3开始,新项目的初始ContentView使用maxWidth和maxHeight。一种聪明的替代方案。

struct ContentView : View {
    var body: some View {
        Text("Hello World")
            .frame(maxWidth: .infinity, maxHeight: .infinity)
    }
}

为什么在使用SwiftUI时,Xcode中默认的macOS项目会在AppDelegate中定义窗口大小?正如您在答案中所示,显然可以使用内容视图框架来定义窗口大小。 - wigging
你的AppDelegate代码是什么样子的?你从AppDelegate中移除了NSWindow吗? - wigging
1
我在beta3中添加了更新。macOS的新项目,请使用.frame(maxWidth:.infinity,maxHeight:.infinity),这是另一种实现相同效果的很酷的方法。 - kontiki
非常感谢Beta 3更新,添加.frame行帮助我打开了一个最大化窗口大小的WKWebView :) - esaruoho
设置contentView的大小实际上与直接设置窗口大小相同。但是,我更喜欢使用idealWidth / idealHeight以及最小值;这样应用程序就可以以良好的窗口大小启动(而macOS仍然记住用户首选大小)。 - green_knight
显示剩余5条评论

16

我确实是用这种方式做的。 它有一个启动大小和可调整大小。

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)
  }
}

建议编辑可在此处进行。

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)
    }
}

如果你将 windowSizelet 改为 static,你可以节省很多内存分配。现在每次都会初始化四个结构体的副本。 - Barnyard
当然。谢谢。已经更改了代码。 - iPadawan
我认为你仍在分配结构体,尽管是空的。此外,这会导致错误,因为该结构体不再具有成员变量minWidth等。我认为你应该使用以下语法:.frame(minWidth: windowSize.minWidth, minHeight: windowSize.minHeight) - Fred Appelman

11

只需为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
    }
}

1
在 Mac 上作为新的目标工作是理想的。 - Salvador
1
这是有史以来最好的答案。 - Duck
1
太好了,我不知道有这个“windowResizability”功能。谢谢 @Mojtaba - undefined

8

SwiftUI

你可以在主结构中轻松使用编译器指令(compiler directives)。 在我这种情况下,我用它来检测应用程序正在运行的屏幕,因为我正在开发一个多平台应用程序(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
        }
    }
}

4
它对macOS没有影响。 - thedp

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接