在 Swift 动画中实现 CSS

Posted

技术标签:

【中文标题】在 Swift 动画中实现 CSS【英文标题】:Implement CSS into Swift Animation 【发布时间】:2020-06-28 13:59:08 【问题描述】:

我有一个在 html 中用 CSS 编写的加载器。 我想在我的 ios 应用中实现完全相同的目标。

这里是加载器的 CSS。

<!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>
如果有任何库、工具或任何东西可以帮助我在我的 iOS 应用程序中获得完全相同的工具,那将非常有帮助。

我尝试修改相同的外观,但即使调整了这么多,我也无法获得完全相同的结果。

提前致谢。

【问题讨论】:

你可以先分解发生了什么,然后翻译成 SwiftUI。从我所见(尽管对 css 的经验为零),您有 3 个形状(弧线),它们以 1.5、2 和 3 秒的持续时间无限期地围绕同一中心旋转(具有线性时序曲线)。所以问题可能变成“我如何在 SwiftUI 中制作形状并为其设置动画?” @Alladinian我试过了,但我无法获得此处发布的相同体验 如果您编辑问题以包含所需效果的动画 GIF,您将获得更多帮助。您还需要包含您编写的 Swift 代码。 @robmayoff 只需点击上面代码下方的“运行代码 sn-p”按钮另外,我已经添加了我的代码,您可以查看它,如果您有任何改进建议,那么您最受欢迎。 重温这一点,我无法理解为什么我发布了一个SwiftUI 实现(每个人都非常好,不让我在这件事上喊出来????)。无论如何,很高兴您找到了解决方案。如果有人对 SUI 方法感兴趣,我会保留答案。 【参考方案1】:

似乎是一个有趣的练习,所以这里有一个可能的解决方案:

首先,我们需要一种制作弧线的方法。我们可以在 SwiftUI 中使用 Shape 来做到这一点:

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)
    

请注意,这里有两个输入,radiusangle(以度为单位)。我们还用 1px 的线描边路径,并定义了一个方便的扩展来获取矩形的中心。

然后我们需要一些东西,可以让任何视图在一段时间内无限旋转:

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)
    

请注意,在上面的 sn-p 中,我将持续时间减半以匹配 css(我注意到在 50% 处有一个完整旋转的关键帧)

最后,让我们把一些形状放在一个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
        
    

预览结果如下:

struct LoadingView_Previews: PreviewProvider 
    static var previews: some View 
        LoadingView()
    

我猜你可以使用参数来让它恰到好处

【讨论】:

【参考方案2】:

我想我已经实现了我最初想要的。

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")
        
    

【讨论】:

以上是关于在 Swift 动画中实现 CSS的主要内容,如果未能解决你的问题,请参考以下文章

CSS可见性动画不起作用

使用 Swift 在 iOS 中平滑搜索栏动画

如何在Canvas中实现自定义路径动画

如何在Canvas中实现自定义路径动画

在 iOS 中实现 AnticipateOvershootInterpolator 动画

CSS过渡动画