1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142
|
//
// Copyright (C) 2024 Apple Inc. All rights reserved.
//
#if canImport(WritingTools) && canImport(UIKit)
import OSLog
import WebKit
import WebKitSwift
internal import UIKit_Private
@_spi(TextEffects) import UIKit
@objc public enum WKTextAnimationType: Int {
case initial
case source
case final
}
@objc(WKSTextAnimationManager)
@MainActor public final class TextAnimationManager: NSObject {
private static let logger = Logger(subsystem: "com.apple.WebKit", category: "TextAnimationType")
final class TextEffectChunk: UITextEffectTextChunk {
public let uuid: UUID
public init(uuid: UUID) {
self.uuid = uuid
}
}
private var currentEffect: UITextEffectView.EffectID?
private lazy var effectView = UITextEffectView(source: self)
private var chunkToEffect = [UUID: UITextEffectView.EffectID]()
@objc public weak var delegate: WKSTextAnimationSourceDelegate?
@objc(initWithDelegate:) public init(with delegate: any WKSTextAnimationSourceDelegate) {
super.init()
self.delegate = delegate
delegate.containingViewForTextAnimationType().addSubview(self.effectView)
}
@objc(addTextAnimationForAnimationID:withStyleType:) public func beginEffect(for uuid: UUID, style: WKTextAnimationType) {
switch style {
case .initial:
let newEffect = self.effectView.addEffect(UITextEffectView.PonderingEffect(chunk: TextEffectChunk(uuid: uuid), view: self.effectView) as UITextEffectView.TextEffect)
self.chunkToEffect[uuid] = newEffect
case .source:
let newEffect = self.effectView.addEffect(UITextEffectView.ReplacementTextEffect(chunk: TextEffectChunk(uuid: uuid), view: self.effectView, delegate:self) as UITextEffectView.TextEffect)
self.chunkToEffect[uuid] = newEffect
case .final:
break
// Discard `.final` since we don't manually start the 2nd part of the animation on iOS.
}
}
@objc(removeTextAnimationForAnimationID:) public func endEffect(for uuid: UUID) {
if let effectID = chunkToEffect.removeValue(forKey: uuid) {
self.effectView.removeEffect(effectID)
}
}
}
@_spi(TextEffects)
extension TextAnimationManager: UITextEffectViewSource {
public func targetedPreview(for chunk: UITextEffectTextChunk) async -> UITargetedPreview {
guard let delegate = self.delegate else {
Self.logger.debug("Can't obtain Targeted Preview. Missing delegate." )
return UITargetedPreview(view: UIView(frame: .zero))
}
let defaultPreview = UITargetedPreview(view: UIView(frame: .zero), parameters: UIPreviewParameters(), target: UIPreviewTarget(container: delegate.containingViewForTextAnimationType(), center: delegate.containingViewForTextAnimationType().center))
guard let uuidChunk = chunk as? TextEffectChunk else {
Self.logger.debug("Can't get text preview. Incorrect UITextEffectTextChunk subclass")
return defaultPreview
}
guard let preview = await delegate.targetedPreview(for: uuidChunk.uuid) else {
Self.logger.debug("Could not generate a UITargetedPreview")
return defaultPreview
}
return preview
}
public func updateTextChunkVisibilityForAnimation(_ chunk: UITextEffectTextChunk, visible: Bool) async {
guard let uuidChunk = chunk as? TextEffectChunk else {
Self.logger.debug("Can't update text visibility. Incorrect UITextEffectTextChunk subclass")
return
}
guard let delegate = self.delegate else {
Self.logger.debug("Can't update Chunk Visibility. Missing delegate." )
return
}
await delegate.updateUnderlyingTextVisibility(forTextAnimationID:uuidChunk.uuid, visible: visible)
}
}
@_spi(TextEffects)
extension TextAnimationManager: UITextEffectView.ReplacementTextEffect.Delegate {
public func performReplacementAndGeneratePreview(for chunk: UITextEffectTextChunk, effect: UITextEffectView.ReplacementTextEffect, animation: UITextEffectView.ReplacementTextEffect.AnimationParameters) async -> UITargetedPreview? {
guard let uuidChunk = chunk as? TextEffectChunk else {
Self.logger.debug("Can't get text preview. Incorrect UITextEffectTextChunk subclass")
return nil
}
guard let delegate = self.delegate else {
Self.logger.debug("Can't obtain Targeted Preview. Missing delegate." )
return nil
}
let preview = await withCheckedContinuation { continuation in
delegate.callCompletionHandler(forAnimationID: uuidChunk.uuid) { preview in
continuation.resume(returning: preview)
}
}
return preview
}
public func replacementEffectDidComplete(_ effect: UITextEffectView.ReplacementTextEffect) {
self.effectView.removeEffect(effect.id)
guard let (animationID, _) = self.chunkToEffect.first(where: { (_, value) in value == effect.id }) else {
return
}
self.chunkToEffect[animationID] = nil
guard let delegate = self.delegate else {
Self.logger.debug("Missing delegate.")
return
}
delegate.callCompletionHandler(forAnimationID: animationID)
delegate.replacementEffectDidComplete();
}
}
#endif // canImport(WritingTools) && canImport(UIKit)
|