概览
在苹果大屏设备上,我们往往需要借助多窗口(Multiwindow)来充分利用海量的显示空间,比如 Mac,iPad 以及 AppleTV 系统 等等。
所幸的是,SwiftUI 对多窗口管理提供了很好的支持。利用 SwiftUI 我们可以非常轻松的设置窗口在屏幕上的位置,大小以及拖动反馈。
在本篇博文中,您将学到如下内容:
- 限制窗口大小
- 任性选择窗口放置位置
- 检测窗口拖动状态
SwiftUI 窗口经过几多进化已经愈发趋于成熟,小伙伴们值得信赖。那还等什么呢?让我们立即开始窗口大冒险吧!
Let‘s go!!!;)
1. 限制窗口大小
从 SwiftUI 5.0(iOS 17,macOS 13)开始,苹果新增了 defaultSize 视图修改器专门用来设置窗口的尺寸:
利用 defaultSize 修改器,我们可以任性设置 Window 的长与宽。如下代码所示,我们将 search 窗口的大小设置成了 500 points x 500 points:
struct SugarBotApp: App {
var body: some Scene {
WindowGroup {
RootView()
}
WindowGroup(id: "search") {
SearchFeatureView()
}
.defaultSize(width: 500, height: 500)
}
}
除了设置窗口大小的初始值以外,我们还可以使用 SwiftUI 中的 windowResizability 修改器来决定后续用户能够如何更改窗口尺寸:
在下面的代码中,我们调用 windowResizability 修改器并传入 .contentSize 实参将 SearchFeatureView 的最大尺寸设置为其内容本身的大小,这意味着用户无法将 SearchFeatureView 窗口的尺寸需改为超越其视图内容“默认”的大小:
import SwiftUI
@main
struct MacTextApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
.defaultSize(.init(width: 300, height: 300))
WindowGroup(id: "search") {
SearchFeatureView()
}
.defaultSize(width: 200, height: 300)
.windowResizability(.contentSize)
}
}
struct SearchFeatureView: View {
var body: some View {
Text("Searching Something...")
.font(.largeTitle.weight(.black))
.foregroundStyle(.green.gradient)
.frame(minWidth: 200, minHeight: 300)
}
}
struct ContentView: View {
@Environment(\.openWindow) var openWindow
var body: some View {
VStack {
Image("panda")
.imageScale(.large)
.foregroundStyle(.tint)
Text("Hello, 大熊猫侯佩!!")
.font(.largeTitle)
Button("打开搜索") {
openWindow(id: "search")
}
}
.padding()
}
}
#Preview {
ContentView()
}
运行效果如下所示:
通过为 windowResizability 修改器传入 .contentMinSize 实参,我们还可以限制窗口的最小尺寸为其默认大小,但“放任”窗口的最大尺寸:
WindowGroup(id: "search") {
SearchFeatureView()
}
.defaultSize(width: 200, height: 300)
.windowResizability(.contentMinSize)
运行看一下,正是我们所需要的效果:
2. 任性选择窗口放置位置
默认情况下,每当使用 openWindow 环境值(environment value)打开窗口时,新窗口都会出现在上次打开的窗口前面。不过如果我们愿意的话,可以使用 defaultWindowPlacement 视图修改器来控制新窗口的放置。
struct SugarBotApp: App {
var body: some Scene {
WindowGroup {
RootView()
}
WindowGroup(id: "search") {
SearchFeatureView()
}
.defaultWindowPlacement { content, context in
#if os(visionOS)
if context.windows.last?.id == "search" {
return WindowPlacement(.trailing(context.windows.last!))
} else {
return WindowPlacement(.utilityPanel)
}
#else
// ...
#endif
}
}
}
正如小伙伴们在上面示例中所看到的那样:在 visionOS 里,我们使用 defaultWindowPlacement 视图修改器调整了窗口的放置位置。defaultWindowPlacement 修改器闭包返回 WindowPlacement 类型的实例。该类型用来定义窗口位置,允许我们控制要在其上放置窗口的“边”。
我们在 SwiftUI 中还定义了 visionOS 上 utilityPanel 的位置,该位置将窗口显示在当前窗口的略下方。我们还获得了上一个显示窗口的标识符:以便当它是搜索窗口(search)时将其显示在尾部边缘上(trailing edge)。
而在 macOS 中,我们并不访问 utilityPanel 的位置,而是可以通过闭包上下文中的显示属性访问当前显示器的信息。我们同样能够使用 content 实参去测量窗口内容的尺寸并计算窗口在屏幕上的精确位置:
struct SugarBotApp: App {
var body: some Scene {
WindowGroup {
RootView()
}
WindowGroup(id: "search") {
SearchFeatureView()
}
.defaultSize(width: 500, height: 800)
.windowResizability(.contentSize)
.defaultWindowPlacement { content, context in
#if os(visionOS)
// ...
#else
let size = content.sizeThatFits(.unspecified)
let positionX = context.defaultDisplay.bounds.midX - (size.width / 2)
let positionY = context.defaultDisplay.bounds.maxY - size.height
let position = CGPoint(x: positionX, y: positionY)
return WindowPlacement(position, size: size)
#endif
}
}
}
3. 检测窗口拖动状态
从 macOS 15 开始,SwiftUI 专门为窗口提供了一个 WindowDragGesture 拖动手势:
利用它我们可以轻而易举的监听指定窗口的拖动状态,并据此来调整我们应用的显示逻辑:
struct SecretView: View {
@GestureState private var isWindowDragging = false
var body: some View {
Text("密码:1234567890")
.font(.title.weight(.heavy))
.foregroundStyle(.red.gradient)
.redacted(reason: isWindowDragging ? .placeholder : [])
.gesture(
WindowDragGesture()
.updating($isWindowDragging) { _, state, _ in
state = true
}
)
}
}
从上面的代码可以看到,当我们通过密码 Text 视图拖动窗口时,密码会被遮挡显示:
注意,上面的代码需要用 Xcode 16beta 在 macOS 15 上编译运行。
现在,利用 SwiftUI 提供的架海擎天般的 Window 管理 API,我们可以随心所欲的管理和定制我们的窗口尺寸和拖动事件啦,棒棒哒!💯
总结
在本篇博文中,我们讨论了在 SwiftUI 中如何稳妥的管理和定制窗口(Window)外观;并介绍了如何利用 SwiftUI 6.0 最新增加的窗口拖拽手势实时的监听窗口的拖动状态。
感谢观赏,再会啦!8-)