Search code examples
swiftuikituibuttonsf-symbols

How to apply a .symbolEffect (ie, the animation such as pulse) to an SF system image to a UIButton in UIKit?


Is it possible to apply a .symbolEffect (ie, the animation such as pulse) to an SF system image to a UIButton in UIKit, ie not a SwiftUI app or insert ?

(Pls note that there are a 100 questions explaining how to animate images in general, I am asking how to apply symbolEffect specifically, is it possible in UIKit apps?? - ie not SwiftUI ?)


Solution

  • In UIKit, support for adding symbol effects only exists for UIImageView and UIBarButtonItem. It's really odd that it's not supported for UIButton.

    Here's an extension to UIButton that provides the same API as UIImageView:

    extension UIButton {
        private var imageView: UIImageView? {
            for subview in subviews {
                if let iv = subview as? UIImageView {
                    return iv
                }
            }
            return nil
        }
    
        func addSymbolEffect(_ effect: some IndefiniteSymbolEffect & SymbolEffect, options: SymbolEffectOptions = .default, animated: Bool = true, completion: UISymbolEffectCompletion? = nil) {
            imageView?.addSymbolEffect(effect, options: options, animated: animated, completion: completion)
        }
    
        func addSymbolEffect(_ effect: some DiscreteSymbolEffect & IndefiniteSymbolEffect & SymbolEffect, options: SymbolEffectOptions = .default, animated: Bool = true, completion: UISymbolEffectCompletion? = nil) {
            imageView?.addSymbolEffect(effect, options: options, animated: animated, completion: completion)
        }
    
        func addSymbolEffect(_ effect: some DiscreteSymbolEffect & SymbolEffect, options: SymbolEffectOptions = .default, animated: Bool = true, completion: UISymbolEffectCompletion? = nil) {
            imageView?.addSymbolEffect(effect, options: options, animated: animated, completion: completion)
        }
    
        func removeSymbolEffect(ofType effect: some IndefiniteSymbolEffect & SymbolEffect, options: SymbolEffectOptions = .default, animated: Bool = true, completion: UISymbolEffectCompletion? = nil) {
            imageView?.removeSymbolEffect(ofType: effect, options: options, animated: animated, completion:  completion)
        }
    
        func removeSymbolEffect(ofType effect: some DiscreteSymbolEffect & IndefiniteSymbolEffect & SymbolEffect, options: SymbolEffectOptions = .default, animated: Bool = true, completion: UISymbolEffectCompletion? = nil) {
            imageView?.removeSymbolEffect(ofType: effect, options: options, animated: animated, completion:  completion)
        }
    
        func removeSymbolEffect(ofType effect: some DiscreteSymbolEffect & SymbolEffect, options: SymbolEffectOptions = .default, animated: Bool = true, completion: UISymbolEffectCompletion? = nil) {
            imageView?.removeSymbolEffect(ofType: effect, options: options, animated: animated, completion:  completion)
        }
    
        func removeAllSymbolEffects(options: SymbolEffectOptions = .default, animated: Bool = true) {
            imageView?.removeAllSymbolEffects(options: options, animated: animated)
        }
    }
    

    Here's some test code you can put in a Playground (along with the above extension):

    import UIKit
    import PlaygroundSupport
    
    let image = UIImage(systemName: "eye.fill")
    var cfg = UIButton.Configuration.bordered()
    cfg.image = image
    let button = UIButton(configuration: cfg)
    button.addSymbolEffect(.pulse)
    PlaygroundPage.current.liveView = button
    

    This will show a button with a pulsing eye symbol.