[Swift] UISliderをカスタマイズしてみる

iOSの標準コントロールに、UISliderというのがありますが、現在の値を表示したいと思ったことはないでしょうか。
プロパティを探してもそれらしいものが見つけられなかったので、UISliderを継承してオブジェクトを作ってみました。

UISlider


今回作成するオブジェクト



環境

MacOSX 10.14
Xcode 10.1
Swift 4
Target iOS 9.0


実現する内容

  • スライダーの値を表示する
  • スライダーの上に見出し(タイトル)を表示する
  • 増加量(ステップ量)を指定できるようにする


実装

SUISlider

UISliderを継承して、SUISliderを作成します。
ソースの全体像は下記の通りです。

import UIKit

public class SUISlider : UISlider {
    var labelTitle: UILabel
    var valueWidth: CGFloat = 60
    var stepValue : Float   = 0

    private var labelValue: UILabel
    private let SPACING: CGFloat = 8
    
    public override init(frame: CGRect) {

        labelTitle        = UILabel()
        labelValue        = UILabel()

        labelValue.textAlignment        = NSTextAlignment.right

        super.init(frame: frame)

        self.addSubview(labelTitle)
        self.addSubview(labelValue)

        self.addTarget(self,
                       action: #selector(self.onValueChanged(sender: )),
                       for: .valueChanged)
    }
    
    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    func setup() {
        let pos             = CGPoint(x: self.frame.width - valueWidth - SPACING,
                                      y: (self.layer.bounds.origin.y) )
        labelValue.frame    = CGRect(x: pos.x , y: pos.y ,
                                     width: valueWidth,
                                     height: self.layer.frame.height  )

        let sz  = labelTitle.sizeThatFits(self.frame.size)
        labelTitle.frame    = CGRect(x: SPACING, y: SPACING,
                                     width: sz.width,
                                     height: sz.height)
    }
    
    override public func sizeThatFits(_ size: CGSize) -> CGSize {
        var szLayer = super.sizeThatFits(size)
        let szTitle = labelTitle.sizeThatFits(size)
        if szTitle.height > 0 {
            szLayer.height = szLayer.height + SPACING * 2
        }
        szLayer.height = szLayer.height + szTitle.height
        return szLayer
    }
    
    public override func layoutSubviews() {
        updateLabel()
        setup()
        super.layoutSubviews()
    }
    
    override public func trackRect(forBounds bounds: CGRect) -> CGRect {
        let sz    = labelTitle.sizeThatFits(self.frame.size)
        var frame = super.trackRect(forBounds: bounds)

        frame.size.width  = frame.width - valueWidth
        frame.size.height = 5
        
        if sz.height > 0 {
            frame.origin.y = sz.height + SPACING + (super.bounds.height - frame.size.height - (SPACING * 2 + sz.height))/2
        } else {
            frame.origin.y = super.bounds.midY
        }
        return frame
    }

    func updateLabel(){
        if stepValue > 0 {
            let intg : Int = Int(self.value / stepValue)
            self.value = Float(intg) * stepValue
        }
        labelValue.text  = String(format: "%0.2f", self.value )


    }
    
    @objc func onValueChanged(sender: SUISlider) {
        updateLabel()
    }
    
}

解説

レイアウト



コード


    override public func sizeThatFits(_ size: CGSize) -> CGSize {
        var szLayer = super.sizeThatFits(size)
        let szTitle = labelTitle.sizeThatFits(size)
        if szTitle.height > 0 {
            szLayer.height = szLayer.height + SPACING * 2
        }
        szLayer.height = szLayer.height + szTitle.height
        return szLayer
    }


sizeThatFitsをオーバーライドして、計算後の描画サイズを返します。
ここでは、見出しラベルに値がセットされている場合は、ラベルの高さ+上下余白(8 x 2)を高さとして返します。
ラベルがない場合は、加算値が0になるため、変更なしとなります。


    override public func trackRect(forBounds bounds: CGRect) -> CGRect {
        let sz    = labelTitle.sizeThatFits(self.frame.size)
        var frame = super.trackRect(forBounds: bounds)

        frame.size.width  = frame.width - valueWidth
        frame.size.height = 5
        
        if sz.height > 0 {
            frame.origin.y = sz.height + SPACING + (super.bounds.height - frame.size.height - (SPACING * 2 + sz.height))/2
        } else {
            frame.origin.y = super.bounds.midY
        }
        return frame
    }


trackRectをオーバーライドして、バーの表示エリアのサイズを返します。
ここでは、見出しラベル表示分だけ下にずらしてます。(見出しラベルがなければ何もせずそのまま)
表示幅は、右端の値表示エリア分だけ少なくしてます。(初期値は60)


    func setup() {
        let pos             = CGPoint(x: self.frame.width - valueWidth - SPACING,
                                      y: (self.layer.bounds.origin.y) )
        labelValue.frame    = CGRect(x: pos.x , y: pos.y ,
                                     width: valueWidth,
                                     height: self.layer.frame.height  )

        let sz  = labelTitle.sizeThatFits(self.frame.size)
        labelTitle.frame    = CGRect(x: SPACING, y: SPACING,
                                     width: sz.width,
                                     height: sz.height)
    }

setup内でスライド値の表示ラベル、見出しラベルを配置してます。


    func updateLabel(){
        if stepValue > 0 {
            let intg : Int = Int(self.value / stepValue)
            self.value = Float(intg) * stepValue
        }
        labelValue.text  = String(format: "%0.2f", self.value )


    }

updateLabelでスライダ値をラベルテキストにセットしてます。 スライダ値の表示を少数以下2桁表示に固定するため、フォーマットを設定してます。 また、ステップ値を設定している場合は、スライド値をステップ値単位に丸める計算をしています。
updateLabelは、layoutSubviews(オーバーライド)とvalueChangeのイベントから呼び出してます。


使い方


    let sld1 = SUISlider()
    sld1.minimumValue = 0.0
    sld1.maximumValue = 1.0
    sld1.value        = 0.5
    sld1.stepValue    = 0.1
    sld1.backgroundColor = UIColor.lightText
    sld1.labelTitle.text = "Test"

    let sz1 = sld1.sizeThatFits((self.view?.bounds.size)!)
    sld1.frame = CGRect(x:0,y:100,
                       width:self.view.bounds.width,
                       height:sz1.height)

    self.view.addSubview(sld1)

使いたい画面で上記のような実装をします。

見出しラベルと値表示をセットにしているので、色々と使いやすいと思います。




コメント

このブログの人気の投稿

[Swift] StoryBoardを使用しない - UITextFieldで編集不可にする方法

[Music] DTM初心者のためのドラム打ち込み その2