Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fabric support for iOS #955

Merged
merged 15 commits into from
Jan 17, 2023
Merged
3 changes: 3 additions & 0 deletions apps/fabric/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ const App = () => {
source={require('./LottieLogo1.json')}
autoPlay
loop
onAnimationFinish={() => {
console.log('finished');
}}
enableMergePathsAndroidForKitKatAndAbove
/>
);
Expand Down
11 changes: 5 additions & 6 deletions apps/fabric/ios/Podfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,12 @@ PODS:
- lottie-ios (3.5.0)
- lottie-react-native (6.0.0-rc.1):
- lottie-ios (~> 3.5.0)
- lottie-react-native/fabric (= 6.0.0-rc.1)
- React-Core
- lottie-react-native/fabric (6.0.0-rc.1):
- lottie-ios (~> 3.5.0)
- RCT-Folly
alfonsocj marked this conversation as resolved.
Show resolved Hide resolved
- RCTRequired
- RCTTypeSafety
- React-Codegen
- React-Core
- React-RCTFabric
- ReactCommon/turbomodule/core
- RCT-Folly (2021.07.22.00):
- boost
- DoubleConversion
Expand Down Expand Up @@ -787,7 +786,7 @@ SPEC CHECKSUMS:
hermes-engine: bb344d89a0d14c2c91ad357480a79698bb80e186
libevent: 4049cae6c81cdb3654a443be001fb9bdceff7913
lottie-ios: c55158d67d0629a260625cc2ded2052b829e3c3e
lottie-react-native: 7bf794e0ee88accc91af65598efbbfacc2d5295f
lottie-react-native: 2c0a420b4f9ef28fceb6b050fb95fad0720ddfd1
RCT-Folly: 0080d0a6ebf2577475bda044aa59e2ca1f909cda
RCTRequired: 5cf7e7d2f12699724b59f90350257a422eaa9492
RCTTypeSafety: 3f3ead9673d1ab8bb1aea85b0894ab3220f8f06e
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,11 @@ import com.airbnb.android.react.lottie.LottieAnimationViewManagerImpl.setTextFil
import com.airbnb.lottie.LottieAnimationView
import com.facebook.react.bridge.Arguments
import com.facebook.react.bridge.ReadableArray
import com.facebook.react.bridge.ReadableMap;
import com.facebook.react.module.annotations.ReactModule
import com.facebook.react.uimanager.SimpleViewManager
import com.facebook.react.uimanager.ThemedReactContext
import com.facebook.react.uimanager.UIManagerHelper
import com.facebook.react.uimanager.ViewManagerDelegate
import com.facebook.react.uimanager.annotations.ReactProp
import com.facebook.react.uimanager.events.RCTModernEventEmitter
Expand Down Expand Up @@ -52,6 +54,7 @@ class LottieAnimationViewManager : SimpleViewManager<LottieAnimationView>(),
event.putBoolean("isCancelled", isCancelled)

val screenContext = view.context

if (screenContext is ThemedReactContext) {
screenContext.getJSModule(RCTModernEventEmitter::class.java)
?.receiveEvent(
Expand Down Expand Up @@ -210,4 +213,9 @@ class LottieAnimationViewManager : SimpleViewManager<LottieAnimationView>(),
override fun setTextFiltersIOS(view: LottieAnimationView?, value: ReadableArray?) {
//ignore - do nothing here
}

// Only here to solve an iOS issue with codegen. Check dummy prop in LottieAnimationViewNativeComponent.ts
override fun setDummy(view: LottieAnimationView, value: ReadableMap?) {
//ignore - do nothing here
}
}
27 changes: 27 additions & 0 deletions packages/core/ios/Fabric/LottieAnimationViewComponentView.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
#ifdef __cplusplus
matinzd marked this conversation as resolved.
Show resolved Hide resolved
#import <React/RCTViewComponentView.h>
#import <react/renderer/components/lottiereactnative/Props.h>
#import "LottieContainerView.h"

NS_ASSUME_NONNULL_BEGIN

@interface LottieAnimationViewComponentView : RCTViewComponentView <LottieContainerViewDelegate>
@end

namespace facebook {
namespace react {
// In order to compare these structs we need to add the == operator for each
bool operator==(const LottieAnimationViewColorFiltersStruct& a, const LottieAnimationViewColorFiltersStruct& b)
alfonsocj marked this conversation as resolved.
Show resolved Hide resolved
{
return b.keypath == a.keypath && b.color == a.color;
}
bool operator==(const LottieAnimationViewTextFiltersIOSStruct& a, const LottieAnimationViewTextFiltersIOSStruct& b)
{
return b.keypath == a.keypath && b.text == a.text;
}
}
}

NS_ASSUME_NONNULL_END

#endif
140 changes: 140 additions & 0 deletions packages/core/ios/Fabric/LottieAnimationViewComponentView.mm
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
#import "LottieAnimationViewComponentView.h"

#import <react/renderer/components/lottiereactnative/ComponentDescriptors.h>
#import <react/renderer/components/lottiereactnative/EventEmitters.h>
#import <react/renderer/components/lottiereactnative/Props.h>
#import <react/renderer/components/lottiereactnative/RCTComponentViewHelpers.h>

#import "RCTFabricComponentsPlugins.h"
#import <React/RCTView.h>
#import "LottieContainerView.h"
#import "RNLottieFabricConversions.h"

using namespace facebook::react;

@interface LottieAnimationViewComponentView () <RCTLottieAnimationViewViewProtocol>
@end

@implementation LottieAnimationViewComponentView {
LottieContainerView *_view;
}

- (instancetype) initWithFrame:(CGRect)frame
{
if (self = [super initWithFrame:frame]) {
static const auto defaultProps = std::make_shared<const LottieAnimationViewProps>();
_props = defaultProps;

_view = [LottieContainerView new];
_view.delegate = self;

self.contentView = _view;
}

return self;
}

#pragma mark - RCTComponentViewProtocol

+ (ComponentDescriptorProvider)componentDescriptorProvider
{
return concreteComponentDescriptorProvider<LottieAnimationViewComponentDescriptor>();
}

- (void)updateProps:(Props::Shared const &)props oldProps:(Props::Shared const &)oldProps {
const auto &oldLottieProps = *std::static_pointer_cast<const LottieAnimationViewProps>(_props);
const auto &newLottieProps = *std::static_pointer_cast<const LottieAnimationViewProps>(props);

if(oldLottieProps.resizeMode != newLottieProps.resizeMode) {
[_view setResizeMode:RCTNSStringFromString(newLottieProps.resizeMode)];
}

if(oldLottieProps.sourceJson != newLottieProps.sourceJson) {
[_view setSourceJson:RCTNSStringFromString(newLottieProps.sourceJson)];
}

if(oldLottieProps.sourceName != newLottieProps.sourceName) {
[_view setSourceName:RCTNSStringFromString(newLottieProps.sourceName)];
}

if(oldLottieProps.sourceURL != newLottieProps.sourceURL) {
[_view setSourceURL:RCTNSStringFromString(newLottieProps.sourceURL)];
}

if(oldLottieProps.progress != newLottieProps.progress) {
[_view setProgress:newLottieProps.progress];
}

if(oldLottieProps.loop != newLottieProps.loop) {
[_view setLoop:newLottieProps.loop];
}

if(oldLottieProps.speed != newLottieProps.speed) {
[_view setSpeed:newLottieProps.speed];
}

if(oldLottieProps.colorFilters != newLottieProps.colorFilters) {
[_view setColorFilters:convertColorFilters(newLottieProps.colorFilters)];
}

if(oldLottieProps.textFiltersIOS != newLottieProps.textFiltersIOS) {
[_view setTextFiltersIOS:convertTextFilters(newLottieProps.textFiltersIOS)];
}

if(oldLottieProps.renderMode != newLottieProps.renderMode) {
[_view setRenderMode:RCTNSStringFromString(newLottieProps.renderMode)];
}

[super updateProps:props oldProps:oldProps];
}

#pragma mark - Native Commands

- (void)handleCommand:(const NSString *)commandName args:(const NSArray *)args
{
RCTLottieAnimationViewHandleCommand(self, commandName, args);
}

- (void) play:(NSInteger)startFrame endFrame:(NSInteger)endFrame
{
if (startFrame != -1 && endFrame != -1) {
[_view playFromFrame:@(startFrame) toFrame:endFrame];
} else {
[_view play];
}
}

- (void) reset
{
[_view reset];
}

- (void) pause
{
[_view pause];
}

- (void) resume
{
[_view resume];
}

- (void)onAnimationFinishWithIsCancelled:(bool)isCancelled
{
if(!_eventEmitter) {
return;
}

LottieAnimationViewEventEmitter::OnAnimationFinish event = {
.isCancelled = isCancelled
};

std::dynamic_pointer_cast<const LottieAnimationViewEventEmitter>(_eventEmitter)->onAnimationFinish(event);
}

@end

Class<RCTComponentViewProtocol> LottieAnimationViewCls(void)
{
return LottieAnimationViewComponentView.class;
}
33 changes: 33 additions & 0 deletions packages/core/ios/Fabric/LottieContainerView.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
@class UITraitCollection;
@class NSDictionary;
@class NSString;
@class NSCoder;

#import <React/RCTView.h>

@protocol LottieContainerViewDelegate
- (void)onAnimationFinishWithIsCancelled:(BOOL)isCancelled;
@end

@interface LottieContainerView : RCTView
alfonsocj marked this conversation as resolved.
Show resolved Hide resolved
@property (nonatomic, weak) id <LottieContainerViewDelegate> _Nullable delegate;
- (void)traitCollectionDidChange:(UITraitCollection * _Nullable)previousTraitCollection;
- (void)setSpeed:(CGFloat)newSpeed;
- (void)setProgress:(CGFloat)newProgress;
- (void)reactSetFrame:(CGRect)frame;
- (void)setLoop:(BOOL)isLooping;
- (void)setTextFiltersIOS:(NSArray<NSDictionary *> * _Nonnull)newTextFilters;
- (void)setRenderMode:(NSString * _Nonnull)newRenderMode;
- (void)setSourceURL:(NSString * _Nonnull)newSourceURLString;
- (void)setSourceJson:(NSString * _Nonnull)newSourceJson;
- (void)setSourceName:(NSString * _Nonnull)newSourceName;
- (void)setResizeMode:(NSString * _Nonnull)resizeMode;
- (void)setColorFilters:(NSArray<NSDictionary *> * _Nonnull)newColorFilters;
- (void)play;
- (void)playFromFrame:(NSNumber * _Nullable)fromFrame toFrame:(CGFloat)toFrame;
- (void)reset;
- (void)pause;
- (void)resume;
- (nonnull instancetype)initWithFrame:(CGRect)frame;
- (nullable instancetype)initWithCoder:(NSCoder * _Nonnull)coder;
@end
29 changes: 29 additions & 0 deletions packages/core/ios/Fabric/RNLottieFabricConversions.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
#ifdef __cplusplus
#import <React/RCTConversions.h>

inline NSArray<NSDictionary *> *convertColorFilters(std::vector<facebook::react::LottieAnimationViewColorFiltersStruct> colorFilterStructArr)
matinzd marked this conversation as resolved.
Show resolved Hide resolved
{
NSMutableArray *filters = [NSMutableArray arrayWithCapacity:colorFilterStructArr.size()];

for (auto colorFilter : colorFilterStructArr) {
[filters addObject:@{
@"color": RCTUIColorFromSharedColor(colorFilter.color),
@"keypath": RCTNSStringFromString(colorFilter.keypath),
}];
}
return filters;
}

inline NSArray<NSDictionary *> *convertTextFilters(std::vector<facebook::react::LottieAnimationViewTextFiltersIOSStruct> textFilterStructArr)
{
NSMutableArray *filters = [NSMutableArray arrayWithCapacity:textFilterStructArr.size()];

for (auto textFilter : textFilterStructArr) {
[filters addObject:@{
@"text": RCTNSStringFromString(textFilter.text),
@"keypath": RCTNSStringFromString(textFilter.keypath),
}];
}
return filters;
}
#endif
34 changes: 28 additions & 6 deletions packages/core/ios/LottieReactNative/ContainerView.swift
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
import Lottie
import Foundation

@objc protocol LottieContainerViewDelegate {
func onAnimationFinish(isCancelled: Bool);
}

@objc(LottieContainerView)
class ContainerView: RCTView {
private var speed: CGFloat = 0.0
private var progress: CGFloat = 0.0
Expand All @@ -11,9 +16,11 @@ class ContainerView: RCTView {
private var colorFilters: [NSDictionary] = []
private var textFilters: [NSDictionary] = []
private var renderMode: RenderingEngineOption = .automatic
@objc weak var delegate: LottieContainerViewDelegate? = nil
alfonsocj marked this conversation as resolved.
Show resolved Hide resolved

@objc var onAnimationFinish: RCTBubblingEventBlock?
var animationView: LottieAnimationView?

override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
super.traitCollectionDidChange(previousTraitCollection)
if #available(iOS 13.0, tvOS 13.0, *) {
Expand Down Expand Up @@ -190,43 +197,58 @@ class ContainerView: RCTView {
applyProperties()
}

// There is no Nullable CGFloat in Objective-C, so this function uses a Nullable NSNumber and converts it later
@objc(playFromFrame:toFrame:)
func objcCompatiblePlay(fromFrame: NSNumber? = nil, toFrame: AnimationFrameTime) {
let convertedFromFrame = fromFrame != nil ? CGFloat(truncating: fromFrame!) : nil;
play(fromFrame: convertedFromFrame, toFrame: toFrame);
}

func play(fromFrame: AnimationFrameTime? = nil, toFrame: AnimationFrameTime) {
let callback: LottieCompletionBlock = { animationFinished in
if let onFinish = self.onAnimationFinish {
onFinish(["isCancelled": !animationFinished])
}
self.delegate?.onAnimationFinish(isCancelled: !animationFinished);
}

animationView?.backgroundBehavior = .pauseAndRestore
animationView?.play(fromFrame: fromFrame, toFrame: toFrame, loopMode: self.loop, completion: callback);
}

func play() {
@objc func play() {
let callback: LottieCompletionBlock = { animationFinished in
if let onFinish = self.onAnimationFinish {
onFinish(["isCancelled": !animationFinished])
}
self.delegate?.onAnimationFinish(isCancelled: !animationFinished);
}

animationView?.backgroundBehavior = .pauseAndRestore
animationView?.play(completion: callback)
}

func reset() {
@objc func reset() {
animationView?.currentProgress = 0;
animationView?.pause()
}

func pause() {
@objc func pause() {
animationView?.pause()
}

func resume() {
@objc func resume() {
play()
}

// reset the animationView frame whenever the view's frame changes
override var frame: CGRect {
matinzd marked this conversation as resolved.
Show resolved Hide resolved
didSet {
animationView?.reactSetFrame(frame)
}
}

// MARK: Private

func replaceAnimationView(next: LottieAnimationView) {
animationView?.removeFromSuperview()

Expand Down