0
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

SwiftUIのbody分割、computedプロパティと別Structは何が違うのか

0
Posted at

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が変わった時:

  1. ParentViewのbodyが再評価される
  2. SwiftUIが新しいViewツリーを作る
  3. 前回のViewツリーとの差分を見て、必要な更新を判断する
  4. 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アプリの設計判断が変わります。

0
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?