Sponsor Link
環境&対象
- macOS Ventura 13.2.1
- Xcode 14.3 Beta
- iOS 16.0
Picker
Picker は、SwiftUI の View の1で、複数の選択肢から排他的に1つ選ばせることのできる View です。
シンプルな使い方
以下は、Picker のシンプルな使用例です。
struct ContentView: View {
@State private var intValue: Int = 3
var body: some View {
VStack {
Picker(selection: $intValue, content: {
ForEach(0..<10, id: \.self) { num in
Text("\(num)").tag(num)
}
}, label: { Text("Picker") })
}
.padding()
}
}
Picker で選択された 値が、intValue に設定されます。
動作的には、Text に紐づけられた tag 値を使用して selection に値が設定されます。
tag で指定せずとも、ForEach 対象が、Identifiable であったり、Hashable であるとき、自動的に tag が付与されています。
上のコードで言うと、.tag(num) を省略しても、同じ動作をします。
selection の型が Optional の場合
Picker での選択値の保存型が Optional のときがあります。
アプリの仕様的に、選択すること自体が Optional であることもありますし、データ構造の都合で、Optional であることもあります。
このようなケースでは、明示的に tag を指定しないと期待通りに動作しません。
例えば、以下のコードでは、intValue に 3 を初期設定していますが、Picker 表示時に 3 が設定されません。さらに、リスト中の値を選択しても正しく選択されません。
変更点は、selection に与える変数を Int型 から Int? 型にしただけです。
struct ContentView: View {
@State private var intValue: Int? = 3
var body: some View {
VStack {
Picker(selection: $intValue, content: {
ForEach(0..<10, id: \.self) { num in
Text("\(num)")
}
}, label: { Text("Picker") })
}
.padding()
}
}
動作しない理由は、selection に渡される型が Int? であることに対して、Text に付与される型が、Int であるからです。
このような場合は、明示的に Int? の tag を付与することで期待通りの動作になります。
struct ContentView: View {
@State private var intValue: Int? = 3
var body: some View {
VStack {
Picker(selection: $intValue, content: {
ForEach(0..<10, id: \.self) { num in
Text("\(num)").tag(Optional(num))
}
}, label: { Text("Picker") })
}
.padding()
}
}
上記のコードでは、selection の Int? 型に対し、Text の tag も Int? 型で一致していることがわかります。
List
SwiftUI の List は、要素を列挙するだけでなく、その中から選択することができる View です。
シンプルな使い方
以下は、List のシンプルな使用例です。
struct ContentView: View {
@State private var intValue: Int = 3
var body: some View {
VStack {
List(selection: $intValue, content: {
ForEach(0..<10, id: \.self) { num in
Text("\(num)").tag(num)
}
})
}
.padding()
}
}
List から選択された 値が、intValue に設定されます。
動作的には、Picker での動作と同様に Text に紐づけられた tag 値を使用して selection に値が設定されます。
tag で指定せずとも、ForEach 対象が、Identifiable であったり、Hashable であるとき、自動的に tag が付与されます。
上のコードで .tag(num) を省略しても、同じ動作をする点も Picker と同様です。
struct ContentView: View {
@State private var intValue: Int = 3
var body: some View {
VStack {
List(selection: $intValue, content: {
ForEach(0..10, id: \.self) { num in
Text("\(num)").tag(num)
}
})
}
.padding()
}
}
selection の型が Optional の場合
selection の 型が Optional の場合、Picker と同様の状態になります。
Picker/List と tag
先の例でもありましたが、自動的に振られる tag だけでは期待通りに動作しないこともあり、tag 指定したいケースが増えてきます。
# 以下では、Picker を例として使用していますが、List でも同様です。
選択肢と選択値のマッピング
自分で tag 指定したいケースの一つが、表示したい値と 設定したい値が異なる時です。
struct ContentView: View {
@State private var intValue: Int = 3
var body: some View {
VStack {
Picker(selection: $intValue, content: {
ForEach(0..<10, id: \.self) { num in
Text("\(num)").tag(Optional(num))
}
}, label: { Text("Picker") })
Picker(selection: $intValue, content: {
Text("小さな値").tag(1)
Text("中くらい").tag(3)
Text("大きな値").tag(7)
}, label: { Text("Picker") })
}
.padding()
}
}
上のコードでは、”小さな値” という選択肢を選択するとその値としては 1 が設定され、”中くらい” では 3 が、”大きな値” では 7 が設定されます。
このように表示と実際の選択値とをそれぞれ設定することが可能です。
ただし、tag で設定するデータの型と selection の データの型が一致している必要があります。
選択しない状態を持つ Picker
すこし 凝った Picker を作り始めると、選択されていない状態もとりえる Picker が欲しくなります。
表示された選択値を選ぶのではなく、明示的に “選んでいない” という状態です。
このような時には、selection を Optional 型にして、Picker を使えば良さそうです。
未選択の状態は、selection に nil が入るのが、自然に見えます。
先の例での selection を Int? 型にして、未選択状態 向けの選択肢、”未選択” を追加し、tag で nil を設定すればよさそうです
・・・が、うまくいきません。
struct ContentView: View {
@State private var intValue: Int? = 3
var body: some View {
VStack {
Picker(selection: $intValue, content: {
Text("未選択").tag(nil)
ForEach(0..<10, id: \.self) { num in
Text("\(num)").tag(Optional(num))
}
}, label: { Text("Picker") })
}
.padding()
}
}
以下のような コンパイルエラーになります。
Generic parameter 'V' could not be inferred
このエラーは、.tag(nil) で発生しています。
エラーメッセージの意味を確認するために、.tag の定義を見てみます。
func tag<V>(_ tag: V) -> some View where V : Hashable
エラーメッセージは、上記の V が確定できないのでエラー というものです。
tag に nil を設定する方法
型が確定できないのでエラーとなっていたので、型が推測できるようにしてセットします。
なお、上記の例で期待する型は Int? です。(selection と同じ型です)
具体的な値を与えればコンパイラーは型を推測してくれますが、nil では推測してくれません。
言い方を変えると、その部分が確定できるように設定すればよいわけです。
Optional型を拡張して V が確定した nil を作る
Optional の定義を見てみると以下のようになっています。
Optional 型は、Wrapped 型を(文字通り) wrap しています。
@frozen enum Optional<Wrapped>
Apple のドキュメントは、こちら。
そこで、例えば、nil が Int? 型の nil であることを以下のように明示的に宣言します。
extension Optional where Wrapped == Int {
static var nilAsInt: Wrapped? {
nil
}
}
こうすることで、nilAsInt は、Int? 型の nil であることを明示的に宣言することができます。
この nilAsInt を使ったコードに変更すると コンパイルできますし、動作も期待通りです。
struct ContentView: View {
@State private var intValue: Int? = 3
var body: some View {
VStack {
Picker(selection: $intValue, content: {
Text("未選択").tag(Optional.nilAsInt)
ForEach(0..10, id: \.self) { num in
Text("\(num)").tag(Optional(num))
}
}, label: { Text("Picker") })
}
.padding()
}
}
extension Optional where Wrapped == Int {
static var nilAsInt: Wrapped? {
nil
}
}
まとめ
Picker/List での tag の意味合いと tag に nil を設定する方法
- tag の型は、selection と一致していないといけない
- selection が、Optional 型を取るときは、tag にも Optional で設定する
- tag に nil を設定したいときは、型を明示した nil を使う
説明は以上です。
不明な点やおかしな点ありましたら、こちらまで。
SwiftUI おすすめ本
SwiftUI を理解するには、以下の本がおすすめです。
SwiftUI ViewMatery
SwiftUI で開発していくときに、ViewやLayoutのための適切なmodifierを探すのが大変です。
英語での説明になってしまいますが、以下の”SwiftUI Views Mastery Bundle”という本がビジュアル的に確認して探せるので、便利です。
英語ではありますが、1ページに コードと画面が並んでいるので、非常にわかりやすいです。
View に適用できる modifier もわかりやすく説明されているので、ビューの理解だけではなく、どのような装飾ができるかも簡単にわかります。
超便利です
販売元のページは、こちらです。
SwiftUI 徹底入門
# SwiftUI は、毎年大きく改善されていますので、少し古くなってしまいましたが、いまでも 定番本です。
Swift学習におすすめの本
詳解Swift
Swift の学習には、詳解 Swift という書籍が、おすすめです。
著者は、Swift の初期から書籍を出していますし、Swift の前に主力言語だった Objective-C という言語についても同様の書籍を出しています。
Sponsor Link
[…] ・@Publishedを使う Viewの変更を受け取りたい場合、tagを使うのとOptionalで囲う必要がある https://software.small-desk.com/development/2023/03/02/swiftui-howto-pickerandtagwithnil/#google_vig… […]