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