0

I have a Button that acts as an SOS Button. I would like to only accept longpresses on that button (something like two seconds long press) and animate the button while pressing to let the user know he has to longpress.

The Button is just a round Button:

let SOSButton = UIButton()
    SOSButton.backgroundColor = Colors.errorRed
    SOSButton.setImage(UIImage(systemName: "exclamationmark.triangle.fill"), for: .normal)
    SOSButton.translatesAutoresizingMaskIntoConstraints = false
    SOSButton.tintColor = Colors.justWhite
    SOSButton.clipsToBounds = true
    SOSButton.layer.cornerRadius = 25
    SOSButton.addTarget(self, action: #selector(tappedSOSButton(sender:)), for: .touchUpInside)
    SOSButton.translatesAutoresizingMaskIntoConstraints = false
    view.addSubview(SOSButton)

which looks something like this: enter image description here

Now, when the button is getting long-pressed, I'd like to animate a stroke like a circular progress view. It would start from 0* and fill the whole circle to finally look like this: enter image description here I know it looks the same because the background is white, but there is a white stroke around it. If the user lets go of the button before the circle fills up, it should animate back to zero in the same speed. If the user holds on long enough, only then should the action get executed.

How would I go about designing such a button? I have not found anything I can work off right now. I know I can animate stuff but animating while long-pressing seems like I'd need to implement something very custom.

Interested in hearing ideas.

Jan L
  • 233
  • 1
  • 10

1 Answers1

0

You can create a custom class of UIView and add layer to it.

class CircularProgressBar: UIView {
    
    private var circularPath: UIBezierPath = UIBezierPath()
    var progressLayer: CAShapeLayer!
    
    var progress: Double = 0 {
        willSet(newValue) {
            progressLayer?.strokeEnd = CGFloat(newValue)
        }
    }
    
    override init(frame: CGRect) {
        super.init(frame: frame)
    }
    
    required init?(coder: NSCoder) {
        super.init(coder: coder)
    }
    
    override func layoutSubviews() {
        super.layoutSubviews()
        removeAllSubviewAndSublayers()
        setupCircle()
    }
    
    private func removeAllSubviewAndSublayers() {
        layer.sublayers?.forEach { $0.removeFromSuperlayer() }
        subviews.forEach { $0.removeFromSuperview() }
    }
    
    func setupCircle() {
        let x = self.frame.width / 2
        let y = self.frame.height / 2
        let center = CGPoint(x: x, y: y)
        
        circularPath = UIBezierPath(arcCenter: center, radius: x, startAngle: -0.5 * CGFloat.pi, endAngle: 1.5 * CGFloat.pi, clockwise: true)
        
        progressLayer = CAShapeLayer()
        progressLayer.path = circularPath.cgPath
        progressLayer.strokeColor = UIColor.white.cgColor
        progressLayer.fillColor = UIColor.clear.cgColor
        progressLayer.lineWidth = x/10
        progressLayer.lineCap = .round
        progressLayer.strokeEnd = 0
        layer.addSublayer(progressLayer)
    }
    
    func addStroke(duration: Double = 2.0) {
        let animation = CABasicAnimation(keyPath: "strokeEnd")
        animation.fromValue = 0.0
        animation.toValue = 1.0
        animation.duration = duration
        animation.beginTime = CACurrentMediaTime()
        progressLayer.add(animation, forKey: "strokeEnd")
    }
    
    func removeStroke(duration: Double = 0.0) {
        let revAnimation = CABasicAnimation(keyPath: "strokeEnd")
        revAnimation.duration = duration
        revAnimation.fromValue = progressLayer.presentation()?.strokeEnd
        revAnimation.toValue = 0.0
        progressLayer.removeAllAnimations()
        progressLayer.add(revAnimation, forKey: "strokeEnd")
    }
}

In UIViewController create a UIImageView and CircularProgressBar. Set isUserInteractionEnabled to true of imageView and add progressView to it.

In viewDidLayoutSubviews() method set the frame of progressView equal to bounds of imageView. You also need to set Timer to execute action. Here is the full code.

class ViewController: UIViewController {
    
    let imageView = UIImageView(image: UIImage(named: "icon_sos")!)
    let progressView = CircularProgressBar()
    
    var startTime: Date?
    var endTime: Date?
    var longPress: UILongPressGestureRecognizer?
    var timer: Timer?
    let longPressDuration: Double = 2.0
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        longPress = UILongPressGestureRecognizer(target: self, action: #selector(longPressAction(_:)))
        longPress?.minimumPressDuration = 0.01
        
        imageView.frame = CGRect(x: 100, y: 100, width: 30, height: 30)
        imageView.isUserInteractionEnabled = true
        imageView.addGestureRecognizer(longPress!)
        imageView.addSubview(progressView)
        self.view.addSubview(imageView)
    }
    
    override func viewDidLayoutSubviews() {
        super.viewDidLayoutSubviews()
        progressView.frame = imageView.bounds
    }
    
    private func setupTimer() {
        timer = Timer.scheduledTimer(timeInterval: self.longPressDuration, target: self, selector: #selector(fireTimer), userInfo: nil, repeats: false)
    }
    
    @objc private func fireTimer() {
        longPress?.isEnabled = false
        longPress?.isEnabled = true
        progressView.removeStroke()
        
        if let timer = timer {
            timer.invalidate()
            self.timer = nil
        }
        
        // execute button action here
        print("Do something")
    }
    
    @objc private func longPressAction(_ sender: UILongPressGestureRecognizer) {
        
        if sender.state == .began {
            print("Long Press Began: ", Date())
            startTime = Date()
            self.progressView.addStroke(duration: self.longPressDuration)
            setupTimer()
        }
        if sender.state == .changed {
            print("Long Press Changed: ", Date())
        }
        if sender.state == .cancelled {
            print("Long Press Cancelled: ", Date())
            endTime = Date()
            
            if let startTime = startTime, let endTime = endTime {
                let interval = DateInterval(start: startTime, end: endTime)
                progressView.removeStroke(duration: interval.duration.magnitude)
                if interval.duration.magnitude < self.longPressDuration {
                    timer?.invalidate()
                    timer = nil
                }
            }
        }
        
        if sender.state == .ended {
            print("Long Press Ended: ", Date())
            endTime = Date()
            
            if let startTime = startTime, let endTime = endTime {
                let interval = DateInterval(start: startTime, end: endTime)
                progressView.removeStroke(duration: interval.duration.magnitude)
                if interval.duration.magnitude < self.longPressDuration {
                    timer?.invalidate()
                    timer = nil
                }
            }
        }
    }
}
MBT
  • 1,381
  • 1
  • 6
  • 10