Animate constraints, how to grow height in desired direction - swift

Im using SnapKit to alter the height of a UIView based on a touch event. So when the user touches the cell it expands. This works fine like this:
#objc func didRecieveTouch() {
let originalHeight: CGFloat = 90
let expandedHeight: CGFloat = 244
if !expanded {
self.snp.updateConstraints { make in
make.height.equalTo(expandedHeight)
}
} else {
self.snp.updateConstraints { make in
make.height.equalTo(originalHeight)
}
}
self.expanded = !self.expanded
UIView.animate(withDuration: 0.25, animations: {
self.superview?.layoutIfNeeded()
})
}
The problem is that the direction the view grows in looks weird and wrong to me. The view actually "hops" down to the new allocated space and grows upwards instead of growing downwards which is the desired behaviour I want.

Add your code inside the animation block.
UIView.animate(withDuration: 0.25) {
if !expanded {
self.snp.updateConstraints { make in
make.height.equalTo(expandedHeight)
}
} else {
self.snp.updateConstraints { make in
make.height.equalTo(originalHeight)
}
}
self.expanded = !self.expanded
self.superview?.layoutIfNeeded()
}

Related

How to get current texture from current frame of animation in SKAction?

I'm trying to animate something that spins / left to right, but whenever I call
spinLeft() or spinRight() then the animation always starts from frame 0.
In other words, I want to be able to say spin something 4 out of 10 frames, stop, then
spin in the opposite direction, FROM frame 4. Right now, it resets to frame 0.
var textures = [SKTexture]() // Loaded with 10 images later on.
var sprite = SKSpriteNode()
func spinLeft() {
let action = SKAction.repeatForever(.animate(with: textures, timePerFrame: 0.1))
sprite.run(action)
}
func spinRight() {
let action = SKAction.repeatForever(.animate(with: textures, timePerFrame: 0.1)).reversed()
sprite.run(action)
}
You could do this (Syntax may be a little off, but you get the point):
The key here is the .index(of: ... ) which will get you the index.
func spinUp() {
let index = textures.index(of: sprite.texture)
if index == textures.count - 1 {
sprite.texture = textures[0]
}
else {
sprite.texture = textures[index + 1]
}
}
func spinDown() {
let index = textures.index(of: sprite.texture)
if index == 0 {
sprite.texture = textures[textures.count - 1]
}
else {
sprite.texture = textures[index - 1]
}
}
func changeImage(_ isUp: Bool, _ amountOfTime: CGFloat) {
let wait = SKAction.wait(duration: amountOfTime)
if isUp {
run(wait) {
self.imageUp()
}
}
else {
run(wait) {
self.imageDown()
}
}
}
If you use something like a swipe gesture recognizer, you can use it's direction to set the isUp Bool value and the velocity of that swipe for the amountOfTime for the changeImage function.
The changeImage will only change the image once, so you will need to handle this somewhere else, or create another function if you want it to continuously spin or die off eventually.
Hope this helps!

UICollectionView cell. trying to animate subviews

trying to animate a subview inside a collection view cell but am only getting an abrupt change between states.
...
func animate (){
if self.signOut.hidden == false{
UIView.animateWithDuration(0.2) {
self.signOut.hidden = true
}
}else{
UIView.animateWithDuration(0.2) {
self.signOut.hidden = false
}
}
...
any tips much appreciated!
You need to decrease the alpha inside an animation block for the view to disappear smoothly
UIView.animateWithDuration(0.33, delay: 0.0, options: [.CurveEaseInOut], animations: {
self.signOut.alpha = 0.0
}) { finished in
self.signOut.hidden = true
}

UITableView Cell display issue

I have a UITableView, its top and bottom distance to superview is 0.
When keyboard appears I update the bottom constraint, so that keyboard will not hide the table. But on updating the bottom constraint, last two or three cells are not completely visible. I am using following code to update the bottom constraint.
func keyboardWillChangeFrameWithNotification(notification: NSNotification, showsKeyboard: Bool) {
let userInfo = notification.userInfo!
let animationDuration: NSTimeInterval = (userInfo[UIKeyboardAnimationDurationUserInfoKey] as! NSNumber).doubleValue
// Convert the keyboard frame from screen to view coordinates.
let keyboardScreenBeginFrame = (userInfo[UIKeyboardFrameBeginUserInfoKey] as! NSValue).CGRectValue()
if showsKeyboard
{
cntInputViewBottom.constant = keyboardScreenBeginFrame.size.height
}
else
{
cntInputViewBottom.constant = 0
}
}
You are using UIKeyboardFrameBeginUserInfoKey. This is size of the keyboard at the beginning of frame change animation. You should better use UIKeyboardFrameEndUserInfoKey in order to get final frame.
if x is the height of the view with send button.
if showsKeyboard
{
cntInputViewBottom.constant = keyboardScreenBeginFrame.size.height+ x
}
else
{
cntInputViewBottom.constant = x
}
Problem is you are not informing your view/controller to update your constraint before and after.
if showsKeyboard
{
self.view.layoutIfNeeded()
cntInputViewBottom.constant = keyboardScreenBeginFrame.size.height
self.view.layoutIfNeeded()
}
else
{
self.view.layoutIfNeeded()
cntInputViewBottom.constant = 0
self.view.layoutIfNeeded()
}
just add self.view.layoutIfNeeded() before & after updating constraint as displayed.

Showing and hiding button with custom animation

Now I'm having such animation:
UIView.animateWithDuration(0.5) { () -> Void in
self.scrollToTopBtn.alpha = self.isVisible(self.searchBar.searchBar) ? 0 : 0.6
}
Button is located in bottom-right corner of screen. I want her to appear moving from right to left and dissapear moving from left to right.
How can I do this?
One easy way to achieve this is:
1.- Lets say that let buttonWidth is the width of your scrollToTopBtn UIButton.
2.- Put your scrollToTopBtn in the bottom-right corner of screen, but all the way right out of the screen bounds. This will be your initial state for the button.
3.- When you want it to appear, just call following function:
Swift 2
func showButton() {
UIView.animateWithDuration(0.35, animations: { () -> Void in
self.scrollToTopBtn.transform = CGAffineTransformTranslate(self.scrollToTopBtn.transform, -buttonWidth, 0)
}, nil)
}
Swift 3, 4, 5
func showButton() {
UIView.animate(withDuration: 0.35, animations: { () -> Void in
self.scrollToTopBtn.transform = self.scrollToTopBtn.transform.translatedBy(x: -buttonWidth, y: 0)
}, completion: nil)
}
4.- If you want it to disappear:
Swift 2
func hideButton() {
UIView.animateWithDuration(0.35, animations: { () -> Void in
self.scrollToTopBtn.transform = CGAffineTransformTranslate(self.scrollToTopBtn.transform, buttonWidth, 0)
}, nil)
}
Swift 3, 4, 5
func hideButton() {
UIView.animate(withDuration: 0.35, animations: { () -> Void in
self.scrollToTopBtn.transform = self.scrollToTopBtn.transform.translatedBy(x: buttonWidth, y: 0)
}, completion: nil)
}
Notice that a good practice may be to create the button in runtime, adding it to the hierarchy when needed and then removing it when done using it.
There are a bunch of ways to do it. The easiest would be setting frame of the button. Something like this:
UIView.animateWithDuration(0.5) { () -> Void in
self.scrollToTopBtn.alpha = self.isVisible(self.searchBar.searchBar) ? 0 : 0.6
let superViewWidth = self.scrollToTopBtn.superview!.bounds.size.width
let buttonWidth = self.scrollToTopBtn.frame.size.width
if(self.isVisible(self.searchBar.searchBar)) { // move it off of the view (towards right)
self.scrollToTopBtn.frame.origin.x = superViewWidth + buttonWidth
}
else { // bring it back to it's position
self.scrollToTopBtn.frame.origin.x = 10 // or whatever the normal x value for the button is
}
}
The better way to do it
The better way would be to animate the constraint constant. Here's how you could do it:
First create an outlet for the constraint in your view controller:
#IBOutlet weak var buttonLeadingConstraint: NSLayoutConstraint!
Go to Interface builder and connect the button's leading constraint to buttonLeadingConstraint outlet.
Now you can animate it like this:
if(self.isVisible(self.searchBar.searchBar)) { // move it off the screen
self.buttonLeadingConstraint.constant = self.view.bounds.width + 10
}
else {
self.buttonLeadingConstraint.constant = 10 // or whatever the normal value is
}
// Now animate the change
UIView.animateWithDuration(0.5) { () -> Void in
self.view.layoutIfNeeded()
}
Note that you set the constant on constraint before you call animateWithDuration, and inside the animations block, you only need to call `layoutIfNeeded' on the parent view.

UITesting Xcode 7: How to tell if XCUIElement is visible?

I am automating an app using UI Testing in Xcode 7. I have a scrollview with XCUIElements (including buttons, etc) all the way down it. Sometimes the XCUIElements are visible, sometimes they hidden too far up or down the scrollview (depending on where I am on the scrollview).
Is there a way to scroll items into view or maybe tell if they are visible or not?
Thanks
Unfortunately Apple hasn't provided any scrollTo method or a .visible parameter on XCUIElement. That said, you can add a couple helper methods to achieve some of this functionality. Here is how I've done it in Swift.
First for checking if an element is visible:
func elementIsWithinWindow(element: XCUIElement) -> Bool {
guard element.exists && !CGRectIsEmpty(element.frame) && element.hittable else { return false }
return CGRectContainsRect(XCUIApplication().windows.elementBoundByIndex(0).frame, element.frame)
}
Unfortunately .exists returns true if an element has been loaded but is not on screen. Additionally we have to check that the target element has a frame larger than 0 by 0 (also sometimes true) - then we can check if this frame is within the main window.
Then we need a method for scrolling a controllable amount up or down:
func scrollDown(times: Int = 1) {
let topScreenPoint = app.mainWindow().coordinateWithNormalizedOffset(CGVector(dx: 0.5, dy: 0.05))
let bottomScreenPoint = app.mainWindow().coordinateWithNormalizedOffset(CGVector(dx: 0.5, dy: 0.90))
for _ in 0..<times {
bottomScreenPoint.pressForDuration(0, thenDragToCoordinate: topScreenPoint)
}
}
func scrollUp(times: Int = 1) {
let topScreenPoint = app.mainWindow().coordinateWithNormalizedOffset(CGVector(dx: 0.5, dy: 0.05))
let bottomScreenPoint = app.mainWindow().coordinateWithNormalizedOffset(CGVector(dx: 0.5, dy: 0.90))
for _ in 0..<times {
topScreenPoint.pressForDuration(0, thenDragToCoordinate: bottomScreenPoint)
}
}
Changing the CGVector values for topScreenPoint and bottomScreenPoint will change the scale of the scroll action - be aware if you get too close to the edges of the screen you will pull out one of the OS menus.
With these two methods in place you can write a loop that scrolls to a given threshold one way until an element becomes visible, then if it doesn't find its target it scrolls the other way:
func scrollUntilElementAppears(element: XCUIElement, threshold: Int = 10) {
var iteration = 0
while !elementIsWithinWindow(element) {
guard iteration < threshold else { break }
scrollDown()
iteration++
}
if !elementIsWithinWindow(element) { scrollDown(threshold) }
while !elementIsWithinWindow(element) {
guard iteration > 0 else { break }
scrollUp()
iteration--
}
}
This last method isn't super efficient, but it should at least enable you to find elements off screen. Of course if you know your target element is always going to be above or below your starting point in a given test you could just write a scrollDownUntil or a scrollUpUntill method without the threshold logic here.
Hope this helps!
Swift 5 Update
func elementIsWithinWindow(element: XCUIElement) -> Bool {
guard element.exists && !element.frame.isEmpty && element.isHittable else { return false }
return XCUIApplication().windows.element(boundBy: 0).frame.contains(element.frame)
}
func scrollDown(times: Int = 1) {
let mainWindow = app.windows.firstMatch
let topScreenPoint = mainWindow.coordinate(withNormalizedOffset: CGVector(dx: 0.5, dy: 0.05))
let bottomScreenPoint = mainWindow.coordinate(withNormalizedOffset: CGVector(dx: 0.5, dy: 0.90))
for _ in 0..<times {
bottomScreenPoint.press(forDuration: 0, thenDragTo: topScreenPoint)
}
}
func scrollUp(times: Int = 1) {
let mainWindow = app.windows.firstMatch
let topScreenPoint = mainWindow.coordinate(withNormalizedOffset: CGVector(dx: 0.5, dy: 0.05))
let bottomScreenPoint = mainWindow.coordinate(withNormalizedOffset: CGVector(dx: 0.5, dy: 0.90))
for _ in 0..<times {
topScreenPoint.press(forDuration: 0, thenDragTo: bottomScreenPoint)
}
}
func scrollUntilElementAppears(element: XCUIElement, threshold: Int = 10) {
var iteration = 0
while !elementIsWithinWindow(element: element) {
guard iteration < threshold else { break }
scrollDown()
iteration += 1
}
if !elementIsWithinWindow(element: element) {
scrollDown(times: threshold)
}
while !elementIsWithinWindow(element: element) {
guard iteration > 0 else { break }
scrollUp()
iteration -= 1
}
}
What i had to do to address this problem is to actually swipe up or down in my UI testing code. Have you tried this?
XCUIApplication().swipeUp()
Or you can also do WhateverUIElement.swipUp() and it will scroll up/down with respect to that element.
Hopefully they will fix the auto scroll or auto find feature so we don't have to do this manually.
You should check isHittable property.
If view is not hidden, the corresponding XCUIElement is hittable. But there is a caveat. "View 1" can be overlapped by "View 2", but the element corresponding to "View 1" can be hittable.
Since you have some XCUIElements in the bottom of the tableview (table footer view), the way of scrolling the tableview all the way to the bottom in the UI test, supposing your tableview has a lot data, is by tap().
.swipeUp() also does the job but the problem is when your test data is huge, it takes forever to swipe, as oppose to .tap() which directly jumps to the bottom of the tableView.
More specially:
XCUIElementsInTheBottomOrTableFooterView.tap()
XCTAssert(XCUIElementsInTheBottomOrTableFooterView.isHittable, "message")
Looks like this is a known bug :-(
https://forums.developer.apple.com/thread/9934