Пошаговое руководство по реализации удобного и привлекательного элемента пользовательского интерфейса

Кнопка «слайд к действию» — это тип кнопки, которая требует, чтобы пользователь провел пальцем по кнопке, чтобы активировать ее. Это популярный шаблон дизайна в интерфейсах мобильных приложений, который помогает предотвратить случайные щелчки или касания, а также обеспечивает более привлекательный и интерактивный опыт для пользователей.

Собственные UI-фреймворки Apple не предоставляют нам этот компонент. В этой серии статей я покажу вам, как это реализовать с помощью фреймворков UIKit и SwiftUI. В этой статье рассматривается реализация UIKit.

Разложение

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

Вот разбивка кнопки слайд-к-действию на ее подпредставления:

  1. Основной вид. Это основной вид кнопки перехода к действию, который служит фоном или основой кнопки. Обычно он включает цвет фона, форму и любой текст или графику на кнопке;
  2. Просмотр ручки. Это представление, представляющее ручку или ползунок кнопки перехода к действию;
  3. Перетаскиваемый вид. Это вид, который появляется, когда пользователь перетаскивает дескриптор представления в определенную точку.

Реализация UIKit

Это кнопка, которую мы собираемся создать в этой главе:

В дополнение к фону у кнопки есть три подпредставления: метка, дескриптор и перетаскиваемый вид. Чтобы создать эту кнопку, нам нужно создать подкласс UIView и назвать его SlideToActionButton.

class SlideToActionButton: UIView {

    init() {
        super.init(frame: .zero)
    }
     
    required init?(coder: NSCoder) {
        super.init(coder: coder)
    }

}

Прежде чем мы сможем добавить и настроить подпредставления, нам нужно добавить структуру Colors в наш проект. Эта структура содержит все цвета, которые нам понадобятся для нашей кнопки перехода к действию.

struct Colors {
    static let background = #colorLiteral(red: 0.657153666, green: 0.8692060113, blue: 0.6173200011, alpha: 1)
    static let draggedBackground = #colorLiteral(red: 0.462745098, green: 0.7843137255, blue: 0.5764705882, alpha: 1)
    static let tint = #colorLiteral(red: 0.1019607843, green: 0.4588235294, blue: 0.6235294118, alpha: 1)
}

Теперь пришло время добавить метку, дескриптор и перетаскиваемые представления. Мы будем использовать UIView с изображением внутри в качестве дескриптора.

 
let handleView: UIView = {
    let view = UIView()
    view.translatesAutoresizingMaskIntoConstraints = false
    view.backgroundColor = Colors.draggedBackground
    view.layer.cornerRadius = 12
    view.layer.masksToBounds = true
    view.layer.borderWidth = 3
    view.layer.borderColor = Colors.tint.cgColor
    return view
}()
 
let handleViewImage: UIImageView = {
    let view = UIImageView()
    view.translatesAutoresizingMaskIntoConstraints = false
    view.image = UIImage(systemName: "chevron.right.2", withConfiguration: UIImage.SymbolConfiguration(font: .systemFont(ofSize: 40, weight: .bold)))?.withRenderingMode(.alwaysTemplate)
    view.contentMode = .scaleAspectFit
    view.tintColor = Colors.tint
    return view
}()
 
let draggedView: UIView = {
    let view = UIView()
    view.translatesAutoresizingMaskIntoConstraints = false
    view.backgroundColor = Colors.draggedBackground
    view.layer.cornerRadius = 12
    return view
}()

let titleLabel: UILabel = {
    let label = UILabel()
    label.translatesAutoresizingMaskIntoConstraints = false
    label.textAlignment = .center
    label.textColor = Colors.tint
    label.font = .systemFont(ofSize: 24, weight: .semibold)
    label.text = "Slide me!"
    return label
}()

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

Чтобы добиться желаемой функциональности, нам нужно будет переместить handleView с помощью жеста панорамирования. Это означает, что нам понадобится ссылка на ограничение, которое соединяет левую сторону handleView с левой стороной его суперпредставления. Изменяя значение смещения этого ограничения, мы сможем перемещать handleView и отслеживать его положение.

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

private var leadingThumbnailViewConstraint: NSLayoutConstraint?

Теперь нам нужно добавить все подпредставления в суперпредставление и настроить их ограничения. Создайте для этой цели метод setup():

func setup() {
    backgroundColor = Colors.background
    layer.cornerRadius = 12
    addSubview(titleLabel)
    addSubview(draggedView)
    addSubview(handleView)
    handleView.addSubview(handleViewImage)
    
    //MARK: - Constraints
    
    leadingThumbnailViewConstraint = handleView.leadingAnchor.constraint(equalTo: leadingAnchor)
    
    NSLayoutConstraint.activate([
        leadingThumbnailViewConstraint!,
        handleView.topAnchor.constraint(equalTo: topAnchor),
        handleView.bottomAnchor.constraint(equalTo: bottomAnchor),
        handleView.widthAnchor.constraint(equalToConstant: 80),
        draggedView.topAnchor.constraint(equalTo: topAnchor),
        draggedView.bottomAnchor.constraint(equalTo: bottomAnchor),
        draggedView.leadingAnchor.constraint(equalTo: leadingAnchor),
        draggedView.trailingAnchor.constraint(equalTo: handleView.trailingAnchor),
        handleViewImage.topAnchor.constraint(equalTo: handleView.topAnchor, constant: 10),
        handleViewImage.bottomAnchor.constraint(equalTo: handleView.bottomAnchor, constant: -10),
        handleViewImage.centerXAnchor.constraint(equalTo: handleView.centerXAnchor),
        titleLabel.leadingAnchor.constraint(equalTo: leadingAnchor),
        titleLabel.trailingAnchor.constraint(equalTo: trailingAnchor),
        titleLabel.centerYAnchor.constraint(equalTo: centerYAnchor)
    ])
}

handleView должен иметь ограничения на верхнюю, нижнюю и левую стороны своего супервида, а также фиксированную ширину 80. handleImageView и titleLabel должны располагаться в центре своих супервидов.

Теперь давайте подробнее рассмотрим ограничения draggedView. draggedView должен иметь верхнее, нижнее и ведущее ограничения для суперпредставления, а также конечное ограничение для handleView. Вы можете заметить, что замыкающее ограничение равно замыкающему ограничению handleView, а не его ведущему ограничению. Это из-за углового радиуса handleView.

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

Вызовите метод setup() внутри файла init().

init() {
    super.init(frame: .zero)
    setup()
}

Теперь, когда пользовательский интерфейс настроен, мы можем добавить SlideToActionButton на экран и посмотреть, как он выглядит. Однако на данный момент это просто статическое представление. Чтобы добавить функцию смахивания, нам нужно прикрепить UIPanGestureRecognizer к файлу handleView.

Для начала нам нужно создать свойство для UIPanGestureRecognizer, а также функцию-обработчик, которая будет реагировать на жест.

private var panGestureRecognizer: UIPanGestureRecognizer!

@objc private func handlePanGesture(_ sender: UIPanGestureRecognizer) { }

Добавьте распознавание жестов в метод handleView внутри setup():

  panGestureRecognizer = UIPanGestureRecognizer(target: self, action: #selector(self.handlePanGesture(_:)))
  panGestureRecognizer.minimumNumberOfTouches = 1
  handleView.addGestureRecognizer(panGestureRecognizer)

Теперь давайте сосредоточимся на методе handlePanGesture, где мы реализуем логику смахивания. Наш первый шаг — получить текущую позицию пальца пользователя и присвоить это значение смещению leadingThumbnailViewConstraint.

@objc private func handlePanGesture(_ sender: UIPanGestureRecognizer) {
    let translatedPoint = sender.translation(in: self).x
    leadingThumbnailViewConstraint?.constant = translatedPoint
}

Запустите свой проект, чтобы увидеть, как он работает. Вы нашли проблему? Я сделал. Мы можем переместить наш дескриптор за пределы кнопки.

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

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

private var xEndingPoint: CGFloat {
    return (bounds.width - handleView.bounds.width)
}

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

private var isFinished = false

И давайте добавим два метода для изменения позиции handleView и сброса состояния ползунка.

private func updateHandleXPosition(_ x: CGFloat) {
    leadingThumbnailViewConstraint?.constant = x
}

func reset() {
    isFinished = false
    updateHandleXPosition(0)
}

Теперь мы можем вернуться к handlePanGesture. У UIPanGestureRecognizer есть несколько состояний, но нам нужно обработать только два из них: .changed и .ended. Для этого мы можем использовать оператор switch внутри handlePanGesture.

@objc private func handlePanGesture(_ sender: UIPanGestureRecognizer) {
    let translatedPoint = sender.translation(in: self).x
  
    switch sender.state {
    case .changed:
        //TODO: - Handle changed state
    case .ended:
        //TODO: - Handle ended state
    default:
        break
    }
}

Состояние UIPanGestureRecongnizer равно .changed, когда мы взаимодействуем с его представлением. Здесь нам нужно обновить позицию handleView и убедиться, что handleView находится внутри границ кнопки.

@objc private func handlePanGesture(_ sender: UIPanGestureRecognizer) {
    let translatedPoint = sender.translation(in: self).x
  
    switch sender.state {
    case .changed:
        if translatedPoint <= 0 {
          updateHandleXPosition(0)
        } else if translatedPoint >= xEndingPoint {
          updateHandleXPosition(xEndingPoint)
        } else {
          updateHandleXPosition(translatedPoint)
        }
    case .ended:
        //TODO: - Handle ended state
    default:
      break
    }
}

С помощью операторов if мы проверяем текущую позицию X жеста. Если translatedPoint равно или меньше 0, мы устанавливаем позицию handleView в 0. Если translatedPoint равно или больше xEndingPoint, мы устанавливаем позицию handleView в значение xEndingPoint. И если translatedPoint находится внутри границ кнопки, мы обновляем позицию handleView до значения translatedPoint.

Теперь мы можем перемещать handleView внутри кнопок, но мы не обрабатываем действие. Действие должно быть обработано, когда пользователь отпускает handleView и handleView достигает позиции xEndingPoint. Мы разберемся с этим в .ended кейсе.

@objc private func handlePanGesture(_ sender: UIPanGestureRecognizer) {
  if isFinished { return }
  let translatedPoint = sender.translation(in: self).x

  switch sender.state {
  case .changed:
      if translatedPoint <= 0 {
          updateHandleXPosition(0)
      } else if translatedPoint >= xEndingPoint {
          updateHandleXPosition(xEndingPoint)
      } else {
          updateHandleXPosition(translatedPoint)
      }
  case .ended:
      if translatedPoint >= xEndingPoint {
          self.updateHandleXPosition(xEndingPoint)
          isFinished = true
          //TODO: - add action handler
      } else {
          UIView.animate(withDuration: 1) {
              self.reset()
          }
      }
  default:
    break
  }
}

Мы проверяем, равно ли translatedPoint или больше, чем xEndingPoint, и если это так, мы устанавливаем позицию handleView на значение xEndingPoint, а свойство isFinished на true. В начале этой функции мы проверяем значение свойства isFinished, и если оно истинно, то ничего не делаем. Если translatedPoint меньше xEndingPoint, мы сбрасываем состояние с плавной анимацией.

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

Создайте протокол, который мы будем использовать в качестве делегата:

protocol SlideToActionButtonDelegate: AnyObject {
    func didFinish()
}

Добавьте свойство delegate к SlideToActionButton:

weak var delegate: SlideToActionButtonDelegate?

А теперь замените комментарий TODO из последнего фрагмента кода на метод delegate:

   if translatedPoint >= xEndingPoint {
       self.updateHandleXPosition(xEndingPoint)
       isFinished = true
       delegate?.didFinish()
   } else {
       UIView.animate(withDuration: 1) {
           self.reset()
       }
   }

Мы сделали. Мы только что создали кнопку «слайд к действию», используя фреймворк UIKit.

Conclusion

Кнопка «слайд к действию» — полезный компонент пользовательского интерфейса. В этой статье я показал вам, как создать его с помощью фреймворка UIKit. В следующей части я покажу вам, как создать его с помощью SwiftUI.

Надеюсь, вам понравилась эта статья и вы нашли ее полезной. Спасибо за ваше время и увидимся позже!

Исходный код можно найти здесь.

Check out my other articles about iOS Development

https://medium.com/@artem.khalilyaev

Повышение уровня кодирования

Спасибо, что являетесь частью нашего сообщества! Перед тем, как ты уйдешь:

  • 👏 Хлопайте за историю и подписывайтесь на автора 👉
  • 📰 Смотрите больше контента в публикации Level Up Coding
  • 💰 Бесплатный курс собеседования по программированию ⇒ Просмотреть курс
  • 🔔 Подписывайтесь на нас: Twitter | ЛинкедИн | "Новостная рассылка"

🚀👉 Присоединяйтесь к коллективу талантов Level Up и найдите прекрасную работу