Adjusting UILabel text with Autoshrink - ios

Background
I am creating an application, and inside it I have an animation that repositions a UILabel and a UITextField when the keyboard is presented. It is set up like this:
NSNotificationCenter.defaultCenter().addObserver(self, selector: #selector(raiseTextField), name: UIKeyboardWillShowNotification, object: nil)
And this is the function:
func raiseTextField(sender: NSNotification){
UIView.animateWithDuration(0.25, animations: { () -> Void in
self.userInputTextFieldBottomContstraint.constant = sender.userInfo![UIKeyboardFrameEndUserInfoKey]!.CGRectValue().size.height
self.view.layoutIfNeeded()
})
}
My storyboard looks like this:
I have constraints set up so that the friendInputView is the right size, and it has Autoshrink so the font adjusts accordingly.
Problem
When I call raiseTextField(), everything works fine, except the font-size of the friendInputView takes a long time to change. Instead of updating while the animation is happening, it updates afterwards.
It does eventually adjust, just not at the preferred speed.
I tried adding self.friendInputLabel.adjustsFontSizeToFitWidth = true inside of animateWithDuration(), but it did't seem to change anything.
Examples
Before raising keyboard:
(text adjusted correctly)
Immediately after raising keyboard:
(text-size too big)
A second or so after raising keyboard:
(text adjusted correctly)
The Bottom Line
Is there any way to make a UILabel resize its text programmatically, rather than just waiting for it to update itself? Similarly to how layoutIfNeeded() re-adjusts constraints, is there any function like that for re-adjusting Autoshrink?
Edit 1
I ended up getting it to work based on an answer I got that said to use a UITextFieldDelegate instead of NSNotificationCenter. I replaced the raiseTextField() function with the following code:
func textFieldShouldBeginEditing(textField: UITextField) -> Bool {
self.userInputTextFieldBottomContstraint.constant = 250
self.view.setNeedsUpdateConstraints()
UIView.animateWithDuration(0.25, animations: { () -> Void in
self.view.layoutIfNeeded()
})
return true
}
func textFieldShouldReturn(textField: UITextField) -> Bool {
textField.resignFirstResponder()
self.userInputTextFieldBottomContstraint.constant = 10
self.view.setNeedsUpdateConstraints()
UIView.animateWithDuration(0.25, animations: { () -> Void in
self.view.layoutIfNeeded()
})
return true
}
However, now I have a new question: How can I get it to use the real keyboard size? Assuming that the keyboard size is always 250 isn't very helpful.
I tried doing so by creating this variable:
var keyboardHeight: CGFloat?
Putting this in viewDidLoad():
NSNotificationCenter.defaultCenter().addObserver(self, selector: #selector(setTheKeyboardHeight), name: UIKeyboardWillShowNotification, object: nil)
And creating this function:
func setTheKeyboardHeight(sender: NSNotification){
keyboardHeight = sender.userInfo![UIKeyboardFrameEndUserInfoKey]?.CGRectValue().height
}
Then I added it to textFieldShouldBeginEditing() like this:
self.userInputTextFieldBottomContstraint.constant = keyboardHeight!
self.view.setNeedsUpdateConstraints()
UIView.animateWithDuration(0.25, animations: { () -> Void in
self.view.layoutIfNeeded()
})
return true
However, it ends up just giving the error:
fatal error: unexpectedly found nil while unwrapping an Optional value
I think it happens because the keyboardHeight gets set after textFieldShouldBeginEditing() is called, but I don't know how to fix it.
Is there any way that I can access the keyboard height while using textFieldShouldBeginEditing(), or do I have to use NSNotificationCenter?

I have done same demo what you have done, and have observed some weird behaviour of UIKeyboardNotification, i'm not sure that it is weird or it is like that only,
What i have observed is when you change any constraint in UIKeyboardNotification it changes the constraint constant (or frame) with animation, even though you have not used UIView animationWithDuration block. So i think this delay might be due to that animation effect added by KeyboardNotification,
func raiseTextField(sender: NSNotification){
self.bottomConstraintTextField.constant = 250//sender.userInfo![UIKeyboardFrameEndUserInfoKey]!.CGRectValue().size.height
self.view.setNeedsUpdateConstraints()
//As you can see i have commented the animation block, still it changes the constraint with animation.
//UIView.animateWithDuration(0.25, animations: { () -> Void in
self.view.layoutIfNeeded()
//})
}
So in your scenario i have added the animation code in textFields delegates and it it working as expected,
func textFieldShouldBeginEditing(textField: UITextField) -> Bool {
self.bottomConstraintTextField.constant = 250//sender.userInfo![UIKeyboardFrameEndUserInfoKey]!.CGRectValue().size.height
self.view.setNeedsUpdateConstraints()
UIView.animateWithDuration(0.25, animations: { () -> Void in
self.view.layoutIfNeeded()
})
return true
}
func textFieldShouldReturn(textField: UITextField) -> Bool {
textField.resignFirstResponder()
self.bottomConstraintTextField.constant = 10
self.view.setNeedsUpdateConstraints()
UIView.animateWithDuration(0.25, animations: { () -> Void in
self.view.layoutIfNeeded()
})
return true
}
Here is the result,
Hope it helps you.
Update:
I have played arround it and found one workaround, so instead of taking a variable to hold keyboard height you can do it as below,
func raiseTextField(sender: NSNotification){
self.bottomConstraintTextField.constant = sender.userInfo![UIKeyboardFrameEndUserInfoKey]!.CGRectValue().size.height
self.view.setNeedsUpdateConstraints()
self.view.layoutIfNeeded()
}
func textFieldShouldBeginEditing(textField: UITextField) -> Bool {
self.bottomConstraintTextField.constant = 200
self.view.setNeedsUpdateConstraints()
UIView.animateWithDuration(0.25, animations: { () -> Void in
self.view.layoutIfNeeded()
})
return true
}
So have both textField delegate and KeyboardNotification and change the bottom constraint in textField delegate it will get called before keyboard notification and by doing this you will not observe the delay in shrinking the label text.
See the updated effect below,

Related

Swift UIImage Disappearing when I change UIText Field

Here is my animation...one weird thing is when I type in a text field and hit done I proceed to set txtdata.text = "".....
and after that my image animation coordinates get all messed up.
override func viewDidAppear(_ animated: Bool) {
UIView.animate(withDuration: 15.0, delay: 0, options: [.repeat, .autoreverse], animations: {
self.smallCloud.center = CGPoint(x: self.screenSize.maxX , y:self.r.origin.y)
}, completion: nil)
} ///////////////////////////////////////////////////////////////
open func textFieldShouldReturn(_ textField: UITextField) -> Bool {
txtData.text = "";
return false
}
so the image basically moves from one side to another...if i dont hit done on the keypad the image moves as expected, however as soon as I hit done and put the UITextField back to " "; the image gets out of wack and goes off the screen anyone have any idea whats going on here?

Remove animation in swift

I have a text field where user should enter info. And a label which points user to text field (like a hint).
I want to stop animation and remove hint label once user presses the text field to enter data.
There is repeating animation on text label. Was created by:
override func viewDidLoad() {
super.viewDidLoad()
textInput.addTarget(self, action: #selector(CalculatorViewController.removeAnimation(_:)), forControlEvents: UIControlEvents.TouchDown)
self.hintLabel.alpha = 0.0
UIView.animateWithDuration(1.5, delay: 0, options: .Repeat
, animations: ({
self.hintLabel.alpha = 1.0
}), completion: nil
)
After it I have created a function to remove annotation
func removeAnimation(textField: UITextField) {
view.layer.removeAllAnimations()
self.view.layer.removeAllAnimations()
print("is it working?!")
}
Should work according to documentation.
My label keeps flashing even though I see the string printed in console. I guess problem is that animation is repeated but have no clue how to resolve this issue.
//Just remove the animation from the label. It will Work
func remove()
{
self.hintLabel.layer.removeAllAnimations()
self.view.layer.removeAllAnimations()
self.view.layoutIfNeeded()
}
Update:
If you want to go nuclear, you could do this, as well:
func nukeAllAnimations() {
self.view.subviews.forEach({$0.layer.removeAllAnimations()})
self.view.layer.removeAllAnimations()
self.view.layoutIfNeeded()
}

UIView.animateWithDuration Not Animating Swift (again)

Note: I’ve already checked the following stack overflow issues:
27907570, 32229252, 26118141, 31604300
All I am trying to do is fade animate in a view (by alpha) when called by an IBAction attached to a button. Then reverse when a button on the view is hit.
My wrinkle may be that I'm using a secondary view that is on the ViewDock in the storyboard View. The view is added to the subview at the time of viewDidLoad where the frame/bounds are set to the same as the superview (for a full layover)
The reason this is done as an overlay view since it is a tutorial indicator.
The result (like many others who've listed this problem) is that the view (and contained controls) simply appears instantly and disappears as instantly. No fade.
I have tried animationWithDuration with delay, with and without completion, with transition, and even started with the old UIView.beginAnimations.
Nothing is working. Suggestions warmly welcomed.
The code is about as straight forward as I can make it:
Edit: Expanded the code to everything relevant
Edit2: TL;DR Everything works with the exception of UIViewAnimateWithDuration which seems to ignore the block and duration and just run the code inline as an immediate UI change. Solving this gets the bounty
#IBOutlet var infoDetailView: UIView! // Connected to the view in the SceneDock
override func viewDidLoad() {
super.viewDidLoad()
// Cut other vDL code that isn't relevant
setupInfoView()
}
func setupInfoView() {
infoDetailView.alpha = 0.0
view.addSubview(infoDetailView)
updateInfoViewRect(infoDetailView.superview!.bounds.size)
}
func updateInfoViewRect(size:CGSize) {
let viewRect = CGRect(origin: CGPointZero, size: size)
infoDetailView.frame = viewRect
infoDetailView.bounds = viewRect
infoDetailView.layoutIfNeeded()
infoDetailView.setNeedsDisplay()
}
override func viewWillTransitionToSize(size: CGSize, withTransitionCoordinator coordinator: UIViewControllerTransitionCoordinator) {
super.viewWillTransitionToSize(size, withTransitionCoordinator: coordinator)
updateInfoViewRect(size)
}
func hideInfoView() {
AFLog.enter(thisClass)
UIView.animateWithDuration(
2.0,
animations:
{
self.infoDetailView.alpha = 0.0
},
completion:
{ (finished) in
return true
}
)
AFLog.exit(thisClass)
}
func showInfoView() {
AFLog.enter(thisClass)
UIView.animateWithDuration(
2.0,
animations:
{
self.infoDetailView.alpha = 0.75
},
completion:
{ (finished) in
return true
}
)
AFLog.exit(thisClass)
}
// MARK: - IBActions
#IBAction func openInfoView(sender: UIButton) {
showInfoView()
}
#IBAction func closeInfoView(sender: UIButton) {
hideInfoView()
}
Please note, I started with the following:
func showInfoView() {
UIView.animateWithDuration(2.0, animations: { () -> Void in
self.infoDetailView.alpha = 0.75
})
}
func hideInfoView() {
UIView.animateWithDuration(2.0, animations: { () -> Void in
self.infoDetailView.alpha = 0.00
})
}
If you infoDetailView is under auto layout constraints you need to call layoutIfNeeded on the parent view inside animateWithDuration:
func showInfoView() {
self.view.layoutIfNeeded() // call it also here to finish pending layout operations
UIView.animate(withDuration: 2.0, animations: {
self.infoDetailView.alpha = 0.75
self.view.layoutIfNeeded()
})
}
Theoretically this should not be needed if you just change the .alpha value, but maybe this could be the problem in this case.
There are several strange things I can see,
first, remove:
infoDetailView.layoutIfNeeded()
infoDetailView.setNeedsDisplay()
Usually you don't need to call those methods manually unless you know exactly what you are doing.
Also, when you are changing the size:
infoDetailView.frame = viewRect
infoDetailView.bounds = viewRect
You never need to set both bounds and frame. Just set frame.
Also, you should probably make sure that the view actually doesn't ignore the frame by setting:
infoDetailView.translatesAutoresizingMaskIntoConstraints = true
Instead of resetting the frame, just set autoresize mask:
infoDetailView.autoresizingMask = [.FlexibleWidth, .FlexibleHeight]
Resulting in:
override func viewDidLoad() {
super.viewDidLoad()
// Cut other vDL code that isn't relevant
setupInfoView()
}
func setupInfoView() {
infoDetailView.alpha = 0.0
infoDetailView.translatesAutoresizingMaskIntoConstraints = true
infoDetailView.autoresizingMask = [.FlexibleWidth, .FlexibleHeight]
infoDetailView.frame = view.bounds
view.addSubview(infoDetailView)
}
func hideInfoView() {
...
}
I think this should actually help because immediate animations are often connected to size problems.
If the problem persists, you should check whether the infoDetailView in your animation is the same object as the infoDetailView you are adding to the controller.
For others looking to start an animation immediately when a view loads...
The animation won't work if you call UIView.animate(...) inside viewDidLoad. Instead it must be called from the viewDidAppear function.
override func viewDidAppear(_ animated: Bool) {
UIView.animate(withDuration: 3) {
self.otherView.frame.origin.x += 500
}
}
If the animation does not seem to execute then consider examining the state of each of your views, before you enter the animation block. For example, if the alpha is already set to 0.4 then the animation that adjusts your view alpha, will complete almost instantly, with no apparent effect.
Consider using a keyframe animation instead. This is what a shake animation in objective c looks like.
+(CAKeyframeAnimation*)shakeAnimation {
CAKeyframeAnimation *animation = [CAKeyframeAnimation animationWithKeyPath:#"transform"];
animation.values = #[[NSValue valueWithCATransform3D:CATransform3DMakeTranslation(-10.0, 0.0, 0.0)],
[NSValue valueWithCATransform3D:CATransform3DMakeTranslation(10.0, 0.0, 0.0)]];
animation.autoreverses = YES;
animation.repeatCount = 2;
animation.duration = 0.07;
return animation;
}
Here is a post that shows you how to adjust alpha with keyframes https://stackoverflow.com/a/18658081/1951992
Make sure infoDetailView's opaque is false.
https://developer.apple.com/library/ios/documentation/UIKit/Reference/UIView_Class/#//apple_ref/occ/instp/UIView/opaque
This property provides a hint to the drawing system as to how it should treat the view. If set to true, the drawing system treats the view as fully opaque, which allows the drawing system to optimize some drawing operations and improve performance. If set to false, the drawing system composites the view normally with other content. The default value of this property is true.
Try Below code. Just play with alpha and duration time to perfect it.
Hide func
func hideInfoView() {
AFLog.enter(thisClass)
UIView.animateWithDuration(
2.0,
animations:
{
self.infoDetailView.alpha = 0.8
},
completion:
{ (finished) in
UIView.animateWithDuration(
2.0,
animations:
{
self.infoDetailView.alpha = 0.4
},
completion:
{ (finished) in
self.infoDetailView.alpha = 0.0
}
)
}
)
AFLog.exit(thisClass)
}
Show func
func showInfoView() {
AFLog.enter(thisClass)
UIView.animateWithDuration(
2.0,
animations:
{
self.infoDetailView.alpha = 0.3
},
completion:
{ (finished) in
UIView.animateWithDuration(
2.0,
animations:
{
self.infoDetailView.alpha = 0.7
},
completion:
{ (finished) in
self.infoDetailView.alpha = 1.0
}
)
}
)
AFLog.exit(thisClass)
}
I've replicated your code and it work well, it's all ok.
Probably you must control constraints, IBOutlet and IBActions connections. Try to isolate this code into a new project if it's necessary.
Update: my code
and my storyboard and project folder photo:
Every object (view and buttons) are with default settings.
I've commented all AFLog lines (probably it's only any more "verbose mode" to help you) , the rest of your code is ok and it do what do you aspected from it, if you press open button the view fade in, and when you tap close button the view fade out.
PS Not relevant but i'm using xCode 7.3 , a new swift 2.2 project.
Use this code:
Swift 2
UIView.animateWithDuration(0.3, animations: { () -> Void in
self.infoDetailView.alpha = 0.0
})
Swift 3, 4, 5
UIView.animate(withDuration: 0.3, animations: { () -> Void in
self.infoDetailView.alpha = 0.0
})
Have you tried changing your showInfoView() to something more like toggleInfoView?
func toggleInfoView() {
let alpha = CGFloat(infoDetailView.alpha == 0 ? 1 : 0)
infoDetailView.alpha = alpha //this is where the toggle happens
}
It says that if your view's alpha is 0, then change it to 1. Else, make it 0.
If you need that to happen in an animation, try
#IBAction func openInfoView(sender: UIButton) {
UIView.animate(withDuration: 2.0, animations: {
self.toggleInfoView() //fade in/out infoDetailView when animating
})
}
You'll still want to keep that infoDetailView.alpha = 0.0 where you have it, coming from the viewDidLoad.
For UILabel component try to changes layer's background color instead.
Try this (Tested on Swift 4):
UIView.animate(withDuration: 0.2, animations: {
self.dateLabel.layer.backgroundColor = UIColor.red.cgColor;
})
Had a similar issue with animation not being performed.
Changed the function call use perform(aSelector: Selector, with: Any?, afterDelay: TimeInterval) in the form of perform(#selector(functionThatDoesAnimationOfAlphaValue), with: nil, afterDelay: 0) and it worked. Even with a TimeInterval set to 0.
In case someone else comes here wondering for a solution.

UIScrollView Paging with Animation Completion Handler

I have a UIScrollview with buttons that I use for paging right and left:
#IBAction func leftPressed(sender: AnyObject) {
self.scrollView!.setContentOffset(CGPointMake(0, 0), animated: true)
}
I'd like to perform an action after the scrollview has finished the paging animation. Something like:
#IBAction func leftPressed(sender: AnyObject) {
self.scrollView!.setContentOffset(CGPointMake(0, 0), animated: true)
secondFunction()
}
The above code doesn't work because the second function runs before the scrollview is finished animating the offset. My initial reaction was to use a completion handler but I'm not sure how to apply one to the setContentOffset function. I've tried:
func animatePaging(completion: () -> Void) {
self.mainScrollView!.setContentOffset(CGPointMake(0, 0), animated: true)
completion()
}
with the call
animatePaging(completion: self.secondFunction())
But I get the error "Cannot invoke 'animatePaging' with an argument list of type '(completion())'. Any thoughts?
The problem is that you need a completion handler for the scrolling animation itself. But setContentOffset(_:animated:) does not have a completion handler.
One solution would be that you animate the scrolling yourself using UIView's static function animateWithDuration(_:animations:completion:). That function has a completion handler that you can use:
UIView.animateWithDuration(0.5, animations: { () -> Void in
self.scrollView.contentOffset = CGPointMake(0, 0)
}) { (finished) -> Void in
self.secondFunction()
}
Update from joern answer - Swift 4.2
UIView.animate(withDuration: 0.5, animations: { [unowned self] in
self.scrollView.contentOffset = .zero
}) { [unowned self] _ in
self.secondFunction()
}

How to stop animating back in swift?

When I start typing in uitextfield i want some animation to happen and it should not reverse back until i finish typing. I am using this code to animate:
func textFieldDidBeginEditing(textField: UITextField) {
println("begin")
UIView.animateWithDuration(1.0, animations:{
self.secondTextfield.frame = CGRectMake(self.secondTextfield.frame.origin.x + 500, self.secondTextfield.frame.origin.y, self.secondTextfield.frame.size.width, self.venueTextField.frame.size.height)
})
}
What I want to do is: when i start typing in first textfield, i want the second text field to hide out from view and when i finish typing, i want it to animate back in.
What my issue is: When I start typing the animation happens and it comes back to the original position.It doesn't wait for me to finish typing.
Why not change alpha to 0, intead change its frame?
You need to identify witch textfield is editing.
You need to deal with textFieldDidEndEditing to get second textField back.
Example:
func textFieldDidBeginEditing(textField: UITextField) {
if textField == firstTextField {
UIView.animateWithDuration(1.0, animations:{
self.secondTextfield.alpha = 0
})
}
}
func textFieldDidEndEditing(textField: UITextField) {
if textField == firstTextField {
UIView.animateWithDuration(1.0, animations:{
self.secondTextfield.alpha = 1
})
}
}
While not knowing exactly what the problem is, I've found a reasonable alternative to solving your problem:
Instead of animating your frame, animate the auto-layout constraint that defines your textField's x coordinate. For instance, the image below consists of two textFields. I want to animate the bottom one when I begin typing in the top one:
The vertical constraint is a centre x alignment that I want to animate. To achieve the effect, I connected an #IBOutlet to my NSLayoutConstraint:
I had my view controller adopt UITextFieldDelegate and implemented three methods:
extension ViewController: UITextFieldDelegate {
func textFieldDidBeginEditing(textField: UITextField) {
self.c.constant += 100 //c is my constraint
UIView.animateWithDuration(1.0) {
self.view.layoutIfNeeded()
}
}
func textFieldDidEndEditing(textField: UITextField) {
println("textFieldDidEndEditing")
self.c.constant -= 100
UIView.animateWithDuration(1.0) {
self.view.layoutIfNeeded()
}
}
func textFieldShouldReturn(textField: UITextField) -> Bool {
a.resignFirstResponder()
return true
}
}
This method needs a bit of auto-layout know how, but I was able to achieve the effect you desired.

Resources