Как реализовать спойлер-эффект как в Telegram на Swift?
Спойлеры стали неотъемлемой частью общения в мессенджерах и социальных сетях. Они позволяют скрывать часть информации до тех пор, пока пользователь не захочет ее увидеть. В Telegram спойлер-эффект сопровождается красивой анимацией рассыпающихся точек. В этой статье мы рассмотрим, как реализовать подобный спойлер-эффект в iOS-приложении на Swift, используя CAEmitterLayer и UITextView.
TeamLead IOS в YuSMP Group, автор материала
Цель статьи
● Показать, как скрывать определенные части текста в UITextView.
● Реализовать спойлер-эффект с анимацией, похожей на Telegram.
● Подробно объяснить каждый шаг и участок кода для полного понимания процесса.
Содержание
Шаг 1: Создание класса SpoilerView
Начнем с создания класса SpoilerView, который будет отвечать за отображение спойлер-эффекта.
import UIKit
class SpoilerView: UIView {
var emitterLayer: CAEmitterLayer!
// Указываем, что слой представления будет CAEmitterLayer
override class var layerClass: AnyClass {
return CAEmitterLayer.self
}
override init(frame: CGRect) {
super.init(frame: frame)
isUserInteractionEnabled = true // Включаем взаимодействие с пользователем
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
// Метод для запуска анимации спойлера
func startAnimation() {
guard let emitterLayer = self.layer as? CAEmitterLayer else { return }
self.emitterLayer = emitterLayer
// Настраиваем параметры эмиттера
emitterLayer.emitterPosition = CGPoint(x: bounds.midX, y: bounds.midY) // Позиция эмиттера в центре SpoilerView
emitterLayer.emitterShape = .rectangle // Форма эмиттера
emitterLayer.emitterSize = CGSize(width: bounds.size.width, height: bounds.size.height) // Размер эмиттера
emitterLayer.emitterMode = .surface // Режим эмиссии частиц с поверхности
emitterLayer.emitterCells = [createEmitterCell()] // Добавляем ячейку эмиттера
}
// Метод для остановки анимации спойлера
func stopAnimation() {
guard emitterLayer != nil else { return }
emitterLayer.emitterCells = nil // Удаляем ячейки эмиттера
}
// Создаем и настраиваем ячейку эмиттера
private func createEmitterCell() -> CAEmitterCell {
let cell = CAEmitterCell()
cell.contents = UIImage(named: "dot")?.cgImage // Изображение частицы
cell.scale = 0.3 // Размер частицы
cell.scaleRange = 0.15 // Разброс размера частицы
cell.emissionRange = .pi * 2.0 // 360 градусов для равномерного распространения
cell.lifetime = 1.5 // Время жизни частицы
cell.birthRate = dotCount() * 2.0 // Количество частиц в секунду, умноженное на 2 для увеличения количества
cell.velocity = 5.0 // Скорость частицы
cell.velocityRange = 10 // Разброс скорости
cell.alphaSpeed = -0.5 // Частицы будут постепенно исчезать
cell.yAcceleration = 5.0 // Эффект гравитации по оси Y
cell.spin = CGFloat.pi // Вращение частиц
cell.spinRange = CGFloat.pi * 2.0 // Разброс вращения
return cell
}
// Создаем и настраиваем ячейку эмиттера
// Вычисляем количество частиц на основе площади SpoilerView
private func dotCount() -> Float {
let area = frame.width * frame.height
let densityFactor: Float = 0.07 // Настройте этот коэффициент для желаемой плотности
let count = area * densityFactor
return count
}
}
Пояснения к коду:
● layerClass: Переопределяем это свойство, чтобы SpoilerView использовал CAEmitterLayer в качестве своего слоя.
● startAnimation (): Настраиваем параметры эмиттера и запускаем анимацию.
● stopAnimation (): Останавливаем анимацию, удаляя ячейки эмиттера.
● createEmitterCell (): Создаем и настраиваем ячейку эмиттера (CAEmitterCell), которая определяет свойства частиц (изображение, размер, скорость, время жизни и т.д.).
● dotCount (): Вычисляем количество частиц на основе ширины SpoilerView, чтобы эффект выглядел одинаково на разных размерах.
Шаг 2: Создание ViewController и настройка UITextView
Теперь перейдем к контроллеру, в котором будем использовать SpoilerView для скрытия определенных частей текста в UITextView.
import UIKit
class ViewController: UIViewController, UIScrollViewDelegate {
let textView = UITextView()
var spoilerRanges: [NSRange] = []
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .white
setupTextView()
}
// Вызывается после установки размеров представлений
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
setupSpoilers()
}
// Настраиваем UITextView и обрабатываем спойлеры в тексте
func setupTextView() {
textView.frame = CGRect(x: 20.0, y: 120.0, width: UIScreen.main.bounds.width - 40.0, height: 300.0)
textView.isEditable = false
textView.isScrollEnabled = true
textView.font = UIFont.systemFont(ofSize: 18)
textView.isSelectable = false
textView.backgroundColor = .white
textView.textColor = .black
textView.text = "Это пример текста с [спойлером, который занимает несколько строк и демонстрирует работу с многострочным текстом], который мы хотим скрыть. А вот еще один [секретный текст]."
view.addSubview(textView)
let attributedText = NSMutableAttributedString(string: textView.text)
// Ищем спойлеры в тексте с помощью регулярного выражения
let pattern = "\\[([^\\]]+)\\]"
let regex = try? NSRegularExpression(pattern: pattern, options: [])
let matches = regex?.matches(in: textView.text, options: [], range: NSRange(location: 0, length: textView.text.utf16.count)) ?? []
for match in matches {
// Скрываем скобки, устанавливая прозрачный цвет
attributedText.addAttribute(.foregroundColor, value: UIColor.clear, range: NSRange(location: match.range.location, length: 1))
attributedText.addAttribute(.foregroundColor, value: UIColor.clear, range: NSRange(location: match.range.location + match.range.length - 1, length: 1))
// Добавляем диапазон спойлера без скобок в массив
let spoilerRange = NSRange(location: match.range.location + 1, length: match.range.length - 2)
spoilerRanges.append(spoilerRange)
}
textView.attributedText = attributedText
}
Пояснения к коду:
textView: Создаем и настраиваем UITextView, в котором будет отображаться текст со спойлерами.
setupTextView (): Метод для настройки textView и обработки спойлеров.
Регулярное выражение: Используем для поиска текста, заключенного в квадратные скобки [ ], который будем считать спойлером
matches: Находим все совпадения спойлеров в тексте.
Скрытие скобок: Устанавливаем прозрачный цвет для скобок, чтобы они не отображались.
spoilerRanges: Сохраняем диапазоны спойлеров без скобок для дальнейшей обработки.
Шаг 3: Создание и настройка SpoilerView для каждого спойлера
Добавим метод setupSpoilers (), который создаст SpoilerView для каждого найденного спойлера и наложит его на соответствующий текст.
extension ViewController {
func setupSpoilers() {
let spoilerRectsArray = getRectsForSpoilerRanges()
for rects in spoilerRectsArray {
// Устанавливаем фрейм SpoilerView
let unionRect = rects.reduce(rects.first!) { $0.union($1) }
let spoilerView = SpoilerView(frame: unionRect)
spoilerView.backgroundColor = .white
textView.addSubview(spoilerView)
// Создаем путь, объединяющий все прямоугольники спойлера, с учетом координат SpoilerView
let combinedPath = UIBezierPath()
for rect in rects {
// Преобразуем координаты прямоугольников в систему координат SpoilerView
let adjustedRect = rect.offsetBy(dx: -unionRect.origin.x, dy: -unionRect.origin.y)
combinedPath.append(UIBezierPath(rect: adjustedRect))
}
// Создаем маску на основе объединенного пути
let maskLayer = CAShapeLayer()
maskLayer.path = combinedPath.cgPath
maskLayer.frame = spoilerView.bounds // Устанавливаем фрейм маски равным bounds SpoilerView
spoilerView.layer.mask = maskLayer
// Запускаем анимацию
spoilerView.startAnimation()
// Добавляем распознаватель жестов для обработки нажатия на спойлер
let tapGesture = UITapGestureRecognizer(target: self, action: #selector(handleSpoilerTap(_:)))
spoilerView.addGestureRecognizer(tapGesture)
}
}
Пояснения к коду:
● getRectsForSpoilerRanges (): Метод, который возвращает массив массивов CGRect для каждого спойлера. Эти прямоугольники соответствуют областям, где находится текст спойлера.
● unionRect: Объединяем все прямоугольники спойлера в один общий прямоугольник, который будет фреймом для SpoilerView.
● SpoilerView: Создаем SpoilerView с фреймом unionRect и добавляем его поверх textView.
● Маска: Создаем маску (maskLayer) на основе объединенного пути из прямоугольников, чтобы SpoilerViewзакрывал только текст спойлера.
● startAnimation (): Запускаем анимацию спойлера.
● Распознаватель жестов: Добавляем UITapGestureRecognizer для обработки нажатия на спойлер и его раскрытия.
Шаг 4: Обработка нажатий на спойлер
Реализуем метод handleSpoilerTap (_:), который будет вызываться при нажатии на спойлер.
extension ViewController {
@objc func handleSpoilerTap(_ sender: UITapGestureRecognizer) {
if let spoilerView = sender.view as? SpoilerView {
if spoilerView.emitterLayer.emitterCells == nil {
// Если анимация не запущена, запускаем ее и устанавливаем белый фон
spoilerView.startAnimation()
spoilerView.backgroundColor = .white
} else {
// Иначе останавливаем анимацию и делаем фон прозрачным
spoilerView.stopAnimation()
spoilerView.backgroundColor = .clear
}
}
}
}
Пояснения к коду:
● Проверка состояния анимации: Если ячейки эмиттера отсутствуют (emitterCells == nil), значит анимация остановлена, и мы запускаем ее.
● Изменение фона: Устанавливаем или убираем фон SpoilerView в зависимости от состояния спойлера.
● startAnimation () и stopAnimation (): Управляем анимацией спойлера.
Шаг 5: Обработка прокрутки и обновление позиций спойлеров
Если UITextView является прокручиваемым, нам нужно обновлять позиции SpoilerView при прокрутке.
extension ViewController {
func scrollViewDidScroll(_ scrollView: UIScrollView) {
updateSpoilerViewsPosition()
}
func updateSpoilerViewsPosition() {
let spoilerRectsArray = getRectsForSpoilerRanges()
var index = 0
for subview in textView.subviews where subview is SpoilerView {
let spoilerView = subview as! SpoilerView
let rects = spoilerRectsArray[index]
// Обновляем фрейм SpoilerView
let unionRect = rects.reduce(rects.first!) { $0.union($1) }
spoilerView.frame = unionRect
// Обновляем маску
let combinedPath = UIBezierPath()
for rect in rects {
let adjustedRect = rect.offsetBy(dx: -unionRect.origin.x, dy: -unionRect.origin.y)
combinedPath.append(UIBezierPath(rect: adjustedRect))
}
let maskLayer = CAShapeLayer()
maskLayer.path = combinedPath.cgPath
maskLayer.frame = spoilerView.bounds
spoilerView.layer.mask = maskLayer
index += 1
}
}
}
Пояснения к коду:
● scrollViewDidScroll (_:): Метод делегата UIScrollViewDelegate, который вызывается при прокрутке textView.
● updateSpoilerViewsPosition (): Обновляем фреймы и маски всех SpoilerView на основе текущего положения текста.
● Перебор спойлеров: Проходим по всем SpoilerView и обновляем их в соответствии с новыми позициями текста.
Шаг 6: Получение прямоугольников для спойлеров
Метод getRectsForSpoilerRanges () возвращает массив массивов CGRect, соответствующих областям спойлеров в UITextView.
extension ViewController {
func getRectsForSpoilerRanges() -> [[CGRect]] {
var rectsArray: [[CGRect]] = []
for range in spoilerRanges {
guard let start = textView.position(from: textView.beginningOfDocument, offset: range.location),
let end = textView.position(from: start, offset: range.length),
let textRange = textView.textRange(from: start, to: end) else {
continue
}
let selectionRects = textView.selectionRects(for: textRange)
var rects: [CGRect] = []
for selectionRect in selectionRects {
let rect = selectionRect.rect
rects.append(rect)
}
rectsArray.append(rects)
}
return rectsArray
}
}
Пояснения к коду:
● Перебор spoilerRanges: Для каждого диапазона спойлера находим соответствующие позиции в textView.
● selectionRects (for:): Получаем массив UITextSelectionRect, каждый из которых представляет прямоугольник выделения текста (учитывает переносы строк).
● Сбор прямоугольников: Извлекаем CGRect из каждого UITextSelectionRect и добавляем в массив rects.
● rectsArray: Массив массивов CGRect, где каждый внутренний массив соответствует одному спойлеру.
Заключение
Мы рассмотрели, как реализовать спойлер-эффект, похожий на Telegram, в iOS-приложении на Swift. Используя CAEmitterLayer, мы создали анимацию частиц, которая скрывает и раскрывает текст спойлера. Мы также разобрались, как работать с UITextView для определения диапазонов спойлеров и наложения SpoilerView поверх нужных частей текста.
Ключевые моменты:
● Использование CAEmitterLayer: Позволяет создавать впечатляющие анимации частиц.
● Работа с UITextView: Поиск и обработка определенных частей текста с помощью регулярных выражений и атрибутов текста.
● Маскирование слоев: Применение маски к SpoilerView для отображения анимации только на области спойлера.
● Обработка многострочных спойлеров: Учет переносов строк при определении областей спойлеров.
Спасибо за внимание! Надеюсь, эта статья была полезной и поможет вам в реализации спойлер-эффекта в ваших приложениях.