I have a multiple collectionView in one controller. One of them on top and the second on bottom. So my question is how to detect which collectionView view is scrolled?(In Objective-c) To detect scrolling i use the method: - (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView . But in this method i can’t detect which collectionView is scrolled. Please help with your advice.
UICollectionView is subclass of UIScrollView so you can compare it
https://developer.apple.com/documentation/uikit/uicollectionview
- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView
{
if (scrollView == collectionViewOne) {
}else if (scrollView == collectionViewTwo) {
}else{
//something else
}
}
Create outlets of your collectionView.
#IBOutlet weak var topCollectionView: UICollectionView!
#IBOutlet weak var bottomCollectionView: UICollectionView!
In scrollView Delegate method:
- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView {
if (scrollView == topCollectionView) {
}
else if (scrollView == bottomCollectionView) {
}
}
This will work as UICollectionView is subclass of UIScrollView.
In Swift You can Check Like This :
func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
if scrollView == topCollectionViewName {
print("Call Top CollectionView")
}else if scrollView == bottomCollectionViewName{
print("Call Bottom CollectionView")
}else{
print("call any other")
}
}
In Objective C You can Check Like This :
-(void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView {
if (scrollView == topCollectionViewName) {
NSLog(#"Call Top CollectionView");
}else if (scrollView == bottomCollectionViewName) {
NSLog(#"Call Bottom CollectionView");
}else{
NSLog(#"Call any other");
}
}
Happy To Help You :)
// create outlets of both collectionview
#IBOutlet weak var collectionViewA: UICollectionView!
#IBOutlet weak var collectionVieB: UICollectionView!
// add delegate method to detect scroll of collectionview
// for swift
func collectionView(_ collectionView: UICollectionView, willDisplay cell: UICollectionViewCell, forItemAt indexPath: IndexPath) {
if collectionView == collectionViewA{
// collectionviewA was scrolled
}else{
// collectionviewB was scrolled
}
}
// same deleagte for objc
- (void)collectionView:(UICollectionView *)collectionView willDisplayCell:(UICollectionViewCell *)cell forItemAtIndexPath:(NSIndexPath *)indexPath {
// compare both collectionview here like upper swift method
}
Related
I am using TableView and on swiping the cell with editing style .delete but the size of cell has been customized as per the requirements of app. the delete Button height is little bigger how can we customize that.
Check the screenshot please:
#averydev update my code in swift you can try this
class CustomTableViewCell: UITableViewCell {
override func didTransition(to state: UITableViewCellStateMask) {
super.willTransition(to: state)
if state == .showingDeleteConfirmationMask {
let deleteButton: UIView? = subviews.first(where: { (aView) -> Bool in
return String(describing: aView).contains("Delete")
})
if deleteButton != nil {
deleteButton?.frame.size.height = 50.0
}
}
}
}
Currently, in iOS 16, the way to do it is by calling the willBeginEditingRowAt table view delegate method in your view controller class and changing the height property of the UISwipeActionPullView. I'm going to refer to your view controller class as NotesViewController here:
class NotesViewController: UIViewController {
...
extension NotesViewController: UITableViewDelegate, UITableViewDataSource {
func tableView(_ tableView: UITableView, willBeginEditingRowAt indexPath: IndexPath) {
// Get the subviews of the table view
let subviews = tableView.subviews
// Iterate through subviews to find _UITableViewCellSwipeContainerView
for subview in subviews {
if String(describing: type(of: subview)) == "_UITableViewCellSwipeContainerView" {
// Get the UISwipeActionPullView
if let swipeActionPullView = subview.subviews.first {
if String(describing: type(of: swipeActionPullView)) == "UISwipeActionPullView" {
// Set the height of the swipe action pull view (set to whatever height your custom cell is)
swipeActionPullView.frame.size.height = 50
}
}
}
}
}
}
}
I have 2 ViewCollection at ViewController and function
func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
}
How can I detect which collection scrolling in current function ?
UICollectionView is just a subclass of UIScrollView. Just keep reference to your collectionViews, and you can check if the scrollview and the collectionview is equal.
So simply use the following code:
func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
if scrollView == self.collectionViewA {
// do something with collectionViewA
} else if scrollView == self.collectionViewB {
// do something with collectionViewB
} else {
// unknown collectionView
}
}
I think this is better in Swift.
func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
switch scrollView {
case collectionViewA:
// do something
case collectionViewB:
// do something
default:
break
}
}
I would like to set the UITableView to match the height for all the contents in the table view.
This is my storyboard
The problem with this is the top and bottom ImageView is always static on the screen.
The there are suppose to be 10 items on the table view but only 7 shows up due to screen size limitation. I would like to show all 10 before user is able to see the bottom ImageView. (btw, all 3 of the views ie. both the image views and tableview is in a uiscrollview)
IDEAL
Some of the other limitations that i have to work with is that the number of items in the table view is dynamic meaning it can be in any amount of usually less than 10 that i will later retrieve from an api. And the cell height is also dynamic depending on the contents.
I have only just started with some simple code
class ExampleViewController: UIViewController, UITableViewDelegate, UITableViewDataSource {
#IBOutlet weak var tableView: UITableView!
var items: [String] = [
"Item 01", "Item 02", "Item 03", "Item 04", "Item 05",
"Item 06", "Item 07", "Item 08", "Item 09", "Item 10"]
override func viewDidLoad() {
super.viewDidLoad()
self.tableView.registerClass(UITableViewCell.self, forCellReuseIdentifier: "cell")
}
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return self.items.count;
}
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell:UITableViewCell = self.tableView.dequeueReusableCellWithIdentifier("cell")! as UITableViewCell
cell.textLabel?.text = self.items[indexPath.row]
return cell
}
}
Subclass your UITableView to override the intrinsicContentSize to be its contentSize, like this:
override var intrinsicContentSize: CGSize {
return contentSize
}
Then use automatic row heights for your table, so your exampleViewController's viewDidLoad would have:
tableView.estimatedRowHeight = 44
And the UITableViewDelegate function:
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
return UITableViewAutomaticDimension
}
When you receive data from your API and reload your table, just call:
tableView.invalidateIntrinsicContentSize()
This will tell your table to resize itself to the same size as its contents (because of the override), and move your bottom image as needed.
If your storyboard throws an error saying that your UIScrollView has an ambiguous height because there's no height constraint on the UITableView, select your UITableView and give it a placeholder intrinsic size in the Size Inspector.
The answers using the subclassing technique are incomplete. You should also override layoutSubviews() like this.
public class DynamicSizeTableView: UITableView
{
override public func layoutSubviews() {
super.layoutSubviews()
if bounds.size != intrinsicContentSize {
invalidateIntrinsicContentSize()
}
}
override public var intrinsicContentSize: CGSize {
return contentSize
}
}
This is what I utilize in production apps:
Swift 5, 2021
import UIKit
class DynamicTableView: UITableView {
/// Will assign automatic dimension to the rowHeight variable
/// Will asign the value of this variable to estimated row height.
var dynamicRowHeight: CGFloat = UITableView.automaticDimension {
didSet {
rowHeight = UITableView.automaticDimension
estimatedRowHeight = dynamicRowHeight
}
}
public override var intrinsicContentSize: CGSize { contentSize }
public override func layoutSubviews() {
super.layoutSubviews()
if !bounds.size.equalTo(intrinsicContentSize) {
invalidateIntrinsicContentSize()
}
}
}
You need to set an IBOutlet to the NSLayoutConstraint that sets the tableView height (first you need create the height constraint with any value, doesn't matter) and then ctrl drag it to your class file
Then in your viewWillAppear you have to calculate the tableView height and set it. Like this:
var tableViewHeight:CGFloat = 0;
for (var i = tableView(self.tableView , numberOfRowsInSection: 0) - 1; i>0; i-=1 ){
tableViewHeight = height + tableView(self.tableView, heightForRowAtIndexPath: NSIndexPath(forRow: i, inSection: 0) )
}
tableViewHeightLayout.constant = tableViewHeight
And that's pretty much it. That will give your scrollView content size and shouldn't raise any warnings.
Update Swift 4
this code working be good
self.scrollView.layoutIfNeeded()
self.view.layoutIfNeeded()
self.tableViewHeightConstraint.constant = CGFloat(self.tableView.contentSize.height)
You probably have to implement the table view intrinsic content size. Please check this answer to see if it helps.
I remember having this problem and even created a custom UITableView subclass.
#import "IntrinsicTableView.h"
#implementation IntrinsicTableView
#pragma mark - UIView
- (CGSize)intrinsicContentSize
{
return CGSizeMake(UIViewNoIntrinsicMetric, self.contentSize.height);
}
#pragma mark - UITableView
- (void)endUpdates
{
[super endUpdates];
[self invalidateIntrinsicContentSize];
}
- (void)reloadData
{
[super reloadData];
[self invalidateIntrinsicContentSize];
}
- (void)reloadRowsAtIndexPaths:(NSArray *)indexPaths withRowAnimation:(UITableViewRowAnimation)animation
{
[super reloadRowsAtIndexPaths:indexPaths withRowAnimation:animation];
[self invalidateIntrinsicContentSize];
}
- (void)reloadSections:(NSIndexSet *)sections withRowAnimation:(UITableViewRowAnimation)animation
{
[super reloadSections:sections withRowAnimation:animation];
[self invalidateIntrinsicContentSize];
}
- (void)insertRowsAtIndexPaths:(NSArray *)indexPaths withRowAnimation:(UITableViewRowAnimation)animation
{
[super insertRowsAtIndexPaths:indexPaths withRowAnimation:animation];
[self invalidateIntrinsicContentSize];
}
- (void)insertSections:(NSIndexSet *)sections withRowAnimation:(UITableViewRowAnimation)animation
{
[super insertSections:sections withRowAnimation:animation];
[self invalidateIntrinsicContentSize];
}
- (void)deleteRowsAtIndexPaths:(NSArray *)indexPaths withRowAnimation:(UITableViewRowAnimation)animation
{
[super deleteRowsAtIndexPaths:indexPaths withRowAnimation:animation];
[self invalidateIntrinsicContentSize];
}
- (void)deleteSections:(NSIndexSet *)sections withRowAnimation:(UITableViewRowAnimation)animation
{
[super deleteSections:sections withRowAnimation:animation];
[self invalidateIntrinsicContentSize];
}
#end
Update for Swift 5. Adding maxHeight so that you can specify how tall you want your tableView to be
class SelfSizingTableView: UITableView {
var maxHeight = CGFloat.infinity
override var contentSize: CGSize {
didSet {
invalidateIntrinsicContentSize()
setNeedsLayout()
}
}
override var intrinsicContentSize: CGSize {
let height = min(maxHeight, contentSize.height)
return CGSize(width: contentSize.width, height: height)
}
}
In that case, don't make your bottom cell static, make it a part of table view and insert this bottom image in last row using table view delegate method - insertRowAtIndexPath
In this type of case add your bottom imageView(red) in a table footer view.
To add footer view in UITableView you can use:
tableViewObj.tableFooterView = footerViewObj;
Try this also
in ViewDidLoad
self.table.estimatedRowHeight = 44.0 ;
self.table.rowHeight = UITableViewAutomaticDimension;
Height for row at index path
-(float)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath{
return UITableViewAutomaticDimension;}
Easy way here.
Step 1: Set a height constraint for the table view
Step 2: Control drag the constraint
Step 3: Before you return the count of the rows. In numberOfRowsInSection method, do
tableViewHeightConstraint.constant = tableView.rowHeight * CGFloat(someArray.count)
Of course you can edit the height anchor programmatically, the logic here is to adjust the table view height according to the cell height and cell number.
Based on solution #nikans, written in Xamarin
[Register(nameof(DynamicSizeTableView)), DesignTimeVisible(true)]
public class DynamicSizeTableView : UITableView
{
public override void LayoutSubviews()
{
base.LayoutSubviews();
if (Bounds.Size != IntrinsicContentSize)
InvalidateIntrinsicContentSize();
}
public override CGSize IntrinsicContentSize => ContentSize;
public DynamicSizeTableView(CGRect frame) : base(frame) { }
public DynamicSizeTableView(IntPtr handle) : base(handle) { }
}
Here is the simplest Solution
First Give a height to the tableView.
Create outlet of that height in view Controller. let's say tableViewHeight
Then do this in viewDidLoad or where you populate the data after calling tableView.reloadData()
var height = 0.0
for i in 0..<items.count {
let frame = tableView.rectForRow(at: IndexPath(row: i, section: 0))
height += frame.size.height
}
tableViewHeight.constant = height
This Also Works with tableViews that have dynamic cell heights
Based on solution of #rr1g0
Updated for Swift 5 in 2020, and works with TableViews with sections too.
Create height constraint for tableView and create an outlet to it. And in viewDidLayoutSubviews() use the code below:
var tableViewHeight: CGFloat = 0
for section in 0..<tableView.numberOfSections {
for row in 0..<tableView.numberOfRows(inSection: section) {
tableViewHeight += tableView(tableView, heightForRowAt: IndexPath(row: row, section: section))
}
}
tableViewHeightConstraint.constant = tableViewHeight
Can you please share code snippets or direct me to the xamarin docs, how to implement UITableView, scroll to load more functionality.
How to detect if the tableview has scrolled to the bottom?
Thanks in advance!
inside the TableViewSource class override this method
public override void WillDisplay (UITableView tableView, UITableViewCell cell, NSIndexPath indexPath)
{
if (indexPath.Row == tableItems.Count - 1) {
//Reload your data here
}
}
constructor TableViewSource class
isScrolling = false;
inside the TableViewSource class override this method
public override void Scrolled (UIScrollView scrollView)
{
// load more bottom
float height = scrollView.Frame.Size.Height;
float distanceFromBottom = scrollView.ContentSize.Height - scrollView.ContentOffset.Y;
if(distanceFromBottom < height && isScrolling == false)
{
// reload data here
}
}
I'm using a UICollectionView with a flow layout to show a list of cells, I also have a page control to indicate current page, but there seems to be no way to get current index path, I know I can get visible cells:
UICollectionView current visible cell index
however there can be more than one visible cells, even if each of my cells occupies full width of the screen, if I scroll it to have two halves of two cells, then they are both visible, so is there a way to get only one current visible cell's index?
Thanks
You can get the current index by monitoring contentOffset in scrollViewDidScroll delegate
it will be something like this
-(void)scrollViewDidScroll:(UIScrollView *)scrollView
{
NSInteger currentIndex = self.collectionView.contentOffset.x / self.collectionView.frame.size.width;
}
Get page via NSIndexPath from center of view.
Works even your page not equal to width of UICollectionView.
func scrollViewDidScroll(scrollView: UIScrollView) {
let xPoint = scrollView.contentOffset.x + scrollView.frame.width / 2
let yPoint = scrollView.frame.height / 2
let center = CGPoint(x: xPoint, y: yPoint)
if let ip = collectionView.indexPathForItemAtPoint(center) {
self.pageControl.currentPage = ip.row
}
}
Definitely you need catch the visible item when the scroll movement is stopped. Use next code to do it.
func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
if let indexPath = myCollectionView.indexPathsForVisibleItems.first {
myPageControl.currentPage = indexPath.row
}
}
Swift 5.1
The easy way and more safety from nil crash
func collectionView(_ collectionView: UICollectionView, didEndDisplaying cell: UICollectionViewCell, forItemAt indexPath: IndexPath) {
if collectionView == newsCollectionView {
if newsPager.currentPage == indexPath.row {
guard let visible = newsCollectionView.visibleCells.first else { return }
guard let index = newsCollectionView.indexPath(for: visible)?.row else { return }
newsPager.currentPage = index
}
}
}
Place PageControl in your view or set by Code.
Set UIScrollViewDelegate
In Collectionview-> cellForItemAtIndexPath (Method) add the below
code for calculate the Number of pages,
int pages = floor(ImageCollectionView.contentSize.width/ImageCollectionView.frame.size.width);
[pageControl setNumberOfPages:pages];
Add the ScrollView Delegate method,
#pragma mark - UIScrollViewDelegate for UIPageControl
- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView
{
CGFloat pageWidth = ImageCollectionView.frame.size.width;
float currentPage = ImageCollectionView.contentOffset.x / pageWidth;
if (0.0f != fmodf(currentPage, 1.0f))
{
pageControl.currentPage = currentPage + 1;
}
else
{
pageControl.currentPage = currentPage;
}
NSLog(#"finishPage: %ld", (long)pageControl.currentPage);
}
I had similar situation where my flow layout was set for UICollectionViewScrollDirectionHorizontal and I was using page control to show the current page.
I achieved it using custom flow layout.
/------------------------
Header file (.h) for custom header
------------------------/
/**
* The customViewFlowLayoutDelegate protocol defines methods that let you coordinate with
*location of cell which is centered.
*/
#protocol CustomViewFlowLayoutDelegate <UICollectionViewDelegateFlowLayout>
/** Informs delegate about location of centered cell in grid.
* Delegate should use this location 'indexPath' information to
* adjust it's conten associated with this cell.
* #param indexpath of cell in collection view which is centered.
*/
- (void)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout *)collectionViewLayout cellCenteredAtIndexPath:(NSIndexPath *)indexPath;
#end
#interface customViewFlowLayout : UICollectionViewFlowLayout
#property (nonatomic, weak) id<CustomViewFlowLayoutDelegate> delegate;
#end
/------------------- Implementation file (.m) for custom header -------------------/
#implementation customViewFlowLayout
- (void)prepareLayout {
[super prepareLayout];
}
static const CGFloat ACTIVE_DISTANCE = 10.0f; //Distance of given cell from center of visible rect
static const CGFloat ITEM_SIZE = 40.0f; // Width/Height of cell.
- (id)init {
if (self = [super init]) {
self.scrollDirection = UICollectionViewScrollDirectionHorizontal;
self.minimumInteritemSpacing = 60.0f;
self.sectionInset = UIEdgeInsetsZero;
self.itemSize = CGSizeMake(ITEM_SIZE, ITEM_SIZE);
self.minimumLineSpacing = 0;
}
return self;
}
- (BOOL)shouldInvalidateLayoutForBoundsChange:(CGRect)newBounds {
return YES;
}
- (NSArray *)layoutAttributesForElementsInRect:(CGRect)rect {
NSArray *attributes = [super layoutAttributesForElementsInRect:rect];
CGRect visibleRect;
visibleRect.origin = self.collectionView.contentOffset;
visibleRect.size = self.collectionView.bounds.size;
for (UICollectionViewLayoutAttributes *attribute in attributes) {
if (CGRectIntersectsRect(attribute.frame, rect)) {
CGFloat distance = CGRectGetMidX(visibleRect) - attribute.center.x;
// Make sure given cell is center
if (ABS(distance) < ACTIVE_DISTANCE) {
[self.delegate collectionView:self.collectionView layout:self cellCenteredAtIndexPath:attribute.indexPath];
}
}
}
return attributes;
}
Your class containing collection view must conform to protocol 'CustomViewFlowLayoutDelegate' I described earlier in custom layout header file. Like:
#interface MyCollectionViewController () <UICollectionViewDataSource, UICollectionViewDelegate, CustomViewFlowLayoutDelegate>
#property (strong, nonatomic) IBOutlet UICollectionView *collectionView;
#property (strong, nonatomic) IBOutlet UIPageControl *pageControl;
....
....
#end
There are two ways to hook your custom layout to collection view, either in xib OR in code like say in viewDidLoad:
customViewFlowLayout *flowLayout = [[customViewFlowLayout alloc]init];
flowLayout.delegate = self;
self.collectionView.collectionViewLayout = flowLayout;
self.collectionView.pagingEnabled = YES; //Matching your situation probably?
Last thing, in MyCollectionViewController implementation file, implement delegate method of 'CustomViewFlowLayoutDelegate'.
- (void)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout *)collectionViewLayout cellCenteredAtIndexPath:(NSIndexPath *)indexPath {
self.pageControl.currentPage = indexPath.row;
}
I hope this would be helpful. :)
Note - I have found andykkt's answer useful but since it is in obj-c converted it to swift and also implemented logic in another UIScrollView delegate for a smoother effect.
func updatePageNumber() {
// If not case to `Int` will give an error.
let currentPage = Int(ceil(scrollView.contentOffset.x / scrollView.frame.size.width))
pageControl.currentPage = currentPage
}
func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
// This will be call when you scrolls it manually.
updatePageNumber()
}
func scrollViewDidEndScrollingAnimation(_ scrollView: UIScrollView) {
// This will be call when you scrolls it programmatically.
updatePageNumber()
}
for swift 4.2
#IBOutlet weak var mPageControl: UIPageControl!
#IBOutlet weak var mCollectionSlider: UICollectionView!
private var _currentIndex = 0
private var T1:Timer!
private var _indexPath:IndexPath = [0,0]
private func _GenerateNextPage(){
self._currentIndex = mCollectionSlider.indexPathForItem(at: CGPoint.init(x: CGRect.init(origin: mCollectionSlider.contentOffset, size: mCollectionSlider.bounds.size).midX, y: CGRect.init(origin: mCollectionSlider.contentOffset, size: mCollectionSlider.bounds.size).midY))?.item ?? 0
self.mPageControl.currentPage = self._currentIndex
}
func scrollViewDidScroll(_ scrollView: UIScrollView) {
_SetTimer(AutoScrollInterval)
_GenerateNextPage()
}
#objc private func _AutoScroll(){
self._indexPath = IndexPath.init(item: self._currentIndex+1, section: 0)
if !(self._indexPath.item < self.numberOfItems){
_indexPath = [0,0]
}
self.mCollectionSlider.scrollToItem(at: self._indexPath, at: .centeredHorizontally, animated: true)
}
private func _SetTimer(_ interval:TimeInterval){
if T1 == nil{
T1 = Timer.scheduledTimer(timeInterval: interval , target:self , selector: #selector(_AutoScroll), userInfo: nil, repeats: true)
}
}
you can skip the function _SetTimer() , thats for auto scroll
With UICollectionViewDelegate methods
func collectionView(_ collectionView: UICollectionView, willDisplay cell: UICollectionViewCell, forItemAt indexPath: IndexPath) {
pageControl.currentPage = indexPath.row
}
func collectionView(_ collectionView: UICollectionView, didEndDisplaying cell: UICollectionViewCell, forItemAt indexPath: IndexPath) {
if pageControl.currentPage == indexPath.row {
pageControl.currentPage = collectionView.indexPath(for: collectionView.visibleCells.first!)!.row
}
}
Swift 5.0
extension youriewControllerName:UIScrollViewDelegate{
func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
let pageWidth = self.collectionView.frame.size.width
pageControl.currentPage = Int(self.collectionView.contentOffset.x / pageWidth)
}
}
(void)scrollViewDidScroll:(UIScrollView *)scrollView
{
CGFloat pageWidth = _cvImagesList.frame.size.width;
float currentPage = _cvImagesList.contentOffset.x / pageWidth;
_pageControl.currentPage = currentPage + 1;
NSLog(#"finishPage: %ld", (long)_pageControl.currentPage);
}