[SwiftUI] 条件で切り替えるビューに同じ ViewModifier を適用する方法

SwiftUI2021

if 等の制御によって切り替わる View に同じ View Modifier をつける方法を説明します。

環境&対象

以下の環境で動作確認を行なっています。

  • macOS Monterey 12.1 beta3
  • Xcode 13.2 beta2
  • iOS 15

同じ View Modifier を適用したい

SwiftUI でアプリの UI を作成していると、条件によって切り替わるビューに 同じ View Modifier を適用したいことがあります。

例えば、以下では モードによって、Text と TextField が切り替わるのですが、どちらの場合でもサイズを一定にするために、.frame を指定しています。

ViewMode

EditMode


//
//  ContentView.swift
//
//  Created by : Tomoaki Yagishita on 2021/12/06
//  © 2021  SmallDeskSoftware
//

import SwiftUI

struct ContentView: View {
    enum ViewMode {
        case view
        case edit
    }
    
    @State private var viewMode = ViewMode.view
    @State private var text: String = "Hello, world!"
    
    var body: some View {
        VStack {
            if viewMode == .view {
                Text(text)
                    .frame(width: 150, height: 80)   // (1)
            } else {
                TextField("text", text: $text)
                    .textFieldStyle(.roundedBorder)
                    .frame(width: 150, height: 80)   // (2)
            }
            Button(action: {
                self.viewMode = viewMode == .view ? .edit : .view
            }, label: {
                VStack {
                    Text("Toggle ViewMode")
                }
            })
                .padding()
            Text("Current: \(viewMode == .view ? "View" : "Edit") mode")
        }

    }
}
コード解説
  1. Text の表示サイズを 150x80 にするために指定
  2. TextField の表示サイズを 150x80 にするために指定

同じサイズにするために指定しているので、(1), (2) で同じ値を指定していなければいけません。

レイアウト調整等で数値を変更することも考えられるので、できれば 1ヶ所で指定できる方が嬉しいです。

どうやって1ヶ所にまとめるか考えてみます。

案1:if に対して View Modifier を適用する

if に対して、.frame が指定できると、以下のように記述できるようになり、1ヶ所で記述できることになります。


if viewMode == .view {
  Text(text)
} else {
  TextField("text", text: $text)
    .textFieldStyle(.roundedBorder)
}
.frame(width: 150, height: 80)

ですが、残念ながら、以下のようなエラーとなります。


cannot infer contextual base in reference to member 'frame'

if の評価結果が View になるわけではないので、エラーとなるのは理解できます。

案2:変数で同じ値にする

数値を直接記述してしまっているために、一方だけ変更して 他方を変更し忘れるということが発生し得るので、変数にしてしまうのも1つです。


//
//  ContentView.swift
//
//  Created by : Tomoaki Yagishita on 2021/12/06
//  © 2021  SmallDeskSoftware
//

import SwiftUI

struct ContentView: View {
    enum ViewMode {
        case view
        case edit
    }
    
    @State private var viewMode = ViewMode.view
    @State private var text: String = "Hello, world!"
    // (1)
    let width: CGFloat = 150
    let height: CGFloat = 80
    
    var body: some View {
        VStack {
            if viewMode == .view {
                Text(text)
                    // (2)
                    .frame(width: width, height: height)
            } else {
                TextField("text", text: $text)
                    .textFieldStyle(.roundedBorder)
                    // (3)
                    .frame(width: width, height: height)
            }
            Button(action: {
                self.viewMode = viewMode == .view ? .edit : .view
            }, label: {
                VStack {
                    Text("Toggle ViewMode")
                }
            })
                .padding()
            Text("Current: \(viewMode == .view ? "View" : "Edit") mode")
        }

    }
}
コード解説
  1. .frame で使用する値を変数として定義しておきます。(実行中に変更する必要がなければ、@State の必要はありません)
  2. 事前に定義された変数を使用して、Text に .frame を指定します。
  3. 事前に定義された変数を使用して、TextField に .frame を指定します。

この方法の問題点は、動的に変更するものでもないのに、変数を定義していることで、よりコードが複雑に見えてしまうことです。

案3:ViewModifier を適用するビューを 内部関数から取得する

SwiftUI では、複雑になってきた ビューの一部を 外部ビューに移動させて、管理しやすくしたり 再利用しやすくしたりします。

同様の方法で 同じ View Modifier を適用することができます。ただし、不必要に 外部ビューにしてしまうと 逆に管理しづらくなってしまうこともあるため、struct 内のメソッドが ビューを返すようにします。


//
//  ContentView.swift
//
//  Created by : Tomoaki Yagishita on 2021/12/06
//  © 2021  SmallDeskSoftware
//

import SwiftUI

struct ContentView: View {
    enum ViewMode {
        case view
        case edit
    }
    
    @State private var viewMode = ViewMode.view
    @State private var text: String = "Hello, world!"
    
    var body: some View {
        VStack {
            // (1)
            textView()
                .frame(width: 150, height: 80)
            Button(action: {
                self.viewMode = viewMode == .view ? .edit : .view
            }, label: {
                VStack {
                    Text("Toggle ViewMode")
                }
            })
                .padding()
            Text("Current: \(viewMode == .view ? "View" : "Edit") mode")
        }
    }
    
    // (2)
    @ViewBuilder
    func textView() -> some View {
        if viewMode == .view {
            Text(text)
        } else {
            TextField("text", text: $text)
                .textFieldStyle(.roundedBorder)
        }
    }
    
}
コード解説
  1. 内部関数 textView が View を返してくるので、.frame を指定します
  2. textView は、条件に応じた View を返します(このとき @ViewBuilder 指定するのがポイントです)

こうすることで、サイズ指定の .frame を1ヶ所に記述することができるようになります。

まとめ:条件で切り替えるビューに同じ ViewModifier を適用する方法

ここまでに見てきた方法で、条件文で切り替えられる View に対して、1ヶ所で View Modifier を指定することができるようになります。

なお、特定の View にのみ適用できる View Modifier を扱う場合は、その View Modifier を適用できる View に制限することが必要となります。

条件で切り替えるビューに同じ ViewModifier を適用する方法
  • @ViewBuilder 指定で、View を返すメソッドを作り、その中で条件により適切な View を返す
  • メソッドから返る View にたいして、View Modifier を適用することで、1ヶ所で ViewModifier を記述することができるようになる

説明は以上です。
不明な点やおかしな点ありましたら、こちらまで。

SwiftUI 学習におすすめの本

SwiftUI 徹底入門

SwiftUI は、グラフィカルなライブラリということもあり、文字だけのテキストよりは、画像が多く入れられた書籍を読むと理解が進みやすいです。

自分で購入した中でおすすめできるものとしては、以下のものです。

2019 年発表の SwiftUI 1.0 相当を対象にしているので、2020/2021 に追加された一部の機能は、説明されていません。

ですが、SwiftUI 入門書としては、非常によくできていますし、わかりやすいです。 この本で学習した後に、追加分を学習しても良いと思います。

SwiftUIViewsMastery

英語での説明になってしまいますが、以下の本もおすすめです。

1ページに、コードと画面が並んでいるので、非常にわかりやすいです。

View に適用できる modifier もわかりやすく説明されているので、ビューの理解だけではなく、どのような装飾ができるかも簡単にわかります。

超便利です

SwiftUIViewsMastery

販売元のページは、こちらです。

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です