Last active
March 17, 2018 13:10
-
-
Save keitaoouchi/641cf78c6198f41b8f4f43eaddf306f7 to your computer and use it in GitHub Desktop.
Dotが3つくらいふわふわするアニメーションつきのローダー
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
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