[SwiftUI]選択可能なUIコンポーネントの作り方【複数選択偏】
人はときにタップをしたらそのビューを選択状態にするコンポーネントが必要になることがあります。
ちょっと手こずったのでブログに作り方を残します。
今回は複数選択する場合を解説したいと思います。
実行環境
- Xcode 11.5
- iOS 13.5
複数選択可能なUIコンポーネント
HTMLだとチェックボックスがいい例ですが、複数選択できるコンポーネントがあると素敵ですよね。
作ってみましょう。
赤、緑、青のViewがあって、タップするとViewに黒枠が追加されて選択状態がわかるようになります。
またコンソールにもタップされたら「red is selected」 。もう一度タップすると「red is not selected」というようなメッセージができるようにしています。
ViewModelの定義
まずはデータまわりを定義していきましょう。
MultipleSelectableViewModel
というクラスを定義します。
final class MultipleSelectableViewModel: ObservableObject {
@Published var isSelectedRed: Bool = false
@Published var isSelectedGreen: Bool = false
@Published var isSelectedBlue: Bool = false
var cancels: [Cancellable] = []
init() {
let redSubscriber = $isSelectedRed.sink { (selected) in
print("red is\(selected ? "" : " not") selected")
}
cancels.append(redSubscriber)
let greenSubscriber = $isSelectedGreen.sink { (selected) in
print("green is\(selected ? "" : " not") selected")
}
cancels.append(greenSubscriber)
let blueSubscriber = $isSelectedBlue.sink { (selected) in
print("blue is\(selected ? "" : " not") selected")
}
cancels.append(blueSubscriber)
}
}
単純に赤、緑、青それぞれのisSelected
フラグを定義して、イニシャライザでそれぞれのフラグが変更されたらprint文を出しているだけです。
一応解説。
let redSubscriber = $isSelectedRed.sink { (selected) in
print("red is\(selected ? "" : " not") selected")
}
cancels.append(redSubscriber)
$isSelectedRed
と$をつけると@Published
のProperty WrapperのプロパティはPublisherとしてみなされます。isSelectedRed
が更新されるとPublisherのイベントが通知されます。
sink
はPublisherからのイベントを受け取るとクロージャーでその値を扱うことができるsubscriberです。ここではフラグがtrueかfalseによって文字を出し分けています。
最後にcancels.append(redSubscriber)
でsubscriberを保持します。
Viewの定義
struct MultipleSelectableView: View {
@ObservedObject var viewModel = MultipleSelectableViewModel()
var body: some View {
ScrollView {
Rectangle()
.foregroundColor(Color.red)
.frame(width: 100, height: 100)
.cornerRadius(10)
.onTapGesture {
self.viewModel.isSelectedRed.toggle()
}
.padding()
.border(Color.black, width: self.viewModel.isSelectedRed ? 4 : 0)
Rectangle()
.foregroundColor(Color.green)
.frame(width: 100, height: 100)
.cornerRadius(10)
.onTapGesture {
self.viewModel.isSelectedGreen.toggle()
}
.padding()
.border(Color.black, width: self.viewModel.isSelectedGreen ? 4 : 0)
Rectangle()
.foregroundColor(Color.blue)
.frame(width: 100, height: 100)
.cornerRadius(10)
.onTapGesture {
self.viewModel.isSelectedBlue.toggle()
}
.padding()
.border(Color.black, width: self.viewModel.isSelectedBlue ? 4 : 0)
}
}
}
ちょっと長いですがやっていることはViewModelを作って、赤、青、緑のそれぞれのViewに適切なフラグをバインドさせているだけです。
@ObservedObject var viewModel = MultipleSelectableViewModel()
@ObservedObject
のProperty WrapperをつけてviewModel
を定義します。これでMultipleSelectableViewModel
の@Published
プロパティを変更するとSwiftUIがその変化を受け取れるようになります。
Rectangle()
.foregroundColor(Color.red)
.frame(width: 100, height: 100)
.cornerRadius(10)
.onTapGesture {
self.viewModel.isSelectedRed.toggle()
}
.padding()
.border(Color.black, width: self.$viewModel.isSelectedRed.wrappedValue ? 4 : 0)
赤い矩形Viewの定義です。青と緑も同じような実装をしています。
Rectangle()
で矩形を作り.foregroundColor(Color.red)
で色を設定しています。
.onTapGesture {
self.viewModel.isSelectedRed.toggle()
}
タップしたらviewModelのisSelectedRed
フラグを切り替えるようにしています。
選択されたら黒い枠線がでてくるのはこのコードです。
.border(Color.black, width: self.viewModel.isSelectedRed ? 4 : 0)
isSelectedRed
フラグは読み取りする場合は$
をつけなくて大丈夫です。
三項演算子でtrueならborder:4をつけています。
リファクタリング
これでOKなのですが、色を変えるだけなのに3つの矩形をそれぞれ定義するのはよくない実装ですね。重複がけっこうあります。
リファクタリングしましょう。
Rectangle
にカーソルをあわせて⌘+クリックをするとメニューがでてきます。
Extract Subviewをクリックすると選択したViewが切り出されます。
SwiftUIではこのようにUIコンポーネントを切り出すことができます。Appleも推奨しているやり方です。Viewが多くなっても処理への影響はわずかなのでどんどん切り出しましょう。
切り出した直後は依存関係エラーがでているので直しましょう。
struct SelectBox: View {
@Binding var isSelected: Bool
let color: Color
var body: some View {
Rectangle()
.foregroundColor(color)
.frame(width: 100, height: 100)
.cornerRadius(10)
.onTapGesture {
self.isSelected.toggle()
}
.padding()
.border(Color.black, width: self.isSelected ? 4 : 0)
}
}
名前をSelectBox
にし、@Binding
のisSelected
フラグとcolor
プロパティを定義します。
それらをViewの定義で依存している箇所に当てはめていきます。
最後に親ビューになったMultipleSelectableView
にSelectBox
を適応していきます。
struct MultipleSelectableView: View {
@ObservedObject var viewModel = MultipleSelectableViewModel()
var body: some View {
ScrollView {
SelectBox(isSelected: $viewModel.isSelectedRed, color: .red)
SelectBox(isSelected: $viewModel.isSelectedGreen, color: .green)
SelectBox(isSelected: $viewModel.isSelectedBlue, color: .blue)
}
}
}
だいぶすっきりしてきましたね。
まとめ
複数選択する場合のUIコンポーネントの実装例を解説しました。
参考になれば幸いです。
Code
今日紹介したコードはGistに上げました。
コード全体を確認したい場合はこちらを御覧ください。
https://gist.github.com/SatoTakeshiX/037f1a4b170aed651e19bcb4795bd031
宣伝
インプレスR&D社より、「1人でアプリを作る人を支えるSwiftUI開発レシピ」発売中です。
「SwiftUIでアプリを作る!」をコンセプトにSwiftUI自体の解説とそれを組み合わせた豊富なサンプルアプリでどんな風にアプリ実装すればいいかが理解できる本となっています。
iOS 14対応、Widgetの作成も一章まるまるハンズオンで解説しています。
ぜひどうぞ。