説明文が広がる間もMacウィンドウのラベル列とステータス列を一定に保つには、外側の列に明示的な幅を指定し、説明文には柔軟なフレームを設定します。兄弟ビュー同士で領域を取り合う場合はレイアウト優先度を使い、必要な軸に限って理想サイズを維持します。
このチュートリアルでは、合成した項目ラベルと説明文を含む、小さくてサイズ変更可能なウィンドウを作成します。ウィンドウの幅が狭くなっても列の位置をそろえ、長い説明文は折り返すようにします。すべてサンプルアプリ内で完結し、レイアウトには通常のSwiftUIビューを使用します。
各サブビューにはどのサイズ調整を使うべきですか?
SwiftUIのレイアウトは、親ビューがサイズを提案するところから始まります。各子ビューは希望するサイズで応答し、コンテナがその結果を配置します。まず、各子ビューのどの寸法を変更可能にするかを決めるとよいでしょう。Appleはこの提案と応答の仕組みをSwiftUIのレイアウトガイドで説明しています。
このウィンドウでは、3つの役割を割り当てます。幅を指定するラベル列、幅の変化を吸収する説明文列、そして幅を指定するステータス列です。数値で指定する固定幅とビューの理想サイズは区別してください。これらは異なる要件を表します。
| 要件 | 制御方法 | 実際の結果 |
|---|---|---|
| 列に特定の幅を確保する | .frame(width: 96) |
生成されるフレームがその幅になります。 |
| 説明文に利用可能な幅を使わせる | .frame(minWidth: 160, maxWidth: .infinity, alignment: .leading) |
最小幅の制約内で、フレームが提案された幅を受け入れます。 |
| 選択した軸でコンテンツの理想サイズを保つ | .fixedSize(horizontal: false, vertical: true) |
幅はレイアウトに応じて変化し、高さは理想サイズを保ちます。 |
| 領域の割り当てで一方の兄弟ビューを優先する | .layoutPriority(1) |
親ビューは優先度の低い兄弟ビューより、その子ビューを優先します。 |
Appleは、数値で寸法を指定する方法を固定フレームモディファイア、最小値と最大値の制約を柔軟なフレームモディファイア、軸ごとの理想サイズ指定をfixedSize(horizontal:vertical:)、兄弟ビュー間の優先設定をlayoutPriority(_:)で説明しています。
最大値に無限大を指定すると、提案されたサイズの範囲内で拡大できます。ウィンドウを無限に広くする要求ではありません。最小値と最大値の両方を指定すると、フレームはその制約内に収まるよう提案された寸法を採用します。alignmentパラメータは、そのフレーム内で子ビューを配置します。テキスト自体が列に割り当てられた領域より狭い場合には、こうした詳細が重要になります。Appleの柔軟なフレームに関するドキュメント
サイズ変更可能なmacOSのサンプルを完成させる
XcodeでSwiftUIを使ったmacOSアプリプロジェクトを作成し、デプロイメントターゲットをmacOS 13以降に設定します。生成されたアプリとコンテンツビューの宣言をこの単一ファイルに置き換え、アプリのエントリポイントは1つだけにします。このサンプルではmacOS 13のウィンドウサイズ変更設定を使います。AppleのWindowResizabilityドキュメント
以下は、サンプル文字列と短い説明文・長い説明文を切り替えるトグルを含む、アプリ全体の例です。調査環境ではSwiftツールチェーンを利用できなかったため、SwiftUIアプリをコンパイルできませんでした。既存プロジェクトに組み込む前に、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ポイントが実用的な下限になります。この設定はアプリのシーンが作成するウィンドウに適用されます。AppleのcontentMinSizeドキュメント
some Viewを返す宣言では、Swiftの不透明な結果型を使っています。具体的な型は隠されますが、Viewへの適合性は利用できます。ネストした戻り値の型を記述せずに、モディファイアチェーンを調整できます。Swiftプログラミング言語: 不透明なプロトコル型とボックス化されたプロトコル型
優先度を変える前に幅の割り当てを計算する
幅の割り当てを計算すると、レイアウトの限界が明確になります。この例では、コンテンツ幅から水平パディング、2つの間隔、2つの固定列を差し引きます。残りが説明文の幅です。以下は設計の例としての寸法であり、ネイティブコントロールの実測値ではありません。
説明文の幅
= コンテンツ幅 - 水平パディング - 間隔 - 固定列
= コンテンツ幅 - (2 × 16) - (2 × 12) - 96 - 72
= コンテンツ幅 - 224
幅640ポイントの場合: 説明文に416ポイントを割り当てられます。
幅420ポイントの場合: 説明文に196ポイントを割り当てられます。
行の最小幅の合計:
32 + 24 + 96 + 160 + 72 = 384ポイント。
計算は、決定的なローカル計算で確認しました。これは、指定したパディングを差し引いたコンテンツ幅が行に割り当てられる場合の値であり、ウィンドウの装飾は含みません。ウィンドウの最小幅を420ポイントにすると、計算上の行の最小幅384ポイントを上回る余裕ができます。周囲のコンテナにインセットがある場合は、それも幅の計算に含めてください。
この計算から、実現できない要件も分かります。合計最小幅を下回ると、3つの列すべてで幅と間隔を同時に維持することはできません。ウィンドウの最小幅を大きくするか、最小寸法を小さくするか、別の配置にしてください。優先度を上げても、領域は増えません。
レイアウト優先度が役立つのはどんな場合ですか?
2つの兄弟ビューがどちらも縮小でき、そのうち一方に多くの領域を残したい場合は、優先度が役立ちます。たとえば、説明的なタイトルを補足注釈より優先したい場合です。その親ビューでの領域割り当てに参加する子ビューに.layoutPriority(1)を設定します。Appleによると、優先度の高いビューは縮小が後になり、拡大が先になります。また、優先度の低い子ビューにも最低限必要な領域が確保されます。AppleのlayoutPriorityに関するドキュメント
完成例では、優先度モディファイアは必要ありません。2つの固定列と1つの柔軟な列で、すでに領域の割り当て方を表現できているからです。また、優先度の値は比例配分の重みではありません。優先度に2と1を設定しても、幅が2対1になるわけではありません。ドキュメントにあるのは、割り当て時に一方を優先する用途です。AppleのlayoutPriorityに関するドキュメント
説明文とステータスの間に別の柔軟な間隔を設けたい場合を除き、横方向のSpacerを追加するのは避けてください。Spacerはスタックの主軸方向に広がり、兄弟ビューを互いに押し離します。説明文のフレームがすでに幅の変化を吸収するため、ここでは追加する必要はありません。AppleのSpacerに関するドキュメント
テキストが収まらなくなったときはどうすればよいですか?
各テキストフィールドについて、折り返すのか、省略するのか、別の配置が必要なのかを個別に決めます。この例の説明文は行数制限なしで折り返せます。.lineLimit(1)を適用すると、そのテキスト要素は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:)メソッドから、提案されたサイズに対する優先サイズを返せます。1回のレイアウトパス内で、異なる提案を受けて複数回呼び出されることがあります。nilを返すと、既定のサイズ調整アルゴリズムが使われます。テキストを折り返すコンポーネントの測定では、折り返さないテキストの幅を常に返すのではなく、提案された幅を考慮してください。Appleのrepresentableのサイズ調整に関するドキュメント
未指定の寸法とゼロは区別してください。SwiftUIでは、未指定の提案は理想サイズを求め、ゼロは最小サイズを調べるために使います。独自の測定処理では、それぞれを意図的に扱う必要があります。AppleのProposedViewSizeドキュメント
AppKitのレイアウトでは、圧縮耐性によって、NSViewが固有サイズより小さくなることへの抵抗の強さが決まります。折り返しコンポーネントが縮小しにくい場合は、SwiftUIのサイズ調整の動作に加えて、この設定も確認してください。AppleのsetContentCompressionResistancePriorityに関するドキュメント
TextKit 2を使うNSTextViewでは、新しいtextLayoutManagerの経路を使用してください。Appleの現在のTextKit概要では、これを従来のlayoutManagerと区別しています。また、移行ガイドによると、従来のプロパティにアクセスすると、そのインスタンスは一方向にTextKit 1へ切り替わる場合があります。兄弟列への領域割り当てを調整するために、テキストエンジンを変更する必要はありません。AppleのTextKit概要、TextKitとテキストビューの新機能
ウィンドウの最小サイズで結果を確認する
サンプルを実行し、この方法を大きなインターフェイスに適用する前に、次の点を確認してください。
- ウィンドウを広げる: ラベルとステータスのフレーム幅は保たれ、説明文の列が広がるはずです。
- サポートする最小幅まで狭める: 長い説明文は折り返され、行の高さが増えるはずです。
長い説明文を表示をオフ・オンにする: 短い説明文も同じ列位置にそろうはずです。- 合成したステータスを長い値に置き換える: そのフィールドを広げるか、折り返すか、配置を変えるか決めてください。
- 利用できる高さを小さくする: すべての行に引き続きアクセスできることを確認してください。
どの層に領域が割り当てられるかを確認するには、説明文の柔軟なフレームの前後に、一時的に枠線を追加します。内側の枠線はテキストの境界を示し、外側の枠線は列に割り当てられた領域を示します。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