UISliderって・・・?
UISliderはその名の通りスライダーコントロール機能を提供するUIKit標準のUIパーツで、
バー(track)とつまみ(thumb)で構成された値を調節する際などに便利なUIです
他のUIViewやUIButtonなどに比べると登場頻度は少ないかもしれませんが、
UISliderの特性を知っておいて損はないと思います
本稿では標準のUISliderをサンプルコードを交えながらカスタムし、
UXや使い心地を向上してみたいと思います
UISliderはthumbのドラッグのみで調節可能
UISliderを用いた値の調節は白い丸のつまみ部分(thumbと呼びます)をドラッグして、
左右に動かすことで直感的な調節操作を実現しています。
UISliderはUIControlクラスを継承しており、
addTarget()することで様々なコントロールイベント(UIControlEvents)をフックすることが可能です。
UISliderで最も使用する頻度が高いUIControlEventsは、.valueChangedでしょう。
下記は、UISliderのvalueの変化をフックするサンプルコードです。
実行してつまみをドラッグすると値が断続的に取れているかと思います。
class ViewController: UIViewController {
@IBOutlet weak var slider: UISlider! // InterfaceBuilderで定義
override func viewDidLoad() {
super.viewDidLoad()
slider.addTarget(self, action: #selector(sliderDidChangeValue(_:)), for: .valueChanged)
}
@objc func sliderDidChangeValue(_ sender: UISlider) { // @IBActionでも可
print(sender.value) // 0.0
}
}
UISliderの個人的にイケてないと感じるポイント
上で説明したように、デフォルトのUISliderは、
thumbをドラッグすることで値を調節することができます。
裏を返せば、thumbをドラッグしなければ値を調節することができません。
thumbをつまもうとしてドラッグしたら、空振りして失敗してしまう
皆さんも一度はこのような経験をされているのではないでしょうか?
何個かの改善方法とともにUISliderのユーザビリティを磨きこんでいきます
改善① UISliderのthumb以外をタップやドラッグしても調節可能にする
デフォルトでは、UISliderのタップ領域 = thumbのタップ領域です。
では、どのようなロジックでスライダーの調節開始を判定しているのでしょうか。
UISliderの親クラスであるUIControlのDocumentを見ると、
beginTracking(_:with:)
というメソッドが用意されています。
func beginTracking(_ touch: UITouch, with event: UIEvent?) -> Bool
このメソッドによって、スライダーの調節を開始するべきか判定しています。
おおよそ、デフォルトの実装はtouchされた座標がthumbの座標内にあるかというのを見て、
調節を開始させるかどうか判定しているのだと思われます。
API提供されてる範囲で実装してみるとこんな感じでしょうか。
func beginTracking(_ touch: UITouch, with event: UIEvent?) -> Bool {
// thumbのrectを算出する
let thumbRect = self.thumbRect(
forBounds: bounds,
trackRect: trackRect(forBounds: bounds), // バー(track)のrect
value: value
)
// tapした座標
let tapPoint = touch.location(in: self)
// tapした座標がthumbの矩形内に含まれていれば調節開始する
return thumbRect.contains(tapPoint)
}
では、このメソッドを無条件にtrueを返却するようにoverrideするとどうなるでしょうか。
どんなtouchでもtrackingを開始するようになるため、
つまみ部分以外の領域を選択しても調節が可能となります。
class TappableSlider: UISlider {
override func beginTracking(_ touch: UITouch, with event: UIEvent?) -> Bool {
return true // どんなtouchでもスライダー調節を行う
}
}
実際に動かしてみるとこんな感じです!
つまみがない部分をタップしたりドラッグしたりできるようになっているのがわかるかと思います。
改善② UISliderのタップ領域を広げる
①で説明したthumb以外をタップしても調節可能にする方法は、
UISliderの領域内であればどこでも選択可能となりました。
しかし、UISliderのbounds外をタップしても反応してくれません。
たとえば、こんな感じの目盛り付きのスライダーUIがあるとします。
黄色い部分はUISliderのbounds領域です。
デフォルトではつまみの選択のみ、スライダーを動かすことができました。
改善①によって、黄色い部分のどこを選択してもスライダーが動くようになりました。
しかし、1-5の目盛りラベルを選択してもスライダーは動きません。
これを反応できるようにタップ領域を拡大してみましょう。
UISliderでタップ領域を決定しているメソッドは、UIControlのpoint(inside:with:)
です。
つまり、このメソッドでtrueが返却された後にbeginTracking(_:with:)が呼ばれます。
func point(inside point: CGPoint, with event: UIEvent?) -> Bool
こちらのメソッドもDocumentを見る限り、
おおよそ、デフォルトの実装は、pointに入ってくる値が自身のbounds内にあるかどうかでしょう。
つまり、こんな感じだと想定されます。
func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
return bounds.contains(point) // pointがbounds内にあるかどうか
}
ラベル部分は自身のbounds外となるため、
そこを選択してもbeginTrackingが呼ばれませんでした。
では、ラベル部分も選択可能にするために、
UISliderのboundsより20px下まで判定を可能にしてみましょう。
class WideTappableSlider: TappableSlider {
override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
var wideBounds = bounds
wideBounds.size.height += 20.0 // boundsを20.0px分下に拡張
return wideBounds.contains(point) // pointがwideBounds内にあるかどうか
}
}
これでどうでしょうか。
目盛り部分をタップするとその位置にスライダーを動かすことができるようになりました
最後に
最後までお読みいただきありがとうございます
いかがだったでしょうか?
UISliderのUX改善について2つの方法をまとめてみました。
どちらの方法も親クラスのUIControlのメソッドをoverrideしているので、
UISlider以外のUIKitにも応用が可能だったりします。
他にもUISliderのUX改善法を知っている方がいましたら、
コメントしていただけると幸いです!
こうした細かいチューニングでアプリのUXをどんどん磨いていきましょう。
それでは