11import UIKit
2+ import ImageIO
3+ import MobileCoreServices
24
35@objcMembers
46open class BSTextAttachment : NSTextAttachment {
@@ -7,7 +9,7 @@ open class BSTextAttachment: NSTextAttachment {
79
810 public var displaySize : CGSize = CGSize ( width: 0 , height: 0 )
911
10- public private ( set ) var state : State = . placeholder
12+ public var state : State = . placeholder
1113
1214 public var cacheKey : String ?
1315
@@ -139,6 +141,127 @@ open class BSTextAttachment: NSTextAttachment {
139141 }
140142}
141143
144+ @objcMembers
145+ open class BSTextAnimatedImageAttachment : BSTextAttachment {
146+
147+ public private( set) var animatedFrames : [ UIImage ] = [ ]
148+ public private( set) var frameDurations : [ TimeInterval ] = [ ]
149+ public private( set) var totalDuration : TimeInterval = 0
150+
151+ private var displayLink : CADisplayLink ?
152+ private var currentFrameIndex : Int = 0
153+ private var elapsedTime : TimeInterval = 0
154+
155+ public override init ( type: BSTextAttachmentType ) {
156+ super. init ( type: type)
157+ if type == . animatedImage {
158+ displaySize = CGSize ( width: 200 , height: 200 )
159+ }
160+ }
161+
162+ public required init ? ( coder: NSCoder ) {
163+ super. init ( coder: coder)
164+ }
165+
166+ public func loadAnimatedImage( from data: Data ) {
167+ guard let source = CGImageSourceCreateWithData ( data as CFData , nil ) else {
168+ state = . failed
169+ delegate? . attachmentDidFailLoading ? ( self )
170+ return
171+ }
172+
173+ let frameCount = CGImageSourceGetCount ( source)
174+ guard frameCount > 0 else {
175+ state = . failed
176+ delegate? . attachmentDidFailLoading ? ( self )
177+ return
178+ }
179+
180+ var frames : [ UIImage ] = [ ]
181+ var durations : [ TimeInterval ] = [ ]
182+ var totalDuration : TimeInterval = 0
183+
184+ for i in 0 ..< frameCount {
185+ guard let cgImage = CGImageSourceCreateImageAtIndex ( source, i, nil ) else {
186+ continue
187+ }
188+
189+ let image = UIImage ( cgImage: cgImage)
190+ frames. append ( image)
191+
192+ var duration : TimeInterval = 0.1
193+
194+ if let properties = CGImageSourceCopyPropertiesAtIndex ( source, i, nil ) as? [ String : Any ] ,
195+ let gifProperties = properties [ kCGImagePropertyGIFDictionary as String ] as? [ String : Any ] {
196+ if let unclampedDelayTime = gifProperties [ kCGImagePropertyGIFUnclampedDelayTime as String ] as? TimeInterval {
197+ duration = unclampedDelayTime
198+ } else if let delayTime = gifProperties [ kCGImagePropertyGIFDelayTime as String ] as? TimeInterval {
199+ duration = delayTime
200+ }
201+ }
202+
203+ if duration < 0.02 {
204+ duration = 0.1
205+ }
206+
207+ durations. append ( duration)
208+ totalDuration += duration
209+ }
210+
211+ self . animatedFrames = frames
212+ self . frameDurations = durations
213+ self . totalDuration = totalDuration
214+ self . state = . loaded
215+
216+ if let firstFrame = frames. first {
217+ self . image = firstFrame
218+ }
219+
220+ delegate? . attachmentDidFinishLoading ? ( self )
221+ }
222+
223+ public func startAnimating( ) {
224+ guard displayLink == nil , animatedFrames. count > 1 else { return }
225+
226+ displayLink = CADisplayLink ( target: self , selector: #selector( updateFrame) )
227+ displayLink? . add ( to: . main, forMode: . common)
228+ }
229+
230+ public func stopAnimating( ) {
231+ displayLink? . invalidate ( )
232+ displayLink = nil
233+ currentFrameIndex = 0
234+ elapsedTime = 0
235+ }
236+
237+ @objc private func updateFrame( _ displayLink: CADisplayLink ) {
238+ guard frameDurations. count > currentFrameIndex else {
239+ currentFrameIndex = 0
240+ return
241+ }
242+
243+ let frameDuration = frameDurations [ currentFrameIndex]
244+ elapsedTime += displayLink. duration
245+
246+ if elapsedTime >= frameDuration {
247+ elapsedTime = 0
248+ currentFrameIndex = ( currentFrameIndex + 1 ) % animatedFrames. count
249+ self . image = animatedFrames [ currentFrameIndex]
250+ }
251+ }
252+
253+ open override func image( forBounds imageBounds: CGRect , textContainer: NSTextContainer ? , characterIndex charIndex: Int ) -> UIImage ? {
254+ if state == . loaded, let firstFrame = animatedFrames. first {
255+ return firstFrame
256+ }
257+ return super. image ( forBounds: imageBounds, textContainer: textContainer, characterIndex: charIndex)
258+ }
259+
260+ deinit {
261+ stopAnimating ( )
262+ }
263+ }
264+
142265@objc public protocol BSTextAttachmentDelegate : NSObjectProtocol {
143266 @objc optional func attachmentDidStartLoading( _ attachment: BSTextAttachment )
144267 @objc optional func attachmentDidFinishLoading( _ attachment: BSTextAttachment )
@@ -154,6 +277,31 @@ public extension BSTextAttachment {
154277 return attachment
155278 }
156279
280+ static func animatedImageAttachment( url: URL , displaySize: CGSize ? = nil ) -> BSTextAnimatedImageAttachment {
281+ let attachment = BSTextAnimatedImageAttachment ( type: . animatedImage)
282+ attachment. url = url
283+ attachment. displaySize = displaySize ?? CGSize ( width: 200 , height: 200 )
284+
285+ Task {
286+ do {
287+ let ( data, _) = try await URLSession . shared. data ( from: url)
288+ attachment. loadAnimatedImage ( from: data)
289+ attachment. startAnimating ( )
290+ } catch {
291+ attachment. state = . failed
292+ }
293+ }
294+
295+ return attachment
296+ }
297+
298+ static func animatedImageAttachment( data: Data , displaySize: CGSize ? = nil ) -> BSTextAnimatedImageAttachment {
299+ let attachment = BSTextAnimatedImageAttachment ( type: . animatedImage)
300+ attachment. displaySize = displaySize ?? CGSize ( width: 200 , height: 200 )
301+ attachment. loadAnimatedImage ( from: data)
302+ return attachment
303+ }
304+
157305 static func emojiAttachment( emoji: String ) -> BSTextAttachment {
158306 let attachment = BSTextAttachment ( type: . image)
159307 let font = UIFont . systemFont ( ofSize: 32 )
0 commit comments