[SwiftUI]選択可能なUIコンポーネントの作り方【ラジオボタン偏】
前回 [SwiftUI]選択可能なUIコンポーネントの作り方【複数選択偏】で複数選択ができるUIコンポーネントをSwiftUIで作る方法を解説しました。
今回はHTMLのラジオボタンのように複数あるViewの内一つのみ選択するコンポーネントを作ってみます。
まずは動きをみていきましょう。
赤、緑、青のビューがあって、赤をタップすると黒枠が追加されて選択状態ということがわかります。デバックエリアにも「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
でタップしたらviewModel
のselectedBox
に.red
を代入し、赤いViewが選択されたことを表現します。
.border(Color.black, width: viewModel.selectedBox == .red ? 4 : 0)
でselectedBox
がredだったら枠線を表示するようにしています。
リファクタリング
さて、現状は赤、緑、青それぞれのViewを定義してしまってかなり重複があります。
リファクタリングしていきましょう。
[SwiftUI]選択可能なUIコンポーネントの作り方【複数選択偏】でも解説しましたが、改めてこちらでも説明します。
Rectangle
にカーソルをあわせて⌘+クリックをするとメニューがでてきます。
Extract Subviewをクリックすると選択したViewが切り出されます。
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
プロパティも追加します。
onTapGesture
のself.selectedBox = .red
や、.border
など依存関係でエラーになっている箇所を直します。
最後にSingleSelectableBoxView
にBoxView
を適応させます。
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
で定義していたviewModel
をParentView
へ移動させます。
Text
を追加して選択されているViewを表示します。
SingleSelectableBoxView
のイニシャライザに@Binding
つきのプロパティselectedBox
を追加して、ParentView
のviewModel
のselectedBox
を渡すようにします。
更新された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になります。
画面下に現在選択しているViewがTextで表示されているのが分かるかと思います。
親ビューでハンドリングしたくなった場合の参考にしてください。
コード
今回解説したコードはGistにあげました。コードの全体像をみたい方はチェックしてください。
https://gist.github.com/SatoTakeshiX/c0c594cb31aa96b57daf7a03e533f427
まとめ
HTMLのラジオボタンのように複数のViewから一つを選ぶコンポーネントの実装例を解説しました。
またリファクタリングや親ビューの追加など実際の開発でよくやる手順についても解説しました。
参考になれば嬉しいです。
宣伝
インプレスR&D社より、「1人でアプリを作る人を支えるSwiftUI開発レシピ」発売中です。
「SwiftUIでアプリを作る!」をコンセプトにSwiftUI自体の解説とそれを組み合わせた豊富なサンプルアプリでどんな風にアプリ実装すればいいかが理解できる本となっています。
iOS 14対応、Widgetの作成も一章まるまるハンズオンで解説しています。
SwiftUIを学びたい方、ぜひこちらのリンクをチェックしてください!