问题
I have a loader written in CSS inside HTML. And I want to achieve exact same thing in my iOS App.
Here is the CSS of the loader.
<!DOCTYPE html>
<html>
<head>
<style>
@-webkit-keyframes spin {
0% {
-webkit-transform: rotate(0deg);
-ms-transform: rotate(0deg);
transform: rotate(0deg);
}
50% {
-webkit-transform: rotate(360deg);
-ms-transform: rotate(360deg);
transform: rotate(360deg);
}
100% {
-webkit-transform: rotate(360deg);
-ms-transform: rotate(360deg);
transform: rotate(360deg);
}
}
@keyframes spin {
0% {
-webkit-transform: rotate(0deg);
-ms-transform: rotate(0deg);
transform: rotate(0deg);
}
100% {
-webkit-transform: rotate(360deg);
-ms-transform: rotate(360deg);
transform: rotate(360deg);
}
}
.loader {
display: block;
width: 75px;
height: 75px;
margin: 0 auto;
border-radius: 50%;
border: 1px solid transparent;
border-top-color: #ff9000;
-webkit-animation: spin 2s linear infinite;
animation: spin 2s linear infinite;
position: fixed;
z-index: 9999;
left: 0;
right: 0;
top: 50%;
transform: translateY(-50%)
}
.loader:before {
content: "";
position: absolute;
top: 5px;
left: 5px;
right: 5px;
bottom: 5px;
border-radius: 50%;
border: 1px solid transparent;
border-top-color: #f00;
-webkit-animation: spin 3s linear infinite;
animation: spin 3s linear infinite
}
.loader:after {
content: "";
position: absolute;
top: 11px;
left: 11px;
right: 11px;
bottom: 11px;
border-radius: 50%;
border: 1px solid transparent;
border-top-color: #0a00b2;
-webkit-animation: spin 1.5s linear infinite;
animation: spin 1.5s linear infinite
}
</style>
</head>
<body>
<div class="loader"></div>
</body>
</html>
I tried to modify the same look but even after tweaking so much, I was unable to exact same result.
Thanks in advance.
回答1:
Seems like a fun exercise, so here is a possible solution:
First, we need a way to make arcs. We can do that with Shape in SwiftUI:
struct Arc: Shape {
let radius: CGFloat
let angle: Double
func path(in rect: CGRect) -> Path {
Path { path in
path.addArc(center: rect.center,
radius: radius,
startAngle: Angle(degrees: 0),
endAngle: Angle(degrees: angle),
clockwise: false)
}
.strokedPath(StrokeStyle(lineWidth: 1))
}
}
extension CGRect {
var center: CGPoint {
.init(x: midX, y: midY)
}
}
Note here that we have two inputs, radius & angle (in degrees). We also stroke the path with 1px line and define a convenient extension for getting the center of a rect.
Then we need something that will animate any view as an indefinite rotation for some duration:
private extension View {
func rotationAnimation(duration: Double) -> Animation {
Animation.linear(duration: duration / 2)
.repeatForever(autoreverses: false)
}
func rotateFor(_ duration: Double, when isAnimating: Bool) -> some View {
self.rotationEffect(Angle(degrees: isAnimating ? 360 : 0.0))
.animation(isAnimating ? rotationAnimation(duration: duration) : .default)
}
}
Note in the above snippet that I'm halving the duration to match the css (I noticed that there is a keyframe at 50% for a full spin)
Finally, let's put some shapes together in a ZStack:
struct LoadingView: View {
@State var isAnimating: Bool = false
var body: some View {
ZStack {
Arc(radius: 20, angle: 90)
.rotateFor(1.5, when: isAnimating)
.foregroundColor(.blue)
Arc(radius: 25, angle: 90)
.rotateFor(3.0, when: isAnimating)
.foregroundColor(.red)
Arc(radius: 30, angle: 90)
.rotateFor(2.0, when: isAnimating)
.foregroundColor(.orange)
}
.frame(width: 100, height: 100)
.onAppear {
self.isAnimating = true
}
}
}
which when previewed results to this:
struct LoadingView_Previews: PreviewProvider {
static var previews: some View {
LoadingView()
}
}
I guess you can play with the params to get it just right
回答2:
I guess I have achieved what I initially wanted.
import UIKit
class CustomLoader: UIViewController {
static let shared = CustomLoader()
var arcsNotAdded: Bool = true
override func viewDidLoad() {
super.viewDidLoad()
self.view.backgroundColor = .clear
}
func showLoader(_ interactionEnabled: Bool = true) {
if let appDelegate = UIApplication.shared.delegate, let window = appDelegate.window, let rootViewController = window?.rootViewController {
var topViewController = rootViewController
let topVCArray = topViewController.children
for obj in topVCArray where obj is CustomLoader { return }
while topViewController.presentedViewController != nil {
topViewController = topViewController.presentedViewController!
}
if topVCArray.count == 1 && topVCArray.first is InitialLandingController { //HOTFix for initial landing controller
topViewController = topVCArray.first!
}
topViewController.addChild(self)
topViewController.view.addSubview(view)
didMove(toParent: topViewController)
view.autoresizingMask = [.flexibleWidth, .flexibleHeight]
view.frame = CGRect(topViewController.view.center.x, topViewController.view.center.y, 20, 20)
view.center = topViewController.view.center
kAppDelegate.window?.isUserInteractionEnabled = interactionEnabled
arcsNotAdded ? configureArcs() : debugPrint("Arcs are already added")
}
}
func hideLoader() {
self.willMove(toParent: nil)
self.view.removeFromSuperview()
self.removeFromParent()
kAppDelegate.window?.isUserInteractionEnabled = true
}
private func configureArcs() {
arcsNotAdded = false
let arcStartAngle: CGFloat = -CGFloat.pi / 2
let arcEndAngle: CGFloat = 0
let centrePoint = CGPoint(x: view.bounds.midX, y: view.bounds.midY)
let outerCircle = CAShapeLayer()
let middleCircle = CAShapeLayer()
let innerCircle = CAShapeLayer()
let circleColorOuter: CGColor = UIColor(red: 255/255, green: 144/255, blue: 0/255, alpha: 1.0).cgColor
let circleColorMiddle: CGColor = UIColor(red: 255/255, green: 0/255, blue: 0/255, alpha: 1.0).cgColor
let circleColorInner: CGColor = UIColor(red: 10/255, green: 0/255, blue: 178/255, alpha: 1.0).cgColor
let outerPath = UIBezierPath(arcCenter: centrePoint, radius: CGFloat(77/2), startAngle: arcStartAngle, endAngle: arcEndAngle, clockwise: true)
let midPath = UIBezierPath(arcCenter: centrePoint, radius: CGFloat(65/2), startAngle: arcStartAngle, endAngle: arcEndAngle, clockwise: true)
let innerPath = UIBezierPath(arcCenter: centrePoint, radius: CGFloat(53/2), startAngle: arcStartAngle, endAngle: arcEndAngle, clockwise: true)
configureArcLayer(outerCircle, forView: view, withPath: outerPath.cgPath, withBounds: view.bounds, withColor: circleColorOuter)
configureArcLayer(middleCircle, forView: view, withPath: midPath.cgPath, withBounds: view.bounds, withColor: circleColorMiddle)
configureArcLayer(innerCircle, forView: view, withPath: innerPath.cgPath, withBounds: view.bounds, withColor: circleColorInner)
animateArcs(outerCircle, middleCircle, innerCircle)
}
private func configureArcLayer(_ layer: CAShapeLayer, forView view: UIView, withPath path: CGPath, withBounds bounds: CGRect, withColor color: CGColor) {
layer.path = path
layer.frame = bounds
layer.lineWidth = 1.5
layer.strokeColor = color
layer.fillColor = UIColor.clear.cgColor
layer.isOpaque = true
view.layer.addSublayer(layer)
}
private func animateArcs(_ outerCircle: CAShapeLayer, _ middleCircle: CAShapeLayer, _ innerCircle: CAShapeLayer) {
DispatchQueue.main.async {
let outerAnimation = CABasicAnimation(keyPath: "transform.rotation")
outerAnimation.toValue = 2 * CGFloat.pi
outerAnimation.duration = 1.5
outerAnimation.repeatCount = Float(UINT64_MAX)
outerAnimation.isRemovedOnCompletion = false
outerCircle.add(outerAnimation, forKey: "outerCircleRotation")
let middleAnimation = outerAnimation.copy() as! CABasicAnimation
middleAnimation.duration = 1
middleCircle.add(middleAnimation, forKey: "middleCircleRotation")
let innerAnimation = middleAnimation.copy() as! CABasicAnimation
innerAnimation.duration = 0.75
innerCircle.add(innerAnimation, forKey: "middleCircleRotation")
}
}
}
来源:https://stackoverflow.com/questions/62623548/implement-css-into-swift-animation