You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

SwiftUI 3.0 macOS应用如何正确实现窗口缩放功能

实现方案

你之前用GeometryReader拿不到正确尺寸是因为GeometryReader默认会撑满父容器,拿到的是父视图尺寸而非子内容尺寸,用PreferenceKey从子视图向上传递真实尺寸即可解决,完整实现步骤如下:

1. 定义尺寸捕获工具和全局状态

首先定义PreferenceKey捕获内容视图的真实尺寸,同时创建Observable对象存储内容尺寸供AppKit层调用:

import SwiftUI
import AppKit

// 捕获内容视图实际尺寸的PreferenceKey
struct ContentSizeKey: PreferenceKey {
    static var defaultValue: CGSize = .zero
    static func reduce(value: inout CGSize, nextValue: () -> CGSize) {
        value = nextValue()
    }
}

// 全局存储内容尺寸的状态对象
class WindowState: ObservableObject {
    @Published var contentSize: CGSize = .zero
}

2. 调整ContentView上报自身尺寸

在内容视图的background层嵌套GeometryReader,拿到的尺寸就是内容的真实大小,通过PreferenceKey向上传递:

struct ContentView: View {
    var body: some View {
        Text("This is an experimental piece of text.")
            .padding(50)
            .border(Color.red, width: 5)
            .fixedSize()
            // 捕获当前视图的真实尺寸
            .background(GeometryReader { geo in
                Color.clear
                    .preference(key: ContentSizeKey.self, value: geo.size)
            })
    }
}

3. 实现自定义WindowDelegate关联状态

不要全局扩展NSWindowController,自定义delegate绑定存储尺寸的状态对象,计算最优尺寸时要加上窗口标题栏的高度:

class CustomWindowDelegate: NSObject, NSWindowDelegate {
    let windowState: WindowState
    
    init(windowState: WindowState) {
        self.windowState = windowState
    }
    
    func windowWillUseStandardFrame(_ window: NSWindow, defaultFrame newFrame: NSRect) -> NSRect {
        // 叠加标题栏高度,避免内容被挤压
        let titleBarHeight = window.frame.height - window.contentView!.frame.height
        let optimalSize = CGSize(
            width: windowState.contentSize.width,
            height: windowState.contentSize.height + titleBarHeight
        )
        return NSRect(origin: window.frame.origin, size: optimalSize)
    }
}

4. 主App结构绑定逻辑

在主App中注入状态、监听尺寸变化、给窗口绑定自定义delegate,同时设置全局最小窗口尺寸:

@main
struct ZoomApp: App {
    @StateObject private var windowState = WindowState()
    @State private var windowDelegate: CustomWindowDelegate?
    
    var body: some Scene {
        WindowGroup {
            ContentView()
                .frame(minWidth: 50, maxWidth: .infinity, minHeight: 50, maxHeight: .infinity)
                // 接收内容视图上报的尺寸更新全局状态
                .onPreferenceChange(ContentSizeKey.self) { newSize in
                    windowState.contentSize = newSize
                }
                // 获取当前窗口绑定自定义delegate
                .background(WindowAccessor { window in
                    guard let window = window, window.delegate == nil else { return }
                    let delegate = CustomWindowDelegate(windowState: windowState)
                    window.delegate = delegate
                    self.windowDelegate = delegate
                })
        }
        // 全局设置窗口最小尺寸,禁止缩到更小
        .windowResizability(.contentSize(minimumSize: CGSize(width: 50, height: 50)))
    }
}

// 辅助工具:获取当前SwiftUI视图对应的NSWindow实例
struct WindowAccessor: NSViewRepresentable {
    let callback: (NSWindow?) -> Void
    
    func makeNSView(context: Context) -> NSView {
        let view = NSView()
        DispatchQueue.main.async {
            callback(view.window)
        }
        return view
    }
    
    func updateNSView(_ nsView: NSView, context: Context) {}
}

该实现完全符合需求:窗口小于最小尺寸时无法继续缩放,点击缩放按钮会自动适配内容最优尺寸,正常拖拽可以自由放大窗口。


内容的提问来源于stack exchange,提问作者fankibiber

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.09.23 14:54:00