1 import Lottie
2 import Foundation
3 
4 @objc protocol LottieContainerViewDelegate {
onAnimationFinishnull5     func onAnimationFinish(isCancelled: Bool);
onAnimationFailurenull6     func onAnimationFailure(error: String);
7 }
8 
9 /* There are Two Views being implemented here:
10  1- The RCTView for React Native that has all of the normal props, and
11  2- a LottieAnimationView that is a child of the RCTView and is bound to the same coordinates, just on top of it
12  */
13 @objc(LottieContainerView)
14 class ContainerView: RCTView {
15     private var speed: CGFloat = 0.0
16     private var progress: CGFloat = 0.0
17     private var autoPlay: Bool = false
18     private var loop: LottieLoopMode = .playOnce
19     private var sourceJson: String = ""
20     private var resizeMode: String = ""
21     private var sourceName: String = ""
22     private var colorFilters: [NSDictionary] = []
23     private var textFilters: [NSDictionary] = []
24     private var renderMode: RenderingEngineOption = .automatic
25     @objc weak var delegate: LottieContainerViewDelegate? = nil
26     var animationView: LottieAnimationView?
27     @objc var onAnimationFinish: RCTBubblingEventBlock?
28     @objc var onAnimationFailure: RCTBubblingEventBlock?
29 
30     @objc var completionCallback: LottieCompletionBlock {
31         return { [weak self] animationFinished in
32             guard let self = self else { return }
33 
34             if let onFinish = self.onAnimationFinish {
35                 onFinish(["isCancelled": !animationFinished])
36             }
37 
38             self.delegate?.onAnimationFinish(isCancelled: !animationFinished);
39         };
40     }
41 
42     @objc var failureCallback: (_ error: String) -> Void {
43         return { [weak self] error in
44             guard let self = self else { return }
45 
46             if let onFinish = self.onAnimationFailure {
47                 onFinish(["error": error])
48             }
49 
50             self.delegate?.onAnimationFailure(error: error)
51         };
52     }
53 
54 #if !(os(OSX))
traitCollectionDidChangenull55     override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
56         super.traitCollectionDidChange(previousTraitCollection)
57         if #available(iOS 13.0, tvOS 13.0, *) {
58             if (self.traitCollection.hasDifferentColorAppearance(comparedTo: previousTraitCollection)) {
59                 if(!colorFilters.isEmpty) {
60                     applyColorProperties()
61                 }
62             }
63         }
64     }
65 #endif
66 
setSpeednull67     @objc func setSpeed(_ newSpeed: CGFloat) {
68         speed = newSpeed
69 
70         if (newSpeed != 0.0) {
71             animationView?.animationSpeed = newSpeed
72             if (!(animationView?.isAnimationPlaying ?? true)) {
73                 animationView?.play()
74             }
75         } else if (animationView?.isAnimationPlaying ?? false) {
76             animationView?.pause()
77         }
78     }
79 
setProgressnull80     @objc func setProgress(_ newProgress: CGFloat) {
81         progress = newProgress
82         animationView?.currentProgress = progress
83     }
84 
setLoopnull85     @objc func setLoop(_ isLooping: Bool) {
86         loop = isLooping ? .loop : .playOnce
87         animationView?.loopMode = loop
88     }
89 
setAutoPlaynull90     @objc func setAutoPlay(_ autoPlay: Bool) {
91         self.autoPlay = autoPlay
92         playIfNeeded()
93     }
94 
setTextFiltersIOSnull95     @objc func setTextFiltersIOS(_ newTextFilters: [NSDictionary]) {
96         textFilters = newTextFilters
97 
98         if (textFilters.count > 0) {
99             var filters = [String:String]()
100             for filter in textFilters {
101                 let key = filter.value(forKey: "keypath") as! String
102                 let value = filter.value(forKey: "text") as! String
103                 filters[key] = value;
104             }
105 
106             let nextAnimationView = LottieAnimationView()
107             nextAnimationView.textProvider = DictionaryTextProvider(filters)
108             nextAnimationView.animation = animationView?.animation
109             replaceAnimationView(next: nextAnimationView)
110         }
111     }
112 
113     var lottieConfiguration: LottieConfiguration {
114         return LottieConfiguration(
115             renderingEngine: renderMode
116         )
117     }
118 
setRenderModenull119     @objc func setRenderMode(_ newRenderMode: String) {
120         switch newRenderMode {
121         case "SOFTWARE":
122             if (renderMode == .mainThread) {
123                 return
124             }
125             renderMode = .mainThread
126         case "HARDWARE":
127             if (renderMode == .coreAnimation) {
128                 return
129             }
130             renderMode = .coreAnimation
131         case "AUTOMATIC":
132             fallthrough
133         default:
134             if (renderMode == .automatic) {
135                 return
136             }
137             renderMode = .automatic
138         }
139 
140         if (animationView != nil) {
141             let nextAnimationView = LottieAnimationView(
142                 animation: animationView?.animation,
143                 configuration: lottieConfiguration
144             )
145 
146             replaceAnimationView(next: nextAnimationView)
147         }
148     }
149 
setSourceDotLottieURInull150     @objc func setSourceDotLottieURI(_ uri: String) {
151         if(checkReactSourceString(uri)) {
152             return
153         }
154 
155         guard let url = URL(string: uri) else {
156             return
157         }
158 
159         _ = LottieAnimationView(
160             dotLottieUrl: url,
161             configuration: lottieConfiguration,
162             completion: { [weak self] view, error in
163                 guard let self = self else { return }
164 
165                 if let error = error {
166                     self.failureCallback(error.localizedDescription)
167                     return
168                 }
169 
170                 self.replaceAnimationView(next: view)
171             }
172         )
173     }
174 
setSourceURLnull175     @objc func setSourceURL(_ newSourceURLString: String) {
176         if(checkReactSourceString(newSourceURLString)) {
177             return
178         }
179 
180         var url = URL(string: newSourceURLString)
181 
182         if(url?.scheme == nil) {
183             // interpret raw URL paths as relative to the resource bundle
184             url = URL(fileURLWithPath: newSourceURLString, relativeTo: Bundle.main.resourceURL)
185         }
186 
187         guard let url = url else { return }
188 
189         self.fetchRemoteAnimation(from: url)
190     }
191 
setSourceJsonnull192     @objc func setSourceJson(_ newSourceJson: String) {
193         if(checkReactSourceString(newSourceJson)) {
194             return
195         }
196 
197         sourceJson = newSourceJson
198 
199         guard let data = sourceJson.data(using: String.Encoding.utf8),
200               let animation = try? JSONDecoder().decode(LottieAnimation.self, from: data) else {
201             failureCallback("Unable to create the lottie animation object from the JSON source")
202             return
203         }
204 
205         let nextAnimationView = LottieAnimationView(
206             animation: animation,
207             configuration: lottieConfiguration
208         )
209 
210         replaceAnimationView(next: nextAnimationView)
211     }
212 
setSourceNamenull213     @objc func setSourceName(_ newSourceName: String) {
214         if(checkReactSourceString(newSourceName)) {
215             return
216         }
217 
218         if (newSourceName == sourceName) {
219             return
220         }
221 
222         sourceName = newSourceName
223 
224         let nextAnimationView = LottieAnimationView(
225             name: sourceName,
226             configuration: lottieConfiguration
227         )
228 
229         replaceAnimationView(next: nextAnimationView)
230     }
231 
setResizeModenull232     @objc func setResizeMode(_ resizeMode: String) {
233         switch (resizeMode) {
234         case "cover":
235             animationView?.contentMode = .scaleAspectFill
236         case "contain":
237             animationView?.contentMode = .scaleAspectFit
238         case "center":
239             animationView?.contentMode = .center
240         default: break
241         }
242     }
243 
setColorFiltersnull244     @objc func setColorFilters(_ newColorFilters: [NSDictionary]) {
245         colorFilters = newColorFilters
246         applyColorProperties()
247     }
248 
249     // There is no Nullable CGFloat in Objective-C, so this function uses a Nullable NSNumber and converts it later
250     @objc(playFromFrame:toFrame:)
objcCompatiblePlaynull251     func objcCompatiblePlay(fromFrame: NSNumber? = nil, toFrame: AnimationFrameTime) {
252         let convertedFromFrame = fromFrame != nil ? CGFloat(truncating: fromFrame!) : nil;
253         play(fromFrame: convertedFromFrame, toFrame: toFrame);
254     }
255 
playnull256     func play(fromFrame: AnimationFrameTime? = nil, toFrame: AnimationFrameTime) {
257         animationView?.play(fromFrame: fromFrame, toFrame: toFrame, loopMode: self.loop, completion: completionCallback);
258     }
259 
playnull260     @objc func play() {
261         animationView?.play(completion: completionCallback)
262     }
263 
resetnull264     @objc func reset() {
265         animationView?.currentProgress = 0;
266         animationView?.pause()
267     }
268 
pausenull269     @objc func pause() {
270         animationView?.pause()
271     }
272 
resumenull273     @objc func resume() {
274         play()
275     }
276 
277     // The animation view is a child of the RCTView, so if the bounds ever change, add those changes to the animation view as well
278     override var bounds: CGRect {
279         didSet {
280             animationView?.frame = self.bounds
281         }
282     }
283 
284     // MARK: Private
replaceAnimationViewnull285     func replaceAnimationView(next: LottieAnimationView) {
286         super.removeReactSubview(animationView)
287 
288         let contentMode = animationView?.contentMode ?? .scaleAspectFit
289 
290         animationView = next
291 
292         animationView?.contentMode = contentMode
293         animationView?.backgroundBehavior = .pauseAndRestore
294         animationView?.animationSpeed = speed
295         animationView?.loopMode = loop
296         animationView?.frame = self.bounds
297 
298         addSubview(next)
299 
300         applyColorProperties()
301         playIfNeeded()
302     }
303 
304 
305 
applyColorPropertiesnull306     func applyColorProperties() {
307         guard let animationView = animationView else { return }
308 
309         if (colorFilters.count > 0) {
310             for filter in colorFilters {
311                 let keypath: String = "\(filter.value(forKey: "keypath") as! String).**.Color"
312                 let fillKeypath = AnimationKeypath(keypath: keypath)
313                 let colorFilterValueProvider = ColorValueProvider((filter.value(forKey: "color") as! PlatformColor).lottieColorValue)
314                 animationView.setValueProvider(colorFilterValueProvider, keypath: fillKeypath)
315             }
316         }
317     }
318 
playIfNeedednull319     func playIfNeeded() {
320         if(autoPlay && animationView?.isAnimationPlaying == false) {
321             self.play()
322         }
323     }
324 
checkReactSourceStringnull325     private func checkReactSourceString(_ sourceStr: String?) -> Bool {
326         guard let sourceStr = sourceStr else {
327             return false
328         }
329 
330         return sourceStr.isEmpty
331     }
332 
fetchRemoteAnimationnull333     private func fetchRemoteAnimation(from url: URL) {
334         URLSession.shared.dataTask(with: url) { [weak self] data, response, error in
335             guard let self = self else { return }
336 
337             if let error = error {
338                 self.failureCallback("Unable to fetch the Lottie animation from the URL: \(error.localizedDescription)")
339                 return
340             }
341 
342             guard let data = data else {
343                 self.failureCallback("No data received for the Lottie animation from the URL.")
344                 return
345             }
346 
347             do {
348                 let animation = try JSONDecoder().decode(LottieAnimation.self, from: data)
349 
350                 DispatchQueue.main.async { [weak self] in
351                     guard let self = self else { return }
352 
353                     let nextAnimationView = LottieAnimationView(
354                         animation: animation,
355                         configuration: self.lottieConfiguration
356                     )
357 
358                     self.replaceAnimationView(next: nextAnimationView)
359                 }
360             } catch {
361                 self.failureCallback("Unable to decode the Lottie animation object from the fetched URL source: \(error.localizedDescription)")
362             }
363         }.resume()
364     }
365 }
366