You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何制作类似iOS音量滑块的可滑动调节自定义UI组件?

自定义滑动数值组件实现方案

需求概述

需要实现一款支持滑动操作调整数值的UI组件,样式特点为:带有分段刻度标记、圆角轨道、自定义滑块样式(或分段式选择视觉)。

现有问题

尝试使用系统UISlider实现,但无法配置出符合需求的样式,现有代码如下:

lazy var timeSlider : UISlider = {
        let slider = UISlider()
        slider.minimumValue = 1
        slider.maximumValue = 10
        slider.value = 1
        slider.layer.cornerRadius = 10
        slider.tintColor = UIColor(hexColor: "#F9F5EF")
        slider.addTarget(self, action: #selector(sliderValueChanged), for: .valueChanged)
        addSectionMarkers(for: slider)
        return slider
    }()

解决方案

方案一:自定义UISlider外观

通过重写UISlider的轨道和滑块布局方法,搭配自定义刻度标记,实现所需样式:

  1. 创建自定义Slider类
class CustomSegmentSlider: UISlider {
    // 自定义轨道高度
    private let trackHeight: CGFloat = 8
    // 刻度总数(对应1-10共10个刻度)
    var markerCount: Int = 10
    
    // 调整轨道尺寸和位置
    override func trackRect(forBounds bounds: CGRect) -> CGRect {
        var rect = super.trackRect(forBounds: bounds)
        rect.size.height = trackHeight
        rect.origin.y = bounds.midY - trackHeight / 2
        return rect
    }
    
    // 自定义滑块尺寸和位置
    override func thumbRect(forBounds bounds: CGRect, trackRect rect: CGRect, value: Float) -> CGRect {
        var thumbRect = super.thumbRect(forBounds: bounds, trackRect: rect, value: value)
        thumbRect.size = CGSize(width: 20, height: 20)
        thumbRect.origin.y = rect.midY - thumbRect.height / 2
        return thumbRect
    }
    
    // 添加底部刻度标记
    func addMarkers() {
        let trackRect = self.trackRect(forBounds: bounds)
        let spacing = trackRect.width / CGFloat(markerCount - 1)
        
        for i in 0..<markerCount {
            let markerView = UIView()
            markerView.backgroundColor = .gray
            markerView.frame = CGRect(
                x: trackRect.origin.x + spacing * CGFloat(i),
                y: trackRect.maxY + 4,
                width: 2,
                height: 8
            )
            addSubview(markerView)
        }
    }
}
  1. 初始化并使用自定义Slider
lazy var timeSlider: CustomSegmentSlider = {
    let slider = CustomSegmentSlider()
    slider.minimumValue = 1
    slider.maximumValue = 10
    slider.value = 1
    // 轨道圆角设为高度一半,实现圆润效果
    slider.layer.cornerRadius = slider.trackHeight / 2
    slider.tintColor = UIColor(hexColor: "#F9F5EF")
    // 替换为自定义滑块图片(若需要)
    slider.setThumbImage(UIImage(named: "custom_slider_thumb"), for: .normal)
    slider.addTarget(self, action: #selector(sliderValueChanged), for: .valueChanged)
    slider.addMarkers()
    return slider
}()

@objc func sliderValueChanged(_ sender: UISlider) {
    // 处理数值变化逻辑,可根据需要取整
    let currentValue = Int(sender.value)
    print("当前选中数值:\(currentValue)")
}

方案二:UICollectionView实现分段滑动选择

如果UISlider的自定义程度无法满足需求,可使用UICollectionView实现分段式滑动选择器:

  1. 创建刻度单元格
class SliderMarkerCell: UICollectionViewCell {
    static let reuseID = "SliderMarkerCell"
    
    private let markerView = UIView()
    private let valueLabel = UILabel()
    
    override init(frame: CGRect) {
        super.init(frame: frame)
        setupSubviews()
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    private func setupSubviews() {
        // 刻度线
        markerView.backgroundColor = .gray
        markerView.translatesAutoresizingMaskIntoConstraints = false
        addSubview(markerView)
        
        // 数值标签
        valueLabel.font = .systemFont(ofSize: 12)
        valueLabel.textAlignment = .center
        valueLabel.translatesAutoresizingMaskIntoConstraints = false
        addSubview(valueLabel)
        
        NSLayoutConstraint.activate([
            markerView.widthAnchor.constraint(equalToConstant: 2),
            markerView.heightAnchor.constraint(equalToConstant: 8),
            markerView.centerXAnchor.constraint(equalTo: centerXAnchor),
            markerView.topAnchor.constraint(equalTo: topAnchor),
            
            valueLabel.topAnchor.constraint(equalTo: markerView.bottomAnchor, constant: 4),
            valueLabel.leadingAnchor.constraint(equalTo: leadingAnchor),
            valueLabel.trailingAnchor.constraint(equalTo: trailingAnchor),
            valueLabel.heightAnchor.constraint(equalToConstant: 16)
        ])
    }
    
    func configure(with value: Int, isSelected: Bool) {
        valueLabel.text = "\(value)"
        let selectedColor = UIColor(hexColor: "#F9F5EF")
        markerView.backgroundColor = isSelected ? selectedColor : .gray
        valueLabel.textColor = isSelected ? selectedColor : .darkGray
    }
}
  1. 配置CollectionView并实现逻辑
class YourViewController: UIViewController {
    private var currentSelectedValue = 1
    private let totalValues = 10 // 1-10
    
    lazy var segmentSlider: UICollectionView = {
        let layout = UICollectionViewFlowLayout()
        layout.scrollDirection = .horizontal
        layout.itemSize = CGSize(width: 40, height: 40)
        layout.minimumInteritemSpacing = 0
        
        let collectionView = UICollectionView(frame: .zero, collectionViewLayout: layout)
        collectionView.showsHorizontalScrollIndicator = false
        collectionView.delegate = self
        collectionView.dataSource = self
        collectionView.register(SliderMarkerCell.self, forCellWithReuseIdentifier: SliderMarkerCell.reuseID)
        collectionView.backgroundColor = .clear
        return collectionView
    }()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        setupLayout()
        // 默认选中第一个
        scrollToSelectedValue()
    }
    
    private func setupLayout() {
        segmentSlider.translatesAutoresizingMaskIntoConstraints = false
        view.addSubview(segmentSlider)
        
        NSLayoutConstraint.activate([
            segmentSlider.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20),
            segmentSlider.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -20),
            segmentSlider.heightAnchor.constraint(equalToConstant: 40),
            segmentSlider.centerYAnchor.constraint(equalTo: view.centerYAnchor)
        ])
    }
    
    private func scrollToSelectedValue() {
        let indexPath = IndexPath(item: currentSelectedValue - 1, section: 0)
        segmentSlider.scrollToItem(at: indexPath, at: .centeredHorizontally, animated: false)
    }
    
    @objc private func sliderValueChanged() {
        print("当前选中数值:\(currentSelectedValue)")
        // 执行数值变化后的业务逻辑
    }
}

// MARK: - UICollectionView DataSource & Delegate
extension YourViewController: UICollectionViewDataSource, UICollectionViewDelegate {
    func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
        return totalValues
    }
    
    func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        let cell = collectionView.dequeueReusableCell(withReuseIdentifier: SliderMarkerCell.reuseID, for: indexPath) as! SliderMarkerCell
        let value = indexPath.item + 1
        cell.configure(with: value, isSelected: value == currentSelectedValue)
        return cell
    }
    
    func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
        updateSelectedValue(from: scrollView)
    }
    
    func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) {
        if !decelerate {
            updateSelectedValue(from: scrollView)
        }
    }
    
    private func updateSelectedValue(from scrollView: UIScrollView) {
        let centerPoint = CGPoint(x: scrollView.contentOffset.x + scrollView.bounds.width / 2, y: scrollView.bounds.midY)
        guard let indexPath = segmentSlider.indexPathForItem(at: centerPoint) else { return }
        
        currentSelectedValue = indexPath.item + 1
        segmentSlider.reloadData()
        sliderValueChanged()
    }
}

内容的提问来源于stack exchange,提问作者Gagen

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.14 02:53:11