I am creating a custom progress bar and have ran into more issues. I have a progress bar that show an image going from full to nothing with a shadow that is red in the background. Basically this is a normal progress bar but with a custom image, I want to add a label or something like that on to that will go from 0 to 100% in the time it takes to animate. I tried adding counters to the animate but they do not increment like I want. This is my animation block so far.
override func viewDidAppear(_ animated: Bool) {
UIView.animate(withDuration: 3.0, animations: {
self.catScanE.frame = CGRect(x: 27 , y: 200, width: 320, height: 0)
}, completion: {finished in self.performSegue(withIdentifier: "lastSegue", sender: nil)})
}
Two things about your issue:
You need to set initial frame for catScanE before the animation.
The destination height of catScanE is 0. Did you mean to make it go from
large to small?
Related
I have a code that creates a new UIImageView everytime a button is clicked and that UIImageView is then animated. However, whenever the button is clicked after it is clicked for the first time, I think the animation applied to the second UIImageView that is created affects the first UIImageView. In other words, both start to move very fast (much faster that programmed). This is my code:
#IBAction func buttonClicked(_ sender: Any) {
var imageName: String = "ImageName"
var image = UIImage(named: imageName)
var imageView = UIImageView(image: image!)
imageView.frame = CGRect(x: 0,y: 0,width: 50,height: 50)
view.addSubView(imageView)
moveIt(imageView)
}
func moveIt(_ imageView: UIImageView) {
UIView.animate(withDuration:TimeInterval(0.00001), delay: 0.0, options: .curveLinear, animations: {
imageView.center.x+=3
}, completion: {(_) in self.moveIt(imageView)
})
}
I am relatively new with code and swift. Thanks in advance because I cannot quite seem to figure this out.
You should not try to do a whole bunch of chained animations every 0.00001 seconds. Most things on iOS have a time resolution of ≈ .02 seconds.
You are trying to run an animation every 10 microseconds and move the image view 10 points each step. That comes to an animation of a million points per second. Don't do that.
Just create a single animation that moves the image view to it's final destination over your desired time-span. The system will take care of pacing the animation for you. (Say you move it 100 pixels in 0.3 seconds. Just specify a duration of 0.3 seconds, and the body of the animation moves the image view by 100 pixels. There you go. It will just work.
Note that your image views don't have any auto-layout constraints, so they will likely act strangely once the animation is complete.
I use the following code to scroll to top of the UICollectionView:
scrollView.scrollRectToVisible(CGRect(origin: .zero, size: CGSize(width: 1, height: 1)), animated: true)
However, on iOS 11 and 12 the scrollView only scrolls to the top, without revealing the large title of the UINavigationBar (when prefersLargeTitle has ben set to true.)
Here is how it looks like:
The result I want to achieve:
It works as it is designed, you are scrolling to position y = 0, assign your controller to be UIScrollView delegate and print out scroll offset:
override func scrollViewDidScroll(_ scrollView: UIScrollView) {
print(scrollView.contentOffset)
}
You will see when Large title is displayed and you move your scroll view a but and it jumps back to the Large title it will not print (0.0, 0.0) but (0.0, -64.0) or (0.0, -116.0) - this is the same value as scrollView.adjustedContentInset, so if you want to scroll up and display large title you should do:
scrollView.scrollRectToVisible(CGRect(x: 0, y: -64, width: 1, height: 1), animated: true)
You don't want to use any 'magic values' (as -64 in the currently accepted answer). These may change (also, -64 isn't correct anyway).
A better solution is to observe the SafeAreaInsets changes and save the biggest top inset. Then use this value in the setContentOffset method. Like this:
class CollectioViewController: UIViewController {
var biggestTopSafeAreaInset: CGFloat = 0
override func viewSafeAreaInsetsDidChange() {
super.viewSafeAreaInsetsDidChange()
self.biggestTopSafeAreaInset = max(ui.safeAreaInsets.top, biggestTopSafeAreaInset)
}
func scrollToTop(animated: Bool) {
ui.scrollView.setContentOffset(CGPoint(x: 0, y: -biggestTopSafeAreaInset), animated: animated)
}
}
It seems that using a negative content offset is the way to go.
I really like the idea of Demosthese to keep track of the biggest top inset.
However, there is a problem with this approach.
Sometime large titles cannot be displayed, for example, when an iPhone is in landscape mode.
If this method is used after a device has been rotated to landscape then the offset of the table will be too much because the large title is not displayed in the navigation bar.
An improvements to this technique is to consider biggestTopSafeAreaInset only when the navigation bar can display a large title.
Now the problem is to understand when a navigation bar can display a large title.
I did some test on different devices and it seems that large titles are not displayed when the vertical size class is compact.
So, Demosthese solution can be improved in this way:
class TableViewController: UITableViewController {
var biggestTopSafeAreaInset: CGFloat = 0
override func viewSafeAreaInsetsDidChange() {
super.viewSafeAreaInsetsDidChange()
self.biggestTopSafeAreaInset = max(view.safeAreaInsets.top, biggestTopSafeAreaInset)
}
func scrollToTop(animated: Bool) {
if traitCollection.verticalSizeClass == .compact {
tableView.setContentOffset(CGPoint(x: 0, y: -view.safeAreaInsets.top), animated: animated)
} else {
tableView.setContentOffset(CGPoint(x: 0, y: -biggestTopSafeAreaInset), animated: animated)
}
}
}
There is still a case that could cause the large title to not be displayed after the scroll.
If the user:
Open the app with the device rotated in landscape mode.
Scroll the view.
Rotate the device in portrait.
At this point biggestTopSafeAreaInset has not yet had a chance to find the greatest value and if the scrollToTop method is called the large title will be not displayed.
Fortunately, this is a case that shouldn't happen often.
Quite late here but I have my version of the story.
Since iOS 11 there is the adjustedContentInset on the scroll view.
That however reflects only the current state of the UI thus if the large navigation title is not revealed, it won't be taken into account.
So my solution is to make couple of extra calls to make the system consider the large title size and calculate it to the adjustedContentInset:
extension UIScrollView {
func scrollToTop(animated: Bool = true) {
if animated {
// 1
let currentOffset = contentOffset
// 2
setContentOffset(CGPoint(x: 0, y: -adjustedContentInset.top - 1), animated: false)
// 3
let newAdjustedContentInset = adjustedContentInset
// 4
setContentOffset(currentOffset, animated: false)
// 5
setContentOffset(CGPoint(x: 0, y: -newAdjustedContentInset.top), animated: true)
} else {
// 1
setContentOffset(CGPoint(x: 0, y: -adjustedContentInset.top - 1), animated: false)
// 2
setContentOffset(CGPoint(x: 0, y: -adjustedContentInset.top), animated: false)
}
}
}
Here is what's happening:
When animated:
Get the current offset to be able to apply it again (important for achieving the animation)
Scroll without animating to the currently calculated adjustedContentInset plus some more because the large title was not considered when calculating the adjustedContentInset
Now the system takes into account the large title so get the current adjustedContentInset that will include its size so store it to a constant that will be used in the last step
Scroll back to the original offset without animating so no visual changes will be noticed
Scroll to the previously calculated adjustedContentInset this time animating to achieve the desired animated scrolling
When !animated:
Scroll without animation to the adjustedContentInset plus some more. At this stage the system will consider the large title so...
Scroll to the current adjustedContentInset as it was calculated with the large title in it
Kind of a hack but does work.
I'm experiencing a glitch where my UITextField's text jumps to its final position (it doesn't animate) when animating the textfield's width constraint. Take a look at this gif:
When the "Grow" button is tapped, the textfield's width grows. But "hello world" jumps immediately to the center instead of gliding there. When the "Shrink" button is tapped, "hello world" jumps immediately back to the left.
My animation function looks like this:
func animateGrowShrinkTextFields(grow: Bool) {
if grow {
UIView.animate(withDuration: 0.5, animations: {
self.widthConstraint.constant = 330
self.view.layoutIfNeeded()
}, completion: nil)
} else {
UIView.animate(withDuration: 0.5, animations: {
self.widthConstraint.constant = 100
self.view.layoutIfNeeded()
}, completion: nil)
}
}
I have tried the following list suggestions; none of them worked.
I called self.view.layoutIfNeeded() and self.helloWorldTextField.layoutIfNeeded() before and within the animation block as suggested in this answer: https://stackoverflow.com/a/32996503/2179970
I tried self.view.layoutSubviews and self.helloWorldTextField.layoutSubview as suggested in this answer: https://stackoverflow.com/a/30845306/2179970
Also tried setNeedsLayout() UITextField text jumps iOS 9
I even tried changing the font as suggested here: https://stackoverflow.com/a/35681037/2179970
I tried resignFirstResponder (although though I never tap or edit the textfield in my tests, so it should not involve the firstResponder) as suggested here: https://stackoverflow.com/a/33334567/2179970
I tried subclassing UITextField as seen here: https://stackoverflow.com/a/40279630/2179970
I also tried using a UILabel and got the same jumpy result.
The following question is also very similar to mine but does not have an answer yet: UITextfield text position not animating while width constraint is animated
Here is my project on Github: https://github.com/starkindustries/ConstraintAnimationTest
Solution Demo
I've found a working solution. It feels a little hackish but it works. Here is a gif of the final result. Notice that helloWorldTextField has a blue border to show its location within the second textfield behind it.
Instructions
Make two textfields: helloWorldTextField (the original from the question) and borderTextField (a new textfield). Remove helloWorldTextFields's border and background color. Keep borderTextField's border and background color. Center helloWorldTextField within borderTextField. Then animate the width of borderTextField.
Github link and Code
Here is the project on Github: https://github.com/starkindustries/ConstraintAnimationTest
Here is the code within MyViewController class. Everything else is setup in the storyboard which can be viewed on Github at the link above.
class MyViewController: UIViewController {
// Hello World TextField Border var
#IBOutlet weak var borderTextFieldWidth: NSLayoutConstraint!
// Button Vars
#IBOutlet weak var myButton: UIButton!
var grow: Bool = false
func animateGrowShrinkTextFields(grow: Bool, duration: TimeInterval) {
if grow {
UIView.animate(withDuration: duration, animations: {
self.borderTextFieldWidth.constant = 330
self.view.layoutIfNeeded()
}, completion: { (finished: Bool) in
print("Grow animation complete!")
})
} else {
UIView.animate(withDuration: duration, animations: {
self.borderTextFieldWidth.constant = 115
self.view.layoutIfNeeded()
}, completion: { (finished: Bool) in
print("Shrink animation complete!")
})
}
}
#IBAction func toggle(){
let duration: TimeInterval = 1.0
grow = !grow
let title = grow ? "Shrink" : "Grow"
myButton.setTitle(title, for: UIControlState.normal)
animateGrowShrinkTextFields(grow: grow, duration: duration)
}
}
Notes and References
What led me to this solution was #JimmyJames's comment: "You are just animating the UITextField width, but the content inside is not animated."
I researched how to animate font changes and came across this question: Is there a way to animate changing a UILabel's textAlignment?
In that question #CSmith mentioned that "you can animate the FRAME, not the textAlignment" https://stackoverflow.com/a/19251634/2179970
The accepted answer in that question suggests to use a UILabel within another frame. https://stackoverflow.com/a/19251735/2179970
Hope this helps anyone else who comes across this problem. If anyone has another way to solve this, please post a comment or another answer. Thanks!
Another solution for the issue is set yourLabel.contentMode = .center on init, and animate in animation block as usually
I am creating an application with a fixed background (2208x2208) for every screen, that is why I am setting the background in the application delegate. The current code inside the didFinishLaunchingWithOptions method is:
func setBackground() {
let width = self.window!.frame.size.width// * UIScreen.mainScreen().nativeScale
let height = self.window!.frame.size.height// * UIScreen.mainScreen().nativeScale
let size = max(width, height)
UIGraphicsBeginImageContext(CGSize(width: width, height: height))
UIImage(named: "background.jpg")?.drawInRect(CGRectMake(-(size - width) / 2, -(size - height) / 2, size, size))
let image = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
self.window?.backgroundColor = UIColor(patternImage: image!)
}
This works like it supposes, but with one issue. The image looks like it is scaled down and scaled back to the scale of the screen. So on an iPhone 6 Plus it doesn't look sharp. This might be expected behaviour since the window size is not the full resolution. But when I multiply it with the native scale, only 1/9th the image (upper left) is shown. Is there a way (besides providing separate resolutions for this image) to show it without scaling first?
A second issue, for if anyone know the solution for this. The image is shown behind a UINavigationController where every view inside the UIViewController has a transparent background. This works and shows the background, but one problem, the background turns a little darker when navigation to another view. Probably because the animation. Is there a way to hide this animation? Inside the UIViewControllers (there are only two yet, but this will be inside a class which will be overridden) is the following code:
override func viewWillAppear(animated: Bool) {
self.view.alpha = 1.0
super.viewWillAppear(animated)
}
override func viewWillDisappear(animated: Bool) {
UIView.animateWithDuration(0.25, animations: {
self.view.alpha = 0.0
})
super.viewWillDisappear(animated)
}
Even without this code, it still turns darker before it goes light again. It looks like the view still as a dark opaque background with a very low alpha and when presenting the second screen, it lays both backgrounds on top of each other until the second screen is completely presented and the first one will be removed.
Thanks!
I have the most strange situation with a custom keyboard. First of all, I have set up a dummy view for the textfield, in order to hide the stock keyboard let dummyView : UIView = UIView(frame: CGRect(x: 0, y: 0, width: 1, height: 1))
amountField.inputView = dummyView
Then I have my custom keyboard which animates when editing begins in the text field
func textFieldDidBeginEditing(textField: UITextField) {
keyboardContainer.hidden = false
UIView.animateWithDuration(0.6, animations: {
self.keyboardContainer.frame = self.keyboardScreenPosition!
}, completion: {
finished in
if finished {
//just in case
}
})
}
Also, I have set up a button which should end editing and hide my custom keyboard
#IBAction func calculeaza(sender: AnyObject) {
self.amountField.resignFirstResponder()
UIView.animateWithDuration(0.6, animations: {
self.keyboardContainer.frame.origin.y = self.view.bounds.height
}, completion: {
finished in
if finished {
}
})
}
The most strange part comes with the resignFirstResponder(). Let me explain: If that part is not included, the keyboard hides just fine (but the text field keeps on blinking cursor which is not an option ofc). If the resigning part is included, the keyboard animates from the top to its current position, then on pressing the button again it does slides down as intended. I am really puzzled on why is this happening...I debugged the sizes of the view and the heights are ok so it should slide down from the beginning. I really dont understand what is happening. Any help is much appreciated, many thanks!
EDIT: another strange effect is if I move the resign part (or the super end editing) in the animation ending closure. The keyboard slides just fine, then it reappears on screen
This sounds like an issue with autolayout constraints. Those are updated after the animateWithDuration which is potentially causing this odd behavior. If you have (a) constraint(s) in the Storyboard used for autolayout, try updating that(/those). If you haven't already, you'll need to add it as an IBOutlet and animate the autolayout change in the duration.
For example, say the constraint is called theRelevantConstraint. Then replace the middle lines
self.theRelevantConstraint.constant = valueItShouldBe
UIView.animateWithDuration(0.6, animations: {
self.view.layoutIfNeeded()
}, completion: {
finished in
if finished {
}
})