Тут можно найти реализацию готового проекта

На сегодняшний день во многих приложениях мы можем наблюдать stepper, большинство из них кастомные. Несмотря на то, что Apple предоставляет уже реализацию готового степпера, иногда он не подходит по разным причинам. Это пример подхода к реализации кастомного степпера для кофейни.

Есть разные способы в достижение цели, но сегодня я вам покажу тот, которые не не нашел. В этой статье мы будем использовать верстку кодом, stackView с 2 кнопками (-,+) и лейбл.

Для того, чтобы многократно использовать наш степпер, сделаем его классом, который наследуется от UIView. По сути это полноценный UI-компонент, который можно будет потом взять в свои проекты.

Самое важное - логика, которая будет обрабатывать текущее значения нашего степпера. Для этого будет использовать переменную, которая будет отвечать за текущее значения степпера и обновлять текст лейбла.

final class CustomStepper: UIView {
     private lazy var currentValue = 1
}

Дальше нам нужно создать 2 кнопки и лейбл, из которых и будет состоять наш степпер. Для того, чтобы отслеживать состояние степпера, мы будем использовать enum, который будет управлять состояниями кнопок. Мы использовали теги, для чтобы не делать 2 метода для обработки нажатия кнопок.

    private enum ButtonState: Int, CaseIterable {
        case decrease = 0
        case increase
    }

    private lazy var decreaseButton: UIButton = {
        let button = UIButton()
        button.tag = ButtonState.decrease.rawValue
        button.setTitleColor(.black, for: .normal)
        button.setTitle("-", for: .normal)
        button.addTarget(self, action: #selector(buttonAction), for: .touchUpInside)
        return button
    }()
        
    private lazy var currentStepValueLabel: UILabel = {
        let label = UILabel()
        label.textColor = .black
        label.text = "\(currentValue)"
        label.font = .systemFont(ofSize: 15)
        return label
    }()
        
    private lazy var increaseButton: UIButton = {
        let button = UIButton()
        button.tag = ButtonState.increase.rawValue
        button.setTitle("+", for: .normal)
        button.addTarget(self, action: #selector(buttonAction), for: .touchUpInside)
        button.setTitleColor(.black, for: .normal)
        return button
    }()

Имея все необходимые элементы, мы можем реализовать метод, который отвечает за логику нашего степпера и привязать его к нашим кнопкам.

    //MARK: - Actions
    @objc private func buttonAction(_ sender: UIButton) {
        let buttonState = ButtonState(rawValue: sender.tag)
        
        switch buttonState {
        case .decrease:
            currentValue = currentValue > 1 ? currentValue - 1 : currentValue
        case .increase:
            currentValue += 1
        default:
            return
        }
        currentStepValueLabel.text = "\(currentValue)"
    }

Будем использовать паттерн делегирования, чтобы передать данные степпера в наш контроллер для дальнейшей логики проекта и обновления значения.

protocol CustomStepperOutput: AnyObject {
    func customStepper(_ didChangeValue: Int)
}

protocol CustomStepperInput: AnyObject {
    func update(_ value:Int)
}

Это внутренний интерфейс, через который мы можем проинициализировать счетчик (при необходимости)

//MARK: - CustomStepperInput
extension CustomStepper: CustomStepperInput {
    func update(_ value: Int) {
        currentValue = value
    }
}

Внутри контроллера проинициализируем наш степпер. Контроллер подпишем под делегатом степпера, поэтому контроллер будет реализовывать метод делегата, где получать данные от степпера. С этими данными контроллер может дальше осуществлять логику.

import UIKit

final class MainVC: UIViewController {

    private lazy var stepperView = CustomStepper()
    
    //MARK: - Life Cycle
    override func viewDidLoad() {
        super.viewDidLoad()
        setupViews()
        setupConstraints()
    }
    
    //MARK: - Private
    private func setupViews() {
        view.backgroundColor = .white
        view.addSubview(stepperView)
        stepperView.delegate = self
    }
    
    private func setupConstraints() {
        stepperView.snp.makeConstraints { make in
            make.centerX.centerY.equalToSuperview()
        }
    }
}

//MARK: - CustomStepperOutput
extension MainVC: CustomStepperOutput {
    func customStepper(_ didChangeValue: Int) {
        print(didChangeValue)
    }
}

Готово! Вот конечный результат:

Комментарии (9)


  1. denisromanenko
    23.11.2022 23:09
    +2

    Здорово! Только кнопка "дрожит" по ширине, зафиксировать как-нибудь бы размеры.


    1. storoj
      24.11.2022 07:36
      +1

      1. denisromanenko
        24.11.2022 11:18

        Праздный интерес: а с кастомным фонтом как этого добиться?


    1. house2008
      24.11.2022 12:23

      а как же поддержка динамических шрифтов ?


    1. zontz Автор
      24.11.2022 19:53

      Спасибо за комментарий, все исправлю!


  1. storoj
    24.11.2022 07:29
    +2

    CustomStepper: UIView
    

    почему не UIControl? не надо было бы никакого делегата придумывать (который ещё и неправильный – что если я хочу узнавать об изменениях двух и более разных таких контролов, как отличить от которого из них пришло событие?)

    зачем currentValue приватное? как мне узнавать текущее состояние, чтобы например программно сделать +3? зачем lazy Int?

    зачем вообще всё приватное? как мне кастомизировать внешний вид?


    1. storoj
      24.11.2022 07:31
      +1

      ... зачем теги кнопкам? в обработчике нажатия кнопки можно проверить кто был sender – increaseButton или же decreaseButton


      1. 2Grey
        24.11.2022 12:53
        +1

        Продолжу Ваши рассуждения:

        1. Зачем enum CaseIterable, если это нигде не используется?

        2. Зачем пихать события в один метод, если на каждую кнопку можно повесить свое событие?
          Так можно избавиться от бесполезного enum'а.

        Выглядит так, будто человек прочитал одну (не самую лучшую) книгу по Swift и начал писать не разбираясь в смысле написанного.


        1. zontz Автор
          24.11.2022 19:36

          Спасибо за обратную связь! Учту ваши рекомендации