5.

Xcode でスライダーを作る|UISlider の値取得とカスタマイズ

編集
この記事の要点
  • UIKit のスライダーは UISliderminimumValue / maximumValue / value の 3 つが基本
  • 値は FloatDoubleInt と混ぜるときは明示的に変換する
  • 操作中ずっと通知が来る。重い処理は指を離したとき(.touchUpInside)だけ実行する
  • 整数で扱いたいなら round() で丸め、value に戻して目盛りに吸着させる
  • SwiftUI なら Slider(value: $x, in: 0...100, step: 1) の 1 行

Storyboard で作る(UIKit)

1. 配置して属性を決める

1. Library(Cmd + Shift + L)から Slider をドラッグして配置する
2. Attributes inspector で値を設定する
     Minimum   0
     Maximum   100
     Current   50
     Continuous Updates  ON(操作中も値が届く)
3. 左右の制約を付けて幅を決める

2. コードとつなぐ

スライダー本体を操作したい   → Connection: Outlet, Name: volumeSlider
値が変わったことを知りたい   → Connection: Action, Name: changeVolume,
                              Type: UISlider, Event: Value Changed
import UIKit

class ViewController: UIViewController {

    @IBOutlet weak var volumeSlider: UISlider!
    @IBOutlet weak var valueLabel: UILabel!

    override func viewDidLoad() {
        super.viewDidLoad()
        volumeSlider.minimumValue = 0
        volumeSlider.maximumValue = 100
        volumeSlider.value = 50
        updateLabel()
    }

    @IBAction func changeVolume(_ sender: UISlider) {
        updateLabel()
    }

    private func updateLabel() {
        valueLabel.text = "\(Int(volumeSlider.value)) %"
    }
}

イベントは Value Changed です。ボタンと同じ Touch Up Inside にすると、指を離したときしか呼ばれません。

コードだけで作る

private let slider: UISlider = {
    let s = UISlider()
    s.minimumValue = 0
    s.maximumValue = 100
    s.value = 50
    s.isContinuous = true          // 操作中も通知する(既定 true)
    s.translatesAutoresizingMaskIntoConstraints = false
    return s
}()

override func viewDidLoad() {
    super.viewDidLoad()
    view.addSubview(slider)

    NSLayoutConstraint.activate([
        slider.leadingAnchor.constraint(
            equalTo: view.safeAreaLayoutGuide.leadingAnchor, constant: 24),
        slider.trailingAnchor.constraint(
            equalTo: view.safeAreaLayoutGuide.trailingAnchor, constant: -24),
        slider.centerYAnchor.constraint(equalTo: view.centerYAnchor),
    ])

    slider.addAction(UIAction { [weak self] action in
        guard let s = action.sender as? UISlider else { return }
        self?.valueLabel.text = "\(Int(s.value)) %"
    }, for: .valueChanged)
}

操作中と操作後を分ける

// 操作中: 表示の更新だけ(毎秒何十回も呼ばれる)
slider.addTarget(self, action: #selector(sliding), for: .valueChanged)

// 指を離したとき: 保存や通信はここで
slider.addTarget(self, action: #selector(finished),
                 for: [.touchUpInside, .touchUpOutside])

@objc private func sliding(_ sender: UISlider) {
    valueLabel.text = "\(Int(sender.value)) %"
}

@objc private func finished(_ sender: UISlider) {
    UserDefaults.standard.set(sender.value, forKey: "volume")
    Task { await api.saveVolume(Int(sender.value)) }
}

valueChanged の中で通信や保存をしてはいけません。指を動かしている間に何十回も実行され、動作が重くなり通信量も増えます。

// 操作が止まってから実行したいとき(デバウンス)
private var task: Task<Void, Never>?

@objc private func sliding(_ sender: UISlider) {
    valueLabel.text = "\(Int(sender.value)) %"

    task?.cancel()
    let value = sender.value
    task = Task {
        try? await Task.sleep(for: .milliseconds(300))
        guard !Task.isCancelled else { return }
        await api.saveVolume(Int(value))
    }
}

整数として扱う・目盛りに吸着させる

@objc private func sliding(_ sender: UISlider) {
    // 1 刻みに丸める
    let step: Float = 1
    let rounded = (sender.value / step).rounded() * step

    if sender.value != rounded {
        sender.value = rounded        // つまみを吸着させる
    }
    valueLabel.text = "\(Int(rounded))"
}

// 5 刻みにしたいなら step = 5
// 0.1 刻みなら step = 0.1 として表示は String(format: "%.1f", rounded)

UISlider には SwiftUI の step にあたる機能がありません。刻みが必要なら、このように自分で丸めます。

見た目を変える

slider.minimumTrackTintColor = .systemBlue   // つまみより左(値の側)
slider.maximumTrackTintColor = .systemGray4  // つまみより右(残りの側)
slider.thumbTintColor = .white               // つまみ

// つまみの画像を差し替える
slider.setThumbImage(UIImage(systemName: "circle.fill"), for: .normal)

// 両端にアイコンを置く
slider.minimumValueImage = UIImage(systemName: "speaker.fill")
slider.maximumValueImage = UIImage(systemName: "speaker.wave.3.fill")

// 縦向きにする(回転させる)
slider.transform = CGAffineTransform(rotationAngle: -.pi / 2)

スライダーは横幅が十分ないと操作しづらくなります。左右に余白を取り、両端のアイコンを付ける場合はさらに幅が必要です。

SwiftUI の場合

struct ContentView: View {
    @State private var volume: Double = 50
    @State private var isEditing = false

    var body: some View {
        VStack(spacing: 16) {
            Text("\(Int(volume)) %")

            Slider(value: $volume, in: 0...100, step: 1) {
                Text("音量")                     // VoiceOver 用のラベル
            } minimumValueLabel: {
                Image(systemName: "speaker.fill")
            } maximumValueLabel: {
                Image(systemName: "speaker.wave.3.fill")
            } onEditingChanged: { editing in
                isEditing = editing
                if !editing {
                    save(Int(volume))            // 指を離したときだけ保存する
                }
            }
            .tint(.blue)
        }
        .padding()
    }
}

SwiftUI では step: を指定するだけで刻みが効き、onEditingChanged で「操作中か終わったか」を受け取れます。UIKit で自作していた処理が引数 1 つで済みます。

他の部品と連動させる

@IBOutlet weak var slider: UISlider!
@IBOutlet weak var textField: UITextField!
@IBOutlet weak var previewView: UIView!

// スライダー → テキストフィールド
@IBAction func sliderChanged(_ sender: UISlider) {
    textField.text = String(Int(sender.value))
    previewView.alpha = CGFloat(sender.value / 100)
}

// テキストフィールド → スライダー
@IBAction func textChanged(_ sender: UITextField) {
    guard let text = sender.text, let v = Float(text) else { return }
    // 範囲外の入力を丸めてから反映する
    let clamped = min(max(v, slider.minimumValue), slider.maximumValue)
    slider.setValue(clamped, animated: true)
    previewView.alpha = CGFloat(clamped / 100)
}

setValue(_:animated:) で変えた場合、valueChanged は呼ばれません。コードから変えたときも表示を更新したいなら、更新処理を関数に切り出して両方から呼びます。

private func apply(_ value: Float) {
    slider.value = value
    textField.text = String(Int(value))
    previewView.alpha = CGFloat(value / 100)
}

@IBAction func sliderChanged(_ sender: UISlider) { apply(sender.value) }
override func viewDidLoad() {
    super.viewDidLoad()
    apply(UserDefaults.standard.float(forKey: "volume"))   // 復元
}

音量調整に使う

import AVFoundation

private var player: AVAudioPlayer?

@IBAction func volumeChanged(_ sender: UISlider) {
    player?.volume = sender.value      // 0.0 〜 1.0
}

AVAudioPlayer.volume0.0〜1.0 です。スライダーの範囲を 0〜100 にしているなら sender.value / 100 に直してください。範囲の食い違いは「最小でも音が大きい」という症状になります。

アクセシビリティ

slider.accessibilityLabel = "音量"
slider.accessibilityValue = "\(Int(slider.value)) パーセント"

// 値が変わるたびに読み上げ内容も更新する
@IBAction func changed(_ sender: UISlider) {
    sender.accessibilityValue = "\(Int(sender.value)) パーセント"
}

VoiceOver はスライダーを上下スワイプで操作します。accessibilityLabel が無いと「調整可能」としか読まれず、何のスライダーか分かりません。

範囲スライダーが欲しいとき

UISliderつまみが 1 つだけです。「最小価格〜最大価格」のように 2 つのつまみが要る場合、標準の部品では実現できません。

  • UISlider を 2 つ並べ、互いの値を超えないように制限する
  • UIControl を継承して自作する
  • SwiftUI なら 2 つの @StateSlider を組み合わせる
  • 用途によっては UIStepperUIPickerView のほうが正確に選べる
// 2 本のスライダーで範囲を作る(下限が上限を超えないようにする)
@IBAction func minChanged(_ sender: UISlider) {
    if sender.value > maxSlider.value {
        sender.value = maxSlider.value
    }
    updateLabel()
}

うまくいかないときの確認

症状原因
動かしても呼ばれないイベントが Value Changed になっていない/接続が切れている
指を離すまで反応しないisContinuous = false になっている
値がいつも 0〜1 の範囲になるminimumValue / maximumValue の設定漏れ(既定は 0〜1)
整数にならないvalueFloatInt() は切り捨てなので rounded() を使う
操作が重い・カクつくvalueChanged の中で保存や通信をしている
つまみが端まで行かない幅の制約が足りない/親ビューからはみ出している

関連

編集
Post Share
子ページ

子ページはありません

同階層のページ
  1. マップの追加方法
  2. テンプレートの種類一覧
  3. ファイルの役割一覧
  4. シミュレーターの画面の拡大/縮小をする方法
  5. スライダーの作成とカスタマイズ
  6. ボタンの作成とプログラムと連携
  7. ラベルの作成とプログラムと連携
  8. 【Xcode/Swift】ImageViewのContentMode一覧
  9. エラー一覧
  10. アプリを実機で起動させる方法
  11. ツールバーの設置とボタンの追加
  12. 画像の追加

最近更新/作成されたページ