Reputation: 1247
So I want to make a button animate on press to go to a circle, and then be able to send the button back to its original state. This is my current animation, and as you can see I'm halfway there.
As you also can see I'm having multiple issues here. First of all, when I set my new constraints the X constraint doesn't place the circle in the middle of the parent view. And then my initial thought was that when i call the reset function, that I would also pass the original constraints of the view, but that just isn't working.
My idea is that when i'm using it i'll put a UIView and then have the button inside that view, so I can manipulate the constraints of it. This would also be the case if i'm putting a button in a UIStackView.
extension UIButton {
func animateWhileAwaitingResponse(showLoading: Bool, originalConstraints: [NSLayoutConstraint]) {
let spinner = UIActivityIndicatorView()
let constraints = [
NSLayoutConstraint(item: self, attribute: .centerX, relatedBy: .equal, toItem: self.superview, attribute: .centerX, multiplier: 1, constant: 0),
NSLayoutConstraint(item: self, attribute: .width, relatedBy: .equal, toItem: nil, attribute: .width, multiplier: 1, constant: 45),
NSLayoutConstraint(item: self, attribute: .height, relatedBy: .equal, toItem: nil, attribute: .height, multiplier: 1, constant: 45),
NSLayoutConstraint(item: self, attribute: .top, relatedBy: .equal, toItem: self.superview, attribute: .top, multiplier: 1, constant: 4),
NSLayoutConstraint(item: self, attribute: .bottom, relatedBy: .equal, toItem: self.superview, attribute: .bottom, multiplier: 1, constant: 8),
NSLayoutConstraint(item: spinner, attribute: .centerX, relatedBy: .equal, toItem: self, attribute: .centerX, multiplier: 1, constant: 0),
NSLayoutConstraint(item: spinner, attribute: .centerY, relatedBy: .equal, toItem: self, attribute: .centerY, multiplier: 1, constant: 0),
NSLayoutConstraint(item: spinner, attribute: .height, relatedBy: .equal, toItem: nil, attribute: .height, multiplier: 1, constant: 45),
NSLayoutConstraint(item: spinner, attribute: .width, relatedBy: .equal, toItem: nil, attribute: .width, multiplier: 1, constant: 45)
]
if showLoading {
NSLayoutConstraint.deactivate(self.constraints)
self.translatesAutoresizingMaskIntoConstraints = false
spinner.translatesAutoresizingMaskIntoConstraints = false
self.addSubview(spinner)
self.superview?.addConstraints(constraints)
spinner.color = .white
spinner.startAnimating()
UIView.animate(withDuration: 0.3, delay: 0, usingSpringWithDamping: 0.8, initialSpringVelocity: 0, options: .curveEaseInOut, animations: {
self.setTitleColor(.clear, for: .normal)
self.layer.cornerRadius = 22.5
self.layoutIfNeeded()
}, completion: nil)
} else {
UIView.animate(withDuration: 0.3, delay: 0, usingSpringWithDamping: 0.8, initialSpringVelocity: 0, options: .curveEaseInOut, animations: {
NSLayoutConstraint.deactivate(self.constraints)
self.setTitleColor(.white, for: .normal)
self.superview?.addConstraints(originalConstraints)
NSLayoutConstraint.activate(originalConstraints)
self.layer.cornerRadius = 0
for subview in self.subviews where subview is UIActivityIndicatorView {
subview.removeFromSuperview()
}
self.layoutIfNeeded()
}, completion: nil)
}
}
}
Upvotes: 1
Views: 2893
Reputation: 489
If someone looking for Swift UI code [SWIFT 5].
import SwiftUI
struct ButtonContentView: View {
@State private var isButtonPressed = false
@State private var isLoading = false
var body: some View {
ZStack {
Button(action: {
withAnimation {
isButtonPressed.toggle()
isLoading.toggle()
}
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
withAnimation {
isButtonPressed.toggle()
isLoading.toggle()
}
}
}) {
withAnimation(.easeInOut) {
Text(isButtonPressed ? "" : "Press Me")
.foregroundColor(.white)
.frame(width: isButtonPressed ? 60 : 200, height: isButtonPressed ? 60 : 50)
.background(Color.blue)
.clipShape(isButtonPressed ? RoundedRectangle(cornerRadius: 50) : RoundedRectangle(cornerRadius: 10))
}
}
if isLoading {
CircularProgressView()
.opacity(isButtonPressed ? 1 : 0)
.scaleEffect(isButtonPressed ? 1 : 0)
}
}
}
}
struct CircularProgressView: View {
@State private var rotation = 0.0
var body: some View {
Circle()
.trim(from: 0.08, to: 1.0)
.stroke(style: StrokeStyle(lineWidth: 4, lineCap: .round, lineJoin: .round))
.foregroundColor(.white)
.rotationEffect(.degrees(rotation))
.frame(width: 40, height: 40)
.onAppear {
DispatchQueue.main.async {
withAnimation(.linear(duration: 1)
.repeatForever(autoreverses: false)) {
rotation = 360.0
}
}
}
}
}
struct ButtonContentView_previews: PreviewProvider {
static var previews: some View {
ButtonContentView()
}
}
Upvotes: 0
Reputation: 2198
So I did some changes and it works perfect for me. And I set base constraints for button in interface builder.
// StartAnimation
self.payButton.startAnimating(originalConstraints: sender.constraints)
// StopAnimation
self.payButton.startAnimating(false, originalConstraints: sender.constraints)
func startAnimating(_ showLoading: Bool = true, originalConstraints: [NSLayoutConstraint]) {
lazy var activityIndicator: UIActivityIndicatorView = {
let activityIndicator = UIActivityIndicatorView()
activityIndicator.translatesAutoresizingMaskIntoConstraints = false
activityIndicator.isUserInteractionEnabled = false
activityIndicator.color = .white
activityIndicator.startAnimating()
activityIndicator.alpha = 0
return activityIndicator
}()
let spinnerConst = [
activityIndicator.widthAnchor.constraint(equalToConstant: 40.0),
activityIndicator.heightAnchor.constraint(equalToConstant: 40.0),
activityIndicator.centerXAnchor.constraint(equalTo: self.centerXAnchor)
]
let buttonConst = [
self.widthAnchor.constraint(equalToConstant: 40.0),
self.heightAnchor.constraint(equalToConstant: 40.0)
]
if showLoading {
NSLayoutConstraint.deactivate(originalConstraints)
self.addSubview(activityIndicator)
self.superview?.addConstraints(buttonConst)
self.addConstraints(buttonConst)
self.addConstraints(spinnerConst)
UIView.animate(withDuration: 0.3, delay: 0, usingSpringWithDamping: 0.8, initialSpringVelocity: 0, options: .curveEaseInOut, animations: {
self.layer.cornerRadius = 20.0
activityIndicator.alpha = 1
self.titleLabel?.alpha = 0
self.layoutIfNeeded()
}, completion: nil)
} else {
UIView.animate(withDuration: 0.3, delay: 0, usingSpringWithDamping: 0.8, initialSpringVelocity: 0, options: .curveEaseInOut, animations: {
for subview in self.subviews where subview is UIActivityIndicatorView {
subview.removeFromSuperview()
}
self.removeConstraints(spinnerConst)
self.removeConstraints(buttonConst)
self.superview?.removeConstraints(buttonConst)
self.superview?.addConstraints(originalConstraints)
self.addConstraints(originalConstraints)
NSLayoutConstraint.activate(originalConstraints)
self.titleLabel?.alpha = 1
self.layer.cornerRadius = 6
self.layoutIfNeeded()
}, completion: nil)
}
}
Upvotes: 0
Reputation: 1
I am updating code to set dynamic button height and manage the circle according to button parent height. You can use
button.frame.height
to make a circle.
extension UIButton {
func animateWhileAwaitingResponse(showLoading: Bool, originalConstraints: [NSLayoutConstraint]) {
let spinner = UIActivityIndicatorView()
spinner.isUserInteractionEnabled = false
// Constraints which will add in supper view
let constraints = [
NSLayoutConstraint(item: self, attribute: .centerX, relatedBy: .equal, toItem: self.superview, attribute: .centerX, multiplier: 1, constant: 0),
NSLayoutConstraint(item: self, attribute: .centerY, relatedBy: .equal, toItem: self.superview, attribute: .centerY, multiplier: 1, constant: 0),
NSLayoutConstraint(item: spinner, attribute: .centerX, relatedBy: .equal, toItem: self, attribute: .centerX, multiplier: 1, constant: 0),
NSLayoutConstraint(item: spinner, attribute: .centerY, relatedBy: .equal, toItem: self, attribute: .centerY, multiplier: 1, constant: 0),
NSLayoutConstraint(item: spinner, attribute: .height, relatedBy: .equal, toItem: nil, attribute: .height, multiplier: 1, constant: self.frame.height),
NSLayoutConstraint(item: spinner, attribute: .width, relatedBy: .equal, toItem: nil, attribute: .width, multiplier: 1, constant: self.frame.height)
]
// Constrains which will add in button
let selfCostraints = [
NSLayoutConstraint(item: self, attribute: .width, relatedBy: .equal, toItem: nil, attribute: .width, multiplier: 1, constant: self.frame.height),
NSLayoutConstraint(item: self, attribute: .height, relatedBy: .equal, toItem: nil, attribute: .height, multiplier: 1, constant: self.frame.height),
]
// Keeping this outside of condition due to adding constrains programatically.
self.translatesAutoresizingMaskIntoConstraints = false
spinner.translatesAutoresizingMaskIntoConstraints = false
if showLoading {
// Remove width constrains of button from superview
// Identifier given in storyboard constrains
self.superview?.constraints.forEach({ (constraint) in
if constraint.identifier == "buttonWidth" {
constraint.isActive = false
}
})
NSLayoutConstraint.deactivate(self.constraints)
self.addSubview(spinner)
self.superview?.addConstraints(constraints)
self.addConstraints(selfCostraints)
spinner.color = .white
spinner.startAnimating()
spinner.alpha = 0
UIView.animate(withDuration: 0.3, delay: 0, usingSpringWithDamping: 0.8, initialSpringVelocity: 0, options: .curveEaseInOut, animations: {
self.setTitleColor(.clear, for: .normal)
self.layer.cornerRadius = self.frame.height / 2
spinner.alpha = 1
self.layoutIfNeeded()
}, completion: nil)
} else {
UIView.animate(withDuration: 0.3, delay: 0, usingSpringWithDamping: 0.8, initialSpringVelocity: 0, options: .curveEaseInOut, animations: {
for subview in self.subviews where subview is UIActivityIndicatorView {
subview.removeFromSuperview()
}
self.removeConstraints(selfCostraints)
NSLayoutConstraint.deactivate(self.constraints)
self.setTitleColor(.white, for: .normal)
self.superview?.addConstraints(originalConstraints)
NSLayoutConstraint.activate(originalConstraints)
self.layer.cornerRadius = 0
self.layoutIfNeeded()
}, completion: nil)
}
}
}
Upvotes: 0
Reputation: 5823
I have updated your button extension code as follow, which is adding and removing constraints with animation.
extension UIButton {
func animateWhileAwaitingResponse(showLoading: Bool, originalConstraints: [NSLayoutConstraint]) {
let spinner = UIActivityIndicatorView()
spinner.isUserInteractionEnabled = false
// Constraints which will add in supper view
let constraints = [
NSLayoutConstraint(item: self, attribute: .centerX, relatedBy: .equal, toItem: self.superview, attribute: .centerX, multiplier: 1, constant: 0),
NSLayoutConstraint(item: self, attribute: .centerY, relatedBy: .equal, toItem: self.superview, attribute: .centerY, multiplier: 1, constant: 0),
NSLayoutConstraint(item: spinner, attribute: .centerX, relatedBy: .equal, toItem: self, attribute: .centerX, multiplier: 1, constant: 0),
NSLayoutConstraint(item: spinner, attribute: .centerY, relatedBy: .equal, toItem: self, attribute: .centerY, multiplier: 1, constant: 0),
NSLayoutConstraint(item: spinner, attribute: .height, relatedBy: .equal, toItem: nil, attribute: .height, multiplier: 1, constant: 45),
NSLayoutConstraint(item: spinner, attribute: .width, relatedBy: .equal, toItem: nil, attribute: .width, multiplier: 1, constant: 45)
]
// Constrains which will add in button
let selfCostraints = [
NSLayoutConstraint(item: self, attribute: .width, relatedBy: .equal, toItem: nil, attribute: .width, multiplier: 1, constant: 45),
NSLayoutConstraint(item: self, attribute: .height, relatedBy: .equal, toItem: nil, attribute: .height, multiplier: 1, constant: 45),
]
// Keeping this outside of condition due to adding constrains programatically.
self.translatesAutoresizingMaskIntoConstraints = false
spinner.translatesAutoresizingMaskIntoConstraints = false
if showLoading {
// Remove width constrains of button from superview
// Identifier given in storyboard constrains
self.superview?.constraints.forEach({ (constraint) in
if constraint.identifier == "buttonWidth" {
constraint.isActive = false
}
})
NSLayoutConstraint.deactivate(self.constraints)
self.addSubview(spinner)
self.superview?.addConstraints(constraints)
self.addConstraints(selfCostraints)
spinner.color = .white
spinner.startAnimating()
spinner.alpha = 0
UIView.animate(withDuration: 0.3, delay: 0, usingSpringWithDamping: 0.8, initialSpringVelocity: 0, options: .curveEaseInOut, animations: {
self.setTitleColor(.clear, for: .normal)
self.layer.cornerRadius = 22.5
spinner.alpha = 1
self.layoutIfNeeded()
}, completion: nil)
} else {
UIView.animate(withDuration: 0.3, delay: 0, usingSpringWithDamping: 0.8, initialSpringVelocity: 0, options: .curveEaseInOut, animations: {
for subview in self.subviews where subview is UIActivityIndicatorView {
subview.removeFromSuperview()
}
self.removeConstraints(selfCostraints)
NSLayoutConstraint.deactivate(self.constraints)
self.setTitleColor(.white, for: .normal)
self.superview?.addConstraints(originalConstraints)
NSLayoutConstraint.activate(originalConstraints)
self.layer.cornerRadius = 0
self.layoutIfNeeded()
}, completion: nil)
}
}
}
I have added following constrains to button:
Also, added identifier of button's width constraint to remove from super which will add runtime from original constrains.
Then I have change width of button programatically by taking outlet of width constrains:
@IBOutlet weak var const_btnAnimation_width : NSLayoutConstraint!
inside viewDidLoad
method
self.const_btnAnimation_width.constant = UIScreen.main.bounds.width - 40
where 40 is sum of leading and trailing space.
on button click
@IBAction func btnAnimationPressed(_ sender: UIButton) {
sender.isSelected = !sender.isSelected
if sender.isSelected {
self.btnAnimation.animateWhileAwaitingResponse(showLoading: true, originalConstraints: sender.constraints)
} else {
self.btnAnimation.animateWhileAwaitingResponse(showLoading: false, originalConstraints: self.btnAnimationConstraints)
}
}
btnAnimationConstraints
is array of NSLayoutConstraint
as follow:
var btnAnimationConstraints = [NSLayoutConstraint]()
So I just assign all constrains of button inside viewDidAppear
method as follow:
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
self.btnAnimationConstraints = self.btnAnimation.constraints
}
I hope this will help you.
Output:
Upvotes: 3
Reputation: 3903
I noticed you set self.translatesAutoresizingMaskIntoConstraints = false
in the first animation but you didn't set it back to true
in your second animation.
That may be a source of the problem: you need to set self.translatesAutoresizingMaskIntoConstraints = true
during your second animation.
It will probably be less confusing if you turn off translatesAutoresizingMaskIntoConstraints
in Interface Builder (or wherever you create the button initially), and do all your layout with normal constraints.
Upvotes: 0