Спойлеры стали неотъемлемой частью общения в мессенджерах и социальных сетях. Они позволяют скрывать часть информации до тех пор, пока пользователь не захочет ее увидеть. В 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 для отображения анимации только на области спойлера.
● Обработка многострочных спойлеров: Учет переносов строк при определении областей спойлеров.
Спасибо за внимание! Надеюсь, эта статья была полезной и поможет вам в реализации спойлер-эффекта в ваших приложениях.
ссылка на оригинал статьи https://habr.com/ru/articles/847236/
Добавить комментарий