若要讓 Mac 視窗中的標籤欄和狀態欄保持穩定,同時讓描述欄隨空間擴展,請為外側欄指定明確寬度,並為描述欄設定彈性框架。當同層視圖爭用空間時,使用版面配置優先順序,並只在需要的軸向保留理想尺寸。
本教學會建立一個小型、可調整大小的視窗,內含虛構的項目標籤和描述。目標是在保持欄位對齊的同時,讓較長的描述在視窗變窄時換行。所有內容都屬於範例 App;版面配置使用一般的 SwiftUI 檢視。
每個子視圖應使用哪種尺寸控制?
SwiftUI 版面配置會從父視圖提出尺寸開始。每個子視圖會回應其偏好的尺寸,而容器會排列這些結果。實用的起點是先決定每個子視圖可以變動哪些尺寸。Apple 在SwiftUI 版面配置指南中說明了這個提案與回應的流程。
在這個視窗中,指定三種角色:寬度自訂的標籤欄、吸收寬度變化的描述欄,以及寬度自訂的狀態欄。請區分數值固定寬度與檢視的理想尺寸:兩者表達的是不同需求。
| 需求 | 控制項 | 實際效果 |
|---|---|---|
| 保留特定欄寬 | .frame(width: 96) |
產生的框架具有該寬度。 |
| 讓描述欄吸收可用寬度 | .frame(minWidth: 160, maxWidth: .infinity, alignment: .leading) |
框架會接受提案的寬度,但受其最小值限制。 |
| 在選定軸向保留內容的理想尺寸 | .fixedSize(horizontal: false, vertical: true) |
寬度可隨版面配置調整,而高度保留理想尺寸。 |
| 在空間分配時優先考慮某個同層視圖 | .layoutPriority(1) |
父視圖會優先分配空間給該子視圖,而非優先順序較低的同層視圖。 |
Apple 在固定框架修飾器中說明數值尺寸,在彈性框架修飾器中說明最小與最大限制,在fixedSize(horizontal:vertical:)中說明依軸向設定理想尺寸,並在layoutPriority(_:)中說明同層視圖的優先順序。
無限大的最大值代表允許在提案範圍內擴展;它不會要求視窗無限寬。提供最小與最大限制後,框架會採用提案的尺寸,並將其限制在這些條件內。對齊參數會決定子視圖在框架內的位置。當文字本身比分配給欄位的空間窄時,這些細節就很重要。Apple 的彈性框架說明文件
建立完整的可調整大小 macOS 範例
在 Xcode 中使用 SwiftUI 建立 macOS App 專案,並將部署目標設為 macOS 13 或更新版本。將產生的 App 和內容檢視宣告替換為這個單一檔案,並只保留一個 App 進入點。此範例使用 macOS 13 的視窗可調整性控制項。Apple 的 WindowResizability 說明文件
以下是完整的 App 範例,包含範例字串,以及用來切換短版與長版描述的開關。由於研究環境中沒有 Swift 工具鏈,因此無法編譯這個 SwiftUI App;將它整合到現有專案之前,請先在 Xcode 中檢查其行為。
import SwiftUI
@main
struct ColumnAllocationApp: App {
var body: some Scene {
WindowGroup {
AllocationView()
.frame(minWidth: 420, minHeight: 260)
}
.windowResizability(.contentMinSize)
}
}
struct AllocationView: View {
@State private var showLongDescriptions = true
var body: some View {
VStack(alignment: .leading, spacing: 16) {
Text("Item overview")
.font(.title2)
Toggle(
"Show longer descriptions",
isOn: $showLongDescriptions
)
ScrollView {
VStack(alignment: .leading, spacing: 16) {
AllocationRow(
label: "Item A",
summary: showLongDescriptions
? "A blue folder with several sample notes and a longer descriptive label."
: "Blue folder",
status: "Ready"
)
AllocationRow(
label: "Item B",
summary: showLongDescriptions
? "A green notebook containing a short list of synthetic example entries."
: "Green notebook",
status: "Draft"
)
}
.frame(maxWidth: .infinity, alignment: .leading)
}
}
.padding(16)
}
}
struct AllocationRow: View {
let label: String
let summary: String
let status: String
var body: some View {
HStack(alignment: .top, spacing: 12) {
Text(label)
.frame(width: 96, alignment: .leading)
Text(summary)
.fixedSize(horizontal: false, vertical: true)
.frame(
minWidth: 160,
maxWidth: .infinity,
alignment: .leading
)
Text(status)
.fixedSize(horizontal: true, vertical: false)
.frame(width: 72, alignment: .trailing)
}
}
}
描述欄是唯一具有水平彈性的欄位。它的 fixedSize 呼叫讓水平尺寸維持彈性,同時保留換行文字所需的垂直空間。接著,其彈性框架會填滿分配到的欄位。狀態文字會在獨立的 72 點框架內保留理想文字寬度。Apple 提醒,理想尺寸的內容可能超出父視圖的邊界,因此這種狀態欄設定假設所提供的短值為 Ready 和 Draft。Apple 的 fixedSize 說明文件
這些列沒有固定高度。這是刻意安排的:較窄的描述欄可能需要更多行。當所有列的總高度超過可用區域時,捲動檢視可讓使用者瀏覽各列。請避免只為了讓初始排列看起來整齊,就加入過度限制的列高。
視窗使用 .contentMinSize,其最小尺寸取決於內容,且沒有最大尺寸限制。在此範例中,根視圖的 420 點最小寬度設定了實用的下限。此控制項適用於 App 場景建立的視窗。Apple 的 contentMinSize 說明文件
回傳 some View 的宣告使用 Swift 不透明結果型別:具體型別會隱藏,但仍可使用其對 View 的遵循。你可以調整修飾器鏈,而不必明確寫出巢狀的回傳型別。Swift 程式語言:不透明與裝箱協定型別
變更優先順序前,先計算寬度預算
寬度預算能明確呈現版面配置的限制。在此範例中,從視窗內容寬度扣除水平內距、兩個間距和兩個固定欄位。剩餘寬度就是描述欄的空間。這些是示意用的設計尺寸,並非原生控制項的測量值。
描述欄寬度
= 內容寬度 - 水平內距 - 間距 - 固定欄位
= 內容寬度 - (2 × 16) - (2 × 12) - 96 - 72
= 內容寬度 - 224
寬度為 640 點時:描述欄剩餘 416 點。
寬度為 420 點時:描述欄剩餘 196 點。
列的最小寬度預算:
32 + 24 + 96 + 160 + 72 = 384 點。
以上算術已透過確定性的本機計算進行檢查。它描述此列在取得扣除所述內距後的內容寬度時的情況;不包含視窗裝飾。選定的 420 點視窗最小寬度,高於此列計算出的 384 點最小寬度。如果外層容器增加了內縮,也要將其納入預算。
這項計算也能指出無法達成的要求。低於合計最小預算時,三個欄位無法同時保留各自的寬度與間距。請選擇更大的視窗最小寬度、較小的最小尺寸,或改用不同排列方式。提高優先順序無法創造額外空間。
版面配置優先順序何時有幫助?
當兩個同層視圖都可以縮小,而你希望其中一個保留較多空間時,優先順序就很有用。例如,描述性標題可能比次要註記更值得優先保留。請將 .layoutPriority(1) 加到參與該父視圖空間分配決策的子視圖上。Apple 表示,優先順序較高的視圖會較晚縮小、較早擴展,並會為優先順序較低子視圖的最低需求保留空間。Apple 的 layoutPriority 說明文件
完整範例不需要優先順序修飾器:兩個固定欄位和一個彈性欄位已表達其分配規則。優先順序值也不是比例權重。設定優先順序為 2 和 1,不會形成二比一的寬度比例;其文件說明的用途是在空間分配時表示偏好。Apple 的 layoutPriority 說明文件
除非你想要額外的彈性間距,否則請避免在描述欄與狀態欄之間加入水平 Spacer。Spacer 會沿著堆疊的主軸擴展,將同層視圖推開。由於描述欄的框架已能吸收寬度變化,這裡不需要加入額外的空間分配對象。Apple 的 Spacer 說明文件
文字無法完整顯示時該怎麼辦?
請分別決定每個文字欄位應該換行、截斷,還是改用不同排列方式。範例描述可以不設行數限制而換行。套用 .lineLimit(1) 會將受影響的每個文字元素限制為一行,因而改變原本的需求。設定行數上限為 nil 會移除此限制,但不會取消外層框架所施加的限制。Apple 的 lineLimit 說明文件
若狀態值較長,請重新考慮 72 點欄寬。若可接受換行,就移除水平理想尺寸保留設定;也可以增加欄寬,或將狀態移到描述欄下方。在外層框架變窄時,保留 .fixedSize(horizontal: true, vertical: false) 可能讓文字超出該框架。Apple 的 fixedSize 說明文件
如果精簡視窗應重新排列內容,macOS 13 或更新版本提供了 ViewThatFits。先提供水平排列,再提供堆疊排列。它會選擇第一個在受限軸向上符合其理想尺寸的子視圖,因此當寬度是決定因素時,請使用 ViewThatFits(in: .horizontal) 評估水平空間。請確保最後一個候選排列在支援的最小寬度下仍可使用。Apple 的 ViewThatFits 說明文件
請將其視為另一種版面配置規則。目前的範例刻意保留欄位排列,並設定視窗最小寬度。若維持水平排列的重要性低於允許視窗縮得更窄,請使用堆疊式備用排列。
嵌入 AppKit 和 TextKit 檢視時,做法有何不同?
如果之後將描述欄改為 AppKit 文字元件,請使用 NSViewRepresentable 整合該檢視。Apple 明確指出,SwiftUI 會控制所表示檢視的 frame 和 bounds;直接從自己的程式碼修改這些屬性會與 SwiftUI 版面配置衝突。請透過 representable 的生命週期設定內容,並使用 SwiftUI 表達外層尺寸。Apple 的 NSViewRepresentable 說明文件
在 macOS 13 或更新版本中,representable 的 sizeThatFits(_:nsView:context:) 方法可以回傳其在特定提案下的偏好尺寸。在一次版面配置過程中,此方法可能會針對不同提案呼叫多次。回傳 nil 代表要求使用預設尺寸演算法。會換行的元件在測量時應考慮提案寬度,而不是永遠回傳未換行文字的寬度。Apple 的 representable 尺寸設定說明文件
請區分未指定尺寸與零:SwiftUI 使用未指定的尺寸提案來要求理想尺寸,而零可以用來探測最小尺寸。自訂測量程序必須明確處理這些情況。Apple 的 ProposedViewSize 說明文件
在 AppKit 版面配置中,壓縮阻力會控制 NSView 抵抗縮小到其內在尺寸以下的程度。若換行元件抗拒壓縮,請同時檢查這項設定及其 SwiftUI 尺寸行為。Apple 的 setContentCompressionResistancePriority 說明文件
對於使用 TextKit 2 的 NSTextView,請使用新版的 textLayoutManager 路徑。Apple 目前的 TextKit 概覽將其與舊版 layoutManager 區分,而遷移指南說明,存取舊版屬性可能會讓該執行個體單向切換至 TextKit 1。為了分配同層欄位而更換文字引擎並無必要。Apple 的 TextKit 概覽、TextKit 和文字檢視的新功能
在視窗尺寸的限制下檢查結果
執行範例,並在將此模式套用到較大型介面前檢查下列情況:
- 加寬視窗:描述欄應隨之擴展,而標籤和狀態框架應維持原有寬度。
- 將視窗縮到支援的最小尺寸:較長的描述應換行並增加列高。
- 開啟和關閉
Show longer descriptions:短描述應繼續與相同欄位對齊。 - 換成較長的虛構狀態值:決定要加寬、換行,還是重新排列該欄位。
- 減少可用高度:確認所有列仍可瀏覽。
若要查看空間分配到哪個層級,請暫時在描述欄彈性框架之前和之後立即加入邊框。內側邊框會顯示文字的範圍;外側邊框則會顯示分配給該欄的空間。Apple 建議使用彩色邊框,實際檢查堆疊尺寸與內距。SwiftUI 版面配置指南
隨著介面擴展,請維持明確的尺寸規則:在對齊需要的地方保留寬度,為可變內容提供明確的彈性區域,並決定各欄位在最小寬度下的行為。採用這些規則並計算寬度預算後,你就能變更標籤或新增內容,而不必猜測應該提高哪個修飾器的優先順序。
來源
- SwiftUI 版面配置指南 — Apple
- frame(width:height:alignment:) — Apple
- frame(minWidth:idealWidth:maxWidth:minHeight:idealHeight:maxHeight:alignment:) — Apple
- fixedSize(horizontal:vertical:) — Apple
- layoutPriority(_:) — Apple
- WindowResizability — Apple
- contentMinSize — Apple
- 不透明與裝箱協定型別 — Swift.org
- Spacer — Apple
- lineLimit(_:) — Apple
- ViewThatFits — Apple
- NSViewRepresentable — Apple
- sizeThatFits(_:nsView:context:) — Apple
- ProposedViewSize — Apple
- setContentCompressionResistancePriority(_:for:) — Apple
- TextKit — Apple
- TextKit 和文字檢視的新功能 — Apple