SwiftUIのbody分割、computedプロパティと別Structは何が違うのか
はじめに
SwiftUIで画面を作っていると、bodyがどんどん長くなっていきます。
「分割したいけど、computedプロパティ?関数?別Struct?どれで分ければいいの?」
実は7つの分割パターンがあり、それぞれ使うべき場面が違います。さらに1つだけ更新範囲の最適化において根本的に異なるものがあります。
この記事では全パターンを整理し、「いつ・どれを使うべきか」を明確にします。
全パターン一覧
| # | パターン | 引数 | 分岐 | selfアクセス | body評価 |
|---|---|---|---|---|---|
| 1 | computedプロパティ | ❌ | ❌ | ✅ | 親Viewのbody評価の一部として扱われる |
| 2 |
@ViewBuilder computedプロパティ |
❌ | ✅ | ✅ | 親Viewのbody評価の一部として扱われる |
| 3 |
some Viewを返す関数 |
✅ | ❌ | ✅ | 親Viewのbody評価の一部として扱われる |
| 4 |
@ViewBuilderメソッド |
✅ | ✅ | ✅ | 親Viewのbody評価の一部として扱われる |
| 5 | extensionに置く | 置くものによる | 置くものによる | ✅ | 親Viewのbody評価の一部として扱われる |
| 6 | staticプロパティ/関数 | 関数なら可 |
@ViewBuilderなら可 |
❌ | 親Viewのbody評価の一部として扱われる |
| 7 | 別Structに切り出し | initで渡す | ✅ | — | ⚡️独立した単位として扱われ、更新範囲の最適化が効きやすい |
1〜6は全て「コードの整理方法」で、SwiftUIから見ると親bodyの一部です。
7だけが根本的に異なり、SwiftUIが独立した単位として扱いやすくなります。
1. computedプロパティ
最もシンプルな分割方法。引数なしで、bodyの一部を切り出します。
struct TicketDetailView: View {
let ticket: Ticket
var body: some View {
VStack {
clinicNameView
priceView
}
}
private var clinicNameView: some View {
Text(ticket.clinicName)
.font(.system(size: 12))
.foregroundColor(.gray)
}
private var priceView: some View {
Text("\(ticket.price)円")
.font(.system(size: 16, weight: .bold))
}
}
✅ 使う場面: bodyが長くなったので見やすく分割したい時
❌ ダメな場面: 引数を渡したい時(computedプロパティは引数を取れない)
2. @ViewBuilder computedプロパティ
computedプロパティに@ViewBuilderを付けると、if/elseで異なる型のViewを返せるようになります。
struct HeaderView: View {
let isLoggedIn: Bool
var body: some View {
headerContent
}
@ViewBuilder
private var headerContent: some View {
if isLoggedIn {
Text("ようこそ") // ← Text型
} else {
Image(systemName: "person.circle") // ← Image型(Textと違う)
}
}
}
@ViewBuilderなしだと、if/elseで異なる型を返すとコンパイルエラーになります。
✅ 使う場面: 引数不要だけど条件分岐でViewを出し分けたい時
❌ ダメな場面: 引数が必要な時
3. some Viewを返す関数
引数を渡せる分割方法。ただしif/elseで異なる型は返せません。
struct StationListView: View {
let stations: [String]
var body: some View {
HStack {
stationTag(name: "渋谷駅")
stationTag(name: "表参道駅")
}
}
private func stationTag(name: String) -> some View {
Text(name)
.font(.system(size: 10, weight: .bold))
.padding(.horizontal, 12)
.background(
RoundedRectangle(cornerRadius: 10)
.stroke(Color.orange, lineWidth: 1)
)
}
}
✅ 使う場面: 同じ見た目で中身だけ変えたい(引数あり、分岐なし)
❌ ダメな場面: if/elseで違う型のViewを返したい時 → @ViewBuilderメソッド(4)を使う
4. @ViewBuilderメソッド
最も万能な分割方法。引数も取れるし、if/elseで異なる型も返せます。
struct TagListView: View {
var body: some View {
HStack {
tagView(type: .preReservation)
tagView(type: .repeatablePurchase)
}
}
@ViewBuilder
private func tagView(type: TagType) -> some View {
switch type {
case .preReservation:
HStack {
Image(systemName: "bolt.fill")
Text("即時予約")
}
.background(Color.teal)
case .repeatablePurchase:
Text("何度でも購入可")
.background(Color.green)
}
}
}
✅ 使う場面: 引数に応じてViewを出し分けたい時
❌ ダメな場面: 単純な分割には過剰(computedプロパティで十分)
5. extensionに置く
1〜4のどれでもextensionに置ける。コードの置き場所が違うだけで、SwiftUIにとっての違いはゼロです。
struct TicketDetailView: View {
let ticket: Ticket
var body: some View {
VStack {
clinicNameView
reviewSection(showScore: true)
}
}
}
// ファイル内で関連するパーツをグループ化
extension TicketDetailView {
var clinicNameView: some View {
Text(ticket.clinicName)
}
@ViewBuilder
func reviewSection(showScore: Bool) -> some View {
if showScore {
HStack {
Text("★")
Text("\(ticket.reviewScore)")
}
} else {
Text("レビューなし")
}
}
}
ファイル内の整理目的。computedプロパティだけでなく、メソッドや@ViewBuilderも置けます。
✅ 使う場面: 1つのViewが500行超えて整理したい時
❌ ダメな場面: パフォーマンス改善目的(意味がない)
6. staticプロパティ/関数
self(インスタンス)にアクセスできない。データに依存しない固定のView用。
struct TicketDetailView: View {
var body: some View {
VStack {
Text("チケット詳細")
Self.divider
Self.divider(color: .blue)
Text("施術について")
}
}
static var divider: some View {
Rectangle()
.fill(Color.gray.opacity(0.3))
.frame(height: 1)
.padding(.vertical, 8)
}
// static関数なら引数も取れる
static func divider(color: Color) -> some View {
Rectangle()
.fill(color)
.frame(height: 1)
.padding(.vertical, 8)
}
}
static関数なら引数も取れます。@ViewBuilder static funcにすれば条件分岐も可能です。
✅ 使う場面: 区切り線やスペーサーなどデータに依存しない固定パーツ
❌ ダメな場面: 親のプロパティを使いたい時(コンパイルエラーになる)
7. 別Structに切り出し ⚡️
ここが一番重要です。 1〜6と根本的に違います。
なぜ違うのか
別StructとしてViewを定義すると、SwiftUIのViewツリー上で独立した単位として扱われます。
そのため、computedプロパティやメソッドで分割した場合よりも、差分検出や更新範囲の最適化が効きやすくなります。
ただし「プロパティが同じなら必ずbodyが呼ばれない」という意味ではありません。SwiftUIではbodyの再評価と実際の画面描画は別で、bodyが再評価されても最終的な画面更新が抑えられることがあります。
用語の整理
| 用語 | 意味 |
|---|---|
| bodyの再評価 | bodyのコードを実行して新しいViewツリーを作ること(CPU処理) |
| 画面描画 | 実際にピクセルを書き換えること(GPU処理) |
SwiftUIはbodyの再評価後に前回との差分を取り、必要な部分だけ画面更新します。別Structにすることで、bodyの再評価自体の範囲を小さくできる可能性があるのがポイントです。
1〜6の場合(computedプロパティ等)
struct ParentView: View {
@State var name: String = "太郎"
@State var age: Int = 25
var body: some View {
nameView // ← ageが変わってもbodyの再評価に含まれる
ageView // ← nameが変わってもbodyの再評価に含まれる
}
private var nameView: some View { Text(name) }
private var ageView: some View { Text("\(age)歳") }
}
nameだけ変わってもageViewがbodyの再評価に含まれます。SwiftUIから見るとbodyにインライン展開されたコードと同じだからです。
別Structの場合
struct ParentView: View {
@State var name: String = "太郎"
@State var age: Int = 25
var body: some View {
NameView(name: name) // ← nameに依存する独立したView
AgeView(age: age) // ← ageに依存する独立したView
}
}
struct NameView: View {
let name: String
var body: some View { Text(name) }
}
struct AgeView: View {
let age: Int
var body: some View { Text("\(age)歳") }
}
nameが変わった時:
-
ParentViewのbodyが再評価される - SwiftUIが新しいViewツリーを作る
- 前回のViewツリーとの差分を見て、必要な更新を判断する
-
AgeViewの入力が変わっていなければ、更新範囲を小さくできる可能性がある
じゃあ全部別Structにすべき?
No。 Textを1つ表示するだけのViewはbodyが再評価されても一瞬で終わります。Structに分けるメリットよりコード量増加のデメリットの方が大きい。
別Structにすべき判断基準:
- bodyが重い(大量のForEach、複雑なレイアウト、画像を含む重めの表示)
- 親の状態変更が頻繁(1秒に何回も更新される等)
- 他のViewからも再利用する
迷った時の選び方フロー
このパーツは他のViewからも使う?
→ Yes → 別Struct(7)
このパーツのbodyは重い?
→ Yes → 別Struct(7)
引数が必要?
→ No → computedプロパティ(1)
分岐があるなら @ViewBuilder computedプロパティ(2)
→ Yes → @ViewBuilderメソッド(4)
ファイルが長すぎる?
→ Yes → extension(5)
データに依存しない固定パーツ?
→ Yes → static(6)
迷ったらこの3つだけ覚えればOK:
- 単純な分割 → computedプロパティ(1)
- 引数が必要 → @ViewBuilderメソッド(4)
- 重い/再利用する → 別Struct(7)
まとめ
| ポイント | 内容 |
|---|---|
| 1〜6の違い | Swiftの言語機能としての違い。親Viewのbody評価の一部として扱われる |
| 7だけ特別 | SwiftUIが独立した単位として扱いやすくなり、更新範囲の最適化が効きやすい |
| 判断基準 | 「このパーツは親の別の状態変更の影響を受けるべきか?」 |
| 実務では | 3パターン(computed / @ViewBuilder / 別Struct)で9割カバー |
見た目は似ていても、別Structへの切り出しだけはSwiftUIのViewツリー上の単位が変わる — これを知っているかどうかで、SwiftUIアプリの設計判断が変わります。