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

Погрузитесь в исходный код

Если вам интересно изучить исходный код Telegram, то вы обнаружите, что для реализации эффекта спойлера они используют классы CAEmitterLayer и CAEmitterCell из Core Animation. CAEmitterLayer — это мощный класс, который позволяет создавать эффект частиц, вроде огня, дыма или снега. В случае спойлер‑эффекта Telegram, CAEmitterLayer используется для генерации облака частиц, которые скрывают текст спойлера:

let emitter = CAEmitterCell()
emitter.contents = UIImage(bundleImageName: "Components/TextSpeckle")?.cgImage
emitter.contentsScale = 1.8
emitter.emissionRange = .pi * 2.0
emitter.lifetime = 1.0
emitter.scale = 0.5
emitter.velocityRange = 20.0
emitter.name = "dustCell"
emitter.alphaRange = 1.0
emitter.setValue("point", forKey: "particleType")
emitter.setValue(3.0, forKey: "mass")
emitter.setValue(2.0, forKey: "massRange")

Мы будем переиспользовать их конфигурацию, но в итоге мы хотим получить что-то вроде этого:

import SwiftUI

struct ContentView: View {

    @State var spoilerIsOn = true

    var body: some View {
        Text("Everything will be good")
            .font(.title)
            .spoiler(isOn: $spoilerIsOn)
    }
}

CAEmitterLayer в SwiftUI

Мы можем создать пользовательский подкласс UIView, который был спроектирован специально для работы с CAEmitterLayer:

final class EmitterView: UIView {

    override class var layerClass: AnyClass {
        CAEmitterLayer.self
    }

    override var layer: CAEmitterLayer {
        super.layer as! CAEmitterLayer
    }

    override func layoutSubviews() {
        super.layoutSubviews()
        layer.emitterPosition = .init(x: bounds.size.width / 2,
                                      y: bounds.size.height / 2)
        layer.emitterSize = bounds.size
    }
}

Мы переопределяем свойство layerClass, чтобы указать, что слой для этого вью должен быть с типом CAEmitterLayer. Мы также переопределяем свойство layer для приведения super.layer к типу CAEmitterLayer, что позволяет нам легче получить доступ к свойствам и методам слоя эмиттера.

Обратите внимание на функцию layoutSubviews(). В ней необходимо установить позицию и размер для эмиттера.

Далее мы используем обертку UIViewRepresentable и добавим настройки для ячеек эмиттера:

struct SpoilerView: UIViewRepresentable {

    var isOn: Bool

    func makeUIView(context: Context) -> EmitterView {
        let emitterView = EmitterView()

        let emitterCell = CAEmitterCell()
        emitterCell.contents = UIImage(named: "textSpeckle_Normal")?.cgImage
        emitterCell.color = UIColor.black.cgColor
        emitterCell.contentsScale = 1.8
        emitterCell.emissionRange = .pi * 2
        emitterCell.lifetime = 1
        emitterCell.scale = 0.5
        emitterCell.velocityRange = 20
        emitterCell.alphaRange = 1
        emitterCell.birthRate = 4000

        emitterView.layer.emitterShape = .rectangle
        emitterView.layer.emitterCells = [emitterCell]

        return emitterView
    }

    func updateUIView(_ uiView: EmitterView, context: Context) {
        if isOn {
            uiView.layer.beginTime = CACurrentMediaTime()
        }
        uiView.layer.birthRate = isOn ? 1 : 0
    }
}

Свойство isOn в SpoilerView используется для изменения видимости эффекта частиц через свойство birthRate. Изображение textSpeckle_Normal используется повторно из Telegram, это простая белая точка, мы можем использовать любое изображение для частиц или генерировать его во время выполнения. И, конечно, мы можем спокойно передать свойствам любые константы, но, чтобы упростить пример, мы их захаркодили.

Хардкод

Модификаторы и расширения

Мы реализуем структуру SpoilerModifier, которая добавляет спойлер к любому представлению. Она принимает логическое значение для переключения видимости эффекта.

struct SpoilerModifier: ViewModifier {

    let isOn: Bool

    func body(content: Content) -> some View {
        content.overlay {
            SpoilerView(isOn: isOn)
        }
    }
}

Теперь мы можем использовать его как обычный модификатор:

import SwiftUI

struct ContentView: View {

    var body: some View {
        Text("Everything will be good")
            .font(.title)
            .opacity(0)
            .modifier(SpoilerModifier(isOn: true))
    }
}

Мы также можем расширить протокол View и добавить некоторые полезные модификаторы:

extension View {

    func spoiler(isOn: Binding) -> some View {
        self
            // 1
            .opacity(isOn.wrappedValue ? 0 : 1)
            // 2
            .modifier(SpoilerModifier(isOn: isOn.wrappedValue))
            // 3
            .animation(.default, value: isOn.wrappedValue)
            // 4
            .onTapGesture {
                isOn.wrappedValue.toggle()
            }
    }
}

Давайте рассмотрим здесь каждую строку кода:

  1. Скрывает содержимое, если спойлер включен.

  2. Добавляет модификатор спойлера.

  3. Добавляет анимацию по умолчанию для плавных переходов между состояниями. Тип анимации также можно передать через параметры, чтобы менять его извне.

  4. Добавляет жест касания для изменения состояний.

Теперь мы можем применять эти модификаторы к любому вью в наших приложениях. Он не работает точно как в Telegram, т.к. должен накладываться на каждое слово отдельно и убирать частицы, начиная от области касания. Согласно интерфейсу Text в SwiftUI и изначальной мудреной реализации (привет, private API) — этот способ будет простым и полезным для прямоугольников, таких как однострочный текст или изображения. Если вы знаете, как улучшить его, не стесняйтесь писать мне в Twitter.

Если вы хотите поиграть с SpoilerView самостоятельно, ознакомьтесь с проектом SpoilerViewExample на Github.

Ссылки:

Комментарии (0)