You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Swift 4中如何同步执行多个自定义动画函数?

Hey there! Let's work through this sequential animation challenge for your travel iOS app—sounds like a really useful feature for helping users visualize transit routes. The key issue here is avoiding messy nested closures while ensuring each animation finishes completely before the next one starts, right? Let's break down a couple of clean, maintainable solutions.

The modern Swift concurrency model makes sequential async operations trivial to write and read. We'll wrap your existing animation functions to use async/await by capturing their completion events with withCheckedContinuation.

First, let's update your linear animation function to support async:

func animateLinearRoute(transportMode: TransportAnnot, startcoor: CLLocationCoordinate2D, destcoor: CLLocationCoordinate2D) async {
    await withCheckedContinuation { continuation in
        UIView.animate(withDuration: 3, animations: {
            if transportMode.coordinate.latitude == startcoor.latitude && transportMode.coordinate.longitude == startcoor.longitude {
                transportMode.coordinate = destcoor
            } else {
                transportMode.coordinate = startcoor
            }
        }, completion: { _ in
            // Resume the async task once animation finishes
            continuation.resume()
        })
    }
}

Next, we need to handle the CAKeyframeAnimation for non-linear routes. Since Core Animation doesn't have a built-in completion closure, we'll create a lightweight delegate to capture when the animation stops:

// Delegate to catch Core Animation completion events
class AnimationCompletionDelegate: NSObject, CAAnimationDelegate {
    private let completion: () -> Void
    
    init(completion: @escaping () -> Void) {
        self.completion = completion
        super.init()
    }
    
    func animationDidStop(_ anim: CAAnimation, finished flag: Bool) {
        completion()
    }
}

// Updated non-linear animation function with async support
func animateNonLinearRoute(transportMode: TransportAnnot, animroute: [CLLocationCoordinate2D]) async {
    await withCheckedContinuation { continuation in
        guard !animroute.isEmpty else {
            continuation.resume()
            return
        }
        
        let path = UIBezierPath()
        let startPoint = self.mapView.convert(animroute[0], toPointTo: self.mapView)
        path.move(to: startPoint)
        
        for coor in animroute {
            let point = self.mapView.convert(coor, toPointTo: self.mapView)
            path.addLine(to: point)
        }
        
        let animation = CAKeyframeAnimation(keyPath: "position")
        animation.path = path.cgPath
        animation.duration = 5.0
        animation.isRemovedOnCompletion = false
        // Assign our custom delegate to trigger completion
        animation.delegate = AnimationCompletionDelegate {
            transportMode.coordinate = animroute.last!
            continuation.resume()
        }
        
        if let transportview = self.mapView.view(for: transportMode) {
            transportview.layer.add(animation, forKey: "animate position along path")
        }
        CATransaction.commit()
    }
}

Now, let's create an enum to represent different route segments (linear/non-linear) for clean iteration:

enum RouteSegment {
    case linear(transport: TransportAnnot, start: CLLocationCoordinate2D, end: CLLocationCoordinate2D)
    case nonLinear(transport: TransportAnnot, route: [CLLocationCoordinate2D])
}

Finally, a function to execute all segments in order—no nested closures, just straightforward linear code:

func executeSequentialRoute(segments: [RouteSegment]) async {
    for segment in segments {
        switch segment {
        case .linear(let transport, let start, let end):
            await animateLinearRoute(transportMode: transport, startcoor: start, destcoor: end)
        case .nonLinear(let transport, let route):
            await animateNonLinearRoute(transportMode: transport, animroute: route)
        }
    }
}

How to Use It

// Example setup (replace with your actual TransportAnnot objects and coordinates)
let cableCar = TransportAnnot(...)
let shuttleBus = TransportAnnot(...)
let boat = TransportAnnot(...)

let coordA = CLLocationCoordinate2D(latitude: 40.7128, longitude: -74.0060)
let coordB = CLLocationCoordinate2D(latitude: 40.7200, longitude: -74.0080)
let coordC = CLLocationCoordinate2D(latitude: 40.7300, longitude: -74.0100)
let coordD = CLLocationCoordinate2D(latitude: 40.7400, longitude: -74.0120)
let shuttleRoadRoute = [coordB, /* intermediate road coordinates */, coordC]

// Build your route segments
let routeSegments: [RouteSegment] = [
    .linear(transport: cableCar, start: coordA, end: coordB),
    .nonLinear(transport: shuttleBus, route: shuttleRoadRoute),
    .linear(transport: boat, start: coordC, end: coordD)
]

// Execute on the main thread (all UI operations must run here)
Task { @MainActor in
    await executeSequentialRoute(segments: routeSegments)
}

Solution 2: OperationQueue (For Pre-Swift 5.5 Compatibility)

If you need to support older iOS versions, OperationQueue is a reliable way to manage sequential tasks with dependencies. We'll wrap each animation in an Operation and chain them together.

First, create functions to build animation operations:

func createLinearAnimationOperation(transportMode: TransportAnnot, startcoor: CLLocationCoordinate2D, destcoor: CLLocationCoordinate2D) -> Operation {
    let op = BlockOperation()
    op.addExecutionBlock { [weak self] in
        guard let self = self else { return }
        let semaphore = DispatchSemaphore(value: 0)
        
        DispatchQueue.main.async {
            UIView.animate(withDuration: 3, animations: {
                if transportMode.coordinate.latitude == startcoor.latitude && transportMode.coordinate.longitude == startcoor.longitude {
                    transportMode.coordinate = destcoor
                } else {
                    transportMode.coordinate = startcoor
                }
            }, completion: { _ in
                semaphore.signal()
            })
        }
        
        semaphore.wait()
    }
    return op
}

func createNonLinearAnimationOperation(transportMode: TransportAnnot, animroute: [CLLocationCoordinate2D]) -> Operation {
    let op = BlockOperation()
    op.addExecutionBlock { [weak self] in
        guard let self = self else { return }
        let semaphore = DispatchSemaphore(value: 0)
        
        DispatchQueue.main.async {
            guard !animroute.isEmpty else {
                semaphore.signal()
                return
            }
            
            let path = UIBezierPath()
            let startPoint = self.mapView.convert(animroute[0], toPointTo: self.mapView)
            path.move(to: startPoint)
            
            for coor in animroute {
                let point = self.mapView.convert(coor, toPointTo: self.mapView)
                path.addLine(to: point)
            }
            
            let animation = CAKeyframeAnimation(keyPath: "position")
            animation.path = path.cgPath
            animation.duration = 5.0
            animation.isRemovedOnCompletion = false
            animation.delegate = AnimationCompletionDelegate {
                transportMode.coordinate = animroute.last!
                semaphore.signal()
            }
            
            if let transportview = self.mapView.view(for: transportMode) {
                transportview.layer.add(animation, forKey: "animate position along path")
            }
            CATransaction.commit()
        }
        
        semaphore.wait()
    }
    return op
}

Then, execute the sequence with dependencies:

func executeSequentialRouteWithOperationQueue(segments: [RouteSegment]) {
    let queue = OperationQueue()
    queue.maxConcurrentOperationCount = 1 // Ensure only one operation runs at a time
    
    var previousOperation: Operation? = nil
    for segment in segments {
        let currentOp: Operation
        
        switch segment {
        case .linear(let transport, let start, let end):
            currentOp = createLinearAnimationOperation(transportMode: transport, startcoor: start, destcoor: end)
        case .nonLinear(let transport, let route):
            currentOp = createNonLinearAnimationOperation(transportMode: transport, animroute: route)
        }
        
        // Make current operation depend on the previous one finishing
        if let prevOp = previousOperation {
            currentOp.addDependency(prevOp)
        }
        
        queue.addOperation(currentOp)
        previousOperation = currentOp
    }
}

Key Notes for Both Solutions

  • Main Thread Safety: All UI and map-related operations must run on the main thread. The async solution uses @MainActor, while the OperationQueue solution uses DispatchQueue.main.async.
  • Avoid Retain Cycles: Use [weak self] in closures to prevent memory leaks.
  • Core Animation Cleanup: We set transportMode.coordinate to the final position after the CAAnimation finishes, since keyframe animations don't update the layer's actual position by default.

内容的提问来源于stack exchange,提问作者Sebastian

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.28 07:31:26