Когда я занимался разработкой своего последнего проекта, я столкнулся с задачей создания удобного и интуитивно понятного интерфейса для отображения и переключения данных с физических датчиков по анализу качества окружающей среды. В этой статье я использую в качестве примеров параметры «Качество воздуха» и «УФ-индекс». Я не буду вдаваться в подробности работы с MapKit и маркерами, но пишу об этом для введения в контекст.
Предполагается, что с помощью этого компонента можно будет переключать значения, чтобы отображать разные параметры на маркерах карты. Я назвал компонент SensorView
. Ниже я предоставлю код элемента целиком, но также оставляю ссылку на репозиторий проекта.
Мне был нужен аналог контекстного меню, но ТЗ требовало, чтобы появление опций появлялось не в отдельном нативном вью. Это должно было работать так: при нажатии на кнопку она разворачивается вниз, предлагая выбрать параметр или нажать на инфокнопку "What is this".
В этой статье я расскажу о том, как я создал компонентный вью с помощью UIKit в коде без использования Сториборда. Вот какие компоненты я использовал:
Переменные:
Константы для размеров: Здесь мы определяем размеры, которые будем использовать для размещения и стилизации наших элементов интерфейса.
Основная и второстепенная UIStackView: Эти элементы используются для группировки и вертикального размещения кнопок. Стеки было удобно использовать, потому что при параметре
.isHidden = true
у кнопок (кнопки скрыты) StackView автоматически сворачивается.Кнопки: Здесь мы создаём основную кнопку, которая отображает текущий параметр, а также отвечает за активацию меню, кнопку для отображения качества воздуха, кнопку для отображения УФ-индекса, информационную кнопку и круглую кнопку для отображения пиктограммы текущего параметра. Сейчас она не отрабатывает событий.
Функции:
setupViews()
: Эта функция отвечает за добавление всех элементов интерфейса на главное представление и их базовую настройку.setupConstraints()
: Здесь мы устанавливаем ограничения для всех элементов интерфейса, определяя их положение и размеры относительно друг друга и главного представления.createButton(...)
: Это универсальная функция для создания кнопок с различными свойствами, что позволяет избежать дублирования кода при создании каждой новой кнопки.
Отработка событий:
Нажатие на основную кнопку: Функция
mainButtonTapped()
вызывается, когда пользователь нажимает на основную кнопку, что приводит к разворачиванию или сворачиванию меню.Нажатие на кнопку качества воздуха: Функция
airQualityButtonTapped()
вызывается, когда пользователь выбирает параметр "Качество воздуха".Нажатие на кнопку УФ-индекса: Функция
uvIndexButtonTapped()
активируется при выборе параметра "УФ-индекс".
Вот код этого элемента с моими комментариями:
import UIKit import SnapKit
// Перечисление для определения текущего параметра enum Parameter { case airQuality case uvIndex }
// Основной класс представления датчика class SensorView: UIView {
// Структура для определения размеров и констант макета
struct Layout {
static let screenWidth = UIScreen.main.bounds.width
}
// Текущий выбранный параметр
var currentParameter: Parameter = .airQuality
// Основной вертикальный stack view
let primaryStackView: UIStackView = {
let stack = UIStackView()
stack.backgroundColor = .white
stack.axis = .vertical
stack.layer.cornerRadius = 26
let insets = UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 15)
stack.layoutMargins = insets
stack.isLayoutMarginsRelativeArrangement = true
return stack
}()
// Второстепенный вертикальный stack view
let secondaryStackView: UIStackView = {
let stack = UIStackView()
stack.backgroundColor = .white
stack.axis = .vertical
let insets = UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 0)
stack.layoutMargins = insets
stack.isLayoutMarginsRelativeArrangement = true
return stack
}()
// Главная кнопка, показывающая текущий параметр
private lazy var mainButton: UIButton = {
let button: UIButton = createButton(title: "Air Quality".uppercased(), action: #selector(mainButtonTapped), height: 58, target: self)
button.setImage(UIImage.init(systemName: "chevron.down"), for: .normal)
button.contentHorizontalAlignment = .left
button.imageEdgeInsets = UIEdgeInsets(top: 0, left: 20, bottom: 0, right: -10)
button.titleEdgeInsets = UIEdgeInsets(top: 0, left: 40, bottom: 0, right: 0)
return button
}()
// Кнопка для выбора качества воздуха
private lazy var airQualityButton: UIButton = {
let button = createButton(title: "Air Quality", action: #selector(airQualityButtonTapped), height: 45, target: self)
button.isHidden = true
button.contentHorizontalAlignment = .leading
button.setImage(UIImage.init(systemName: "wind"), for: .normal)
button.contentEdgeInsets = UIEdgeInsets(top: 0, left: 50, bottom: 0, right: 0)
button.imageEdgeInsets = UIEdgeInsets(top: 0, left: -10, bottom: 0, right: 0)
return button
}()
// Кнопка для выбора индекса ультрафиолетового излучения
private lazy var uvIndexButton: UIButton = {
let button = createButton(title: "UV index", action: #selector(uvIndexButtonTapped), height: 45, target: self)
button.isHidden = true
button.contentHorizontalAlignment = .leading
button.setImage(UIImage.init(systemName: "sun.max.fill"), for: .normal)
button.contentEdgeInsets = UIEdgeInsets(top: 0, left: 50, bottom: 0, right: 0)
button.imageEdgeInsets = UIEdgeInsets(top: 0, left: -10, bottom: 0, right: 0)
return button
}()
// Кнопка для вывода дополнительной информации
private lazy var infoButton: UIButton = {
let button = createButton(title: "What is this?", action: #selector(mainButtonTapped), height: 58, target: self)
button.isHidden = true
if let currentTitle = button.title(for: .normal) {
let attributes: [NSAttributedString.Key: Any] = [
.underlineStyle: NSUnderlineStyle.single.rawValue
]
let attributedTitle = NSAttributedString(string: currentTitle, attributes: attributes)
button.setAttributedTitle(attributedTitle, for: .normal)
}
return button
}()
// Круглая кнопка для визуализации текущего параметра
private lazy var roundButton: UIButton = {
let button = UIButton()
let roundButtonDiameter: CGFloat = 75
button.backgroundColor = .cyan
button.layer.cornerRadius = roundButtonDiameter / 2
button.widthAnchor.constraint(equalToConstant: roundButtonDiameter).isActive = true
button.heightAnchor.constraint(equalToConstant: roundButtonDiameter).isActive = true
button.setImage(UIImage.init(systemName: "wind"), for: .normal)
return button
}()
// Инициализатор
init() {
super.init(frame: .zero)
setupViews()
setupConstraints()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
// Метод для настройки представлений
func setupViews() {
self.addSubview(primaryStackView)
primaryStackView.addArrangedSubview(mainButton)
primaryStackView.addArrangedSubview(secondaryStackView)
secondaryStackView.addArrangedSubview(airQualityButton)
secondaryStackView.addArrangedSubview(uvIndexButton)
primaryStackView.addArrangedSubview(infoButton)
self.addSubview(roundButton)
}
// Метод для настройки ограничений
func setupConstraints() {
primaryStackView.snp.makeConstraints { make in
make.top.bottom.right.left.equalToSuperview().inset(30)
}
roundButton.snp.makeConstraints { make in
make.centerY.equalTo(mainButton)
make.right.equalTo(primaryStackView).offset(20)
}
}
// Метод для создания кнопок, чтобы не копировать код
private func createButton(title: String, action: Selector, height: CGFloat, target: Any) -> UIButton {
let button = UIButton()
button.setTitle(title, for: .normal)
button.setTitleColor(.black, for: .normal)
button.backgroundColor = .clear
button.layer.shadowColor = UIColor.black.cgColor
button.layer.shadowOffset = CGSize(width: 0, height: 2)
button.layer.shadowOpacity = 0.2
button.layer.shadowRadius = 4
button.addTarget(target, action: action, for: .touchUpInside)
button.widthAnchor.constraint(equalToConstant: Layout.screenWidth * 0.6).isActive = true
button.heightAnchor.constraint(equalToConstant: height).isActive = true
return button
}
// Действие при нажатии на главную кнопку
@objc private func mainButtonTapped() {
let areButtonsHidden = airQualityButton.isHidden
airQualityButton.isHidden.toggle()
uvIndexButton.isHidden.toggle()
infoButton.isHidden.toggle()
let image = areButtonsHidden ? UIImage(systemName: "chevron.up") : UIImage(systemName: "chevron.down")
mainButton.setImage(image, for: .normal)
}
// Действие при нажатии на кнопку качества воздуха
@objc private func airQualityButtonTapped() {
mainButton.setTitle("Air Quality".uppercased(), for: .normal)
airQualityButton.isHidden = true
uvIndexButton.isHidden = true
infoButton.isHidden = true
roundButton.setImage(UIImage.init(systemName: "wind"), for: .normal)
mainButton.setImage(UIImage.init(systemName: "chevron.down"), for: .normal)
}
// Действие при нажатии на кнопку индекса ультрафиолетового излучения
@objc private func uvIndexButtonTapped() {
mainButton.setTitle("UV index".uppercased(), for: .normal)
airQualityButton.isHidden = true
uvIndexButton.isHidden = true
infoButton.isHidden = true
roundButton.setImage(UIImage.init(systemName: "sun.max.fill"), for: .normal)
mainButton.setImage(UIImage.init(systemName: "chevron.down"), for: .normal)
}
}