Skip to content

Instantly share code, notes, and snippets.

@fxm90
Last active December 22, 2021 19:31
Show Gist options
  • Save fxm90/723b5def31b46035cd92a641e3b184f6 to your computer and use it in GitHub Desktop.
Save fxm90/723b5def31b46035cd92a641e3b184f6 to your computer and use it in GitHub Desktop.
Animate the `alpha` value of a UIView and update the `isHidden` flag accordingly.
//
// UIView+AnimateAlpha.swift
//
// Created by Felix Mau on 17/12/18.
// Copyright © 2018 Felix Mau. All rights reserved.
//
import UIKit
extension UIView {
// MARK: - Config
/// The default duration for fading-animations, measured in seconds.
public static let defaultFadingAnimationDuration: TimeInterval = 1.0
// MARK: - Public methods
/// Updates the view visiblity.
///
/// - Parameters:
/// - isHidden: The new view visibility.
/// - duration: The duration of the animation, measured in seconds.
/// - completion: Closure to be executed when the animation sequence ends. This block has no return value and takes a single Boolean
/// argument that indicates whether or not the animations actually finished before the completion handler was called.
///
/// - SeeAlso: https://developer.apple.com/documentation/uikit/uiview/1622515-animatewithduration
public func animate(isHidden: Bool, duration: TimeInterval = UIView.defaultFadingAnimationDuration, completion: ((Bool) -> Void)? = nil) {
if isHidden {
fadeOut(duration: duration,
completion: completion)
} else {
fadeIn(duration: duration,
completion: completion)
}
}
/// Fade out the current view by animating the `alpha` to zero and update the `isHidden` flag accordingly.
///
/// - Parameters:
/// - duration: The duration of the animation, measured in seconds.
/// - completion: Closure to be executed when the animation sequence ends. This block has no return value and takes a single Boolean
/// argument that indicates whether or not the animations actually finished before the completion handler was called.
///
/// - SeeAlso: https://developer.apple.com/documentation/uikit/uiview/1622515-animatewithduration
public func fadeOut(duration: TimeInterval = UIView.defaultFadingAnimationDuration, completion: ((Bool) -> Void)? = nil) {
UIView.animate(withDuration: duration,
animations: {
self.alpha = 0.0
},
completion: { isFinished in
// Update `isHidden` flag accordingly:
// - set to `true` in case animation was completely finished.
// - set to `false` in case animation was interrupted, e.g. due to starting of another animation.
self.isHidden = isFinished
completion?(isFinished)
})
}
/// Fade in the current view by setting the `isHidden` flag to `false` and animating the `alpha` to one.
///
/// - Parameters:
/// - duration: The duration of the animation, measured in seconds.
/// - completion: Closure to be executed when the animation sequence ends. This block has no return value and takes a single Boolean
/// argument that indicates whether or not the animations actually finished before the completion handler was called.
///
/// - SeeAlso: https://developer.apple.com/documentation/uikit/uiview/1622515-animatewithduration
public func fadeIn(duration: TimeInterval = UIView.defaultFadingAnimationDuration, completion: ((Bool) -> Void)? = nil) {
if isHidden {
// Make sure our animation is visible.
isHidden = false
}
UIView.animate(withDuration: duration,
animations: {
self.alpha = 1.0
},
completion: completion)
}
}
@fxm90
Copy link
Author

fxm90 commented Oct 28, 2019

Feel free to copy the following code into a test-case.

//
//  UIView+AnimateAlphaTestCase.swift
//
//  Created by Felix Mau on 28.10.19.
//  Copyright © 2019 Felix Mau. All rights reserved.
//

import XCTest

class UIViewAnimateIsHiddenTestCase: XCTestCase {
    // MARK: - Private properties

    private var window: UIWindow!
    private var view: UIView!

    // MARK: - Public methods

    override func setUp() {
        super.setUp()

        // In order for UIView animations to be executed correctly, the corresponding view has to be attached to a visible window.
        // Therefore we're gonna use the current key-window, add our testing view here in `setUp()` and remove it later in `tearDown()`.
        window = UIApplication.shared.windows.first { $0.isKeyWindow }

        view = UIView()
        window.addSubview(view)
    }

    override func tearDown() {
        view.removeFromSuperview()
        view = nil

        window = nil

        super.tearDown()
    }

    // MARK: - Test method `animate(isHidden:)`

    func testAnimateIsHiddenShouldShowViewAndCallCompletionHandler() {
        // Given
        let expectation = self.expectation(description: "Expect completion handler to be called.")

        // Hide view to validate fade-in.
        view.alpha = 0.0
        view.isHidden = true

        // When
        view.animate(isHidden: false, duration: 0.1) { _ in
            expectation.fulfill()
        }

        // Then
        wait(for: [expectation], timeout: 0.2)

        XCTAssertFalse(view.isHidden)
        XCTAssertEqual(view.alpha, 1.0, accuracy: CGFloat.ulpOfOne)
    }

    func testAnimateIsHiddenShouldHideViewAndCallCompletionHandler() {
        // Given
        let expectation = self.expectation(description: "Expect completion handler to be called.")

        // When
        view.animate(isHidden: true, duration: 0.1) { _ in
            expectation.fulfill()
        }

        // Then
        wait(for: [expectation], timeout: 0.2)

        XCTAssertTrue(view.isHidden)
        XCTAssertEqual(view.alpha, 0.0, accuracy: CGFloat.ulpOfOne)
    }

    func testAnimateIsHiddenWithCancelationShouldResetIsHiddenFlagAndCallCompletionHandler() {
        // Given
        let expectation = self.expectation(description: "Expect completion handler to be called.")

        // When
        view.animate(isHidden: true, duration: 0.1) { _ in
            expectation.fulfill()
        }

        // Cancel animation.
        view.layer.removeAllAnimations()

        // Then
        wait(for: [expectation], timeout: 0.2)

        XCTAssertFalse(view.isHidden, "As we've interrupted the animation, we expect the `isHidden` flag to still be `false`.")
    }

    // MARK: - Test method `fadeIn()`

    func testFadeInShouldShowViewAndCallCompletionHandler() {
        // Given
        let expectation = self.expectation(description: "Expect completion handler to be called.")

        // Hide view to validate fade-in.
        view.alpha = 0.0
        view.isHidden = true

        // When
        view.fadeIn(duration: 0.1) { _ in
            expectation.fulfill()
        }

        // Then
        wait(for: [expectation], timeout: 0.2)

        XCTAssertFalse(view.isHidden)
        XCTAssertEqual(view.alpha, 1.0, accuracy: CGFloat.ulpOfOne)
    }

    // MARK: - Test method `fadeOut()`

    func testFadeOutShouldHideViewAndCallCompletionHandler() {
        // Given
        let expectation = self.expectation(description: "Expect completion handler to be called.")

        // When
        view.fadeOut(duration: 0.1) { _ in
            expectation.fulfill()
        }

        // Then
        wait(for: [expectation], timeout: 0.2)

        XCTAssertTrue(view.isHidden)
        XCTAssertEqual(view.alpha, 0.0, accuracy: CGFloat.ulpOfOne)
    }

    func testFadeOutWithCancelationShouldResetIsHiddenFlagAndCallCompletionHandler() {
        // Given
        let expectation = self.expectation(description: "Expect completion handler to be called.")

        // When
        view.fadeOut(duration: 0.1) { _ in
            expectation.fulfill()
        }

        // Cancel animation.
        view.layer.removeAllAnimations()

        // Then
        wait(for: [expectation], timeout: 0.2)

        XCTAssertFalse(view.isHidden, "As we've interrupted the animation, we expect the `isHidden` flag to still be `false`.")
    }
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment