Created
January 13, 2019 03:19
-
-
Save Zhendryk/1400a933410ffaba8ca7bbcbdaab85b3 to your computer and use it in GitHub Desktop.
Custom activity indicator that natively renders After Effects vector animations with an optional message NOTE: This depends on airbnb/lottie-ios which can be found here: https://github.com/airbnb/lottie-ios
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// | |
// CustomActivityIndicator.swift | |
// | |
// Created by Jonathan Bailey on 9/10/18. | |
// Copyright © 2018 Jonathan Bailey. All rights reserved. | |
// | |
import UIKit | |
import Lottie | |
class CustomActivityIndicator: UIView { | |
/// MARK: - Public variables | |
/// The label containing the title text of the view | |
lazy var titleLabel: UILabel = UILabel() | |
/// The label containing the message text of the view | |
lazy var messageLabel: UILabel = UILabel() | |
/// The LOTAnimation view which runs Adobe After Effects animations | |
lazy var animationView: LOTAnimationView = LOTAnimationView() | |
/// Whether or not this view will fade out when animation stops | |
@IBInspectable var hidesWhenStopped: Bool = true | |
/// If this view has a title and message on it, or just an animation | |
@IBInspectable var textEnabled: Bool = true | |
/// MARK: - Private variables | |
private let container: UIStackView = { | |
let stackView = UIStackView(frame: .zero) | |
stackView.axis = .vertical | |
stackView.alignment = .center | |
stackView.distribution = .fill | |
stackView.spacing = 5 | |
stackView.isLayoutMarginsRelativeArrangement = true | |
stackView.layoutMargins = UIEdgeInsets(top: 10, left: 10, bottom: 0, right: 10) | |
return stackView | |
}() | |
/// Instantiates a CustomActivityIndicator with the given title, message and animation | |
/// | |
/// - Parameters: | |
/// - title: The title for the view | |
/// - message: The message for the view | |
/// - animation: The name of the lottie animation .json file (no extension) | |
convenience init(title: String, message: String, animation: String) { | |
self.init() | |
self.titleLabel.text = title | |
self.messageLabel.text = message | |
self.animationView = LOTAnimationView(name: animation) | |
setupViews() | |
} | |
/// Instantiates a CustomActivityIndicator with only the given animation, text is disabled | |
/// | |
/// - Parameter animation: The filename of the lottie animation .json file (no extension) | |
convenience init(animation: String) { | |
self.init() | |
self.textEnabled = false | |
self.animationView = LOTAnimationView(name: animation) | |
setupViews() | |
} | |
override var intrinsicContentSize: CGSize { | |
let value = Swift.min(UIScreen.main.bounds.size.width/2, UIScreen.main.bounds.size.height/2) | |
return CGSize(width: value, height: value) | |
} | |
/// Begin animating and loop the animation until stopAnimating is called | |
func startAnimating() { | |
fadeIn() | |
animationView.loopAnimation = true | |
animationView.play() | |
} | |
/// Stop the view from animating and fade out if hidesWhenStopped is true | |
func stopAnimating() { | |
animationView.stop() | |
if hidesWhenStopped { | |
fadeOut() | |
} | |
} | |
/// Animate the view through a single loop and stop with its current settings | |
func playOnce() { | |
if animationView.isAnimationPlaying { | |
animationView.stop() | |
} | |
animationView.loopAnimation = false | |
self.fadeIn() | |
animationView.play { finished in | |
if finished { | |
self.stopAnimating() | |
} | |
} | |
} | |
/// Set the title, message and animation of the view to loop once and run the completion handler when the animation is finished | |
/// | |
/// - Parameters: | |
/// - title: The new title of the view | |
/// - message: The new message of the view | |
/// - animation: The new animation of the view | |
/// - completion: The code to run after the animation is complete | |
func playOnce(title: String, message: String, animation: String, _ completion: (@escaping () -> ()) = {}) { | |
if animationView.isAnimationPlaying { | |
animationView.stop() | |
} | |
self.titleLabel.text = title | |
self.messageLabel.text = message | |
animationView.setAnimation(named: animation) | |
animationView.loopAnimation = false | |
self.fadeIn() | |
animationView.play { finished in | |
if finished { | |
self.stopAnimating() | |
completion() | |
} | |
} | |
} | |
/// Set new text for the view | |
/// | |
/// - Parameters: | |
/// - title: The new title | |
/// - body: The new message | |
func setText(title: String, message: String) { | |
self.titleLabel.text = title | |
self.messageLabel.text = message | |
} | |
/// Set a new animation to the view | |
/// | |
/// - Parameter animation: The filename of the lottie animation .json file (no extension) | |
func setAnimation(animation: String) { | |
self.animationView.setAnimation(named: animation) | |
} | |
private func setupViews() { | |
backgroundColor = .darkGray | |
isOpaque = false | |
alpha = 0.0 | |
layer.cornerRadius = 15 | |
isUserInteractionEnabled = false | |
if textEnabled { | |
titleLabel.textColor = .white | |
titleLabel.textAlignment = .center | |
titleLabel.numberOfLines = 1 | |
titleLabel.font = UIFont.boldSystemFont(ofSize: 24) | |
titleLabel.setContentCompressionResistancePriority(.defaultLow, for: .horizontal) | |
titleLabel.setContentCompressionResistancePriority(.defaultLow, for: .vertical) | |
container.addArrangedSubview(titleLabel) | |
messageLabel.textColor = .white | |
messageLabel.textAlignment = .center | |
messageLabel.adjustsFontSizeToFitWidth = true | |
messageLabel.numberOfLines = 0 | |
messageLabel.lineBreakMode = .byWordWrapping | |
messageLabel.font = UIFont.systemFont(ofSize: 17) | |
messageLabel.setContentCompressionResistancePriority(.defaultLow, for: .horizontal) | |
messageLabel.setContentCompressionResistancePriority(.defaultLow, for: .vertical) | |
container.addArrangedSubview(messageLabel) | |
} | |
animationView.contentMode = .scaleAspectFit | |
animationView.setContentCompressionResistancePriority(.defaultLow, for: .horizontal) | |
animationView.setContentCompressionResistancePriority(.defaultLow, for: .vertical) | |
container.addArrangedSubview(animationView) | |
addSubview(container) | |
setupLayout() | |
} | |
private func setupLayout() { | |
container.translatesAutoresizingMaskIntoConstraints = false | |
container.topAnchor.constraint(equalTo: safeAreaLayoutGuide.topAnchor).isActive = true | |
container.leadingAnchor.constraint(equalTo: safeAreaLayoutGuide.leadingAnchor).isActive = true | |
container.bottomAnchor.constraint(equalTo: safeAreaLayoutGuide.bottomAnchor).isActive = true | |
container.trailingAnchor.constraint(equalTo: safeAreaLayoutGuide.trailingAnchor).isActive = true | |
} | |
} | |
extension UIView { | |
func fadeIn(withDuration duration: TimeInterval = 0.5) { | |
UIView.animate(withDuration: duration, animations: { | |
self.alpha = 1.0 | |
}) | |
} | |
func fadeOut(withDuration duration: TimeInterval = 0.5) { | |
UIView.animate(withDuration: duration) { | |
self.alpha = 0.0 | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment