diff --git a/.github/workflows/lint-test.yml b/.github/workflows/lint-test.yml new file mode 100644 index 00000000..25e9cbce --- /dev/null +++ b/.github/workflows/lint-test.yml @@ -0,0 +1,39 @@ +name: Setup & Lint Package + +on: + pull_request: + branches: + - master + +# Only run on the latest workflow run +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + setup-and-lint-package: + runs-on: macos-latest + + steps: + - name: Checkout Repository + uses: actions/checkout@v3 + + - name: Set up Node.js + uses: actions/setup-node@v3 + with: + node-version: 18 + + - name: Enable corepack + run: corepack enable + + - name: Install CocoaPods + run: sudo gem install cocoapods + + - name: Install Dependencies + run: yarn install --immutable + + - name: Build project + run: yarn setup + + - name: SwiftLint + run: yarn lint:swift diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index e3c5de7b..4343d420 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -3,7 +3,6 @@ Contributions are welcome and are greatly appreciated! Every little bit helps, and credit will always be given. - ## Setting up your environment After forking to your own github org, do the following steps to get started: @@ -56,6 +55,19 @@ codebase, however you can always check to see if the source code is compliant by npm run lint ``` +For linting the native iOS package, we are using [Swift lint](https://github.com/realm/SwiftLint). You need to install it on your machine using the following command: + +```bash +brew install swiftlint +``` + +And then you can run it by calling it from JS using: + +```bash +yarn lint:swift +``` + +Or let it work on its own, as it is part of the build phases for the iOS project ### Building Docs @@ -69,7 +81,6 @@ After this, you can open up your browser to the specified port (usually http://l The browser will automatically refresh when there are changes to any of the source files. - ## Pull Request Guidelines Before you submit a pull request from your forked repo, check that it meets these guidelines: diff --git a/package.json b/package.json index 1c8bcb67..69a4860e 100644 --- a/package.json +++ b/package.json @@ -11,6 +11,7 @@ "fabric:ios": "yarn workspace fabric-example ios", "paper:android": "yarn workspace paper-example android", "paper:ios": "yarn workspace paper-example ios", + "lint:swift": "yarn workspace lottie-react-native lint:swift", "docs:clean": "rimraf _book", "docs:prepare": "gitbook install", "docs:build": "yarn docs:prepare && gitbook build", diff --git a/packages/core/ios/.swiftlint.yml b/packages/core/ios/.swiftlint.yml new file mode 100644 index 00000000..cc3926ea --- /dev/null +++ b/packages/core/ios/.swiftlint.yml @@ -0,0 +1,6 @@ +disabled_rules: + - line_length + - identifier_name +type_body_length: + - 300 + - 400 diff --git a/packages/core/ios/LottieReactNative/AnimationViewManagerModule.swift b/packages/core/ios/LottieReactNative/AnimationViewManagerModule.swift index 248e0021..83beee3f 100644 --- a/packages/core/ios/LottieReactNative/AnimationViewManagerModule.swift +++ b/packages/core/ios/LottieReactNative/AnimationViewManagerModule.swift @@ -18,21 +18,21 @@ class AnimationViewManagerModule: RCTViewManager { return ContainerView() } - @objc override func constantsToExport() -> [AnyHashable : Any]! { + @objc override func constantsToExport() -> [AnyHashable: Any]! { return ["VERSION": 1] } @objc(play:fromFrame:toFrame:) public func play(_ reactTag: NSNumber, startFrame: NSNumber, endFrame: NSNumber) { - self.bridge.uiManager.addUIBlock { (uiManager, viewRegistry) in + self.bridge.uiManager.addUIBlock { (_, viewRegistry) in guard let view = viewRegistry?[reactTag] as? ContainerView else { - if (RCT_DEBUG == 1) { + if RCT_DEBUG == 1 { print("Invalid view returned from registry, expecting ContainerView") } return } - if (startFrame.intValue != -1 && endFrame.intValue != -1) { + if startFrame.intValue != -1 && endFrame.intValue != -1 { view.play(fromFrame: AnimationFrameTime(truncating: startFrame), toFrame: AnimationFrameTime(truncating: endFrame)) } else { view.play() @@ -42,9 +42,9 @@ class AnimationViewManagerModule: RCTViewManager { @objc(reset:) public func reset(_ reactTag: NSNumber) { - self.bridge.uiManager.addUIBlock { uiManager, viewRegistry in + self.bridge.uiManager.addUIBlock { _, viewRegistry in guard let view = viewRegistry?[reactTag] as? ContainerView else { - if (RCT_DEBUG == 1) { + if RCT_DEBUG == 1 { print("Invalid view returned from registry, expecting ContainerView") } return @@ -56,9 +56,9 @@ class AnimationViewManagerModule: RCTViewManager { @objc(pause:) public func pause(_ reactTag: NSNumber) { - self.bridge.uiManager.addUIBlock { uiManager, viewRegistry in + self.bridge.uiManager.addUIBlock { _, viewRegistry in guard let view = viewRegistry?[reactTag] as? ContainerView else { - if (RCT_DEBUG == 1) { + if RCT_DEBUG == 1 { print("Invalid view returned from registry, expecting ContainerView") } return @@ -70,9 +70,9 @@ class AnimationViewManagerModule: RCTViewManager { @objc(resume:) public func resume(_ reactTag: NSNumber) { - self.bridge.uiManager.addUIBlock { uiManager, viewRegistry in + self.bridge.uiManager.addUIBlock { _, viewRegistry in guard let view = viewRegistry?[reactTag] as? ContainerView else { - if (RCT_DEBUG == 1) { + if RCT_DEBUG == 1 { print("Invalid view returned from registry, expecting ContainerView") } return diff --git a/packages/core/ios/LottieReactNative/ContainerView.swift b/packages/core/ios/LottieReactNative/ContainerView.swift index 467f0995..67f6ce90 100644 --- a/packages/core/ios/LottieReactNative/ContainerView.swift +++ b/packages/core/ios/LottieReactNative/ContainerView.swift @@ -2,8 +2,8 @@ import Lottie import Foundation @objc protocol LottieContainerViewDelegate { - func onAnimationFinish(isCancelled: Bool); - func onAnimationFailure(error: String); + func onAnimationFinish(isCancelled: Bool) + func onAnimationFailure(error: String) } /* There are Two Views being implemented here: @@ -22,215 +22,211 @@ class ContainerView: RCTView { private var colorFilters: [NSDictionary] = [] private var textFilters: [NSDictionary] = [] private var renderMode: RenderingEngineOption = .automatic - @objc weak var delegate: LottieContainerViewDelegate? = nil + @objc weak var delegate: LottieContainerViewDelegate? var animationView: LottieAnimationView? @objc var onAnimationFinish: RCTBubblingEventBlock? @objc var onAnimationFailure: RCTBubblingEventBlock? - + @objc var completionCallback: LottieCompletionBlock { return { [weak self] animationFinished in guard let self = self else { return } - + if let onFinish = self.onAnimationFinish { onFinish(["isCancelled": !animationFinished]) } - - self.delegate?.onAnimationFinish(isCancelled: !animationFinished); - }; + + self.delegate?.onAnimationFinish(isCancelled: !animationFinished) + } } - + @objc var failureCallback: (_ error: String) -> Void { return { [weak self] error in guard let self = self else { return } - + if let onFinish = self.onAnimationFailure { onFinish(["error": error]) } - + self.delegate?.onAnimationFailure(error: error) - }; + } } - + #if !(os(OSX)) override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { super.traitCollectionDidChange(previousTraitCollection) if #available(iOS 13.0, tvOS 13.0, *) { - if (self.traitCollection.hasDifferentColorAppearance(comparedTo: previousTraitCollection)) { - if(!colorFilters.isEmpty) { + if self.traitCollection.hasDifferentColorAppearance(comparedTo: previousTraitCollection) { + if !colorFilters.isEmpty { applyColorProperties() } } } } #endif - + @objc func setSpeed(_ newSpeed: CGFloat) { speed = newSpeed - - if (newSpeed != 0.0) { + + if newSpeed != 0.0 { animationView?.animationSpeed = newSpeed - if (!(animationView?.isAnimationPlaying ?? true)) { + if !(animationView?.isAnimationPlaying ?? true) { animationView?.play() } - } else if (animationView?.isAnimationPlaying ?? false) { + } else if animationView?.isAnimationPlaying ?? false { animationView?.pause() } } - + @objc func setProgress(_ newProgress: CGFloat) { progress = newProgress animationView?.currentProgress = progress } - + @objc func setLoop(_ isLooping: Bool) { loop = isLooping ? .loop : .playOnce animationView?.loopMode = loop } - + @objc func setAutoPlay(_ autoPlay: Bool) { self.autoPlay = autoPlay playIfNeeded() } - + @objc func setTextFiltersIOS(_ newTextFilters: [NSDictionary]) { textFilters = newTextFilters - - if (textFilters.count > 0) { - var filters = [String:String]() + + if textFilters.count > 0 { + var filters = [String: String]() for filter in textFilters { - let key = filter.value(forKey: "keypath") as! String - let value = filter.value(forKey: "text") as! String - filters[key] = value; + guard let key = filter.value(forKey: "keypath") as? String, + let value = filter.value(forKey: "text") as? String else { break } + filters[key] = value } - + let nextAnimationView = LottieAnimationView() nextAnimationView.textProvider = DictionaryTextProvider(filters) nextAnimationView.animation = animationView?.animation replaceAnimationView(next: nextAnimationView) } } - + var lottieConfiguration: LottieConfiguration { return LottieConfiguration( renderingEngine: renderMode ) } - + @objc func setRenderMode(_ newRenderMode: String) { switch newRenderMode { case "SOFTWARE": - if (renderMode == .mainThread) { + if renderMode == .mainThread { return } renderMode = .mainThread case "HARDWARE": - if (renderMode == .coreAnimation) { + if renderMode == .coreAnimation { return } renderMode = .coreAnimation - case "AUTOMATIC": - fallthrough default: - if (renderMode == .automatic) { + if renderMode == .automatic { return } renderMode = .automatic } - - if (animationView != nil) { + + if animationView != nil { let nextAnimationView = LottieAnimationView( animation: animationView?.animation, configuration: lottieConfiguration ) - + replaceAnimationView(next: nextAnimationView) } } - + @objc func setSourceDotLottieURI(_ uri: String) { - if(checkReactSourceString(uri)) { + if checkReactSourceString(uri) { return } - + guard let url = URL(string: uri) else { return } - + _ = LottieAnimationView( dotLottieUrl: url, configuration: lottieConfiguration, completion: { [weak self] view, error in guard let self = self else { return } - if let error = error { self.failureCallback(error.localizedDescription) return } - self.replaceAnimationView(next: view) } ) } - + @objc func setSourceURL(_ newSourceURLString: String) { - if(checkReactSourceString(newSourceURLString)) { + if checkReactSourceString(newSourceURLString) { return } - + var url = URL(string: newSourceURLString) - - if(url?.scheme == nil) { + + if url?.scheme == nil { // interpret raw URL paths as relative to the resource bundle url = URL(fileURLWithPath: newSourceURLString, relativeTo: Bundle.main.resourceURL) } - + guard let url = url else { return } - + self.fetchRemoteAnimation(from: url) } - + @objc func setSourceJson(_ newSourceJson: String) { - if(checkReactSourceString(newSourceJson)) { + if checkReactSourceString(newSourceJson) { return } - + sourceJson = newSourceJson - + guard let data = sourceJson.data(using: String.Encoding.utf8), let animation = try? JSONDecoder().decode(LottieAnimation.self, from: data) else { failureCallback("Unable to create the lottie animation object from the JSON source") return } - + let nextAnimationView = LottieAnimationView( animation: animation, configuration: lottieConfiguration ) - + replaceAnimationView(next: nextAnimationView) } - + @objc func setSourceName(_ newSourceName: String) { - if(checkReactSourceString(newSourceName)) { + if checkReactSourceString(newSourceName) { return } - - if (newSourceName == sourceName) { + + if newSourceName == sourceName { return } - + sourceName = newSourceName - + let nextAnimationView = LottieAnimationView( name: sourceName, configuration: lottieConfiguration ) - + replaceAnimationView(next: nextAnimationView) } - + @objc func setResizeMode(_ resizeMode: String) { - switch (resizeMode) { + switch resizeMode { case "cover": animationView?.contentMode = .scaleAspectFill case "contain": @@ -240,121 +236,121 @@ class ContainerView: RCTView { default: break } } - + @objc func setColorFilters(_ newColorFilters: [NSDictionary]) { colorFilters = newColorFilters applyColorProperties() } - + // 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); + let convertedFromFrame = fromFrame != nil ? CGFloat(truncating: fromFrame!) : nil + play(fromFrame: convertedFromFrame, toFrame: toFrame) } - + func play(fromFrame: AnimationFrameTime? = nil, toFrame: AnimationFrameTime) { - animationView?.play(fromFrame: fromFrame, toFrame: toFrame, loopMode: self.loop, completion: completionCallback); + animationView?.play(fromFrame: fromFrame, toFrame: toFrame, loopMode: self.loop, completion: completionCallback) } - + @objc func play() { animationView?.play(completion: completionCallback) } - + @objc func reset() { - animationView?.currentProgress = 0; + animationView?.currentProgress = 0 animationView?.pause() } - + @objc func pause() { animationView?.pause() } - + @objc func resume() { play() } - + // The animation view is a child of the RCTView, so if the bounds ever change, add those changes to the animation view as well override var bounds: CGRect { didSet { animationView?.frame = self.bounds } } - + // MARK: Private func replaceAnimationView(next: LottieAnimationView) { super.removeReactSubview(animationView) - + let contentMode = animationView?.contentMode ?? .scaleAspectFit - + animationView = next - + animationView?.contentMode = contentMode animationView?.backgroundBehavior = .pauseAndRestore animationView?.animationSpeed = speed animationView?.loopMode = loop animationView?.frame = self.bounds - + addSubview(next) - + applyColorProperties() playIfNeeded() } - - - + func applyColorProperties() { guard let animationView = animationView else { return } - - if (colorFilters.count > 0) { + + if colorFilters.count > 0 { for filter in colorFilters { - let keypath: String = "\(filter.value(forKey: "keypath") as! String).**.Color" + guard let key = filter.value(forKey: "keypath") as? String, + let platformColor = filter.value(forKey: "color") as? PlatformColor else { break } + let keypath: String = "\(key).**.Color" let fillKeypath = AnimationKeypath(keypath: keypath) - let colorFilterValueProvider = ColorValueProvider((filter.value(forKey: "color") as! PlatformColor).lottieColorValue) + let colorFilterValueProvider = ColorValueProvider(platformColor.lottieColorValue) animationView.setValueProvider(colorFilterValueProvider, keypath: fillKeypath) } } } - + func playIfNeeded() { - if(autoPlay && animationView?.isAnimationPlaying == false) { + if autoPlay && animationView?.isAnimationPlaying == false { self.play() } } - + private func checkReactSourceString(_ sourceStr: String?) -> Bool { guard let sourceStr = sourceStr else { return false } - + return sourceStr.isEmpty } - + private func fetchRemoteAnimation(from url: URL) { - URLSession.shared.dataTask(with: url) { [weak self] data, response, error in + URLSession.shared.dataTask(with: url) { [weak self] data, _, error in guard let self = self else { return } if let error = error { self.failureCallback("Unable to fetch the Lottie animation from the URL: \(error.localizedDescription)") return } - + guard let data = data else { self.failureCallback("No data received for the Lottie animation from the URL.") return } - + do { let animation = try JSONDecoder().decode(LottieAnimation.self, from: data) - + DispatchQueue.main.async { [weak self] in guard let self = self else { return } - + let nextAnimationView = LottieAnimationView( animation: animation, configuration: self.lottieConfiguration ) - + self.replaceAnimationView(next: nextAnimationView) } } catch { diff --git a/packages/core/package.json b/packages/core/package.json index b0d99595..5f1717bf 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -35,7 +35,9 @@ }, "scripts": { "build": "bob build", - "release": "release-it" + "release": "release-it", + "lint:swift": "swiftlint ios", + "lint-fix:swift": "swiftlint --fix ios" }, "peerDependencies": { "react": "*",