Skip to content

Commit

Permalink
Merge pull request #19 from jduigoudev/feature/ESOB007-Animation-free
Browse files Browse the repository at this point in the history
Feature/esob007 animation free
  • Loading branch information
jduigoudev authored Jan 31, 2024
2 parents 68ba81c + 8ef3d74 commit 0569cc7
Show file tree
Hide file tree
Showing 15 changed files with 356 additions and 2 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
/*
* ecoCode iOS plugin - Help the earth, adopt this green plugin for your applications
* Copyright © 2023 green-code-initiative (https://www.ecocode.io/)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package io.ecocode.ios.swift.checks.sobriety;

import io.ecocode.ios.swift.SwiftRuleCheck;
import io.ecocode.ios.swift.antlr.generated.Swift5Parser;
import org.antlr.v4.runtime.tree.ParseTree;
import org.sonar.check.Rule;

import java.util.Arrays;
import java.util.List;


/**
* Check the use of "UIScreen.main.brightness" and triggers when set.
*/
@Rule(key = "ESOB007")
public class AnimationFreeCheck extends SwiftRuleCheck {

private static final String DEFAULT_ISSUE_MESSAGE = "Usage of Animations must absolutely be avoided";

private static final List<String> ANIMATION_METHODS = Arrays.asList("UIView.animate", "UIView.animateKeyframes",
"UIView.transition", "CABasicAnimation", "CAKeyframeAnimation", "CATransition");

private static final List<String> SWIFTUI_ANIMATION_METHODS = Arrays.asList("withAnimation", "Animation",
"AnyTransition");


public void apply(ParseTree tree) {
if (tree instanceof Swift5Parser.ExpressionContext) {
Swift5Parser.ExpressionContext id = (Swift5Parser.ExpressionContext) tree;
String expressionText = id.getText();

boolean containsAnimationMethod = ANIMATION_METHODS.stream()
.anyMatch(expressionText::contains);

boolean containsUISwiftAnimationMethod = SWIFTUI_ANIMATION_METHODS.stream()
.anyMatch(expressionText::contains);

if (containsAnimationMethod || containsUISwiftAnimationMethod) {
this.recordIssue(id.getStart().getStartIndex(), DEFAULT_ISSUE_MESSAGE);
}
}
}
}
3 changes: 2 additions & 1 deletion swift-lang/src/main/resources/ecocode_swift_profile.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
"ESOB002",
"ESOB003",
"ESOB005",
"ESOB006"
"ESOB006",
"ESOB007"
]
}
26 changes: 26 additions & 0 deletions swift-lang/src/main/resources/io/ecocode/rules/swift/ESOB007.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<img src="http://www.neomades.com/extern/partage/ecoCode/2sur5_1x.png">
<p>Animations play a crucial role in creating a dynamic and engaging user interface in iOS applications. Both UIKit and SwiftUI provide powerful tools to create smooth and appealing animations. Developers need to be familiar with various methods and properties to implement animations effectively.</p>
<h2>Animation Methods in UIKit and SwiftUI</h2>
<h2>Noncompliant Code Example UIKit</h2>
<ul>
<li><code>UIView.animate(withDuration:animations:)</code></li>
<li><code>UIView.animate(withDuration:animations:completion:)</code></li>
<li><code>UIView.animate(withDuration:delay:options:animations:completion:)</code></li>
<li><code>UIView.animateKeyframes(withDuration:delay:options:animations:completion:)</code></li>
<li><code>UIView.transition(with:duration:options:animations:completion:)</code></li>
<li><code>CABasicAnimation</code></li>
<li><code>CAKeyframeAnimation</code></li>
<li><code>CATransition</code></li>
</ul>
<h2>Noncompliant Code Example SwiftUI</h2>
<ul>
<li><code>withAnimation</code></li>
<li><code>Animation</code></li>
<li><code>AnyTransition</code></li>
<li><code>.animation(Animation?)</code></li>
<li><code>.transition(AnyTransition)</code></li>
<li><code>.onAppear(perform:)</code></li>
<li><code>.onDisappear(perform:)</code></li>
</ul>
<p>To ensure animations are used effectively without draining the device's battery, it's important to check for unnecessary animations and optimize them. This can be done by reviewing the use of functions like <code>withAnimation(::)</code>, the <code>animation(_:value:)</code> view modifier, and the <code>binding’s animation(_:) method</code> in SwiftUI, as well as checking the use of UIKit's animation methods.</p>
This revised HTML content now focuses on the animation functionalities provided by UIKit and SwiftUI, which are essential for iOS app development. The original content related to screen brightness has
19 changes: 19 additions & 0 deletions swift-lang/src/main/resources/io/ecocode/rules/swift/ESOB007.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
{
"key": "ESOB007",
"title": "Animation Methods in UIKit and SwiftUI",
"defaultSeverity": "Major",
"description": "Animations play a crucial role in creating a dynamic and engaging user interface in iOS applications. Both UIKit and SwiftUI provide powerful tools to create smooth and appealing animations. Developers need to be familiar with various methods and properties to implement animations effectively.\n\nAnimation Methods in UIKit and SwiftUI\n\nNoncompliant Code Example UIKit\n\n- UIView.animate(withDuration:animations:)\n- UIView.animate(withDuration:animations:completion:)\n- UIView.animate(withDuration:delay:options:animations:completion:)\n- UIView.animateKeyframes(withDuration:delay:options:animations:completion:)\n- UIView.transition(with:duration:options:animations:completion:)\n- CABasicAnimation\n- CAKeyframeAnimation\n- CATransition\n\nNoncompliant Code Example SwiftUI\n\n- withAnimation\n- Animation\n- AnyTransition\n- .animation(Animation?)\n- .transition(AnyTransition)\n- .onAppear(perform:)\n- .onDisappear(perform:)\n\nTo ensure animations are used effectively without draining the device's battery, it's important to check for unnecessary animations and optimize them. This can be done by reviewing the use of functions like withAnimation(::), the animation(_:value:) view modifier, and the binding’s animation(_:) method in SwiftUI, as well as checking the use of UIKit's animation methods.",
"status": "ready",
"remediation": {
"func": "Constant/Issue",
"constantCost": "5min"
},
"tags": [
"sobriety",
"environment",
"ecocode",
"eco-design"
],
"type": "CODE_SMELL"
}

Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ public void testMetadata() {

@Test
public void testRegistredRules() {
assertThat(repository.rules()).hasSize(9);
assertThat(repository.rules()).hasSize(10);
}

@Test
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
/*
* ecoCode iOS plugin - Help the earth, adopt this green plugin for your applications
* Copyright © 2023 green-code-initiative (https://www.ecocode.io/)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package io.ecocode.ios.swift.checks.sobriety;

import io.ecocode.ios.swift.checks.CheckTestHelper;

import org.junit.Test;
import org.sonar.api.batch.sensor.internal.SensorContextTester;
import org.sonar.api.batch.sensor.issue.Issue;
import org.sonar.api.batch.sensor.issue.IssueLocation;

import static org.assertj.core.api.Assertions.assertThat;

public class AnimationFreeCheckTest {

// @Test
// public void shouldTriggerOnAnyTransition() {
// assertAnimationFreeIssue("checks/sobriety/AnimationFreeCheck_AnyTransition_trigger.swift", 11);
// }

@Test
public void shouldTriggerOnCAKeyframeAnimation() {
assertAnimationFreeIssue("checks/sobriety/AnimationFreeCheck_CAKeyframeAnimation_trigger.swift", 11);
}

@Test
public void shouldTriggerOnCABasicAnimation() {
assertAnimationFreeIssue("checks/sobriety/AnimationFreeCheck_CABasicAnimation_trigger.swift", 11);
}

@Test
public void shouldTriggerOnCATransition() {
assertAnimationFreeIssue("checks/sobriety/AnimationFreeCheck_CATransition_trigger.swift", 11);
}

@Test
public void shouldTriggerOnUIViewAnimate() {
assertAnimationFreeIssue("checks/sobriety/AnimationFreeCheck_UIViewAnimate_trigger.swift", 11);
}

@Test
public void shouldTriggerOnUIViewAnimateKeyframes() {
assertAnimationFreeIssue("checks/sobriety/AnimationFreeCheck_UIViewAnimateKeyframes_trigger.swift", 11);
}

@Test
public void shouldTriggerOnUIViewTransition() {
assertAnimationFreeIssue("checks/sobriety/AnimationFreeCheck_UIViewTransition_trigger.swift", 11);
}

@Test
public void shouldTriggerOnWithAnimation() {
assertAnimationFreeIssue("checks/sobriety/AnimationFreeCheck_WithAnimation_trigger.swift", 10);
}

@Test
public void shouldNoDetectAnimationUsage() {
SensorContextTester context = CheckTestHelper.analyzeTestFile("checks/sobriety/AnimationFreeCheck_no_trigger.swift");
assertThat(context.allIssues()).isEmpty();
}

private void assertAnimationFreeIssue(String file, int line) {
SensorContextTester context = CheckTestHelper.analyzeTestFile(file);
assertThat(context.allIssues()).hasSize(1)
.allSatisfy(issue -> {
assertIssue(issue, line);
});
}

private void assertIssue(Issue issue, int line) {
assertThat(issue.ruleKey().rule()).isEqualTo("ESOB007");
assertThat(issue.ruleKey().repository()).isEqualTo("ecoCode-swift");
IssueLocation location = issue.primaryLocation();
assertThat(location.textRange().start().line()).isEqualTo(line);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import Foundation
import SwiftUI

final class AppDelegate: NSObject, UIApplicationDelegate {
func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil
) -> Bool {

// Should trigger
AnyTransition.opacity.animation(.easeInOut(duration: 0.5))

return true
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import Foundation
import SwiftUI

final class AppDelegate: NSObject, UIApplicationDelegate {
func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil
) -> Bool {

// Should trigger
let animation = CABasicAnimations(keyPath: "opacity")
animation.fromValue = 1.0
animation.toValue = 0.0
animation.duration = 1.0
view.layer.add(animation, forKey: "opacity")

return true
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import Foundation
import SwiftUI

final class AppDelegate: NSObject, UIApplicationDelegate {
func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil
) -> Bool {

// Should trigger
let animation = CAKeyframeAnimation(keyPath: "position")
animation.values = [NSValue(cgPoint: CGPoint(x: 0, y: 0)), NSValue(cgPoint: CGPoint(x: 50, y: 50)), NSValue(cgPoint: CGPoint(x: 100, y: 0)), NSValue(cgPoint: CGPoint(x: 0, y: 0))]
animation.duration = 2.0
animation.repeatCount = .infinity
view.layer.add(animation, forKey: "position")

return true
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import Foundation
import SwiftUI

final class AppDelegate: NSObject, UIApplicationDelegate {
func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil
) -> Bool {

// Should trigger
let transition = CATransition()
transition.type = .push
transition.subtype = .fromRight
transition.duration = 0.5

return true
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import Foundation
import SwiftUI

final class AppDelegate: NSObject, UIApplicationDelegate {
func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil
) -> Bool {

// Should trigger
UIView.animateKeyframes(withDuration: 2.0, delay: 0.0, options: [], animations: {
UIView.addKeyframe(withRelativeStartTime: 0.0, relativeDuration: 0.25) {
view.transform = CGAffineTransform(scaleX: 1.1, y: 1.1)
}
UIView.addKeyframe(withRelativeStartTime: 0.25, relativeDuration: 0.5) {
view.transform = CGAffineTransform(rotationAngle: .pi)
}
UIView.addKeyframe(withRelativeStartTime: 0.75, relativeDuration: 0.25) {
view.transform = CGAffineTransform.identity
}
}, completion: nil)
return true
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import Foundation
import SwiftUI

final class AppDelegate: NSObject, UIApplicationDelegate {
func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil
) -> Bool {

// Should trigger
UIView.animate(withDuration: 0.5)
view.alpha = 0.0
return true
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import Foundation
import SwiftUI

final class AppDelegate: NSObject, UIApplicationDelegate {
func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil
) -> Bool {

// Should trigger
let test = UIView.transition(with: containerView, duration: 0.5, options: [.transitionFlipFromLeft], animations: {
containerView.addSubview(view)
}, completion: nil)

return true
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import Foundation
import SwiftUI

final class AppDelegate: NSObject, UIApplicationDelegate {
func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil
) -> Bool {
// Should trigger
withAnimation {
self.isAnimating.toggle()
}

return true
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import Foundation
import SwiftUI

final class AppDelegate: NSObject, UIApplicationDelegate {
func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil
) -> Bool {

// No trigger

return true
}
}

0 comments on commit 0569cc7

Please sign in to comment.