Using animateWithDuration when a button is pressed but it has no effect (swift 2) - ios

I'm using the following code to have a label slide onto the screen when a button is pressed, but it's having no effect.
override func viewDidLayoutSubviews() {
summaryLabel.alpha = 0
}
#IBAction func searchButton(sender: AnyObject) {
UIView.animateWithDuration(2) { () -> Void in
self.summaryLabel.center = CGPointMake(self.summaryLabel.center.x - 400, self.summaryLabel.center.y)
self.summaryLabel.alpha = 1
self.summaryLabel.center = CGPointMake(self.summaryLabel.center.x + 400, self.summaryLabel.center.y)
}
//button code continues...
I've tested what's going on by fixing the alpha at 1, but the label just stays where it is and does not move when the button is pressed. What am I missing?

A couple of things:
First of all, your two changes to the view's center cancel each other out. The animation applies the full set of changes that are inside the animation block all in one animation. If the end result is no change, then no change is applied. As Ramy says in his comment, you either need 2 animations, timed so the second one takes place after the first one has completed, or you nee to apply the first change before the animation begins. I would suggest starting with a single change, and a single animation, and get that working first.
Second problem: View controllers use auto layout by default. With auto layout, you can't animate the position of a view directly. It doesn't work reliably. Instead, you have to put a constraint on the view, connect it to an outlet, and the animate a change to the constraint's constant value by changing the constant and calling layoutIfNeeded() inside the animation block. The call to layoutIfNeeded() inside the animation block causes the view's position to be changed, and since it's inside the animation block, the change is applied with animation.

Related

View/ViewController hierarchy and positioning issues

I have a problem which I try to solve with 2 approaches. Both work to some extent, but then fail. The help with any of them will be highly appreciated. Also any other working approaches are welcome.
This is the task that needs to be accomplished:
I have an input component. For simplicity, let's say it contains only text field and send button. It is a view controller. The send button is another view controller that is embedded as a child view controller. The input component is then later embedded as a child view controller itself to some higher level container.
This can't be changed and we have to take this as granted.
The input controller should be clever enough to be able to display itself above the keyboard whenever its text field becomes first responder.
So, the first approach is adding the input's component contentView as the text field's inputAccessoryView.
func textFieldDidBeginEditing(_ textField: UITextField) {
// do some staff
// This needs to be done, because in above-keyboard position the input component has another layout compared to when it's inactive
contentView.layoutIfNeeded()
// This is the actual adding itself to be displayed above the keyboard
inputField.inputAccessoryView = contentView
}
As far as I understand more staff happens under the hood including adding and removing child view controllers.
Then, when the user taps return button or the send button, the following code is executed:
func textFieldShouldReturn(_ textField: UITextField) -> Bool {
textField.resignFirstResponder()
return true
}
which triggers the
func textFieldDidEndEditing(_ textField: UITextField) {
// We add contentView back where it was before and reset constraints
view.addSubview(contentView)
contentView.leadingAnchor.constraint(equalTo: view.leadingAnchor).isActive = true
contentView.trailingAnchor.constraint(equalTo: view.trailingAnchor).isActive = true
contentView.bottomAnchor.constraint(equalTo: view.bottomAnchor).isActive = true
}
Hacky as hell, this code works. The only problem is that the view controller hierarchy seems to be corrupted (the input component is no longer displayed in view debugger, but the text field and button are on screen, so I was ok with that).
Then I've found the corner case. When the user dismisses keyboard by tapping anywhere on screen, or when he/she activates another text field, the textFieldShouldReturn is not called, but the textFieldDidEndEditing is called.
As a result, the contentView is removed from the inputAccessoryView, but it doesn't appear where it was before.
Debugging revealed that its superview ends up being nil, which leads me to the conclusion, that iOS at some point removes it even though I've added it as a view's subview.
If I add contentView.removeFromSuperview() before view.addSubview(contentView), then the app crushes with this error message
Thread 1: Exception: "child view
controller:UICompatibilityInputViewController: 0x7fa13b2a3c00 should
have parent view controller:(null) but actual parent
is:UIInputWindowController: 0x7fa136045000"
I don't understand how my code is guilty of this, so probably I do something that I am not supposed to from Apple's perspective.
This is why I tried the second approach.
This time I don't do anything with the inputAccessoryView. Instead I just modify the bottom constraint of the contentView to move up when keyboard is displayed:
func textFieldDidBeginEditing(_ textField: UITextField) {
contentViewBottomConstraint.constant = // whatever the keyboard's height is
UIView.animate(withDuration: viewModel.animationDurationForInputField,
delay: 0,
options: .curveEaseOut, animations: { [weak self] in
self?.view.layoutIfNeeded()
})
}
and to hide
func textFieldDidEndEditing(_ textField: UITextField) {
contentViewBottomConstraint.constant = 0
UIView.animate(withDuration: viewModel.animationDurationForInputField,
delay: 0,
options: .curveEaseOut, animations: { [weak self] in
self?.view.layoutIfNeeded()
})
}
This works like a charm and actually this should be the correct approach without messing around with inputAccessoryView and child-parent relationships of the view controllers.
But, the problems begin when this input component is embedded into the detail part of the master/detail controller. On iPad, the detail part takes around half of the screen. So is the width of our input component. And when the keyboard is displayed, the input component is pushed up, but it's width remains the same - half of the screen.
To make it full screen, I get the reference to the top window, deactivate old leading and trailing constraints and add new one - between top window and contentView.
guard let keyWindow = UIApplication.shared.keyWindow else { return }
contentViewBottomConstraint.constant = viewModel.bottomOffsetForInputField
NSLayoutConstraint.deactivate(originalContentViewConstraints)
contentViewToKeyboardLeadingConstraint = contentView.leadingAnchor.constraint(equalTo: keyWindow.leadingAnchor)
contentViewToKeyboardLeadingConstraint?.isActive = true
contentViewToKeyboardTrailingConstraint = contentView.trailingAnchor.constraint(equalTo: keyWindow.trailingAnchor)
contentViewToKeyboardTrailingConstraint?.isActive = true
And it does become full screen, only half of it is not visible, because it is in the detail part of the master/detail, so it is overlapped by the master part.
And I am not able to bring it to front.
I tried adding it as subview to the window - crash because child/parent relationships are broken.
Moving subview to front - no effect (obviously, but I had to try).
Changing zPosition - no effect.
So, at the moment I've run out of ideas.
This is a very corner case, but I need to make it work because it (master/detail) is the actual layout in the application.
Thank you in advance for any help.

Trouble restraining subview's animation within an animation block

Why restrain a subview from animation? Why not simply carry out its layout changes after the animation?
That's logical but not feasible considering my view hierarchy and this particular use-case.
My view hierarchy:
MyViewController: UIViewController -> MyCustomView: UIView -> MyCustomScrollView: UIScrollView, UIScrollViewDelegate, UICollectionViewDelegate, UICollectionViewDataSource
Why is it not possible?:
1) I do this in MyViewController after various constraint changes:
UIView.animate(withDuration: 0.3, animations: {
self.view.layoutIfNeeded()
})
2) Since MyCustomView is a subview which contains MyCustomScrollView (which in turn contains a UICollectionView as its subview), the layout update triggers CV's willDisplay delegate method under which I'm adding a bunch of labels to MyCustomView, to be precise.
Here's the function in MyCustomView that I'm calling:
func addLabel(forIndexPath indexPath: IndexPath) {
var label: UILabel!
label.frame = Util.labelFrame(forIndex: indexPath, fillWidth: false) // The frame for the label is generated here!
//Will assign text and font to the label which are unnecessary to this context
self.anotherSubView.addSubview(label) //Add the label to MyCustomView's subview
}
3) Since these changes get caught up within the animation block from point 1, I get some unnecessary, undesired animations happening. And so, MyCustomView's layout change is bound with this animation block, forcing me to look for a way to restrain this from happening
Things tried so far:
1) Tried the wrap the addSubView() from addLabel(forIndexPath:) inside a UIView.performWithoutAnimation {} block. - No luck
2) Tried the wrap the addSubView() from addLabel(forIndexPath:) inside another animation block with 0.0 seconds time as to see if this overrides the parent animation block - No luck
3) Explored UIView.setAnimationsEnabled(enabled:) but it seems like this won't cancel/pause the existing animators, and will completely disable all the animations if true (which is not what I want)
To sum this all up, my problem is:
I need to restrain the animations on MyCustomView but I need all my other desired layout changes to take place. Is this even possible? Would really appreciate a hint or a solution, TYIA!
Thanks to this answer, removing all the animations from anotherSubview's layer(inside the addLabel(forIndexPath:)) after adding the label:
self.anotherSubview.addSubview(label)
self.anotherSubview.layer.removeAllAnimations() //Doing this removes the animations queued to animate the label's frame into the view
does exactly what I want!

CollectionView messes with animations in touchesBegan and touchesEnded methods

I have a collection view that holds cells with image views. I trigger an animation when the user presses down on the image view (using touchesBegan), and another animation for when the user releases (using touchesEnded).
The animations work perfectly only if I hold down on my click then release (delayed click), but when I fast click, the animation jumps as if the duration was set to 0.
I believe the issue is because collection view is subclass of scroll view, and scrollViews "temporarily intercepts a touch-down event by starting a timer and, before the timer fires, seeing if the touching finger makes any movement. If the timer fires without a significant change in position, the scroll view sends tracking events to the touched subview of the content view."
https://developer.apple.com/documentation/uikit/uiscrollview#//apple_ref/doc/uid/TP40006922
From what I can gather, I think that the touch interception from the collection view is causing problems with the animation if the click is faster than the touch timer. When I test using a regular view instead of a collection view as the superview, the animation works perfectly and doesn't require a delayed click.
If this is the case, then how is the animation triggered for a fast-click at all? Moreover, how might I be able to tigger the animation without having to use a delayed click?
If this is not the case, then what might be the reason for this issue?
Here is my code for animation and touches:
func animateClickerAndBallPoint(newXpositionForClicker: CGFloat, newXpositionForBallPoint: CGFloat, ballPoint: UIImageView) {
UIView.animate(withDuration: 0.1) {
self.frame.origin.x = newXpositionForClicker
ballPoint.frame.origin.x = newXpositionForBallPoint
}
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
guard let ballPoint = self.ballPoint else {return}
self.animateClickerAndBallPoint(newXpositionForClicker: 288, newXpositionForBallPoint: 11, ballPoint: ballPoint)
}
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
guard let ballPoint = self.ballPoint else {return}
if isInWritingMode == true {
animateClickerAndBallPoint(newXpositionForClicker: 306, newXpositionForBallPoint: 26, ballPoint: ballPoint)
isInWritingMode = false
} else {
animateClickerAndBallPoint(newXpositionForClicker: 297, newXpositionForBallPoint: 17, ballPoint: ballPoint)
isInWritingMode = true
}
}
As an alternative, rather than doing your own touchesBegan and touchesEnded, you might consider hooking into the button's beginTracking and endTracking.
For example, you could subclass the button and provide whatever animation you wanted:
class AnimatedButton: UIButton {
override func beginTracking(_ touch: UITouch, with event: UIEvent?) -> Bool {
UIView.animate(withDuration: 0.25) {
self.transform = .init(scaleX: 0.8, y: 0.8)
}
return true
}
override func endTracking(_ touch: UITouch?, with event: UIEvent?) {
UIView.animate(withDuration: 0.25) {
self.transform = .identity
}
}
}
That yields:
Or, if you wanted to do it based upon the section of the cell, itself, you could hook into the collection view's "highlighting" mechanism as illustrated in https://stackoverflow.com/a/45664054/1271826.
BTW, you're assuming that the problem is the container view messing with touches. Are you 100% sure that's the problem? It could be a basic animation problem. E.g.
For example, if you start a second animation while the first one is still running, it won't finish the first animation but will rather immediately start the second animation from wherever it was mid-animation (and in modern iOS versions, using whatever speed at which it was currently traveling) and transition to the new destination.
For example, here are two views that I'm animating precisely the same distance down for one second down and then back up for another second. But for the view on the right, I started the second animation 0.1 seconds into the first animation (i.e. interrupting the first animation):
As you can see, because this second example is interrupting your animations, it looks like it's just snapping back.
I don't think it's likely in this scenario, but you have to be careful if you're using autolayout. If you do anything that triggers the auto layout engine to re-apply its constraints (and this could be practically any UI action, such as something as innocuous as setting the text in some unrelated label), that will stop whatever animations you have underway and will snap the views back to where the constraints dictated they should be.
If the views were laid out using auto layout, you may want to consider animating them using auto layout, too. For example, rather than adjusting the frame (or whatever), create IBOutlet for your constraint, change the constant for that constraint, and then animate the call to layoutIfNeeded.
Bottom line, you might want to see if you can verify whether the problem is really related to touch events and not some unrelated animation problem.
Unfortunately, there's not enough in your example for us to diagnose what the source of the problem is. You might consider creating a MVCE, a minimal, verifiable, and complete example of the problem. And I'd suggest creating a MVCE without a collection view and another with, so you can confirm whether the collection view is actually the source of the problem. But, bottom line, until we can reproduce your problem, it's hard for us to help you solve the problem.
Have you tried delaysContentTouches = false?
https://developer.apple.com/documentation/uikit/uiscrollview/1619398-delayscontenttouches
Tells the scrollView/collectionView to not delay touches on the cells. Worked for me to make responsive cells immediately when I started tapping on them. Didn't make my scrolling buggy either.
I had the same kind of issues when playing with animations in scrollviews.
I fixed it by using that parameter in the animation: UIViewAnimationOptions.allowUserInteraction
So your animation block would end up like that:
UIView.animate(withDuration: duration,
delay: 0,
usingSpringWithDamping: CGFloat(0.30),
initialSpringVelocity: CGFloat(10.0),
options: UIViewAnimationOptions.allowUserInteraction,
animations: {
applyWhatEverTransform()
},completion: completion)
I'm a bit speculating on your issue, as we don't have code from you. But of course, if you use different sort of animations this could not work ;)
Also if that doesn't help. To help you debug, you could try to disable the scrolling of your collection view to see if that the event that intercept your touch events.

Swift: Rounded corners appear different upon first appearance and reappearing

class ApplyCorners: UIButton {
override func didMoveToWindow() {
self.layer.cornerRadius = self.frame.height / 2
}
}
I apply this class to the buttons in my application and it is working great, but when I apply it to a button used in every cell in a table view the button corners are not round upon entering the view, but if I click one of the buttons I get segued to another view. If I then segue back the corners are "fixed" / round.
The green is the button when returning and the red is upon first entering the view.
Anyone know how to fix this?
I'd suggest layoutSubviews, which captures whenever the frame of the button changes:
class ApplyCorners: UIButton {
override func layoutSubviews() {
super.layoutSubviews()
layer.cornerRadius = frame.height / 2
}
}
This takes care of both the original appearance and any subsequent appearance. It also avoids all sorts of problems related to not only whether the frame was known when the view appeared, but also if you do anything that might change the size of the button (e.g. anything related to constraints, rotation events, etc.).
This sort of thing is likely to be a timing problem. Consider the phrase self.frame.height. At the time didMoveToWindow is called, we may not yet know our frame. If you are going to call a method that depends upon layout, do so when layout has actually occurred.
Gonna propose another alternative: listen to any bounds changes. This avoids the problem of wondering "is my frame set yet when this is called?"
class ApplyCorners: UIButton {
override var bounds: CGRect {
didSet {
layer.cornerRadius = bounds.height / 2
}
}
}
Edited frame to bounds because as #Rob points out, listening for frame changes will cause you to miss the initial load sometimes.
Putting your code in didMoveToWindow() does not make sense to me. I'd suggest implementing layoutSubviews() instead. That method gets called any time a view object's layout changes, so it should update if you resize your view.
(Changed my suggestion based on comments from TNguyen and and Rob.)

Swift: removefromSuperview removes constraints

Currently working on some swift program and I've a call to action where I remove a blurview from the superview and at the same time I'm animating 2 buttons.
Everything works like it should but there is one small problem. When I remove the blurview from my superview the constraints on my 2 buttons are being set to 0 on the bottom and animating from that position.
I don't want them to shift to 0. If I don't remove the blurview my animation is working perfectly. I've checked if my button constraints are related to the blurview, but that isn't the case. Because I assumed that it could only reset my constraints when they are relative to the blurview.
My storyboard looks the following:
view
|-> camera_view
|-> blur_view
|-> record_label
|-> record_button
The code that I'm executing is the following:
#IBAction func recordButton(sender: AnyObject) {
self.blurView?.removeFromSuperview()
UIButton.animateWithDuration(0.3, delay: 0.2, options: .CurveEaseOut, animations: {
var recordButtonFrame = self.recordButton.frame
var recordLabelFrame = self.recordLabel.frame
recordButtonFrame.origin.y -= recordButtonFrame.size.height
recordLabelFrame.origin.y -= recordLabelFrame.size.height
self.recordButton.frame = recordButtonFrame
self.recordLabel.frame = recordLabelFrame
}, completion: { finished in
print("Button moved")
})
}
What am I doing wrong?
Kind regards,
Wouter
Instead of removing blurView from superview you could hide it.
Replace
self.blurView?.removeFromSuperview()
with
self.blurView?.hidden = true
The problem is that you're animating frames while using constraints. You should be animating constrain changes / constraint constant value changes.
When you don't remove the view the layout isn't recalculated so your frame animation 'works'. It isn't correct and will get reorganised at some point in the future.
When you remove the view the layout is recalculated and everything moves around before your animation starts.
You don't give details of your constraints but it seems likely that you should be animating constraints before removing the view, then removing and ensuring the constraints are all sane on completion.

Resources