Open4
SwiftUIで画面下部(キーボードの位置)にWheel Pickerを表示する
実装
//
// ContentView.swift
// testWheelPickerView
//
// Created by 松本 幸太郎 on 2024/09/15.
//
import SwiftUI
struct WheelPickerField<Item: Hashable>: UIViewRepresentable {
@Binding var selected: Item
let items: [Item]
let title: (Item) -> String
var placeholder: String
func makeUIView(context: Context) -> UITextField {
let textField = UITextField()
textField.textAlignment = .left
textField.setContentHuggingPriority(.defaultHigh, for: .vertical)
textField.placeholder = placeholder
let picker = UIPickerView()
picker.delegate = context.coordinator
picker.dataSource = context.coordinator
textField.inputView = picker
return textField
}
func updateUIView(_ uiView: UITextField, context: Context) {
uiView.text = title(selected)
context.coordinator.items = items
if let picker = uiView.inputView as? UIPickerView {
picker.reloadAllComponents() // Pickerをリロード
if let selectedIndex = items.firstIndex(of: selected) {
picker.selectRow(selectedIndex, inComponent: 0, animated: false)
}
}
}
func makeCoordinator() -> Coordinator {
Coordinator(selected: $selected, items: items, title: title)
}
class Coordinator: NSObject, UIPickerViewDelegate, UIPickerViewDataSource {
@Binding var selected: Item
var items: [Item]
let title: (Item) -> String
init(selected: Binding<Item>, items: [Item], title: @escaping (Item) -> String) {
self._selected = selected
self.items = items
self.title = title
}
func numberOfComponents(in pickerView: UIPickerView) -> Int {
1
}
func pickerView(_ pickerView: UIPickerView, numberOfRowsInComponent component: Int) -> Int {
items.count
}
func pickerView(_ pickerView: UIPickerView, titleForRow row: Int, forComponent component: Int) -> String? {
title(items[row])
}
func pickerView(_ pickerView: UIPickerView, didSelectRow row: Int, inComponent component: Int) {
selected = items[row]
}
}
}
使用例
struct Item: Hashable {
var name: String
}
struct ContentView: View {
@State private var selection: Item = .init(name: "BenchPress")
@State private var itemList: [Item] = [
.init(name: "BenchPress"),
.init(name: "Squat"),
.init(name: "DeadLift"),
.init(name: "Chinning"),
.init(name: "SeatedRow"),
.init(name: "Abdominal"),
.init(name: "ArmCurl"),
]
@State private var shouldShowAddItemDialog = false
@State private var newItem: Item = .init(name: "")
var body: some View {
VStack {
WheelPickerField(
selected: $selection,
items: itemList,
title: \.name,
placeholder: "Item"
)
.padding(4)
.background(.tertiary)
Button("Add Item", systemImage: "plus") {
shouldShowAddItemDialog = true
}
}
.alert("Add Item", isPresented: $shouldShowAddItemDialog) {
TextField("new item", text: $newItem.name)
Button("Add") {
addToList()
}
.disabled(newItem.name.isEmpty)
// パターンA 🙆
// 単にもう1つのボタンがある時,`追加ボタン`は正常に動作する
// Button("Cancel") {
// // ...
// }
// パターンB 🙅
// `role: .cancel`のボタンがある時,`追加ボタン`を押下するとpickerの選択肢として反映される場合とされない場合がある. 要調査
// Button("Cancel", role: .cancel) {
// // ...
// }
}
}
private func addToList() {
if !newItem.name.isEmpty {
itemList.append(.init(name: newItem.name))
newItem.name = ""
}
}
}
#Preview {
ContentView()
}
動作検証
Add Buttonが正常に動作する場合
Add Buttonが正常に動作しない場合(Cancel Buttonを追加した場合)
👆のバグはどうやらWheelPickerは関係ない.
iOS 17の時に発生するalert付近の問題らしい.
(iOS 18 simulatorでは問題を再現できなかった.)
また会社の先輩から,別のボタンを追加せずとも「追加ボタン」のroleを.destractiveにするだけで,iOS17なら問題を再現できるとご指摘いただいた.
データ競合の問題かと考え,Xcode16.0 RC版でstrict concurrency: completeでビルドしてもコンパイルエラーは起きなかった.
現状👇のコードが問題の最小構成.
import SwiftUI
struct ContentView: View {
@State private var itemList: [String] = [
"BenchPress",
"Squat",
"DeadLift",
"Chinning",
"SeatedRow",
"Abdominal",
"ArmCurl",
]
@State private var shouldShowAddItemDialog = false
@State private var newItem = ""
var body: some View {
VStack {
ForEach(itemList, id: \.self) { item in
Text(item)
}
Button("Add Item", systemImage: "plus") {
shouldShowAddItemDialog = true
}
}
.alert("Add Item", isPresented: $shouldShowAddItemDialog) {
TextField("new item", text: $newItem)
Button("Add", role: .destructive) {
if !newItem.isEmpty {
itemList.append(newItem)
newItem = ""
}
}
.disabled(newItem.isEmpty)
}
}
}
#Preview {
ContentView()
}