iOS tableview inside pageviewcontroller inside scrollview - ios

I want to put a tableView inside a pageviewcontroller which is inside a scrollview.
So I have in my storyboard :
All constraints are respected like this
I disabled the tableview scroll but my scroll doesn't scroll, my tableView in TestVC1 not expand the PagerPlace in TestVC
How can I make my scrollview scroll and its content size depends on the tableview height + my red view?
EDIT
I tried your solution, then I got a storyboard like this :
storyboard
Then my scrollview doesn't scroll, I don't know why,

In order to make your effect perfect.
Scroll View -> UIPageViewController's view -> UITableView
Scroll View has a subview of UIPageViewController's view,
UIPageViewController has many page, one page ( a controller's view ) has a subview of UITableView
Yeah. You can change the solution.
mainScrollView ( vertical slide ) -> contentScrollView ( horizontal slide ) -> contentStackView ( has many pages) -> UITableView ( one page )
mainScrollView is UIScrollView, slides in vertical,
contentScrollView is UIScrollView, slides in horizontal
contentScrollView.isPagingEnabled = true
that simulates UIPageViewController
contentStackView has many pages, one page is your UITableView
To make it work like this:
How can I make my scrollview scroll and its content size depends on the tableview height + my red view?
for the part above the UITableView
public func scrollViewDidScroll(_ scrollView: UIScrollView) {
if scrollView == mainScrollView {
// vertical
let offsetY = scrollView.contentOffset.y
if offsetY >= sillValue {
scrollView.contentOffset = CGPoint(x: 0, y: sillValue)
currentChildScrollView?.am_isCanScroll = true
scrollView.am_isCanScroll = false
} else {
let negScroll = (scrollView.am_isCanScroll == false)
if negScroll{
scrollView.contentOffset = CGPoint(x: 0, y: sillValue)
}
}
}
}
for the UITableView part
use KVO to controller the base scroll view's offset Y ,
and UITableView's offset Y is by default.
let keyValueObservation = currentChildScrollView?.observe(\.contentOffset, options: [.new, .old], changeHandler: { [weak self] (scrollView, change) in
guard let self = self, change.newValue != change.oldValue else {
return
}
self.childScrollView(didScroll: scrollView)
})
internal func childScrollView(didScroll scrollView: UIScrollView){
let scrollOffset = scrollView.am_originOffset.val
let offsetY = scrollView.contentOffset.y
if scrollView.am_isCanScroll == false {
scrollView.contentOffset = scrollOffset
}
else if offsetY <= scrollOffset.y {
scrollView.contentOffset = scrollOffset
scrollView.am_isCanScroll = false
mainScrollView.am_isCanScroll = true
}
}
the full code in github

Related

How to Make TableView scroll inside ScrollView behave naturally

I need to do this app. The view hierarchy goes like this
UIScrollView
-UIView (Main View)
--UIView (Top View Container)
--UITableview
When scrolling up the Main View, If table view has many cells, the table view should go to the top, and once it reaches the top. The user should be able to scroll the table view cells. I was able to achieve it but it doesn't scroll naturally.
Attached my code https://github.com/iamshimak/FinchHomeScreenRedesign.git
First, never put tableview inside a scrollview, it's a bad practice. You could just use tableview header and embed any type of view do you want before the tableview cells.
here's a snipeste on how I deal with it:
//MARK: ConfigureTableView
private func configureTableView(){
let footerView = UIView()
footerView.frame.size.height = 50
footerView.backgroundColor = .white
self.tableView.tableFooterView = footerView
self.tableView.tableHeaderView = self.headerView
var newFrame = headerView.frame
newFrame.size.width = view.bounds.width
newFrame.size.height = 300
headerView.frame = newFrame
tableView.backgroundView = UIView()
tableView.backgroundView?.addSubview(backgroundTableView)
}
as you can see, I embedded a UIView as a footer and another UIView named headerView as a header
but if you insist of using a tableview inside a scrollview, you can try using a scrollview delegate and detech which scrollview is scrolling
func scrollViewDidScroll(_ scrollView: UIScrollView) {
let yOffset = scrollView.contentOffset.y
if scrollView == self.scrollView {
if yOffset >= scrollViewContentHeight - screenHeight {
// logic when using scrollview
}
}
if scrollView == self.tableView {
if yOffset <= 0 {
// logic when using tableview scrollView
}
}
}

Zoom and Scroll support UICollectionView

I have a requirement to support zoom in UICollectionView.
Requirements:
After zoom in, it has to support to view the UICollectionViewCell’s hidden area ( area out of viewport) by horizontal and vertical scroll.
After Zoom out/in, it has to support the selection of UICollectionViewCell and able to scroll the UICollectionView ( Basically the default UICollectionView behavior on going back to no zoom state. ).
The list of approaches tried:
Added GestureRecognizer
a. Added UIPinchGestureRecognizer to transform the UICollectionView by scale.
b. After Zoom in, it was not possible to move the UICollectionViewcell to view the hidden area.
c. Added UIPanGestureRecognizer to move the center of UICollectionView
d. It was working fine to move the UICollectionView.
e. Now we can’t able to select the UICollectionViewCell and can’t able to scroll UICollectionView.
Added UICollectionView inside UIScrollView
a. Added UIScrollView with delegates.
b. Added UICollectionView as sub view of UIScrollView
c. Zoom out is not happening because UICollectionView (inherited by UIScrollView) consumes the zoom gesture
Added UIColectionView and UIScrollView both as siblings
a. Added UIScrollView and UICollectionView to parent.
b. Bring UIScrollView to front.
c. Zoom is working but not able to pan to see the hidden area.
Please suggest if there any way to fix above approaches or a better strategy to achieve zoom in a collectionView.
I have solved this using a UIScrollView and a UICollectionViewLayout subclass.
1) place a UIScrollView on top of the UICollectionView with the same frame.
self.view.addSubview(scrollView)
scrollView.addSubview(dummyViewForZooming)
scrollView.frame = collectionView.frame
scrollView.bouncesZoom = false
scrollView.minimumZoomScale = 0.5
scrollView.maximumZoomScale = 3.0
2) Set the contentSize of the UIScrollView and zoomingView to be the same as the UICollectionView
override func viewDidLayoutSubviews() {
super.viewWillLayoutSubviews()
scrollView.contentSize = layout.collectionViewContentSize
dummyViewForZooming.frame = CGRect(origin: .zero, size: layout.collectionViewContentSize)
scrollView.frame = collectionView.frame
}
3) Remove all gesture recognizers from the UICollectionView and add a delegate for the UIScrollView. Add a tap gesture recognizer to the UIScrollview
collectionView.gestureRecognizers?.forEach {
collectionView.removeGestureRecognizer($0)
}
let tap = UITapGestureRecognizer.init(target: self, action: #selector(scrollViewWasTapped(sender:)))
tap.numberOfTapsRequired = 1
scrollView.addGestureRecognizer(tap)
scrollView.delegate = self
4) When the ScrollView scrolls or zooms, set the contentOffset of the UICollectionView to be the same as the ScrollView contentOffset, set the layoutScale of your UICollectionViewLayout as the zoomscale and invalidate the layout.
func scrollViewDidZoom(_ scrollView: UIScrollView) {
if let layout = self.layout, layout.getScale() != scrollView.zoomScale {
layout.layoutScale = scrollView.zoomScale
self.layout.invalidateLayout()
collectionView.contentOffset = scrollView.contentOffset
}
}
func viewForZooming(in scrollView: UIScrollView) -> UIView? {
return dummyViewForZooming
}
func scrollViewDidScroll(_ scrollView: UIScrollView) {
collectionView.contentOffset = scrollView.contentOffset
}
5) override the prepare method in the UICollectionViewLayout, scan through all your layoutAttributes and set a transform:
attribute.transformedFrame = attribute.originalFrame.scale(layoutScale)
let ts = CGAffineTransform(scaleX: layoutScale, y: layoutScale)
attribute.transform = ts
let xDifference = attribute.frame.origin.x - attribute.transformedFrame.origin.x
let yDifference = attribute.frame.origin.y - attribute.transformedFrame.origin.y
let t1 = CGAffineTransform(translationX: -xDifference, y: -yDifference)
let t = ts.concatenating(t1)
attribute.transform = t
6) ensure you scale the collectionView content size:
override var collectionViewContentSize: CGSize {
return CGSize(width: width * layoutScale, height: height * layoutScale)
}
7) Intercept taps from the tap gesture recognizer and convert the location in view to a point in the collection view, you can then get the indexPath of that cell using indexPathForItem(point:) and select the cell or pass on events to the underlying views of the cell etc..
hope this helps

Swift: change tableview height on scroll

I have a VC as shown in the image
It has a UICollectionView on top, and a UITableView at the bottom.
CollectionView has 1:3 of the screen and TableView has 2:3 of the screen(set using equalHeight constraint).
I want to change the height of the UICollectionView when the tableView is scrolled.
When the tableView is scrolled up,I want to change the multiplier of equalHeights constraint to like 1:5 and 4:5 of collectionView and tableView respectively.This will ensure that height of tableView increases and collectionView decreases
When the tableView is scrolled down, the multiplier of equalHeights constraint should reset to default.
I've tried adding swipe and pan gestures to tableView, but they are unrecognised.
How to achieve this functionality?
P.S: Would love to achieve this functionality by using a pan gesture, so that dragging up and down changes the heights progressively.
This is the view hierarchy
EDIT/UPDATE
This is the code that I'm using.
class MyConstant {
var height:CGFloat = 10
}
let myConstant = MyConstant()
MainScreenVC
override func viewWillAppear(_ animated: Bool) {
myConstant.height = self.view.frame.size.height
}
func scrollViewDidScroll(_ scrollView: UIScrollView) {
if (self.lastContentOffset < scrollView.contentOffset.y) {
NotificationCenter.default.post(name: Notifications.decreaseHeightNotification.name, object: nil)
self.topViewConstraint.constant = -self.view.frame.height / 6
UIView.animate(withDuration: 0.3, animations: {
self.view.layoutIfNeeded()
})
} else if (self.lastContentOffset > scrollView.contentOffset.y) {
NotificationCenter.default.post(name: Notifications.increaseHeightNotification.name, object: nil)
self.topViewConstraint.constant = 0
UIView.animate(withDuration: 0.3, animations: {
self.view.layoutIfNeeded()
})
}
self.lastContentOffset = scrollView.contentOffset.y
}
Cell.Swift
override func awakeFromNib() {
super.awakeFromNib()
NotificationCenter.default.addObserver(self, selector: #selector(decreaseHeight), name: Notification.Name("decreaseHeightNotification"), object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(increaseHeight), name: Notification.Name("increaseHeightNotification"), object: nil)
self.contentView.translatesAutoresizingMaskIntoConstraints = false
heightConstraint.constant = (myConstant.height / 3) - 10
widthConstraint.constant = heightConstraint.constant * 1.5
}
#objc func decreaseHeight() {
heightConstraint.constant = (myConstant.height / 6) - 10
widthConstraint.constant = (heightConstraint.constant * 1.5)
self.layoutIfNeeded()
}
#objc func increaseHeight() {
heightConstraint.constant = (myConstant.height / 3) - 10
widthConstraint.constant = (heightConstraint.constant * 1.5)
self.layoutIfNeeded()
}
Now when I scroll both simultaneously, the screen freezes. Also is there a better way of resizing the collectionViewCell size?
I haven't tested it but you can do this. Use Autolayout in this view. it will work better with that.
Set the tableview constraint as Top, Left, Bottom, Right => 0, 0, 0, 0 with the main view and put collectionView under the tableview with constraint as Top,Left,Right, height => 0, 0, 0, x with the main view.
Note: Tableview is on top of the collectionView.
Connect your height constraint outlet of CollectionView and also define your defaultOffset variable
#IBOutlet weak var defaultHeightConstraint: NSLayoutConstraint
var defaultOffSet: CGPoint?
In viewDidLoad,
override func viewDidLoad() {
super.viewDidLoad()
tableView.contentInset = UIEdgeInsetsMake(collectionView.size.height, 0, 0, 0)
}
In viewDidAppear, write
override func viewDidAppear(_ animated: Bool) {
defaultOffSet = tableView.contentOffset
}
In ViewDidScroll
func scrollViewDidScroll(_ scrollView: UIScrollView) {
let offset = tableView.contentOffset
if let startOffset = self.defaultOffSet {
if offset.y < startOffset.y {
// Scrolling down
// check if your collection view height is less than normal height, do your logic.
let deltaY = fabs((startOffset.y - offset.y))
defaultHeightConstraint.constant = defaultHeightConstraint.constant - deltaY
} else {
// Scrolling up
let deltaY = fabs((startOffset.y - offset.y))
defaultHeightConstraint.constant = defaultHeightConstraint.constant + deltaY
}
self.view.layoutIfNeeded()
}
}
Hope it helps.
Since UITableView is a subclass of UIScrollView, your table view's delegate can receive UIScrollViewDelegate methods.
You don't need to use PanGesture
First implement UITableviewDelegate and in that
var oldContentOffset = CGPoint.zero
func scrollViewDidScroll(_ scrollView: UIScrollView) {
let contentOffset = scrollView.contentOffset.y - oldContentOffset.y
if (self.oldContentOffset < scrollView.contentOffset.y) {
// moved to top
} else if (self.oldContentOffset > scrollView.contentOffset.y) {
// moved to bottom
} else {
// didn't move
}
oldContentOffset = scrollView.contentOffset
}
Here you can write you logic to change constant value of your constraint
UPDATE/EDIT
Another thing is don't use multiplier for tabelview height.with collection view it is fine you can give height constraint with self.view in ratio of 1/3 . for tableview just give leading ,trailing top (UICollectionView) and bottom.
So when you change height constant of UICollectionView Tableview height automatically decrease and vice-versa for decrease.
You can not change the multiplier from the outlet as it is read only. so you have to constant value to self.view.frame.height / 3 will work for you
Hope it is helpful to you
For a much cleaner approach, why don't you use a UIScrollView as the container view and add the UICollectionView and UITableView inside it and give the collectionView a height constraint while disabling tableView's scroll till the height constraint of your collectionView becomes 0.
Here's the snippet:
Extend your VC with UIScrollViewDelegate and use:
func scrollViewDidScroll(_ scrollView: UIScrollView) {
if scrollView.contentOffset.y >= 0 && scrollView.contentOffset.y <= yourLimit {
heightConstraint.constant = collectionViewHeight - scrollView.contentOffset.y
tableView.isScrollEnabled = false
} else if scrollView.contentOffset.y > yourLimit {
heightConstraint.constant = collectionViewHeight
tableView.isScrollEnabled = true
}
}
Try this snippet after customizing small things in it. It should work mostly, if it doesn't there is another approach which I'd share once this doesn't work.
Why don't you just add Custom Table Cell with your collection view inside, and when indexPath.row == 0 , return your custom table cell.

Scrolling collection view with UIViews on top?

I have a UICollectionViewController and I added two UIViews as subviews. One is the purple UIView and above it is another white UIView, with the blue collection view getting scrolled up behind both.
Below those UIViews, I made the collectionView.contentInset from the top 300 (that's the total size of two UIViews' height). What I'm trying to accomplish is to scroll the collection view along with the two UIViews above. It's almost similar to the solution on this thread (Move a view when scrolling in UITableView), except when I override scrollViewDidScroll, the whole frame gets scrolled up and cells go behind the two Views. All I want is to scroll up the UIViews, and then scroll through the collection views. I feel like this might involve nested scroll views.
This was how I overrode scrollViewDidScroll:
var rect = self.view.frame
rect.origin.y = -scrollView.contentOffset.y - 300
self.view.frame = rect
EDIT: I posted a video that demonstrates what I want to do per iOS Tumblr app: https://youtu.be/elfxtzoiHQo
I have achieved the same requirement through some basic steps as below.
//Declare the view which is going to be added as header view
let requiredView = UIView()
//Add your required view as subview of uicollectionview backgroundView view like as
collectionView.backgroundView = UIView()
collectionView.backgroundView?.addSubview(requiredView)
//After that control the frame of requiredHeaderView in scrollViewDidScroll delegate method like
func scrollViewDidScroll(scrollView: UIScrollView) {
let per:CGFloat = 60 //percentage of required view to move on while moving collection view
let deductValue = CGFloat(per / 100 * requiredView.frame.size.height)
let offset = (-(per/100)) * (scrollView.contentOffset.y)
let value = offset - deductValue
let rect = requiredView.frame
self.requiredView.frame = CGRectMake(rect.origin.x, value, rect.size.width, rect.size.height)
}
It sounds like what you want is a header.
you can specify a class or nib for the header with either of these:
self.collectionView.registerClass(_:, forSupplementaryViewOfKind: UICollectionElementKindSectionHeader, withReuseIdentifier:)
registerNib(_:, forSupplementaryViewOfKind: UICollectionElementKindSectionHeader, withReuseIdentifier: )
you should also specify a reference height if you are using a flow layout: self.flowLayout.headerReferenceHeight = ...
then you can provide the header via your UICollectionViewController in: collectionView(_:, viewForSupplementaryElementOfKind:, at:) by checking for the section header kind.
Here is a decent tutorial on this for reference: https://www.raywenderlich.com/78551/beginning-ios-collection-views-swift-part-2
You have a library CSStickyHeaderFlowLayout
From the ReadMe:
UICollectionView replacement of UITableView. Do even more like Parallax Header, Sticky Section Header. Made for iOS 7.
Try this.
headerViewYConstraint is header view's top y constraint.
Stores the last contact offset.
var lastContentOffset: CGFloat = 0
override func scrollViewDidScroll(scrollView: UIScrollView) {
if scrollView != contentScrollView {
if scrollView.dragging || scrollView.decelerating {
let newOffset = scrollView.contentOffset.y
let headerViewHeight = headerView.frame.width
if headerViewHeight > 0 && scrollView.contentSize.height > view.frame.height + headerViewHeight{
var topOffset = newOffset == 0 ? 0.0 : (headerViewYConstraint.constant + lastContentOffset - newOffset)
topOffset = min(0, max(topOffset, -headerViewHeight))
if headerViewYConstraint.constant > topOffset || newOffset < headerViewHeight || lastDirectionalContentOffset - newOffset > cellHeight(){
headerViewYConstraint.constant = topOffset
}
} else {
headerViewYConstraint.constant = 0
}
lastContentOffset = newOffset
}
}
}

Synchronize scrolling with two collection views in Swift?

Is there a practice to scroll a collection view with cells, and, depending on its content offset, scroll the other collection view?
I have a main collection view that has two cells and scrolls horizontally. Then I have a second collection view with cells that scrolls vertically. What happens is when I scroll up, then scroll horizontally to the second vertical cv, it's not synced with relation to the first.
I overrode the scrollViewDidScroll inside the vertically scrolling collection view (not the main one, because it scrolls horizontally), to no avail (** means the parts that don't work to achieve my effect).
var didScrollPastProfileView: Bool = false
func scrollViewDidScroll(scrollView: UIScrollView) {
let collectionViewDictionary = ["scrollView": self.collectionView]
NSNotificationCenter.defaultCenter().postNotificationName("cvDidScroll", object: nil, userInfo: collectionViewDictionary)
if scrollView.contentOffset.y > 250 {
**self.didScrollPastProfileView = true
if self.didScrollPastProfileView {
let previousContentOffset = scrollView.contentOffset.y
scrollView.contentOffset.y = previousContentOffset
}**
} else if scrollView.contentOffset.y < 250 {
**let previousContentOffset = scrollView.contentOffset.y
scrollView.contentOffset.y = previousContentOffset
self.didScrollPastProfileView = false**
}
}
Here's a visual:
Notice how there's space above when I scroll to the second cell? I want that vertical scroll view to follow the first. Any suggestions?
EDIT: I tried to access the second cell of the horizontal collection view while I was scrolling in the first cell, and I tried to change contentOffSet.y when I scrolled down in the first one, but I got an EXC_BAD_ACCESS error.
if scrollView.contentOffset.y <= 250.0 {
let groupsPosts = childViewControllerForPosts.collectionView(childViewControllerForPosts.collectionView!, cellForItemAtIndexPath: NSIndexPath(forItem: 1, inSection: 0)) as! FeedCell
groupsPosts.collectionView.contentOffset.y = -scrollView.contentOffset.y
self.topVerticalConstraint?.constant = -scrollView.contentOffset.y
You can do something like this when scrolled to the collectionViewB with animated to false so it look like it is synchronised.
let indexPath = collectionViewA.indexPathsForVisibleItems().first
self.collectionViewB.scrollToItemAtIndexPath(indexPath, atScrollPosition: UICollectionViewScrollPosition.Top, animated: false)
Not exactly sure how you set up this two collection. If you do not have access to collectionViewA then you might have to save the current indexPath to a variable and pass it over.

Resources