Skip to content

Instantly share code, notes, and snippets.

@keitaoouchi
Last active March 17, 2018 13:10
Show Gist options
  • Save keitaoouchi/641cf78c6198f41b8f4f43eaddf306f7 to your computer and use it in GitHub Desktop.
Save keitaoouchi/641cf78c6198f41b8f4f43eaddf306f7 to your computer and use it in GitHub Desktop.
Dotが3つくらいふわふわするアニメーションつきのローダー
import UIKit
import IoniconsKit
final class DotsLoader: UIViewController {
@IBOutlet weak var animationView: UIView!
@IBOutlet weak var statusLabel: UILabel!
var dots: [CALayer]?
// MARK: - configurable properties
var onSuccessImage: UIImage = UIImage.ionicon(
with: .androidDone,
textColor: .white,
size: CGSize(width: 40, height: 40)
)
var onFailedImage: UIImage = UIImage.ionicon(
with: .closeRound,
textColor: .white,
size: CGSize(width: 40, height: 40)
)
var onStartText: String = "Requesting"
var onSuccessText: String = "Success!"
var onFailedText: String = "Failed!"
}
// MARK: - lifecycles
extension DotsLoader {
override func viewDidLoad() {
super.viewDidLoad()
self.statusLabel.alpha = 0.0
}
}
extension DotsLoader {
static func show(in parent: UIViewController) -> TXBLoader {
// swiftlint:disable force_unwrapping
let vc = R.storyboard.tXBLoader.instantiateInitialViewController()!
parent.addChildViewController(vc)
parent.view.fill(with: vc.view)
vc.didMove(toParentViewController: parent)
return vc
}
func remove() {
let animation = UIViewPropertyAnimator(duration: 0.33, curve: .easeInOut) {
self.view.alpha = 0.0
self.view.transform = CGAffineTransform(scaleX: 0, y: 0)
}
animation.startAnimation()
animation.addCompletion { _ in
self.view.removeFromSuperview()
self.removeFromParentViewController()
}
}
func start() {
self.dots = self.makeDots(size: 3)
self.dots?.forEach {
self.animationView.layer.addSublayer($0)
}
let x = self.animationView.frame.size.width / 2.0
let y = self.animationView.frame.size.height / 2.0
let circleLayer = self.makeCircle(
center: CGPoint(x: x, y: y),
radius: x
)
self.animationView.layer.addSublayer(circleLayer)
self.statusLabel.text = onStartText
UIViewPropertyAnimator(duration: 0.33, curve: .easeIn) { [weak self] in
self?.statusLabel.alpha = 1.0
}.startAnimation()
}
func stop(success: Bool, delay: TimeInterval = 0.0, onCompleted: (() -> Void)? = nil) {
CATransaction.begin()
self.dots?.forEach { dot in
let removeAnimation = CABasicAnimation(keyPath: "transform.scale")
removeAnimation.fromValue = 1.0
removeAnimation.toValue = 0.0
removeAnimation.duration = 0.33
removeAnimation.timingFunction =
CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseInEaseOut)
removeAnimation.isRemovedOnCompletion = true
dot.transform = CATransform3DMakeScale(0.0, 0.0, 1.0)
dot.add(removeAnimation, forKey: "removeAnimation")
}
CATransaction.setCompletionBlock { [weak self] in
self?.dots?.forEach { $0.removeFromSuperlayer() }
self?.dots = nil
self?.showCheckmark(success: success, delay: delay, onCompleted: onCompleted)
}
CATransaction.commit()
if success {
self.statusLabel.text = onSuccessText
} else {
self.statusLabel.text = onFailedText
}
}
}
private extension DotsLoader {
func showCheckmark(success: Bool, delay: TimeInterval, onCompleted: (() -> Void)? = nil) {
let resultImage: UIImage
if success {
resultImage = self.onSuccessImage
} else {
resultImage = self.onFailedImage
}
let imageView = UIImageView(image: resultImage)
imageView.translatesAutoresizingMaskIntoConstraints = false
self.animationView.addSubview(imageView)
imageView.centerXAnchor.constraint(
equalTo: self.animationView.centerXAnchor).isActive = true
imageView.centerYAnchor.constraint(
equalTo: self.animationView.centerYAnchor).isActive = true
imageView.transform = CGAffineTransform(scaleX: 0.0, y: 0.0)
let animation = UIViewPropertyAnimator(duration: 0.33, curve: .easeInOut) {
imageView.transform = .identity
}
if let onCompleted = onCompleted {
animation.addCompletion({ _ in
if delay > 0.0 {
DispatchQueue.main.asyncAfter(deadline: .now() + delay, execute: {
onCompleted()
})
} else {
onCompleted()
}
})
}
animation.startAnimation()
}
func makeCircle(center: CGPoint, radius: CGFloat) -> CALayer {
let path = UIBezierPath(
arcCenter: center,
radius: radius,
startAngle: 2 * CGFloat.pi / 4,
endAngle: 2 * CGFloat.pi * 5 / 4,
clockwise: true
)
let layer = CAShapeLayer()
layer.path = path.cgPath
layer.fillColor = UIColor.clear.cgColor
layer.strokeColor = UIColor.white.cgColor
layer.lineWidth = 3.0
layer.strokeEnd = 0.0
let animation = CABasicAnimation(keyPath: "strokeEnd")
animation.duration = 0.33
animation.fromValue = 0
animation.toValue = 1
animation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseInEaseOut)
animation.isRemovedOnCompletion = true
layer.strokeEnd = 1.0
layer.add(animation, forKey: "animateCircle")
return layer
}
func makeDots(size: Int) -> [CALayer] {
let dotsAnimation = CAKeyframeAnimation(keyPath: "transform.scale")
dotsAnimation.keyTimes = [0, 0.3, 1]
dotsAnimation.values = [1, 0.6, 1]
// http://qiita.com/naoyashiga/items/530c1e05465c62ecae95
let timingFunction = CAMediaTimingFunction(controlPoints: 0.6, 0.04, 0.98, 0.335)
dotsAnimation.timingFunctions = [timingFunction, timingFunction]
dotsAnimation.duration = 1.0
dotsAnimation.repeatCount = HUGE
dotsAnimation.isRemovedOnCompletion = true
let startTiming = CACurrentMediaTime()
let height = self.animationView.frame.size.height / 2.0
let spacing = (self.animationView.frame.size.width - 24) / CGFloat(size - 1)
let dotSize = CGSize(width: 8, height: 8)
var dots = [CALayer]()
(0 ..< size).forEach { i in
let dot = self.makeDot(size: dotSize, with: UIColor.white.cgColor)
let xPosition = 12 + (spacing - dotSize.width / CGFloat(size - 1)) * CGFloat(i)
let yPosition = height - dotSize.height / 2
let frame = CGRect(x: xPosition,
y: yPosition,
width: dotSize.width,
height: dotSize.height)
dotsAnimation.beginTime = startTiming + CFTimeInterval(0.12 * CGFloat(i))
dot.frame = frame
dot.add(dotsAnimation, forKey: "animationDots")
dots.append(dot)
}
return dots
}
func makeDot(size: CGSize, with color: CGColor) -> CALayer {
let layer = CAShapeLayer()
let path = UIBezierPath()
path.addArc(
withCenter: CGPoint(x: size.width / 2, y: size.height / 2),
radius: size.width / 2,
startAngle: 0.0,
endAngle: 2 * CGFloat.pi,
clockwise: false
)
layer.fillColor = color
layer.backgroundColor = nil
layer.path = path.cgPath
layer.frame = CGRect(x: 0, y: 0, width: size.width, height: size.height)
return layer
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment