UICollectionViewCell scale cell size while scrolling - ios

I'm trying to create a UICollectionView where the UICollectionViewCell is getting scaled down when "leaving" the visible area at the top or bottom. And getting scaled up to normal size while "entering" the visible area.
I've been trying some scale/animation code in:
scrollViewDidScroll()
, but I can't seem to get it right.
My complete function looks like this:
func scrollViewDidScroll(scrollView: UIScrollView) {
var arr = colView.indexPathsForVisibleItems()
for indexPath in arr{
var cell = colView.cellForItemAtIndexPath(indexPath as! NSIndexPath)!
var pos = colView.convertRect(cell.frame, toView: self.view)
if pos.origin.y < 50 && pos.origin.y >= 0{
cell.hidden = false
UIView.animateWithDuration(0.5, animations: { () -> Void in
cell.transform = CGAffineTransformMakeScale(0.02 * pos.origin.y, 0.02 * pos.origin.y)
})
}else if pos.origin.y == 50{
UIView.animateWithDuration(0.5, animations: { () -> Void in
cell.transform = CGAffineTransformMakeScale(1, 1)
})
}
}
}
Is this in some way the right approach, or is there another better way?

Not a complete solution, but a few remarks/pointers:
You should not mess with the collection view cells directly in this way, but rather have a custom UICollectionViewLayout subclass that modifies the UICollectionViewLayoutAttributes to include the desired transform and invalidating the layout whenever necessary.
Doing if pos.origin.y == 50 is definitely not a good idea, because the scrolling might not pass by all values (that is, it might jump from 45 to 53). So, use >= and include some other way if you want to ensure that your animation is only executed once at the "boundary" (for example, store the last position or a flag).

Related

How do I make my horizontal UICollectionView cells swiping work flawlessly in a pagination manner(cells must be centered when swiped)?

I have a UICollectionView in my app, and the use case is that I want it to work like AppStore's Apps tab top collection view, in which swiping each cell makes the next cell centered. I have achieved that somehow in my app without using CollectionView's paging enabled property and did some calculation of sizes and achieved quite a good result, but it is lagging sometimes. It will be very helpful if some-one suggests a better approach to achieve the App store kind of behavior using a UICollectionView :).
App Store Example. See how the top collectionView works.
Wherever, I found this type of implementation, everywhere it has been implemented with UIScrollView. But, I am in search of the exact same behavior with a UICollectionView.
What I have achieved so far in my app.
See the collection View at the bottom. I have implemented this behavior using overriding the default scrollViewWillEndDragging method of scrollView from which a UICollectionView inherits and in that method, I have done the following calculations.
func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>) {
// Stop scrollView sliding:
targetContentOffset.pointee = scrollView.contentOffset
let indexOfMajorCell = self.indexOfMajorCell()
// calculate conditions:
let dataSourceCount = collectionView(cardsCollectionView, numberOfItemsInSection: 0)
let swipeVelocityThreshold: CGFloat = 5.0 // after some trail and error
let hasEnoughVelocityToSlideToTheNextCell = indexOfCellBeforeDragging + 1 < dataSourceCount && velocity.x > swipeVelocityThreshold
let hasEnoughVelocityToSlideToThePreviousCell = indexOfCellBeforeDragging - 1 >= 0 && velocity.x < -swipeVelocityThreshold
let majorCellIsTheCellBeforeDragging = indexOfMajorCell == indexOfCellBeforeDragging
let didUseSwipeToSkipCell = majorCellIsTheCellBeforeDragging && (hasEnoughVelocityToSlideToTheNextCell || hasEnoughVelocityToSlideToThePreviousCell)
if didUseSwipeToSkipCell {
let snapToIndex = indexOfCellBeforeDragging + (hasEnoughVelocityToSlideToTheNextCell ? 1 : -1)
let toValue = collectionViewFlowLayout.itemSize.width * CGFloat(snapToIndex)
UIView.animate(withDuration: 0.3, delay: 0, usingSpringWithDamping: 1, initialSpringVelocity: velocity.x, options: .allowUserInteraction, animations: {
scrollView.contentOffset = CGPoint(x: toValue, y: 0)
scrollView.layoutIfNeeded()
}, completion: nil)
} else {
// This is a much better way to scroll to a cell:
let indexPath = IndexPath(row: indexOfMajorCell, section: 0)
cardsCollectionView.collectionViewLayout.collectionView!.scrollToItem(at: indexPath, at: .centeredHorizontally, animated: true)
}
}
Note: The paging enabled property of the UICollectionView has been set to false in my implementation for this to work. When I turn that property to ON it was showing some weird behavior that some part of each cell becomes hidden whenever I do a swipe.

How can I automatically round the edges of a popup view?

I am working on a popup view for an app I am making. If you take a second to look at the image attached below, you will see that the top edges are rounded, but the bottom edges are not. This is because I only rounded the edges of the view (it is lowest in the hierarchy). I cannot round the edges of the images (the colorful boxes) because they are tables in a scrolling view. The only solution I can think of is a very ugly one where I mask the bottom edges with a UIImageView that appears once the popup has faded in. Does anyone have a better solution? If so, I would greatly appreciate your help. Also, my scrolling view is not yet functional, so that is not referenced here and the solution (if functional) should work regardless.
My code:
allSeenPopover.layer.cornerRadius = 5
userProfile.layer.cornerRadius = 15
colorBackground.layer.cornerRadius = 15
colorBackground.layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner]
#IBAction func loadUserProfile(_ sender: Any) {
if darken.alpha == 0 {
darken.alpha = 1
self.view.addSubview(userProfile)
userProfile.center = self.view.center
userProfile.transform = CGAffineTransform.init(scaleX: 1.3, y: 1.3)
userProfile.alpha = 0
UIView.animate(withDuration: 0.3) {
self.largeDropShadow.alpha = 0.3
self.userProfile.alpha = 1
self.userProfile.transform = CGAffineTransform.identity
}
}
else {
UIView.animate(withDuration: 0.2, animations: {
self.userProfile.transform = CGAffineTransform.init(scaleX: 1.3, y: 1.3)
self.userProfile.alpha = 0
self.darken.alpha = 0
self.largeDropShadow.alpha = 0
}) { (success:Bool) in
self.userProfile.removeFromSuperview()
}
}
}
The image I was referring to:
Since the view you want rounded is inside a table view cell, you must take care that the view is created added only once per reused cell.
Each time a reused cell scrolls into view, check to see if it has had an imageView subview (using a tag that is unique within the cell is a quick way to make that check). If you don't have one, create it and then configure it for the particular row, otherwise just configure it for the particular row...
(warning, I'm not swift fluent, but the idea should be clear)
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell:UITableViewCell = tableView.dequeueReusableCellWithIdentifier(cellReuseIdentifier) as UITableViewCell!
let imageView:UIImageView = cell.viewWithTag(99) as? UIImageView
if (!imageView) {
// this code runs just once per reused cell, so setup
// imageView properties here that are row-independent
imageView = UIImageView()
imageView.tag = 99 // so we'll find this when the cell gets reused
imageView.layer.cornerRadius = 15.0
imageView.clipsToBounds = true
// any other props that are the same for all rows
imageView.frame = // your framing code here
cell.addSubview(imageView)
}
// this code runs each time a row scrolls into view
// so setup properties here that are row-dependent
imageView.image = // some function of indexPath.row
return cell
}

scrolling until last cel of uicollectionview

I have a problem while scrolling my collectionView
after choosing a cell, the last cell is not available for choosing
func changeViewSize(from: CGFloat, to: CGFloat, indexPath: IndexPath) {
UIView.transition(with: myCollection, duration: 0.5, options: .beginFromCurrentState, animations: {() -> Void in
let cell = self.myCollection.cellForItem(at: indexPath)!
let cellCenter = CGPoint(x: cell.center.x, y: cell.center.y + 50)
if cell.bounds.height == from {
let sizee = CGRect(x: cell.center.x, y: cell.center.y, width: cell.frame.width, height: to)
self.myCollection.cellForItem(at: indexPath)?.frame = sizee
self.myCollection.cellForItem(at: indexPath)?.center = cellCenter
for x in self.indexPathss {
if x.row > indexPath.row {
print("this is cells")
print(self.myCollection.visibleCells)
let cell = self.myCollection.cellForItem(at: x)!
cell.center.y += 100
}
}
}
}, completion: {(_ finished: Bool) -> Void in
print("finished animating of cell!!!")
})
}
It looks like you're changing cell sizes and positions manually. The UICollectionView won't be aware of these changes and therefore its size is not changing. The last cells simply move out of the visible area of the collection view.
I haven't done something like this before, but have a look at performBatchUpdates(_:completion:). I'd try to reload the cell that you want to make bigger using that method. Your layout will have to supply the correct attributes, i.e. size.
From the screenshots it looks like maybe you could use a UITableView instead? If that's a possibility it might simplify things. It also has a performBatchUpdates(_:completion:) method, and the UITableViewDelegate would then have to provide the correct heights for the rows.
Because of spacing of cell we used collectionView and I think changing height of tableviewcell is easier than collectionviewcell if you have an idea for spacing of tablviewcell wi will be glad to have your idea.

UITableView.visibleCells not capturing all visible cells

I'm trying to animate a table view cells on the initial load. The animation works fine for all the cells, except for the last one at the bottom of the table which refuses to animate.
This is because the UITableView.visibleCells does not return this cell at the end. (it returns cells 0-12, the last cell is at index 13 and is clearly visible)
Here is the code. Is there anything I can do to ensure all the cells get animated?
override func viewWillAppear(animated: Bool) {
super.viewWillAppear(animated)
tableView.reloadData()
let cells = tableView.visibleCells
let tableHeight: CGFloat = clubsTable.bounds.size.height
for i in cells {
let cell: UITableViewCell = i as UITableViewCell
cell.transform = CGAffineTransformMakeTranslation(0, tableHeight)
}
var index = 0
for a in cells {
let cell: UITableViewCell = a as UITableViewCell
UIView.animateWithDuration(1.5, delay: 0.05 * Double(index), usingSpringWithDamping: 0.8, initialSpringVelocity: 0, options: UIViewAnimationOptions.CurveEaseIn, animations: {
cell.transform = CGAffineTransformMakeTranslation(0, 0);
}, completion: { (complete) in
})
index += 1
}
}
That is because the height of your table is less than the height of 13 cells. Thus it animates 12 cells. What you can do is to make the table height bigger in 30px (or any px until it contains 13 cells), and after the animation is done, change the height of the tableView back to normal.
after you call let cells = tableView.visibleCells can't you just do something like
let indexPath = NSIndexPath(forRow: cells.count, inSection: 0)
cells.append(tableView.cellForRowAtIndexPath(indexPath))
Going off memory on those function signatures but you get the point.

Trying to Annotate a MapView and Load a second View Controller Simultaneously

Inside my Primary View Controller I have a MKMapView and TableView. The tableview is using AlamoFire to call an API of user information to populate its cells.
What I would like to happen:
When you select a cell, a second container view, with more detailed user information, will segue into the portion of the screen where the tableview was. At the same time, the Map View will annotate with that user's location.
The problem:
When the cell is selected, the second container view slides onto the screen (good), the Map Annotates (good), but then the info container view disappears again (bad). With a second tap of the originally selected cell, the info container view slides back onto the screen to stay. I need to get rid of this double tap.
It appears that the Map Annotation is negating this process for some reason. When I comment out the map annotation, the VC transition works fine... with one tap.
Any thoughts would be helpful.
Here is the offending code:
func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
self.localTableView.deselectRowAtIndexPath(indexPath, animated: true)
dispatch_async(dispatch_get_main_queue(),{
self.populateInfoContainerView(indexPath.row)
self.mapView.selectAnnotation(self.users[indexPath.row], animated: true)
if(self.userInfoContainerView.frame.origin.x == UIScreen.mainScreen().bounds.size.width){
self.showInfoView(true)
} else {
self.hideInfoView(true)
}
})
}
func showInfoView(animated: Bool){
UIView.animateWithDuration(0.33, delay: 0.0, usingSpringWithDamping: 0.9, initialSpringVelocity: 1.0, options: UIViewAnimationOptions.CurveEaseInOut, animations: ({
self.userInfoContainerView.frame.origin.x = 0
self.localTableView.frame.origin.x = -UIScreen.mainScreen().bounds.size.width
}), completion: { finished in
//animation complete
})
self.infoVisible = true
}
func hideInfoView(animated: Bool){
let xPos = UIScreen.mainScreen().bounds.size.width
if(animated == true){
UIView.animateWithDuration(0.25, animations: ({
self.userInfoContainerView.frame.origin.x = xPos
self.localTableView.frame.origin.x = 0
}), completion: { finished in
//animation complete
})
} else {
self.userInfoContainerView.frame.origin.x = xPos
}
self.infoVisible = false
}
Thank you.
I believe I have found an answer. I'm not sure why this works and the above version does not, but I'll show you what I did different.
I am now animating the constraints of the TableView and the InfoView.
First, I ctrl+click+drag the constraint from my main.storyboard into my code and created an IBOutlet to animate. I also added a let screenWidth as the constant that I would move them by.
#IBOutlet weak var infoViewLeadingEdge: NSLayoutConstraint!
#IBOutlet weak var tableViewXCenter: NSLayoutConstraint!
let screenWidth = UIScreen.mainScreen().bounds.size.width
Then I replaced the code above in showInfo():
self.userInfoContainerView.frame.origin.x = 0
self.localTableView.frame.origin.x = -UIScreen.mainScreen().bounds.size.width
with this new code:
self.infoViewLeadingEdge.constant -= self.screenWidth
self.tableViewXCenter.constant += self.screenWidth
self.view.layoutIfNeeded()
In my hideInfo() above, I replaced this:
self.userInfoContainerView.frame.origin.x = xPos
self.localTableView.frame.origin.x = 0
with this new code:
self.infoViewLeadingEdge.constant += self.screenWidth
self.tableViewXCenter.constant -= self.screenWidth
self.view.layoutIfNeeded()
Don't forget the self.view.layoutIfNeeded(). If you don't use that code, the animations won't work. Also, the += and -= on the tableViewXCenter seem opposite of what I want the table to do, however, this is the way I needed to put them in order to get the table to move the direction I wanted. It did seem counterintuitive though.
And that's it. I hope this will be helpful to someone.

Resources