はじめに
iPhone 標準の時計アプリのタイマーの時間設定のような Picker を使いたかったのですが SwiftUI の Picker ではそのような見た目にはできないようだったので、UIPickerView で実装することにしたのですが、SwiftUI と組み合わせる情報が見つかりませんでした。そんな中でなんとか実装できたので、記録を残しておきます。
ちなみにいまさらながら初めての iPhone ネイティブアプリ開発でした。
課題
実装するにあたり以下が課題となりました。
- SwiftUI と UIPickerView の連携
- ラベルを固定する
- Picker から値を受け渡す
これを踏まえて実装を順に紹介していきます。
実装
実装を順に説明します。
SwiftUI と UIPickerView の連携
まず、ガワだけを示すと次のようなコードになります。
import SwiftUI
struct TimePickerView: UIViewRepresentable {
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
func makeUIView(context: Context) -> UIPickerView {
let picker = UIPickerView()
picker.dataSource = context.coordinator
picker.delegate = context.coordinator
return picker
}
func updateUIView(_ uiView: UIPickerView, context: Context) {
}
class Coordinator: NSObject, UIPickerViewDataSource, UIPickerViewDelegate {
var timerPickerView: TimePickerView
init(_ view: TimePickerView) {
self.timerPickerView = view
}
func numberOfComponents(in pickerView: UIPickerView) -> Int {
<#code#>
}
func pickerView(_ pickerView: UIPickerView, numberOfRowsInComponent component: Int) -> Int {
<#code#>
}
func pickerView(_ pickerView: UIPickerView, titleForRow row: Int, forComponent component: Int) -> String? {
<#code#>
}
}
}
struct TimePickerView_Previews: PreviewProvider {
static var previews: some View {
TimePickerView()
}
}
Picker の値を選択できるようにする
次に Picker の値を初期化して選択できるようにします。ただし、これだと問題があって時、分、秒のラベルも操作できてしまいます。これは次で修正をします。
import SwiftUI
struct TimePickerView: UIViewRepresentable {
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
func makeUIView(context: Context) -> UIPickerView {
let picker = UIPickerView()
picker.dataSource = context.coordinator
picker.delegate = context.coordinator
return picker
}
func updateUIView(_ uiView: UIPickerView, context: Context) {
}
class Coordinator: NSObject, UIPickerViewDataSource, UIPickerViewDelegate {
var timerPickerView: TimePickerView
var dataList = [["0"], [""], ["0"], [""], ["0"], [""]]
init(_ view: TimePickerView) {
self.timerPickerView = view
for i in 1...23 {
dataList[0].append("\(i)")
}
for i in 1...59 {
dataList[2].append("\(i)")
}
for i in 1...59 {
dataList[4].append("\(i)")
}
}
func numberOfComponents(in pickerView: UIPickerView) -> Int {
return dataList.count
}
func pickerView(_ pickerView: UIPickerView, numberOfRowsInComponent component: Int) -> Int {
return dataList[component].count
}
func pickerView(_ pickerView: UIPickerView, titleForRow row: Int, forComponent component: Int) -> String? {
return dataList[component][row]
}
}
}
struct TimePickerView_Previews: PreviewProvider {
static var previews: some View {
TimePickerView()
}
}
ラベルを固定する
ラベルを固定するために UILabel
を生成して UIPickerView
のサブビューとします。(個人的には他にもっと簡単なやり方があるのではないかと疑ってしまうのですが)
import SwiftUI
struct TimePickerView: UIViewRepresentable {
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
func makeUIView(context: Context) -> UIPickerView {
let picker = UIPickerView()
picker.dataSource = context.coordinator
picker.delegate = context.coordinator
let fontSize:CGFloat = 20
let labelWidth:CGFloat = picker.frame.size.width / CGFloat(picker.numberOfComponents)
let y:CGFloat = (picker.frame.size.height / 2) - (fontSize / 2)
for i in [1, 3, 5] {
let label = UILabel()
switch i {
case 1:
label.text = "時"
case 3:
label.text = "分"
default:
label.text = "秒"
}
label.frame = CGRect(x: labelWidth * CGFloat(i), y: y, width: labelWidth, height: fontSize)
label.font = UIFont.systemFont(ofSize: fontSize, weight: .light)
label.backgroundColor = .clear
label.textAlignment = NSTextAlignment.center
picker.addSubview(label)
}
return picker
}
func updateUIView(_ uiView: UIPickerView, context: Context) {
}
class Coordinator: NSObject, UIPickerViewDataSource, UIPickerViewDelegate {
var timerPickerView: TimePickerView
var dataList = [["0"], [""], ["0"], [""], ["0"], [""]]
init(_ view: TimePickerView) {
self.timerPickerView = view
for i in 1...23 {
dataList[0].append("\(i)")
}
for i in 1...59 {
dataList[2].append("\(i)")
}
for i in 1...59 {
dataList[4].append("\(i)")
}
}
func numberOfComponents(in pickerView: UIPickerView) -> Int {
return dataList.count
}
func pickerView(_ pickerView: UIPickerView, numberOfRowsInComponent component: Int) -> Int {
return dataList[component].count
}
func pickerView(_ pickerView: UIPickerView, titleForRow row: Int, forComponent component: Int) -> String? {
return dataList[component][row]
}
}
}
struct TimePickerView_Previews: PreviewProvider {
static var previews: some View {
TimePickerView()
}
}
Picker から値を受け渡す
値を受け渡すために状態変数を用意します。ただし、アニメーションが終わったタイミングで値が確定されるということに注意が必要です。(アニメーションを中断して値を確定する方法をご存知の方がいれば教えてください)
import SwiftUI
struct TimePickerView: UIViewRepresentable {
@Binding var hour: Int
@Binding var minute: Int
@Binding var second: Int
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
func makeUIView(context: Context) -> UIPickerView {
let picker = UIPickerView()
picker.dataSource = context.coordinator
picker.delegate = context.coordinator
let fontSize:CGFloat = 20
let labelWidth:CGFloat = picker.frame.size.width / CGFloat(picker.numberOfComponents)
let y:CGFloat = (picker.frame.size.height / 2) - (fontSize / 2)
for i in [1, 3, 5] {
let label = UILabel()
switch i {
case 1:
label.text = "時"
case 3:
label.text = "分"
default:
label.text = "秒"
}
label.frame = CGRect(x: labelWidth * CGFloat(i), y: y, width: labelWidth, height: fontSize)
label.font = UIFont.systemFont(ofSize: fontSize, weight: .light)
label.backgroundColor = .clear
label.textAlignment = NSTextAlignment.center
picker.addSubview(label)
}
return picker
}
func updateUIView(_ uiView: UIPickerView, context: Context) {
}
class Coordinator: NSObject, UIPickerViewDataSource, UIPickerViewDelegate {
var timerPickerView: TimePickerView
var dataList = [["0"], [""], ["0"], [""], ["0"], [""]]
init(_ view: TimePickerView) {
self.timerPickerView = view
for i in 1...23 {
dataList[0].append("\(i)")
}
for i in 1...59 {
dataList[2].append("\(i)")
}
for i in 1...59 {
dataList[4].append("\(i)")
}
}
func numberOfComponents(in pickerView: UIPickerView) -> Int {
return dataList.count
}
func pickerView(_ pickerView: UIPickerView, numberOfRowsInComponent component: Int) -> Int {
return dataList[component].count
}
func pickerView(_ pickerView: UIPickerView, titleForRow row: Int, forComponent component: Int) -> String? {
return dataList[component][row]
}
func pickerView(_ pickerView: UIPickerView, didSelectRow row: Int, inComponent component: Int) {
switch component {
case 0:
timerPickerView.hour = row
case 2:
timerPickerView.minute = row
case 4:
timerPickerView.second = row
default: break
}
}
}
}
struct TimePickerView_Previews: PreviewProvider {
static var previews: some View {
TimePickerView(hour: .constant(0), minute: .constant(0), second: .constant(0))
}
}
おわりに
試行錯誤した割にはソースコードは大した分量ではなかったですね。同じようなことで困った方のお役に立てば幸いです。
参考
実装にあたり、以下の記事を参考にさせていただきました。