若要修正 Mac 視窗調整大小時 SwiftUI 內容漂移到螢幕外的問題,請先判斷繪製內容是否超出其版面配置範圍、內容是否拒絕縮小,或嵌入的 AppKit 檢視是否停止追蹤其容器。接著修正該特定邊界。

本指南適用於要修正自家 Mac App 可調整大小介面的開發者:讓標籤維持易讀、控制項方便操作,並讓較長內容可捲動。以下診斷流程使用合成標籤,並區分 SwiftUI 版面配置、視窗大小與 AppKit 文字換行問題。

找出調整大小時失效的邊界

SwiftUI 版面配置從父檢視提出的尺寸建議開始。子檢視會據此選擇自己的尺寸;這項建議並非要求將所有內容裁切在該矩形內。Apple 在 ProposedViewSize 中說明最小、最大與理想尺寸建議。這也解釋了為什麼縮窄視窗不一定會讓每個子檢視都跟著縮小。

先進行可重複的測試:開啟 App 視窗,逐漸縮窄,直到首次出現問題,再將視窗加寬。觀察第一個受影響的元素,而不是最後的排列結果。標籤是否超出容器?整個面板是否移位?文字是否仍維持先前的換行寬度?

暫時在可疑子檢視加上 .border(Color.orange),並在其外層框架或容器加上 .border(Color.blue)。將修飾子放在不同層級,以便比較它們的矩形範圍。Apple 的 offset 說明文件示範了這種邊框技巧,可用來區分原始版面位置與位移後的繪製位置。

觀察到的現象 優先檢查 可採取的下一步
繪製內容移出原本穩定的矩形 offset、position、座標計算 逐一移除位置修飾子
容器縮窄時,標籤仍維持寬度 水平方向的 fixedSize、明確指定的寬度 讓標籤接受受限寬度
水平排列無法容納所有控制項 控制項寬度總和與間距 提供垂直排列方式
高度縮小時,下方內容消失 缺少溢位處理方式 讓內容區域可捲動
由 AppKit 承載的 SwiftUI 面板移位 Hosting View 邊界與尺寸限制 比較宿主尺寸與內容需求
AppKit 文字檢視變窄,但行寬不變 文字容器的寬度追蹤設定 檢查文字檢視與文字容器設定

這些是根據文件所述行為提出的診斷假設,不代表某種現象一定只有一個成因。每次只變更一個可疑項目,並重複相同的調整大小操作。如果用 Text("Sample panel") 取代複雜子檢視後問題消失,就逐步恢復該子檢視的各個層級。

移除會抵抗視窗調整的定位與尺寸規則

檢查 offset 與以中心為基準的定位

offset 會移動顯示內容,但不會改變檢視原本的尺寸。因此,即使父檢視仍保留原本的空間,正向水平 offset 也可能將繪製內容推到右邊界之外。暫時移除 offset;如果繪製內容回到容器內,請改用堆疊、內距或框架對齊來表達位置調整。請參閱 Apple 的 offset 行為說明。

position(x:y:) 會將檢視中心放在父檢視座標空間中的指定座標。若把這些座標當作左上角,就會造成與子檢視尺寸相關的偏差。一次算好的位置,在父檢視尺寸改變時也可能不再合適。一般的標籤與控制項面板,應優先使用能表達元素與容器間預期關係的對齊方式。Apple 在 position(x:y:) 中定義了以中心為基準的行為。

例如,.frame(maxWidth: .infinity, alignment: .leading) 可在彈性框架內採用前緣對齊。它不會移除尺寸過大的子檢視本身的尺寸需求。Apple 的 彈性框架說明文件解釋了框架如何限制尺寸建議並定位子檢視。

檢查固定寬度與理想尺寸文字

檢查失效分支中的明確寬度設定,以及水平方向使用的 fixedSize。Apple 特別說明,.fixedSize(horizontal: true, vertical: false) 會讓文字保留顯示完整字串所需的理想寬度,因此可能超出父檢視的範圍。若是需要換行的內文,請移除這項水平尺寸要求。若適合保留換行文字的完整高度,可考慮使用 .fixedSize(horizontal: false, vertical: true)。請參閱 fixedSize(horizontal:vertical:)。

保留垂直尺寸的前提是有地方容納增加的高度。放在較矮且不可捲動容器中的段落,即使能向下延伸,仍可能無法觸及。若任務要求能讀取全部文字,請使用垂直捲動區域;Apple 在 ScrollView 中說明了支援的捲動軸。

再次檢查以幾何資訊計算的尺寸

GeometryReader 會根據自身尺寸與座標空間來定義內容,並向父檢視回報彈性的偏好尺寸。在檢視外加上它會改變版面配置結構,因此不是中性的測量包裝器。如果引入幾何計算後才開始漂移,請暫時將該分支換成一般的堆疊與框架。Apple 在 GeometryReader 中說明了這些特性。

若比例式版面確實需要幾何資訊,請根據相關容器目前的尺寸推導大小。檢查是否有狀態儲存了初次顯示時擷取的寬度。也要檢查測量出的尺寸是否用來調整同一個被測量的區域;移除這種依賴關係是有效的隔離測試。

建立精簡版面配置並明確處理溢位

以下完整範例示範三個彼此獨立的選擇:文字會換行、控制項會在需要時切換排列方式,且垂直溢位內容仍可觸及。範例尺寸只是設計輸入值,並非 Apple 規定的數值。

在 Xcode 中建立 macOS SwiftUI App,將部署目標設為 macOS 13 或更新版本,並以這個單一檔案取代產生的 App 與內容檢視宣告。只保留一個 @main 宣告。範例使用 macOS 13 起提供的 ViewThatFits 與視窗可調整大小 API。Apple 在 ViewThatFits 與 windowResizability(_:) 中說明了它們的行為。

import SwiftUI

@main
struct ResizeExampleApp: App {
    var body: some Scene {
        WindowGroup {
            ResizeExampleView()
                .frame(minWidth: 280, minHeight: 220)
        }
        .windowResizability(.contentMinSize)
    }
}

struct ResizeExampleView: View {
    @State private var selection = "Sample A"

    private let details = Array(
        repeating: "Sample notes wrap as the panel becomes narrower.",
        count: 12
    ).joined(separator: " ")

    var body: some View {
        ScrollView(.vertical) {
            VStack(alignment: .leading, spacing: 16) {
                Text("Sample panel")
                    .font(.title2)

                Text(details)
                    .fixedSize(horizontal: false, vertical: true)
                    .frame(maxWidth: .infinity, alignment: .leading)

                ViewThatFits(in: .horizontal) {
                    HStack(spacing: 12) {
                        Button("Choose Sample A") {
                            selection = "Sample A"
                        }
                        Button("Choose Sample B") {
                            selection = "Sample B"
                        }
                    }
                    .fixedSize(horizontal: true, vertical: false)

                    VStack(alignment: .leading, spacing: 12) {
                        Button("Choose Sample A") {
                            selection = "Sample A"
                        }
                        Button("Choose Sample B") {
                            selection = "Sample B"
                        }
                    }
                }

                Text("Selected: \(selection)")
            }
            .padding(20)
            .frame(
                minWidth: 0,
                maxWidth: .infinity,
                alignment: .leading
            )
        }
    }
}

ViewThatFits 會依宣告順序評估各個替代方案,並選擇沿受限軸向尺寸符合要求的第一個方案。此處透過水平尺寸適配,在按鈕列與垂直堆疊之間做選擇。按鈕列刻意保留其水平理想尺寸,避免被壓縮的按鈕列成為優先結果。請確保兩種方案都提供相同的可用操作。請參閱 Apple 的選擇規則。

外層框架允許內容欄依可用寬度調整,而垂直捲動檢視則能在換行增加內容高度時提供存取方式。這種排列假設各個控制項在所選的最小寬度內都能容納。標籤較長或字體較大時,可能需要調整最小寬度或改用其他精簡排列方式。

本範例已檢視宣告是否一致及 API 是否可用;由於此處沒有 Swift 編譯器,因此未進行編譯。請在 macOS 上執行,以確認實際的視窗與文字行為。

修正內容版面配置後,再設定視窗大小限制

實用的視窗最小尺寸可避免介面縮小到無法支援的排列方式。使用 .contentMinSize 時,SwiftUI 會根據內容推導視窗的最小尺寸,且不設最大尺寸。在範例中,根框架提供最小寬度與高度的輸入值。請參閱 contentMinSize。

請用支援的最長標籤測試精簡排列方式,以決定最小尺寸。若為了讓錯位的檢視顯示出來而持續增加最小視窗尺寸,可能會掩蓋原本的版面配置錯誤。先移除非預期的位移與過大的子檢視,再設定能讓預期任務維持可用的最小尺寸。

如果由 AppKit 管理視窗並承載 SwiftUI,請另外檢查承載邊界。在 macOS 13 與更新版本中,NSHostingView.sizingOptions 會控制根據 SwiftUI 內容產生的限制。如果宿主是視窗的 contentView,它也會更新視窗內容區域的最小與最大尺寸。Apple 也說明,當宿主框架與 SwiftUI 內容所需尺寸不一致時,內容會置中。請先檢查這些關係,再加入補償用的 offset。請參閱 NSHostingView sizingOptions。

只有在失效檢視使用 AppKit 和 TextKit 時,才檢查它們

讓 SwiftUI 調整所包裝根檢視的尺寸

對於 NSViewRepresentable,SwiftUI 會控制所包裝 AppKit 檢視的 frame 與 bounds。Apple 明確警告,自行設定這些屬性會與 SwiftUI 衝突,並導致未定義行為。如果調整大小處理程式反覆指定所包裝根檢視的框架,請移除這種互相競爭的控制方式,並透過 SwiftUI 邊界表達尺寸要求。請參閱 NSViewRepresentable。

若需要自訂測量方式,macOS 13 與更新版本提供 sizeThatFits(_:nsView:context:)。一次版面配置流程中可能會收到多個尺寸建議;回傳 nil 代表要求採用預設尺寸。請根據目前的尺寸建議與內容計算結果,而不是沿用先前的視窗寬度。對未指定的建議尺寸,應制定明確的測量策略,不要自動將其視為零。請參閱包裝檢視尺寸調整。

Swift 的選擇性繫結可用來檢查並擷取建議尺寸後再使用。在實作尺寸策略時,這有助於避免對選用的寬度或高度強制解包。此語言行為記載於 The Basics: Optional Binding。

分別處理文字換行寬度與文字檢視寬度

如果失效的子檢視是 NSTextView,請比較它目前的寬度與文字容器的寬度。Apple 說明,NSTextContainer.widthTracksTextView 預設為 false;將其設為 true 後,文字檢視寬度改變時,容器也會調整寬度。當文字行應隨文字檢視重新排版時,這是相關的控制項。請參閱 widthTracksTextView。

也要檢查 isHorizontallyResizable。它控制文字檢視是否會調整寬度以符合文字,這與在指定寬度內換行是不同的需求。若是垂直捲動且需要換行的文字區域,請一致設定這兩項策略,並確認外層版面配置確實會調整文字檢視的尺寸。請參閱 isHorizontallyResizable。

保留實作目前使用的文字引擎。Apple 說明,存取 NSTextView 或其文字容器上的 layoutManager,可能會讓它切換至使用 NSLayoutManager 的相容模式。診斷 TextKit 2 檢視時,不要隨意加入這項存取。請使用現有文字引擎與部署目標適用的 API。請參閱 NSTextView。

驗證寬度與高度變化時的修正結果

在自己的 App 中依照以下最後步驟檢查:

  1. 緩慢縮窄視窗,經過原本出問題的尺寸後再加寬。
  2. 單獨降低視窗高度,確認所有必要內容仍可觸及。
  3. 使用較長的合成標籤與多行文字重複測試。
  4. 確認每個控制項在寬版與精簡排列中都能操作。
  5. 對 AppKit 文字檢視,確認文字換行在視窗縮窄與加寬時都會依目前寬度調整。

成功的修正會讓位置隨目前容器調整、讓內容具備可用的精簡排列方式,並讓溢位內容仍可存取。反覆調整視窗大小後,只要這些行為都能維持,就可移除暫用邊框,並保留介面實際需要的最小視窗尺寸。

來源