[SwiftUI]選択可能なUIコンポーネントの作り方【ラジオボタン偏】

[SwiftUI]選択可能なUIコンポーネントの作り方【ラジオボタン偏】

前回 [SwiftUI]選択可能なUIコンポーネントの作り方【複数選択偏】で複数選択ができるUIコンポーネントをSwiftUIで作る方法を解説しました。
今回はHTMLのラジオボタンのように複数あるViewの内一つのみ選択するコンポーネントを作ってみます。

まずは動きをみていきましょう。
Jul-12-2020-15-24-21

赤、緑、青のビューがあって、赤をタップすると黒枠が追加されて選択状態ということがわかります。デバックエリアにも「selected box is red」と出力されます。
他のビュー、例えば緑をタップすると、赤のビューの黒枠は消えて、緑のビューが黒枠でおおわれるようになるというしだいです。

選択するViewのタイプを定義

ユーザーが何を選択したかを判定できるようにBoxTypeというenumを定義します。

enum BoxType: String {
    case unknown
    case red
    case green
    case blue
}

選択されていない状態を表現するためにunknownもつけてます。

ViewModelの定義

つづいて、選択状態をハンドリングできるようにSingleSelectableBoxViewModelというViewModelを定義しましょう。

final class SingleSelectableBoxViewModel: ObservableObject {
    @Published var selectedBox: BoxType = .unknown
    var cancels: [AnyCancellable] = []

    init() {
        let selected = $selectedBox.sink { (box) in
            print("selected box is \(box.rawValue)")
        }
        cancels.append(selected)
    }
}

selectedBoxというプロパティを定義しています。@PublishedのProperty Wrapperをつけているので、値が更新されると自動的にSwiftUIにも通知されViewが更新されます。

一応解説。

let selected = $selectedBox.sink { (box) in
    print("selected box is \(box.rawValue)")
}
cancels.append(selected)

$selectedBoxのように$をつけると@PublishedのProperty WrapperのプロパティはPublisherとしてみなされます。selectedBoxが更新されるとPublisherのイベントが通知されます。
sinkはPublisherからのイベントを受け取るとクロージャーでその値を扱うことができるsubscriberです。ここではユーザーがViewを選択するとイベントが流れてきます。print文で何が選択されたのかをデバックエリアに出力します。
最後にcancels.append(selected)でsubscriberを保持します。

Viewの定義

SingleSelectableBoxViewというViewを定義していきましょう。

struct SingleSelectableBoxView: View {
    @ObservedObject var viewModel = SingleSelectableBoxViewModel()

    var body: some View {
        ScrollView {
            Rectangle()
                .foregroundColor(Color.red)
                .frame(width: 100, height: 100)
                .cornerRadius(10)
                .onTapGesture {
                    self.viewModel.selectedBox = .red
            }
            .padding()
            .border(Color.black, width: viewModel.selectedBox == .red ? 4 : 0)

            Rectangle()
                .foregroundColor(Color.green)
                .frame(width: 100, height: 100)
                .cornerRadius(10)
                .onTapGesture {
                    self.viewModel.selectedBox = .green
            }
            .padding()
            .border(Color.black, width: viewModel.selectedBox == .green ? 4 : 0)

            Rectangle()
                .foregroundColor(Color.blue)
                .frame(width: 100, height: 100)
                .cornerRadius(10)
                .onTapGesture {
                    self.viewModel.selectedBox = .blue
            }
            .padding()
            .border(Color.black, width: viewModel.selectedBox == .blue ? 4 : 0)
        }
    }
}

@ObservedObject var viewModel = SingleSelectableBoxViewModel()でviewModelを保持しています。

これでSingleSelectableBoxViewModel@Publishedプロパティを変更するとSwiftUIがその変化を受け取れるようになります。

続いて赤いViewの定義です。

Rectangle()
    .foregroundColor(Color.red)
    .frame(width: 100, height: 100)
    .cornerRadius(10)
    .onTapGesture {
        self.viewModel.selectedBox = .red
}
.padding()
.border(Color.black, width: viewModel.selectedBox == .red ? 4 : 0)

.onTapGestureでタップしたらviewModelselectedBox.redを代入し、赤いViewが選択されたことを表現します。

.border(Color.black, width: viewModel.selectedBox == .red ? 4 : 0)

selectedBoxがredだったら枠線を表示するようにしています。

リファクタリング

さて、現状は赤、緑、青それぞれのViewを定義してしまってかなり重複があります。
リファクタリングしていきましょう。

[SwiftUI]選択可能なUIコンポーネントの作り方【複数選択偏】でも解説しましたが、改めてこちらでも説明します。

Rectangleにカーソルをあわせて⌘+クリックをするとメニューがでてきます。

----------2020-07-12-1.12.25

Extract Subviewをクリックすると選択したViewが切り出されます。

----------2020-07-12-16.10.15

SwiftUIではこのようにUIコンポーネントを切り出すことができます。Appleも推奨しているやり方です。Viewが多くなっても処理への影響はわずかなのでどんどん切り出しましょう。

切り出した直後は依存関係エラーがでているので直しましょう。

struct BoxView: View {
    @Binding var selectedBox: BoxType
    let color: Color
    let boxType: BoxType
    var body: some View {
        Rectangle()
            .foregroundColor(color)
            .frame(width: 100, height: 100)
            .cornerRadius(10)
            .onTapGesture {
                self.selectedBox = self.boxType
        }
        .padding()
        .border(Color.black, width: selectedBox == boxType ? 4 : 0)
    }
}

名前をBoxViewにし、@BindingつきのプロパティselectedBoxを追加します。@Bindingの理由は親ビューからselectedBoxを渡してもらうためで、親から子に値データを渡す場合は子ビューのプロパティに@Bindingをつけます。
またcolorプロパティとboxTypeプロパティも追加します。

onTapGestureself.selectedBox = .redや、.borderなど依存関係でエラーになっている箇所を直します。

最後にSingleSelectableBoxViewBoxViewを適応させます。

struct SingleSelectableBoxView: View {
    @ObservedObject var viewModel = SingleSelectableBoxViewModel()

    var body: some View {
        ScrollView {
            BoxView(selectedBox: $viewModel.selectedBox, color: .red, boxType: .red)
            BoxView(selectedBox: $viewModel.selectedBox, color: .green, boxType: .green)
            BoxView(selectedBox: $viewModel.selectedBox, color: .blue, boxType: .blue)
        }
    }
}

だいぶすっきりした実装になりました。

親ビューを作る

最後に、SingleSelectableBoxViewの親ビューを作る場合を解説します。SingleSelectableBoxViewで選択されたViewをもとにいろいろな処理を行いたい場合を想定しています。

ParentViewを定義します。

struct ParentView: View {
    @ObservedObject var viewModel = SingleSelectableBoxViewModel()
    var body: some View {
        VStack {
            SingleSelectableBoxView(selectedBox: $viewModel.selectedBox)
            Text("Selected box is \(viewModel.selectedBox.rawValue)")
        }
    }
}

SingleSelectableBoxViewで定義していたviewModelParentViewへ移動させます。
Textを追加して選択されているViewを表示します。
SingleSelectableBoxViewのイニシャライザに@BindingつきのプロパティselectedBoxを追加して、ParentViewviewModelselectedBoxを渡すようにします。

更新されたSingleSelectableBoxViewの定義はこちら。

struct SingleSelectableBoxView: View {
    @Binding var selectedBox: BoxType
    var body: some View {
        ScrollView {
            BoxView(selectedBox: $selectedBox, color: .red, boxType: .red)
            BoxView(selectedBox: $selectedBox, color: .green, boxType: .green)
            BoxView(selectedBox: $selectedBox, color: .blue, boxType: .blue)
        }
    }
}

このコードを動かすとこんなViewになります。

----------2020-07-12-16.32.02

画面下に現在選択しているViewがTextで表示されているのが分かるかと思います。
親ビューでハンドリングしたくなった場合の参考にしてください。

コード

今回解説したコードはGistにあげました。コードの全体像をみたい方はチェックしてください。

https://gist.github.com/SatoTakeshiX/c0c594cb31aa96b57daf7a03e533f427

まとめ

HTMLのラジオボタンのように複数のViewから一つを選ぶコンポーネントの実装例を解説しました。
またリファクタリングや親ビューの追加など実際の開発でよくやる手順についても解説しました。

参考になれば嬉しいです。

宣伝

インプレスR&D社より、「1人でアプリを作る人を支えるSwiftUI開発レシピ」発売中です。
「SwiftUIでアプリを作る!」をコンセプトにSwiftUI自体の解説とそれを組み合わせた豊富なサンプルアプリでどんな風にアプリ実装すればいいかが理解できる本となっています。
iOS 14対応、Widgetの作成も一章まるまるハンズオンで解説しています。
SwiftUIを学びたい方、ぜひこちらのリンクをチェックしてください!




https://nextpublishing.jp/book/12491.html