Dispatch Semaphore for UI

343 views Asked by At

I wanted to animate the alpha of uiviews and i cannot seem to make it. It behaves weirdly and says error that running UI changes arent recommended to be run on background thread, but I dont know how do I make it run on the main one. Can somebody help me? I believe it skips the first UIView.animate block and performs whats in the second without any animation whatsoever.

func animateSemaphore() {

circleRed.alpha = 0.2
circleOrange.alpha = 0.2
circleGreen.alpha = 1

let dispatchSemaphore = DispatchSemaphore(value: 0)
let dispatchQueue = DispatchQueue.global(qos: .background)

dispatchQueue.async {
    UIView.animate(withDuration: 0.5, delay: 5, options: .curveEaseInOut) {
        self.circleOrange.alpha = 1
        self.circleGreen.alpha = 0.2
    } completion: { (_) in
        print("1")
        dispatchSemaphore.signal()
    }
    
    dispatchSemaphore.wait()
    UIView.animate(withDuration: 0.5, delay: 3, options: .curveEaseInOut) {
        self.circleOrange.alpha = 0.2
        self.circleRed.alpha = 1
    } completion: { (_) in
        dispatchSemaphore.signal()
    }
    
    dispatchSemaphore.wait()
    UIView.animate(withDuration: 0.5, delay: 5, options: .curveEaseInOut) {
        self.circleOrange.alpha = 1
    } completion: { (_) in
        dispatchSemaphore.signal()
    }
    
    dispatchSemaphore.wait()
    UIView.animate(withDuration: 0.5, delay: 1, options: .curveEaseInOut) {
        self.circleOrange.alpha = 0.2
        self.circleRed.alpha = 0.2
        self.circleGreen.alpha = 1
    } completion: { (_) in
        self.animateSemaphore()
    }
}

}

3

There are 3 answers

4
Shehata Gamal On BEST ANSWER

You need to insert any ui/animate related code inside main thread not inside a background queue

func animateSemaphore() {
    circleRed.alpha = 0.2
    circleOrange.alpha = 0.2
    circleGreen.alpha = 1
        
    UIView.animate(withDuration: 0.5, delay: 5, options: .curveEaseInOut) {
        self.circleOrange.alpha = 1
        self.circleGreen.alpha = 0.2
    } completion: { (_) in
        UIView.animate(withDuration: 0.5, delay: 3, options: .curveEaseInOut) {
            self.circleOrange.alpha = 0.2
            self.circleRed.alpha = 1
        } completion: { (_) in
            UIView.animate(withDuration: 0.5, delay: 5, options: .curveEaseInOut) {
                self.circleOrange.alpha = 1
            } completion: { (_) in
                UIView.animate(withDuration: 0.5, delay: 1, options: .curveEaseInOut) {
                    self.circleOrange.alpha = 0.2
                    self.circleRed.alpha = 0.2
                    self.circleGreen.alpha = 1
                } completion: { (_) in 
                }
            }
        }
    }
 }

OR play with delay instead of nesting animations

func animateSemaphore() {

    circleRed.alpha = 0.2
    circleOrange.alpha = 0.2
    circleGreen.alpha = 1

    UIView.animate(withDuration: 0.5, delay: 5, options: .curveEaseInOut) {
        self.circleOrange.alpha = 1
        self.circleGreen.alpha = 0.2
    } completion: { (_) in
        print("1")
      
    }
    
   
    UIView.animate(withDuration: 0.5, delay: 8.5, options: .curveEaseInOut) {
        self.circleOrange.alpha = 0.2
        self.circleRed.alpha = 1
    } completion: { (_) in
        
    }
    
   
    UIView.animate(withDuration: 0.5, delay: 14, options: .curveEaseInOut) {
        self.circleOrange.alpha = 1
    } completion: { (_) in
       
    }
    
  
    UIView.animate(withDuration: 0.5, delay: 15.5, options: .curveEaseInOut) {
        self.circleOrange.alpha = 0.2
        self.circleRed.alpha = 0.2
        self.circleGreen.alpha = 1
    } completion: { (_) in
        
    }
}
0
maximkrouk On

You can also create a config for creating animations (draft can be found here) https://gist.github.com/maximkrouk/76163b3f2775dafc73c5633d155368cb And add some flattening

public struct UIViewAnimation {
    private init(
        provider: UIViewAnimatiorProvider?,
        animations: @escaping () -> Void,
        completion: ((Bool) -> Void)? = nil
    ) {
        self.provider = provider
        self.animations = animations
        self.completion = completion
    }
    
    public init(
        config: UIViewAnimatiorProvider,
        animations: @escaping () -> Void,
        completion: ((Bool) -> Void)? = nil
    ) {
        self.init(
            provider: config,
            animations: animations,
            completion: completion
        )
    }
    
    let provider: UIViewAnimatiorProvider?
    let animations: () -> Void
    let completion: ((Bool) -> Void)?
    
    public func run() {
        if let provider = provider {
            provider.makeAnimator(
                for: animations,
                completion: completion ?? { _ in }
            ).animate()
        } else {
            animations()
            completion?(true)
        }
    }
    
    public func appendingCompletion(_ completion: @escaping (Bool) -> Void) -> UIViewAnimation {
        UIViewAnimation(
            provider: provider,
            animations: animations,
            completion: { isFinished in
                self.completion?(isFinished)
                completion(isFinished)
            }
        )
    }
    
    public static let empty: UIViewAnimation = .init(
        provider: nil,
        animations: {},
        completion: nil
    )
}

extension UIViewAnimation {
    public static func sequence(_ animations: UIViewAnimation...) -> UIViewAnimation {
        guard var animation = animations.last else { return .empty }
        animations.dropLast().reversed().forEach { prevAnimation in
            let animate = animation.run
            animation = prevAnimation.appendingCompletion { _ in animate() }
        }
        return animation
    }
}

And use it like

extension UIView {
    func animateSemaphore() {
        let initialBackground = backgroundColor
        UIViewAnimation.sequence(
            UIViewAnimation(config: .init(duration: 2)) {
                self.backgroundColor = .red
            },
            UIViewAnimation(config: .init(duration: 2)) {
                self.backgroundColor = .green
            },
            UIViewAnimation(config: .init(duration: 2)) {
                self.backgroundColor = .red
            },
            UIViewAnimation(config: .init(duration: 2)) {
                self.backgroundColor = .green
            },
            UIViewAnimation(config: .init(duration: 2)) {
                self.backgroundColor = initialBackground
            }
        ).run()
    }
}

UIView().animateSemaphore()
0
Dan On

Or use DispatchSemafore, but with the value 1.

        if presentedViewController != nil {
            let semafore = DispatchSemaphore(value: 1)
            dismiss(animated: true) {
                semafore.signal()
            }
            semafore.wait()
        }