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.
Related
Is there a way to make the corner radius of a UIView adept to the view it belongs to? I'm not comfortable with the idea of hard-coding corner radius values, because as soon as the width or the height of your view changes (on different screen orientations for example), the corners will look totally different. For example, take a look at WhatsApp's chat window.
As you can see, every message container view has a different width and a different height, but the curve of the corners are all exactly the same. This is what I'm trying to achieve. I want the curves of my corners to be the same on every view, no matter what the size of the view is or what screen the view is displayed on. I've tried setting the corner radius relative to the view's height (view.layer.cornerRadius = view.frame.size.height * 0.25) and I've also tried setting it to the view's width, but this doesn't work. The corners still look weird as soon as they are displayed on a different screen size. Please let me know if there's a certain formula or trick to make the curves look the same on every view/screen size.
Here's the best I can do. I don't know if this will be of help, but hopefully it will give you some ideas.
First the code:
class ViewController: UIViewController {
let cornerRadius:CGFloat = 10
let insetValue:CGFloat = 10
var numberOfViews:Int = 0
var myViews = [UIView]()
override func viewDidLoad() {
super.viewDidLoad()
view.translatesAutoresizingMaskIntoConstraints = false
}
override func viewWillLayoutSubviews() {
setNumberOfViews()
createViews()
createViewHierarchy()
addConstraints()
}
func setNumberOfViews() {
var smallerDimension:CGFloat = 0
if view.frame.height < view.frame.width {
smallerDimension = view.frame.height
} else {
smallerDimension = view.frame.width
}
let viewCount = smallerDimension / (insetValue * 2)
numberOfViews = Int(viewCount)
}
func createViews() {
for i in 1...numberOfViews {
switch i % 5 {
case 0:
myViews.append(MyView(UIColor.black, cornerRadius))
case 1:
myViews.append(MyView(UIColor.blue, cornerRadius))
case 2:
myViews.append(MyView(UIColor.red, cornerRadius))
case 3:
myViews.append(MyView(UIColor.yellow, cornerRadius))
case 4:
myViews.append(MyView(UIColor.green, cornerRadius))
default:
break
}
}
}
func createViewHierarchy() {
view.addSubview(myViews[0])
for i in 1...myViews.count-1 {
myViews[i-1].addSubview(myViews[i])
}
}
func addConstraints() {
for view in myViews {
view.topAnchor.constraint(equalTo: (view.superview?.topAnchor)!, constant: insetValue).isActive = true
view.leadingAnchor.constraint(equalTo: (view.superview?.leadingAnchor)!, constant: insetValue).isActive = true
view.trailingAnchor.constraint(equalTo: (view.superview?.trailingAnchor)!, constant: -insetValue).isActive = true
view.bottomAnchor.constraint(equalTo: (view.superview?.bottomAnchor)!, constant: -insetValue).isActive = true
}
}
}
class MyView: UIView {
convenience init(_ backgroundColor:UIColor, _ cornerRadius:CGFloat) {
self.init(frame: CGRect.zero)
self.translatesAutoresizingMaskIntoConstraints = false
self.backgroundColor = backgroundColor
self.layer.cornerRadius = cornerRadius
}
}
Explanation:
This is fairly simple code. The intent was to create as deeply nested a view hierarchy as possible, and, using auto layout, have two main variables: cornerRadius (the view's corner radius) and insetValue (the "frame's" inset). These two variables can be adjusted for experimenting.
The bulk of the logic is in viewWillLayoutSubviews, where the root view frame size is know. Since I'm using 5 different background colors, I'm calculating how many views can fit in the hierarchy. Then I'm creating them, followed by creating the view hierarchy, and finally I'm adding the constraints.
Experimenting and conclusions:
I was able to see what your concern is - yes, if a view's size components are smaller than the corner radius, you end up with inconsistent looking corners. But these values are pretty small - pretty much 10 or less. Most views are unusable at that size. (If I recall even the HIG suggests that a button should be no less than 40 points in size. Sure, even Apple breaks that rule. Still.)
If your 'insetValueis sufficiently larger than the corner radius, you should never have an issue. Likewise, using the iMessage scenario, a singleUILabelcontaining text and/or emoticons should have enough height that a noticeablecornerRadius` can be had.
The key point to set things like cornerRadius and insetValue is in viewWillLayoutSubviews, when you can decide (1) which is the smaller dimension, height or width, (2) how deeply you can nest views, and (3) how large of a corner radius you can set.
Use auto layout! Please note the absolute lack of frames. Other than determining the root view's dimensions at the appropriate time, you can write very compact code without worrying about device size or orientation.
I am trying to work a little bit with scrollViews. So I have a ScrollView on my Controller with bouncing enabled. So now then I scroll the view bounces at the end of the page like I want to.
But now I want to freeze my bouncing. I try to explain it a little bit more:
I scroll up in my ScrollView. The end of the page begins. So now the bouncing begins and its scrolling a little bit up. Then I undrag the scrollView the view bounces back. But I want the scrollView to stay in this position (because I push the view away and it looks weird when its bouncing while I push it).
Things I tried:
1.Set the frame on the scroll view to the current bouncing position. This just edits the frame but still bounces the scrollView.
v2.view.frame = CGRect(x: CGFloat(0), y: CGFloat(CURRENTBOUNCING), width: width, height: height)
2.Turn bounces off. This just ends the current bounce and after you can't bounce anymore. So if I don't want it to bounce back this doesn't work.
scrollView.bounces = false
3.Set contentOffset manually. Changes nothing for me even if I write it after initializing
scrollView.contentOffset.y -= 100 // Or something like this
4.Change the contentSize. This changes nothing I think because of the constraints
scrollView.contentSize = CGSize(width: self.view.frame.width, height: self.view.frame.height - 200)
5.Turn always bouncing vertically off. Same like 2.
scrollView.alwaysBounceVertical = false
6.Paging. I tried this because now I page manually. But then I miss the bouncing into nothing before paging. With paging you can see the next page while scrolling behind the end. But the next page should be loaded after dragging.
Hope someone can help me :)
EDIT
7.Taking a picture of my current scrollview to display it on my screen over the scrollview. This takes an image with an delay and not at the right moment ( so the bounces is at the half then capturing or already done.
let renderer = UIGraphicsImageRenderer(size: scrollView.bounds.size)
pufferImage.image = renderer.image { ctx in
scrollView.drawHierarchy(in: scrollView.bounds, afterScreenUpdates: true) }
Ran into the same problem. The solve is to record the content offset and change the vertical offset of the scrollview to that value while simultaneously turning scrolling off.
private var dismissTriggerd = false
override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
if keyPath == "contentOffset" {
if scrollView.contentOffset.y < -70 && dismissTriggerd == false {
scrollViewTop.constant = scrollView.contentOffset.y * -1
scrollView.isScrollEnabled = false
dismissTriggerd = true
dismiss(animated: true)
}
}
}
deinit {
scrollView.removeObserver(self, forKeyPath: "contentOffset")
}
I had a similar use case, the key was to switch the contentOffset into contentInset this negated the effect of the bounce back
maybe not a 100% complete solution, but i leave it here for inspiration
case UIGestureRecognizerStateEnded:
self.interactor.hasStarted = NO;
if (self.interactor.shouldFinish) {
UIEdgeInsets ei = self.scrollView.contentInset;
ei.top -= self.scrollView.contentOffset.y;
self.scrollView.contentInset = ei;
[self.interactor finishInteractiveTransition];
} else {
[self.interactor cancelInteractiveTransition];
}
break;
default:
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.
I have a view hierarchy similar to the one in the image below (blue is the visible part of the scene):
So I have a UIScrollView with a lot of elements, out of which I am only showing the two button since they are relevant to the question. The first button is visible when the app is run, whereas the other one is positioned outside of the initially visible area. The first button is also the preferredFocusedView.
Now I am changing focus between the two buttons using a UIFocusGuide, and this works (checked it in didUpdateFocusInContext:). However, my scroll view does not scroll down when Button2 gets focused.
The scroll view is pinned to superview and I give it an appropriate content size in viewDidLoad of my view controller.
Any ideas how to get the scroll view to scroll?
Take a look at UIScrollView.panGestureRecognizer.allowedTouchTypes. It is an array of NSNumber with values based on UITouchType or UITouch.TouchType (depending on language version). By default allowedTouchTypes contains 2 values - direct and stylus. It means that your UIScrollView instance will not response to signals from remote control. Add the following line to fix it:
Swift 4
self.scrollView.panGestureRecognizer.allowedTouchTypes = [NSNumber(value: UITouchType.indirect.rawValue)]
Swift 4.2 & 5
self.scrollView.panGestureRecognizer.allowedTouchTypes = [NSNumber(value: UITouch.TouchType.indirect.rawValue)]
Also, don't forget to set a correct contentSize for UIScrollView:
self.scrollView.contentSize = CGSize(width: 1920.0, height: 2000.0)
Finally I solved this by setting scrollView.contentSize to the appropriate size in viewDidLoad.
You need to add pan gesture recognizer. I learned from here: http://www.theappguruz.com/blog/gesture-recognizer-using-swift. I added more code to make it not scrolling strangely, e.g. in horizontal direction.
var currentY : CGFloat = 0 //this saves current Y position
func initializeGestureRecognizer()
{
//For PanGesture Recoginzation
let panGesture: UIPanGestureRecognizer = UIPanGestureRecognizer(target: self, action: Selector("recognizePanGesture:"))
self.scrollView.addGestureRecognizer(panGesture)
}
func recognizePanGesture(sender: UIPanGestureRecognizer)
{
let translate = sender.translationInView(self.view)
var newY = sender.view!.center.y + translate.y
if(newY >= self.view.frame.height - 20) {
newY = sender.view!.center.y //make it not scrolling downwards at the very beginning
}
else if( newY <= 0){
newY = currentY //make it scrolling not too much upwards
}
sender.view!.center = CGPoint(x:sender.view!.center.x,
y:newY)
currentY = newY
sender.setTranslation(CGPointZero, inView: self.view)
}
I have a UIPageControl in my application that looks perfectly fine with around 10 pages (dots), it's possible however for the user to add many different views, and so the number of dots could become say 30.
When this happens the dots just disappear off the edge of the screen, and you can't always see the currently selected page, making it all look terrible.
Is there any way to make the pagecontrol multi-line, or to shift it left or right at the moment the currently visible page disappears of the screen.
I created an eBook application that used UIScrollView that contained a UIWebView. The book had over 100 pages so UIPageControl could not handle it because of the problem you pointed out. I ended up creating a custom "slider" type view that acted similar to UIPageControl but could handle the large number of pages. That's pretty much what you will need to do. UIPage control cannot handle as many pages as you want...
Calculate the pages/dot ratio and store as float value. Then calculate current dot number as current page/pagesPerDot. You may need to advance a few pages before seeing the dot move, but it's very scalable and works well.
I had to implement UIPageControl under my horizontal collection view which had 30 items. So what i did to reduce the number of dots by fixing the number of dots and scroll in those dots only. Follow the code:
#IBOutlet var pageControl: UIPageControl!
var collectionDataList = [String]
let dotsCount = 5
override func viewDidAppear(_ animated: Bool) {
if collectionDataList.count < dotsCount {
self.pageControl.numberOfPages = collectionDataList.count
} else {
self.pageControl.numberOfPages = dotsCount
}
}
func scrollViewDidScroll(_ scrollView: UIScrollView)
{
let pageWidth = scrollView.frame.width
self.currentPage = Int((scrollView.contentOffset.x + pageWidth / 2) / pageWidth)
self.pageControl.currentPage = self.currentPage % dotsCount
}
My solution is setting the maximum number of dots and showing the dot before the last one until the user reaches the end. If the current page number is bigger than maximum dot count we show the one before the last one until the user reaches the end.
var maxDotCount = 8 //Global constant.
func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
let pageWidth = collectionView.frame.size.width
let currentPage = collectionView.contentOffset.x / pageWidth
let pageControlsNumberOfPages = pageControl.numberOfPages - 1
let dataSourceCount = CGFloat(dataSourceArr.count - 1)
let maxDotCountReduced = CGFloat(maxDotCount - 1)
if (dataSourceCount > maxDotCountReduced) {
if (currentPage >= maxDotCountReduced) {
if (currentPage == dataSourceCount) {
pageControl.currentPage = pageControlsNumberOfPages
} else {
pageControl.currentPage = pageControlsNumberOfPages - 1
}
return
}
}
pageControl.currentPage = Int(currentPage)
}
I set the maximum number of pages to 20 (the most that will fit on the iPhone 5S/SE in Portrait Mode).. and if I am on a page over 20, I keep the dot in the same place, but make the dot red. I think people realise what it means, I often have a numeric page number on screen too, and First and Last buttons too, and I feel people are more interested in seeing the dot move on the initial pages.