Implement CSS into Swift Animation

邮差的信 提交于 2020-12-15 06:01:29

问题


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>
If there is any library, tool or anything which can help me to get the exact same tool in my iOS app, it will be very helpful.

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

标签
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!