Swift - Restarting an animation after clicking a button - ios

I was hoping someone could help.
I have two rectangles which move across the screen. The first constraints for the (rectangles) are activated in my setupViews function which is called in my viewDidLoad.
In my viewDidLayoutSubviews I have an animation function which is called moving the two rectangles.
When I click a button however I would like the rectangles to return to their initial position and begin the animation from the beginning.
How could I successfully do this?
I initially did not have my animation being called in my viewDidLayoutSubviews, just in another function (which was being called in my viewDidAppear(_ animated: Bool)), however, upon reading it seems as if calling the animation in the viewDidLayoutSubviews might be the best course of action.
I can successfully stop the animation, but getting it to restart again from its initial starting point is proving to be troublesome.
I have tried creating the constraints again in the buttons sender function. But this doesn’t seem to do anything, as I’m not sure you can change an elements constraints without reloading the viewDidLoad again.
Below is my code………
override func viewDidLoad() {
super.viewDidLoad()
setupViews()
}
func setupViews() {
self.view.addSubview(backgroundImageView)
self.backgroundImageView.addSubview(contentView)
self.backgroundImageView.addSubview(slideDownTintedWhiteView)
self.backgroundImageView.addSubview(restartSlidingAnimation)
self.contentView.snp.makeConstraints { (make) in
make.edges.equalToSuperview()
}
self.slideDownTintedWhiteView.snp.makeConstraints { (make) in
make.top.equalToSuperview()
make.leading.equalToSuperview()
make.trailing.equalToSuperview()
make.bottom.equalTo(backgroundImageView.snp.top)
}
self.restartSlidingAnimation.snp.makeConstraints { (make) in
make.centerX.equalToSuperview()
make..centerY.equalToSuperview()
make.width.equalTo(10)
make.height.equalTo(10)
}
self.restartSlidingAnimation.addTarget(self, action: #selector(restartAnimationFunction(_:)), for: .touchUpInside)
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
UIView.animate(withDuration: 10, delay: 0, animations: {
//Red View:
self.contentView.frame = CGRect(x: self.contentView.frame.origin.x, y: self.contentView.frame.height, width: self.contentView.frame.width, height: self.contentView.frame.height)
//White View:
self.slideDownTintedWhiteView.frame = CGRect(x: self.contentView.frame.origin.x, y: 0, width: self.contentView.frame.width, height: self.contentView.frame.height)
}, completion: nil)
}
restartAnimationFunction() {
// Here is obviously where I need to somehow re-establish the constraints of the rectangles back to their original location or somehow start the animation from the beginning.
}
func stopContentViewAnimation() {
// This is where I am able to stop the animation (called from another button)
pausedTime = self.contentView.layer.convertTime(CACurrentMediaTime(), from: nil)
self.contentView.layer.speed = 0.0
self.contentView.layer.timeOffset = pausedTime
pausedTime = self.slideDownTintedWhiteView.layer.convertTime(CACurrentMediaTime(), from: nil)
self.slideDownTintedWhiteView.layer.speed = 0.0
self.slideDownTintedWhiteView.layer.timeOffset = pausedTime
}
Thanks for any help you can provide.

1- First the animation should not be inside viewDidLayoutSubviews as it's being called many times , best place for it is viewDidAppear
2- You don't set any constraints to backgroundImageView
3- To animate a child remove it's constraints from itself and from it's parent
self.backgroundImageView.removeConstraints(self.backgroundImageView.constraints)
self.contentView.removeConstraints(self.contentView.constraints)
self.slideDownTintedWhiteView.removeConstraints(self.slideDownTintedWhiteView.constraints)
self.restartSlidingAnimation.removeConstraints(self.restartSlidingAnimation.constraints)
4- Re construct the other case constraints and when ready to animate put
self.view.layoutIfNeeded()
inside the animation block
OFF course I don't recommend removing all constraints and constructing them again , but you can have a reference to the constraints you want to change and alter their constants , but for a beginner this may be good to go

Related

UIScrollView Content Offset on Initialization

We are currently working in an older codebase for our iOS application and are running into a weird bug where the UIScrollViews paging is not matching on the initialization but only once a user selects the button to change the view.
Expected Result:
The result we have:
Each ScrollView has three slides nested inside of them. We initialize the ScrollView like this:
override init(frame: CGRect) {
super.init(frame: frame)
self.commonInit()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
self.commonInit()
}
private func commonInit() {
Bundle.main.loadNibNamed("DIScrollView", owner: self, options: nil)
contentView.frame = self.bounds
addSubview(contentView)
contentView.autoresizingMask = [.flexibleHeight,.flexibleWidth]
contentView.layer.borderColor = UIColor.white.cgColor
contentView.layer.borderWidth = 2.0
scrollView.delegate = self
setUpScrollViewer()
}
You can see we call to set up the ScrollView and that is done like this:
public func setUpScrollViewer() {
let slides = self.createSlides()
let defaultIndex = 1
scrollView.Initialize(slides: slides, scrollToIndex: defaultIndex)
pageControl.numberOfPages = slides.count
pageControl.currentPage = defaultIndex
}
Now that all the content is available for each slide, we want to handle the content and we do so with a ScrollView extension:
extension UIScrollView {
//this function adds slides to the scrollview and constraints to the subviews (slides)
//to ensure the subviews are properly sized
func Initialize(slides:[UIView], scrollToIndex:Int) {
//Take second slide to base size from
let frameWidth = slides[1].frame.size.width
self.contentSize = CGSize(width: frameWidth * CGFloat(slides.count), height: 1)
for i in 0 ..< slides.count {
//turn off auto contstraints. We will be setting our own
slides[i].translatesAutoresizingMaskIntoConstraints = false
self.addSubview(slides[i])
//pin the slide to the scrollviewers edges
if i == slides.startIndex {
slides[i].leadingAnchor.constraint(equalTo: self.leadingAnchor).isActive = true
} else { //pin each subsequent slides leading edge to the previous slides trailing anchor
slides[i].leadingAnchor.constraint(equalTo: slides[i - 1].trailingAnchor).isActive = true
}
slides[i].topAnchor.constraint(equalTo: self.topAnchor).isActive = true
slides[i].widthAnchor.constraint(equalTo: self.widthAnchor).isActive = true
slides[i].heightAnchor.constraint(equalTo: self.heightAnchor).isActive = true
}
//the last slides trailing needs to be pinned to the scrollviewers trailing.
slides.last?.trailingAnchor.constraint(equalTo: self.trailingAnchor).isActive = true
self.scrollRectToVisible(CGRect(x: frameWidth * CGFloat(scrollToIndex), y: 0, width: frameWidth, height: 1), animated: false)
}
}
I have tried manually setting contentOffset and nothing seems to be adjusting on the initialization. If the user selects the button it hides and then unhides it to display it properly with no logic adjusting this. Giving me the impression this issue is on the init.
Summary:
When the main view loads, the scrollView is showing me the first slide in the index when i need to be focused on the second slide. However if the user hides and then unhides the scrollView it works as intended.
How do i get the UIScrollView to actually load and initialize updating the scrollView to show the second slide and not initialize on the first slide?
Try explicitely running the scrollRectToVisible in the main thread using
DispatchQueue.main.async {
}
My guess is that all this code runs before the views are positioned by the layout system, and the first slide’s frame is the default 0 x 0 size. When the app returns to this view auto layout has figured out the size of this slide, so the calculation works.
Tap into the layout cycle to scroll to the right place after the layout. Maybe override viewDidLayoutSubviews() to check if it’s in the initial layout and then set the scroll position.
Use constraints for your contentView instead setting frame and autoresizingMask.
Call view.layoutIfNeeded() in the viewController before scrollRectToVisible or setContentOffset(I prefer the last)

Moving view offscreen at load time not working

I have an audio player view at the bottom of my application. I want this audio player view to hide with a slide animation at the bottom of the screen once it finishes the last item in the playlist. At the start of the application, I need this audioplayer view to be hidden until the user taps an audio file to play.
The issue I am having is that the audioplayer view won't move offscreen at the beginning of the VC loading.
What's odd is that I have a similar function that moves the audioplayer view offscreen correctly, and everything works fine. This seems like it is only an issue at load time - that initial hiding of the audioplayer view.
Code:
override func viewDidLoad(){
super.viewDidLoad()
...
footerView.backgroundColor = UIColor.clear
footerView.superview?.backgroundColor = UIColor.clear
footerView.playButton.tintColor = UIColor.red
footerView.playButton.borderColor = UIColor.red
initializeFooterView()
...
}
//print statements called #viewDidLoad, but not the translate function
func initializeFooterView(){
print("initFooterView", String(describing: footerView.superview?.frame.origin.y))
print(String(describing: footerView.frame.size.height))
footerView.superview?.frame.origin.y += footerView.frame.size.height
print("initFooterView", String(describing: footerView.superview?.frame.origin.y))
}
//Working function to show/hide audioplayer view... works during runtime
func hideShowFooterView(){
let animationOptions: UIViewAnimationOptions = .curveEaseOut
let keyframeAnimationOptions: UIViewKeyframeAnimationOptions = UIViewKeyframeAnimationOptions(rawValue: animationOptions.rawValue)
if let footerView = self.footerView{
if (footerView.superview?.isHidden)!{
footerView.superview?.isHidden = false
UIView.animateKeyframes(withDuration: 0.3, delay: 0.0, options: keyframeAnimationOptions , animations: {() in
footerView.superview?.frame.origin.y -= (footerView.superview?.frame.size.height)!
}, completion: nil)
}else{
UIView.animateKeyframes(withDuration: 0.3, delay: 0.0, options: keyframeAnimationOptions , animations: {() in
footerView.superview?.frame.origin.y += (footerView.superview?.frame.size.height)!
}, completion: { (completed) in
if completed{
footerView.superview?.isHidden = true
}
})
}
}
}
Cleaned up print statements called from initializeFooterView() that moves audioPlayer view offscreen within viewDidLoad():
initFooterView() - footerView.superview?.frame.origin.y: 0.0
initFooterView() - footerView.frame.size.height = 75.0
initFooterView() - footerView.superview?.frame.origin.y: 75
If you're wondering why I translate the view on y by footerView.frame.size.height in initializeFooterView(), but translate the view by footerView.superview.frame.size.height in the hideShowFooterView(), it's because the height of the superview at viewDidLoad is 763, for some reason, while the height of the footerView's frame is 75 (the correct amount). It translates correctly during runtime, however, so I use the footerView's superview.frame.
Hierarchy of my views:
Container view set up: (footerviewcontroller is segued from a container view... not sure if that's influential)
I have a feeling that there is a conflict in defining my footerview within a storyboard, then trying to change it programmatically during viewDidLoad(). I don't want to have to define everything about footerView programmatically, though :/
It's a little tough to see what you're doing... Are you setting constraints on the footerView but then explicitly setting the frame? If so, that might be part of the issue.
However, since you say it's working fine in hideShowFooterView(), try moving initializeFooterView() from viewDidLoad() to viewWillAppear()

Animate `backgroundColor` of a `UIView` that implements `drawRect`

I have a custom UIView and I would like to animate its backgroundColor property. This is an animatable property of a UIView.
This is the code:
class ETTimerUIView: UIView {
required init(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
// other methods
func flashBg() {
UIView.animateWithDuration( 1.0, animations: {
self.backgroundColor = UIColor.colorYellow()
})
}
override func drawRect() {
// Something related to a timer I'm rendering
}
This code causes causes the animation to skip and the color to change immediately:
self.backgroundColor = UIColor.colorYellow() // Changes immediately to yellow
If I animate alpha, this animates from 1 to 0 over one second as expected:
self.alpha = 0 // animates
How do I animate a background color change in this situation?
Implementing drawRect blocks backgroundColor animation, but no answer is provided yet.
Maybe this is why you can't combine drawRect and animateWithDuration, but I don't understand it much.
I guess I need to make a separate view--should this go in the storyboard in the same view controller? programmatically created?
Sorry, I'm really new to iOS and Swift.
It is indeed not working when I try it, I had a related question where putting the layoutIfNeeded() method inside the animation worked and made the view smoothly animating (move button towards target using constraints, no reaction?). But in this case, with the backgroundColor, it does not work. If someone knows the answer I will be interested to know.
But if you need a solution right now, you could create a UIView (programmatically or via the storyboard) that is used only as a container. Then you add 2 views inside : one on top, and one below, with the same frame as the container. And you only change the alpha of the top view, which let the user see the view behind :
class MyView : UIView {
var top : UIView!
override init(frame: CGRect) {
super.init(frame: frame)
self.backgroundColor = UIColor.blueColor()
top = UIView(frame: CGRectMake(0,0, self.frame.width, self.frame.height))
top.backgroundColor = UIColor.yellowColor()
self.addSubview(top)
}
override func touchesBegan(touches: NSSet, withEvent event: UIEvent) {
let sub = UIView(frame: CGRectMake(0,0, self.frame.width, self.frame.height))
sub.backgroundColor = UIColor.purpleColor()
self.sendSubviewToBack(sub)
UIView.animateWithDuration(1, animations: { () -> Void in
self.top.alpha = 0
}) { (success) -> Void in
println("anim finished")
}
}
}
The answer is that you cannot animate backgroundColor of a view that implements drawRect. I do not see docs for this anywhere (please comment if you know of one).
You can't animate it with animateWithDuration, nor with Core Animation.
This thread has the best explanation I've found yet:
When you implement -drawRect:, the background color of your view is then drawn into the associated CALayer, rather than just being set on the CALayer as a style property... thus prevents you from getting a contents crossfade
The solution, as #Paul points out, is to add another view above, behind, or wherever, and animate that. This animates just fine.
Would love a good understanding of why it is this way and why it silently swallows the animation instead of hollering.
Not sure if this will work for you, but to animate the background color of a UIView I add this to a UIView extension:
extension UIView {
/// Pulsates the color of the view background to white.
///
/// Set the number of times the animation should repeat, or pass
/// in `Float.greatestFiniteMagnitude` to pulsate endlessly.
/// For endless animations, you need to manually remove the animation.
///
/// - Parameter count: The number of times to repeat the animation.
///
func pulsate(withRepeatCount count: Float = 1) {
let pulseAnimation = CABasicAnimation(keyPath: "backgroundColor")
pulseAnimation.fromValue = <#source UIColor#>.cgColor
pulseAnimation.toValue = <#target UIColor#>.cgcolor
pulseAnimation.duration = 0.4
pulseAnimation.autoreverses = true
pulseAnimation.repeatCount = count
pulseAnimation.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.easeInEaseOut)
self.layer.add(pulseAnimation, forKey: "Pulsate")
CATransaction.commit()
}
}
When pasting this in to a source file in Xcode, replace the placeholders with your two desired colors. Or you can replace the entire lines with something like these values:
pulseAnimation.fromValue = backgroundColor?.cgColor
pulseAnimation.toValue = UIColor.white.cgColor

Perform Animation In viewDidAppear On Objects With Constraints Set By Interface Builder

I have watched the WWDC 2012 presentations on Auto Layout and read the documentation on the view appearance calls.
So I thought I needed to perhaps wait a frame or a second after viewDidAppear just to be safe, but still didn't work. Here was my code:
override func viewDidAppear(animated: Bool)
{
super.viewDidAppear(animated)
view.layoutIfNeeded()
view.autoresizesSubviews = false
_textButton.frame = CGRectMake(0, 0, 0, 0)
println(_textButton.frame)
let delay:Double = 4*Double(NSEC_PER_SEC)
let time = dispatch_time(DISPATCH_TIME_NOW, Int64(delay))
dispatch_after(time, dispatch_get_main_queue())
{
self._textButton.frame = CGRectMake(0, 0, 0, 0)
println(self._textButton.frame)
}
}
This actually prints out (144.0,6.5,32.0,32.0) twice in a row. Which means even AFTER setting the frame to 0, it was set back to its constraint defaults.
Why is this?
I have another ViewController that looks almost the same as this one, with buttons having the same constraints. But when I close the view, I animate the buttons to slide out to the left with the following code:
#IBAction func takePhotoTap(sender: AnyObject)
{
UIView.animateWithDuration(0.5, animations: animations)
_camera.captureImage(receivePhoto)
}
func animations()
{
var height = CGFloat(_distanceBetweenTopAndMiddleBar)/2
_lowerLens.frame = CGRectMake(_lowerLens.frame.origin.x, _lowerLens.frame.origin.y, _lowerLens.frame.width,-height)
_upperLens.frame = CGRectMake(_upperLens.frame.origin.x, _upperLens.frame.origin.y, _upperLens.frame.width,height)
view.autoresizesSubviews = false
slideOffScreenLeft(_gridLinesButton)
slideOffScreenLeft(_swapCameraButton)
slideOffScreenLeft(_flashButton)
}
func slideOffScreenLeft(obj:UIView)
{
obj.frame = CGRectMake((-obj.frame.width), obj.frame.origin.y, obj.frame.width, obj.frame.height)
}
This works JUST FINE! When they hit the button, these buttons slide off the screen. However when I load the next view I want the buttons to slide in from the right. But, as you can see above, even waiting 4 seconds before trying to set the frames of the buttons has no effect.
Can you suggest what I can do to animate some buttons to slide in the from the screen when the view loads? Why are the constraints overriding my changes in the first case but when I animated a View Controller before closing it with UIView.animateWithDuration the constraints were overriden?
When using AutoLayout, it is best practice to create outlets for your constraints and then modify the constraints, rather than the frames of your objects. I was having a similar problem. This also gets rid of the need for the dispatch_after call.
For example:
#IBOutlet weak var buttonHeight: NSLayoutConstraint!
#IBOutlet weak var buttonWidth: NSLayoutConstraint!
override func viewDidAppear(animated: Bool)
{
super.viewDidAppear(animated)
// Modify the constraint rather than the frame
self.buttonHeight.constant = 0
self.buttonWidth.constant = 0
}
This will make the button 0x0. You can then do the same for the x,y position.

redraw a custom uiview when changing a device orientation

If I draw my chart inside - (void)drawRect:(CGRect)rect is just enough to set [_chartView setContentMode:UIViewContentModeRedraw] and this method will be called when device changes it's orienatation and it's possible to calculate f.e. new center point for my chart.
If I create a view like a subview using - (id)initWithFrame:(CGRect)frame and then add it in view controller like [self.view addSubview:chartView];. How in this case I can handle rotation to redraw my chart?
While a preferred solution requires zero lines of code, if you must trigger a redraw, do so in setNeedsDisplay, which in turn invokes drawRect.
No need to listen to notifications nor refactor the code.
Swift
override func layoutSubviews() {
super.layoutSubviews()
self.setNeedsDisplay()
}
Objective-C
- (void)layoutSubviews {
[super layoutSubviews];
[self setNeedsDisplay];
}
Note:
layoutSubviews is a UIView method, not a UIViewController method.
To make your chart rendered correctly when device orientation changes you need to update chart's layout, here is the code that you should add to your view controller:
- (void)viewDidLayoutSubviews {
[super viewDidLayoutSubviews];
_chartView.frame = self.view.bounds;
[_chartView strokeChart];
}
Zero Lines of Code
Use .redraw
Programmatically invoking myView.contentMode = .redraw when creating the custom view should suffice. It is a single flag in IB and, as such, the 0 lines of code prefered way. See Stack Overflow How to trigger drawRect on UIView subclass.
Go here to learn how to receive notifications for when the device orientation changes. When the orientation does change, just call [chartView setNeedsDisplay]; to make drawRect: get called so you can update your view. Hope this helps!
The code you'll add to your view controller:
- (void)updateViewConstraints
{
[super updateViewConstraints];
[_chartView setNeedsDisplay];
}
Unfortunately some answers suggest to override controller methods, but I've some custom UITableViewCells with a shadow around and rotating the device stretches the cells but doesn't redraw the shadow. So I don't want to put my code within a controller to (re)draw a subview.
The solution for me is to listen to a UIDeviceOrientationDidChange notification within my custom UITableViewCell and then call setNeedsDisplay() as suggested.
Swift 4.0 Code example in one of my custom UITableViewCells
override func awakeFromNib() {
super.awakeFromNib()
NotificationCenter.default.addObserver(self, selector: #selector(deviceOrientationDidChangeNotification), name: NSNotification.Name.UIDeviceOrientationDidChange, object: nil)
}
#objc func deviceOrientationDidChangeNotification(_ notification: Any) {
Logger.info("Orientation changed: \(notification)")
setNeedsLayout()
}
BTW: Logger is a typealias to SwiftyBeaver.
Thanks to #Aderis pointing me to the right direction.
I tried "setNeedsDisplay" in a dozen ways and it didn't work for adjusting the shadow of a view I was animating to a new position.
I did solve my problem with this though. If your shadow doesn't seem to want to cooperate/update, you could try just setting the shadow to nil, then setting it again:
UIView.animateKeyframes(withDuration: 0.2 /*Total*/, delay: 0.0, options: UIViewKeyframeAnimationOptions(), animations: {
UIView.addKeyframe(withRelativeStartTime: 0.0, relativeDuration: 10/10, animations:{
// Other things to animate
//Set shadow to nil
self.viewWithShadow.layer.shadowPath = nil
})
}, completion: { finished in
if (!finished) { return }
// When the view is moved, set the shadow again
self.viewWithShadow.layer.shadowPath = UIBezierPath(rect: self.descTextView.bounds).cgPath
})
If you don't even have a shadow yet and need that code, here's that:
func addShadowTo (view: UIView) {
view.layer.masksToBounds = false
view.layer.shadowColor = UIColor.gray.cgColor
view.layer.shadowOffset = CGSize( width: 1.0, height: 1.0)
view.layer.shadowOpacity = 0.5
view.layer.shadowRadius = 6.0
view.layer.shadowPath = UIBezierPath(rect: view.bounds).cgPath
view.layer.shouldRasterize = true
}
Thanks Keenle, for the above ObjC solution. I was messing around with creating programmed constraints all morning, killing my brain, not understanding why they would not recompile/realign the view. I guess because I had created the UIView programmatically using CGRect...this was taking precedence.
However, here was my solution in swift:
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
myUIView.center = CGPoint(x: (UIScreen.mainScreen().bounds.width / 2), y: (UIScreen.mainScreen().bounds.height / 2))
}
So relieved! :)

Resources