Pinch gesture to expand a UICollectionView Cell - ios

An application I'm working on requires a layout very similar to that of the Photos app on iPad. There's a grid of UIViews and the user should be able to pinch into a single one, and as they're pinching in, watch that view grow in size until it's full screen.
So far, I have set up a UICollectionViewController and a custom collection view cell. I added the pinch gesture recognizer to the custom cell. When the user pinches in, the cell grows in size.
The issue I am now having is that the cell isn't layering on top of all other cells as it expands. Rather, it is hiding under the cells loaded after it.
I was thinking that the solution may be that when the collection view controller recognizes the pinch gesture, it could hide the cell being pinched on. Then, create a new UIView that is a copy of the collection view cell, with the bounds equivalent to where the cell was placed. Then, this view can be the one that can be pinched, rotated, and panned. As soon as the user ends the gesture, the view would just animate back to the original cell position and then be set to nil.
However, I'm not sure if this is the optimal solution and maybe there's some easy way to ensure a certain cell is layered on top of all other cells in the collection view.
The code for a snippet of my current implementation of how I'm scaling the cell is shown below. This is the one that creates the issue of the cell hiding under later cells as it expands.
#objc func scalePiece(_ gestureRecognizer : UIPinchGestureRecognizer) {
guard gestureRecognizer.view != nil else { return
if gestureRecognizer.state == .began ||
gestureRecognizer.state == .changed {
gestureRecognizer.view?.transform = (gestureRecognizer.view?.transform.scaledBy(x: gestureRecognizer.scale, y: gestureRecognizer.scale))!
gestureRecognizer.scale = 1.0
}
}

One dirt-simple possibility to remedy the cell being behind others is to invoke bringSubviewToFront(_:) on the collection view, passing the cell.
Caveat however, I'm not sure that this is optimal: since you don't really own the cells/the collection view's hierarchy, generally speaking you shouldn't touch the subviews directly like this.
A closely-related possibility -- not as simple, but probably much more robust and thus preferable -- is to customize the layout. Since the collection view's UICollectionViewLayout object can specify the zIndex of a particular cell, you could reload the cell when the pinch gesture begins and update the Z index for just that cell.

Related

UITableView tap not working first time after constraints change

IMPORTANT: My problem is not that I'm implementing didDeelectRowAt instead of didSelectRowAt. Already checked that :)
I have a UITableView that is shown on part of the screen in a modally presented view controller. When the user is dragging it resizes to full screen and back to some defined min height. I'm doing this by implementing the following methods from the UIScrollViewDelegate:
func scrollViewDidScroll(_ scrollView: UIScrollView) {
guard !scrollView.isDecelerating else { return }
let contentOffset = scrollView.contentOffset.y
if tableViewHeightConstraint.constant < view.frame.height && contentOffset > 0.0 {
tableViewHeightConstraint.constant = min(contentOffset + tableViewHeightConstraint.constant, view.frame.height)
scrollView.contentOffset.y = 0.0
return
}
if tableViewHeightConstraint.constant > minViewHeight && contentOffset < 0.0 {
tableViewHeightConstraint.constant = max(tableViewHeightConstraint.constant + contentOffset, minViewHeight)
scrollView.contentOffset.y = 0.0
}
}
func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>) {
// Here I have some calculations if depending the dragging end position and the velocity the end size should be full screen or `minViewHeight`
// After calculating what the end size should be I'm animating the size change
heightConstraint.constant = newConstraintHeight
UIView.animate(withDuration: TimeInterval(calculatedAnimationDuration), delay: 0.0, options: .curveEaseOut, animations: {
self.view.layoutIfNeeded()
}, completion: nil)
}
Everything about the resizing and the scrolling works fine, but there is a problem that I cannot figure out why it's happening. It's the following:
When the view controller with the table view is shown for the first time with the min height and I tap on a cell it works fine.
If I drag to expand the table view to full screen height and tap on a cell, again it works fine.
If I drag to expand the table view to full screen height and then drag again to return it to the min height and then tap on a cell, nothing is happening, no UIScrollViewDelegate or UITableViewDelegate method is called at all. If I tap once more on a cell everything works fine.
One thing that I noticed is that after dragging the table view back to the min height the scroll indicator does not hide. On the first tap it hides, and on the second tap the didSelectRowAt is called.
UPDATE:
Here is a test repo for the problem: https://github.com/nikmin/DragTest
Please don't mind if the dragging doesn't work perfectly, I just put something so anyone can try it out, but I think the problem is easily reproducible.
Also one more thing... If you drag from full size all the way to the bottom so the table view reaches min height and you continue dragging so the content offset is < 0 and the you release, the problem is not happening.
Drag TableView to return it to the min height and then tap on a cell, nothing is happening because:
When you drag to expand the table view to full screen, scrollView.isDecelerating is true. So the code inside scrollViewDidScroll method will run.
But when you drag TableView to return it to the min height, scrollViewDidScroll is false. So the code inside scrollViewDidScroll method won't run. It's make the first tap do nothing.
Simply remove guard !scrollView.isDecelerating else { return } from scrollViewDidScroll. You will tap cell normally after drag TableView down.
But you need change logic a little, animation will go wrong after remove above line.
Hope it can help you ;)
After trying to figure out a solution to this without result, we (me and a UX designer) decided to change the behaviour a bit.
So in the real scenario in the app I'm implementing this in, the table view is inside another view that has also a title label and some other views above the table view. We decided to add a pan gesture recognizer to this root view and disable the scrolling of the table view when the view has the min size. This way the pan gesture recognizer will take over whenever the user tries to drag anywhere inside the view (including the table view), so the expanding of the view works. And the tap in the cell still works.
When the view has the max height the table view scroll is enabled so the user can scroll. The downside of this approach is that when the user scrolls to the top of the table view and continues scrolling the view will not decrease the size. But he still has the option to drag it down by dragging any of the views above the table view. When dragging down in this way, only the size of the table view changes, and the content offset isn't, which is the root of the problem (changing both at the same time).
It looks like incorrectly set content offset. The first touch cancels incorrect position(unscroll), this is why it is not registered. It might be better if we got an access to the full code to check it, because I can't tell you where exactly the problem lies, but I guess it is in method scrollViewDidScroll.

UICollectionView cell is disappearing when dragging out of initial bounds (Swift3, CollectionView, Drag and Drop)

I have a problem, when dragging cell out of initial bounds.
On drag, I take value from UILongPressGestureRecognizer location and set cell "center" property to this location.
The problem occurs when I scroll down, below initial bounds, the cell doesn't show on the view (although there are cells existing down, probably loaded after first view appearing). When I scroll little up, within same take (press), cell shows up again.
I tried several things
cashing cells, so view alway get the same cell instance on reload
override func viewDidLayoutSubviews which lead in endless loop
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
myCollection.collectionViewLayout.invalidateLayout()
}
collectionView?.clipsToBounds = false
None of these worked for me. Could you help me? Thanks!!!

Twitter profile page iOS Swift dissection (Multiple UITableViews in UIScrollView)

hi... really how do they implement this? there are several tutorial for Twitter profile page. but they don't handle all possibilities...
first... when you scroll top or bottom any where, top view start scrolling until the segmented control, reach at top of the page...then scroll doesn't stop and subtable start scrolling until touching down and middle of way tableview start loading other rows dynamically ... so I don't think that they set content of scrollview statically
second things how do they handle sub tables... are they containerView?
if so, then the structure would be like this
ScrollView
TopView (User Info)
Segmented Controll
scrollView(to swipe right or left changing tables)
ContainerView For TWEETS
ContainerView For TWEETS & REPLIES
ContainerView For MEDIA
ContainerView For LIKES
am I right?
so how do they handle scrolls between sub tables and Top Scroll View to implementing topview position change base on scrolling...
it's mind blowing
this is How I Handel Nested ScrollViews...
i made a childDidScroll Protocol and my child tableviews implement that and in my profile page i can receive all child didscroll event then in
childDidScroll method :
//if child scrollview going up
if(scrollView.panGestureRecognizer.translation(in: scrollView.superview).y > 0)
{
//check top scrollview if it is at bottom or top
//then disable the current scrollview
if mainScrollView.isAtBottom && scrollView.isAtTop{
scrollView.isScrollEnabled = false
}else{
//else enable scrolling for my childs
featuresVC.tableView!.isScrollEnabled = true
categoriesVC.tableView!.isScrollEnabled = true
shopsVC.tableView!.isScrollEnabled = true
}
print("up")
}
else
{
if mainScrollView.isAtTop {
scrollView.isScrollEnabled = false
mainScrollView.scrollToBottom()
}else{
featuresVC.tableView!.isScrollEnabled = true
categoriesVC.tableView!.isScrollEnabled = true
shopsVC.tableView!.isScrollEnabled = true
}
print("down")
}
but this solution has a some cons... and one of the is that first when child scrollview is at top or button, there should be two try to call my parent scrollview handle the scrolling, in first try i disable the child scrollview, and in second try parent scrollview handle the scrolling
** how can i say when you , my child, scrolling up, check if your parent is at top, then let him handle the scroll and when he touching the bottom, you can handle remain up scrolling, or tell the parent scrollview , if you are at top (user info is visible) if you or your child getting up scrolling, first you handle the scroll and when you rich at bottom(user info is not visible), let the remain scrolling on you child**
After a long long investigation that is how i achieve the twitter profile behaviour.
UnderlayScrollView
MasterScrollView
Header ViewController
Bottom ViewController
PagerTabItems [CollectionView]
UIPagerController or any other horizontal scroll (Parchment, XLPagerTabStrip).
UnderlayScrollView is responsible of controlling the scroll gesture. its contentoffset is used to adjust inner scroll contentOffsets. Contentsize of the underlaying scroll is same as the masterscroll's contentsize.
See the source code on github for more. click
I believe you are mostly right, except for the topmost scroll view.
In a recent app, I implemented something similar following this tutorial:
Basically, the trick is to have a class be the scrolling delegate of the bottom UITableViews, listen to the scrollViewDidScroll modifications, and modify the top inset of the UITableView and the TopView.
The structure I would use is like this:
Topview
ScrollView (horizontal scroll)
Segmented Control
ScrollView (horizontal, paging scroll)
UITableView
UITableView
UITableView
UITableView
You are totally right in it being mind blowing. Looks so simple.
I found a library,
https://github.com/maxep/MXSegmentedPager
Its totally works fine

Reordering UICollectionView in iOS7

I want to build a UICollectionView, or use something similar library to make grid view, in which i can rearrange cells. My app supports iOS 7, and, unfortunately UICollectionView method - (BOOL)beginInteractiveMovementForItemAtIndexPath:(NSIndexPath *)indexPath
is only available on iOS 9 and later.
Is there any way i can achieve the effect of 'drag and reorder' collection view cells on iOS7? Thanks.
Well, I happened to come across the very same issue, so let me add my two cents on this.
In order to achieve to get the same effect of what UIKit does when rearranging cells, I followed a more or less similar approach described below:
Add a long-press gesture recognizer into collection view so that
you’d be able to detect which cell was tapped without messing up
with collectionView:didSelectItemAtIndexPath method.
When user long-presses on a cell, take a screenshot of that cell, add the snapshot view into collection view and then hide the cell. As the user moves his finger on the screen, update the snapshot’s center to move it programmatically so it will look like the user is dragging the cell.
The trick is to decide when and how to swap cells. As the snapshot moves around, it will intersect with other cells. When it happens, you should update your data source and call moveItemAtIndexPath so it will swap the positions of original cell and the cell which the snapshot intersects.
When dragging ends, remove the snapshot from collection view and show the original cell.
You can map those steps to gesture recognizer’s states, so that we know what you should do at each state:
Began: 1
Changed: 2, 3
Ended, Cancelled, Failed: 4
I put an example project in my github account, so you can download and play around with it. Here, I will just implement the gesture callback:
Note: The below code may not work because I threw away some implementation specific details. But it should show the basic intent. Please download the full code from my github repo.
func longPressRecognized(recognizer: UILongPressGestureRecognizer) {
let location = recognizer.locationInView(collectionView)
let indexPath = collectionView.indexPathForItemAtPoint(location)
switch recognizer.state {
case .Began:
let cell = collectionView.cellForRowAtIndexPath(indexPath)
let snapshotView = cell.snapshotViewAfterScreenUpdates(true)
snapshot.center = cell.center
collectionView.addSubview(snapshotView)
cell.contentView.alpha = 0.0 // hides original cell
case .Changed:
snapshotView.center = location // will follow user's finger.
dataSource.swap(originalCellIndexPath.item, indexPath.item) // swaps data
collectionView.moveItemAtIndexPath(originalCellIndexPath, toIndexPath: indexPath) // swaps cells
originalCellIndexPath = indexPath
default:
let cell = cellForRowAtIndexPath(originalCellIndexPath)
cell.contentView.alpha = 1.0
snapshotView.removeFromSuperview()
}
}
Here is the effect you will get (recorded on iOS 8.4 Simulator):

Nested Scroll view vs Table View or something else for this requirement

I have a requirement in which I need to have the following functionality -
1)I have a custom segmented control. In order to implement paging to the segmented control I have used horizontal scroll view. Each page has its own vertical scroll view.
Requirement
1)The image should hide as user scrolls up in the respective pages and should show down when user scrolls down in respective pages but keeping the custom segment always at the top of the screen when image is hidden irrespective of the individual page selection-
What I have tried so far -
1st Method
I tried putting the image as header of a table view.
Created a single section with one cell & gave the section header as the custom segment. And in the cell I placed the horizontal scroll view with the cell's height adjusted to cover all portion left out of the superview but it didn't work out as when I scroll the vertical scrolling of individual pages it was not in sync with the table view.
2nd Method
I tried setting the segment initially with a fixed distance from the top & I increased & decreased the constraint inside scrollViewDidScroll(). But it too didn't work as when the user scrolled rapidly ,the changing of constraint value didn't follow correctly.
So is there any other way to achieve the same ?
Please suggest as I can't make out what to do?
You add a tableView and your UIImage on top of it inside a scrollView. The tableView must have the same height & width than your scrollView. Then you disable the pan gesture of the scrollView :
self.scrollView.panGesture.active = false
Then you have to implement a custom scroll in scrollViewDidScroll' of yourtableView`'s delegate:
func scrollViewDidScroll(scrollView: UIScrollView) {
if self.scrollView.contentOffset.y <= 100 {
self.scrollView.contentOffset.y += scrollView.contentOffset.y
self.tableView.contentOffset.y = 0
} else {
// let the tableView scroll normally
}
}
Or, you can have a try with https://github.com/bryankeller/BLKFlexibleHeightBar ;)
It's a great component that can handle many type on animation in the header based on the position of a scrollView.

Resources