How to determine if a cell has scrolled off screen XCUI - swift

I have a XCUI test case in swift where I am trying to determine if a cell has scrolled off screen. However, I've noticed that once a cell has been on screen the static text is always findable, even when the cell scrolls off screen, when using
XCTAssertTrue(app.tables.cells.StaticText["person"].exists)
This also does not work for me
let window = app.windows.elementBoundByIndex(0)
let element = app.tables.cells.staticTexts["person"]
XCTAssertTrue(CGRectContainsRect(window.frame, element.frame))
As that second test will pass, even when the cell has scrolled off screen.
Is there a way to determine whether a table cell is no longer within the view on the screen?

Use the hittable API on XCUIElement to determine if an element both exists and is on screen. You should use it on the cell.
Note: hittable will become isHittable in Swift 3.
let cell = app.tables.cells.containingType(.StaticText, identifier: "person").elementBoundByIndex(0)
XCTAssertTrue(cell.hittable)

A solution to this problem, kind of clunky but works, is that I used a function like this
func tapOnSpecifiedPointOnList(cellNumber: Float) {
let cellSpacing: Float = 75
let xCoordinate: Float = 25
let yOffSet: Float = 90
let yCoordinate = yOffSet + (cellSpacing * cellNumber)
let pointToTap = CGPointMake(CGFloat(xCoordinate), CGFloat(yCoordinate))
map().tapAtPosition(pointToTap)
}
tapOnSpecifiedPointOnList(0)
let startingCell = app.cells.otherElements["callout"].elementBoundByIndex(0).label
app.cells.otherElements["callout"].swipeUp()
for i in 0...numberOfCells {
tapOnSpecifiedPointOnList(i)
let nextCell = app.cells.otherElements["callout"].elementBoundByIndex(0).label
XCTAssertNotEqual(startingCell, nextCell)
}
When a cell was tapped, it opened up a separate callout, which didn't exist before. By attaching an accessibilityId on this callout object, I was able to grab the info from the cell and compare it to other cells which I tapped on. Therefore, if I tapped on all possible cell locations on screen, and none of callouts matched the original, it must have gone offscreen.

Related

*Modern* CollectionView. Waterfall effect? (I.e. Is dynamic cell height / staggering for MULTIPLE columns possible?)

Goal:
UICollectionView with 'waterfall effect', entailing:
Multiple columns of cells (each cell contains only one stackview).
Each stackview contains n items, thus height of cells varies, but width is fixed.
Each cell in each column should butt up against one above it, with fixed padding, let's say CGFloat(50.0).
That produces a 'staggered' look across the columns, but no wasted space or 'empty slots'.
The Problem:
I've tried configuring the collection view using UICollectionViewFlowLayout, and alternatively, UICollectionViewCompositionalLayout without success
Best I've been able to achieve is cells aligned to top of each row, where each row has a uniform height (approx same height as tallest cell in that row). Meaning, next row starts below the bottom of the tallest cell in the previous row, leaving large gaps between shorter cells and cells placed cells in the next row (which is what I don't want).
Afterthoughts:
I suspect that's just how it is... that collection view (particularly compositional layout) doesn't know how to handle multiple columns with independent vertical layouts wherein all columns scroll vertically as one.
I'm not even sure if a custom layout could solve it, as I've never written one yet. Or if I'm just better off creating a big scroll view and layout the stackviews manually without a collection view, or multiple single-column collection views side by side? I don't understand enough of the tradeoffs and possibilities and hoping someone can steer me in the right direction.
Note, I did find this cool hack for making the content of cells top align for UICollectionViewFlow layout, and it works for what it is, but doesn't but doesn't give me the waterfall effect I'm looking e.g. doesn't tie multiple rows together for a seamless vertically sized cell height appearance.
UICollection View Flow Layout Vertical Align
class TopAlignedCollectionViewFlowLayout: UICollectionViewFlowLayout {
override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
// This method is being requested to return the layout attributes for each cell in the rectangle.'
// where the attributes are frame, bounds, center, size, transform3D, transform, alpha, zIndix and isHidden
// It starts by getting a copy attributes the super class has configured each cell:
//
let attributes = super.layoutAttributesForElements(in: rect)?.map { $0.copy() } as? [UICollectionViewLayoutAttributes]
attributes?
// Then it's using the reduce() function to build a new dictionary of tupples consisting of:
// [cgfloat : (cgfloat, [attrs])], which is [ cell center.y : (cell origin.y, [all the other cells]) ]
///
.reduce([CGFloat: (CGFloat, [UICollectionViewLayoutAttributes])]()) { // for each cell, $0 = dict to build, $1 = next cell
guard $1.representedElementCategory == .cell else { return $0 } // only operate on cells (not decorations or supplemental views)
return $0.merging([ceil($1.center.y): ($1.frame.origin.y, [$1])]) { // center.y : origin.y, [merged attributes]
($0.0 < $1.0 ? $0.0 : $1.0, $0.1 + $1.1)
//
// if dict center.y < cell center.y
// (cell ctr.y, [cell/dict merged attributes]
// else
// (dict ctr.y, [cell/dict merged attributes]
}
} // returns dictionary of merged [cgfloat : (cgfloat, [attrs])]
.values.forEach { minY, line in // pull each item up to the top via diff btw center and top line
line.forEach {
$0.frame = $0.frame.offsetBy(
dx: 0,
dy: minY - $0.frame.origin.y
)
$0.frame = CGRectMake($0.frame.origin.x, $0.frame.origin.y + 200, $0.frame.size.width, 50) //$0.frame.size.height - (minY - $0.frame.origin.y))
}
}
return attributes
}
}

Dynamically assigned UIFont does not show immediately in slow simulator

I have a UIView with a UILabel for which, I'd like allow the user to dynamically change the font size. The code below almost works, but the problem is that after the new UIFont is assigned to the label, it doesn't immediately show in the UI. Instead, some other UI activity is required to make it show up.
What I'm looking for is a technique where the font change will show up immediately after the gesture is over.
Starting with the technique shown in this tutorial, I added a gesture recognizer to the UIScrollView. In the associated UIViewController, I implemented handlePinch(UIpinchGestureRecognizer).
Inside that function, I take action only when the gesture begins and ends (no attempt to animate). If the user indicated zoom, I increase the font by 4 points. If the user indicated a shrink, I decrease the font by 4 points. This all works, except for the fact that the screen does not reflect the change until another UI function is executed. I have only tested this on the simulator, but I presume it would be the same on a real device.
I have tried setNeedsDisplay and setNeedsLayout and several other odd things on both the UILabel as well as the view that was passed to the handlePinch() function.
#IBAction func handlePinch(_ gesture: UIPinchGestureRecognizer) {
guard let gestureView = gesture.view else {
return;
}
gestureView.transform = gestureView.transform.scaledBy(x: gesture.scale, y: gesture.scale)
if (gesture.state == UIPinchGestureRecognizer.State.began) {
pinchD = gestureView.transform.d
print("pinch started ", pinchD)
}
if (gesture.state == UIPinchGestureRecognizer.State.ended) {
print("pinch ended ", gestureView.transform.d)
print("compare:", pinchD, gestureView.transform.d)
let larger = gestureView.transform.d > pinchD
let fontSize = multiLineLabel.font.pointSize
if (larger) {
if (fontSize < 28) {
let calculatedFont = UIFont(name: multiLineLabel.font.fontName, size: fontSize + 4)
print("bigger fontSize", fontSize + 4);
multiLineLabel.font = calculatedFont
}
} else {
if (fontSize > 12) {
let calculatedFont = UIFont(name: multiLineLabel.font.fontName, size: fontSize - 4)
print("smaller fontSize", fontSize - 4);
multiLineLabel.font = calculatedFont
}
}
}
}
What needs to be done after the calculatedFont is applied to the UILabel to make it show immediately in the app?
ADDITIONAL CLARIFICATION:
handlePinch() runs as expected; every time a pinch gesture ends, I see bigger fontSize... or smaller fontSize... logged.
After the logging, the simulator screen font size remains the same until I do one of several things. One thing that causes the font size to change is if I swipe to the next item. Before the animation starts, the font increases. The font changes also if I hit the back button (the font change shows briefly before the other view appears).
It turns out that my presumption was incorrect:
I have only tested this on the simulator, but I presume it would be the same on a real device.
I believe that was NOT correct.
I'm testing on a very, very old iMac and the simulator is bogged down such that it takes 15 or 30 seconds to update the screen!
So I conclude that the code is working fine after all.

How can I implement this UICollectionView cell animation?

I've been wanting to implement this nice little UICollectionViewCell animation shown below that was on Dribble.
Do you think it's possible?
I looked around for any guides to get a head start on this but found nothing quite similar.
I have the idea that a custom flow layout is the way to go here. Would it be that I will have to make snapshots of each visible cell, add pan gestures to each cell and based on the movement detected through the gesture recogniser, capture visible cells and animate the snapshot images? Would appreciate any help to understand how I could implement this.
Thank you.
This is a pretty interesting challenge.
Instead of doing a custom layout, I would override scrollViewDidScroll, store the offset every time it's called, compare it with the last stored offset in order to get the velocity, and based off of that, apply a transform to all visibleCells in your collection view.
var lastOffsetX: CGFloat?
func scrollViewDidScroll(_ scrollView: UIScrollView) {
defer { lastOffsetX = scrollView.contentOffset.x }
guard let lastOffsetX = lastOffsetX else { return }
// You'll have to evaluate how large velocity gets to avoid the cells
// from stretching too much
let maxVelocity: CGFloat = 60
let maxStretch: CGFloat = 10
let velocity = min(scrollView.contentOffset.x - lastOffsetX, maxVelocity)
let stretch = velocity / maxVelocity * maxStretch
var cumulativeStretch: CGFloat = 0
collectionView.visibleCells.forEach { cell in
cumulativeStretch += stretch
cell.transform = CGAffineTransform(translateX: cumulativeStretch, y: 0)
}
}
I would start with something like this, and make lastOffsetX = nil when the scroll view stops scrolling (this exercise is left to the reader).
It will probably require some tweaking.

What is the proper way to move view according to UISlider position in swift?

I have a vertical view and vertical slider on screen next to each other. They have almost same size. I want this view to move according to position of slider. I was trying this way:
if oldValue < Int(sender.value) {
viewToMoveTopYConstreint.constant = viewToMoveTopYConstreint.constant - heightOfMovement
oldValue = Int(sender.value)
} else if oldValue > Int(sender.value) {
viewToMoveTopYConstreint.constant = viewToMoveTopYConstreint.constant + heightOfMovement
oldValue = Int(sender.value)
}
heightOfMovement is value to move my view for – e.g. 5 points
If I move slider slowly, view moves pretty accurate, but if I do it quickly, view moves in the way, like I only changed one value instead of twenty.
I know I can try to do it with "for in", but I was wondering is there a more proficient way to do this.
I think the problem is that you don't use the sliders value property, but instead only add some heightOfMovement (nobody except you knows where this value comes from).
I think the following would be more straight-forward:
rename viewToMoveTopYConstreint to viewToMoveTopYConstraint (just a spelling mistake :-)
set the minValue of the slider to 0
set the maxValue of the slider to the height of the view to be moved
in your #IBAction just do the following:
viewToMoveTopYConstraint.constant = sender.value

Move and Jump animation for UIImageView : Swift 4

I need to make my image jump and move down with animation. I am using sprites to show the jump animation. But, when I try to combine it with move down animation, it does not work. I am trying it on click of a button. Below is my code.
My animation works properly for second click but for first click it just jumps at its current location and at second click it jumps and moves down. I am trying to make it jump and move down at first button click.
#IBAction func targetTouchAction(_ sender: Any) {
let images: [UIImage] = (1...10).map { return UIImage(named: "Jump_\($0)")! }
self.jackImage.frame.origin.y = self.jackImage.frame.origin.y + 100.0
self.jackImage.animationImages = images
self.jackImage.animationDuration = 2.0
self.jackImage.animationRepeatCount = 1
self.jackImage.startAnimating()
}
I am trying this from last two three hours but no luck.
You are mixing 2 different types of animation. Don't do that. Using the animationImages property of a UIImageView is separate from using UIView animation methods like UIView.animate(duration:animations:)
Get rid of that UIView animation wrapper, and just specify the animation images and other settings for your image view:
#IBAction func targetTouchAction(_ sender: Any) {
let jumpImages = ["Jump_1","Jump_2","Jump_3","Jump_4","Jump_5","Jump_6","Jump_7","Jump_8","Jump_9","Jump_10"]
var images = [UIImage]()
for image in jumpImages{
images.append(UIImage(named: image)!)
}
self.imageView.frame.origin.y = self.imageView.frame.origin.y + 100.0
self.imageView.animationImages = images
self.imageView.animationDuration = 2.0
self.imageView.animationRepeatCount = 1
self.imageView.startAnimating()
}
Note that loading the array of images into the image view each time you trigger the animation is unnecessary, but it should still work.
I haven't worked with SpriteKit, so I'm not sure how your image view animation will interact with that. (Plus you didn't show that part of your code.)
Also, note that you could create your array of images with 1 line of code, and without using a hard-coded array of filenames:
let images: [UIImage] = (1...10).map { return UIImage(named: "Jump_\($0)") }
That code creates a sequence from 1 to 10. It then uses the map statement to map that sequence into an array of images by feeding the value into a string, and using that string as the image name in a call to UIImage(named:)