Пошаговое руководство по реализации удобного и привлекательного элемента пользовательского интерфейса
Кнопка «слайд к действию» — это тип кнопки, которая требует, чтобы пользователь провел пальцем по кнопке, чтобы активировать ее. Это популярный шаблон дизайна в интерфейсах мобильных приложений, который помогает предотвратить случайные щелчки или касания, а также обеспечивает более привлекательный и интерактивный опыт для пользователей.
Собственные UI-фреймворки Apple не предоставляют нам этот компонент. В этой серии статей я покажу вам, как это реализовать с помощью фреймворков UIKit и SwiftUI. В этой статье рассматривается реализация UIKit.
Разложение
Прежде чем мы начнем кодировать, я хочу разложить слайд на кнопку действия. Это поможет вам лучше понять, что мы собираемся делать.
Вот разбивка кнопки слайд-к-действию на ее подпредставления:
- Основной вид. Это основной вид кнопки перехода к действию, который служит фоном или основой кнопки. Обычно он включает цвет фона, форму и любой текст или графику на кнопке;
- Просмотр ручки. Это представление, представляющее ручку или ползунок кнопки перехода к действию;
- Перетаскиваемый вид. Это вид, который появляется, когда пользователь перетаскивает дескриптор представления в определенную точку.
Реализация 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 и найдите прекрасную работу