Issue with pagination in UICollectionView when orientation changes - iphone

I'm preparing a GridView with all orientation support (height & width resizing) and paging enabled.
In my sample in have taken a 3*3 Grid and to show it like that I have added some dummy transparent cell at the last page.
The issue is when I'm at last page and there I'm changing my orientation from landscape to portrait the pagination doesn't work.
I have used this code in willRotateToInterfaceOrientation:
CGPoint scrollTo = CGPointMake(self.frame.size.width * mPageControl.currentPage, 0);
[self setContentOffset:scrollTo animated:YES];
But still page moves to a page before the last page when going from landscape to portrait and while changing portrait to landscape it works fine.

Had the same problem, my UICollectionView was paginated, when I scrolled to page 2 in portrait mode and then switched to landscape, the content offset would be set to (72, 0), and this would persist.
The controller was registered for orientation changes where it invalidated the layout, but setting the offset in there didn't help.
What did help was setting the offset inside the layout class' prepareForLayout method. I used
self.collectionView.contentOffset =
CGPointMake(ceil(self.collectionView.contentOffset.x/self.collectionView.frame.size.width)*self.collectionView.frame.size.width,
self.collectionView.contentOffset.y);
Ceiling is used because floor would always go back to the previous page (and page 0 always paginates fine).
If you're not using a custom layout, you can still subclass flow layout and override that one function.

I manage to solve this by setting notification when screen orientation changes and reloading cell which set itemsize according to screen orientation and setting indexpath to previous cell. This does work with flowlayout too. Here is the code i wrote:
var cellWidthInLandscape: CGFloat = 0 {
didSet {
self.collectionView.reloadData()
}
}
var lastIndex: Int = 0
override func viewDidLoad() {
super.viewDidLoad()
collectionView.dataSource = self
collectionView.delegate = self
NotificationCenter.default.addObserver(self, selector: #selector(rotated), name: NSNotification.Name.UIDeviceOrientationDidChange, object: nil)
cellWidthInLandscape = UIScreen.main.bounds.size.width
}
deinit {
NotificationCenter.default.removeObserver(self)
}
#objc func rotated() {
// Setting new width on screen orientation change
cellWidthInLandscape = UIScreen.main.bounds.size.width
// Setting collectionView to previous indexpath
collectionView.scrollToItem(at: IndexPath(item: lastIndex, section: 0), at: .right, animated: false)
}
override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
NotificationCenter.default.addObserver(self, selector: #selector(rotated), name: NSNotification.Name.UIDeviceOrientationDidChange, object: nil)
}
func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
// Getting last contentOffset to calculate last index of collectionViewCell
lastIndex = Int(scrollView.contentOffset.x / collectionView.bounds.width)
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
// Setting new width of collectionView Cell
return CGSize(width: cellWidthInLandscape, height: collectionView.bounds.size.height)
}

UIDeviceOrientation orientation = [[UIDevice currentDevice] orientation];
//Ignoring specific orientations
if (orientation == UIDeviceOrientationFaceUp || orientation == UIDeviceOrientationFaceDown || orientation == UIDeviceOrientationUnknown || currentOrientation == orientation)
{
return;
}
[NSObject cancelPreviousPerformRequestsWithTarget:self selector:#selector(relayoutLayers) object:nil];
//Responding only to changes in landscape or portrait
currentOrientation = orientation;
[self performSelector:#selector(handleOrientationChange) withObject:nil afterDelay:0];
Then i'm calling a method whenever orientation is changing
-(void)handleOrientationChange
{
CGRect frame = self.frame;
frame.origin.x = self.frame.size.width * mPageControl.currentPage;
frame.origin.y = 0;
[self scrollRectToVisible:frame animated:YES];
}

Related

Navigation and View Controller help in iOS 15 and Xcode 13

HI i have searched everywhere for a solution to my problem and cannot figure it out or found any other posts with the same problem.
I recently updated my Xcode project to Xcode 13 and my iPhone to iOS 15 and Apple made some significant changes to the navigation and tab bar structure which completely destroyed my app.
Need assistance on the correct structure on how to resolve this problem and am sure this will help lots of people who come across this same issue.
I have a ViewControllerA inside of a navigation controller which is nested inside of tab bar controller. User taps a button and segues to another ViewControllerB which is a modal VC that goes over full screen. User can then swipe down with a panGestureRecognizer.
As user swipes down there is a strange flicker effect which looks awful. The only solution I found is in storyboards to completely remove navigation bar and apply constraints to top of the view(I know this is possible but not scalable as I do not want to determine device size). But then view is covered by top bar.
I have attached video showing problem.
Please help as I do not know where to go from here.
THANK YOU
var initialTouchPoint: CGPoint = CGPoint(x: 0,y: 0)
#IBOutlet weak var tableView: UITableView!
override func viewDidLoad() {
super.viewDidLoad()
tableView.dataSource = self
tableView.delegate = self
tableView.estimatedRowHeight = 636
tableView.rowHeight = UITableView.automaticDimension
}
#IBAction func panGestureRecognizerHandler(_ sender: UIPanGestureRecognizer) {
let touchPoint = sender.location(in: self.view?.window)
if sender.state == UIGestureRecognizer.State.began {
initialTouchPoint = touchPoint
} else if sender.state == UIGestureRecognizer.State.changed {
if touchPoint.y - initialTouchPoint.y > 0 {
self.view.frame = CGRect(x: 0, y: touchPoint.y - initialTouchPoint.y, width: self.view.frame.size.width, height: self.view.frame.size.height)
}
} else if sender.state == UIGestureRecognizer.State.ended || sender.state == UIGestureRecognizer.State.cancelled {
if touchPoint.y - initialTouchPoint.y > 100 {
self.dismiss(animated: true)
} else {
UIView.animate(withDuration: 0.3, animations: {
self.view.frame = CGRect(x: 0, y: 0, width: self.view.frame.size.width, height: self.view.frame.size.height)
})
}
}
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "SwipeDownCell", for: indexPath) as! SwipeDownTableViewCell
cell.postImageView.image = UIImage(named: "food")
cell.postImageView.contentMode = .scaleAspectFill
return cell
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 1
}
https://streamable.com/924xoo
I have the same issue. It is caused by a combination of safeArea and the modalPresentationStyle of the parent view controller. In my case it is UIModalPresentationOverCurrentContext.
If you use other presentation styles, e.g. UIModalPresentationPopover, the flickering disappear.
As this is not appropriate in my case, I found an easy workaround.
You need to set the top constraint of your first item in the child view to refer the superview, not the safe area!
To better support devices without the notch, you can use following helper method implemented in AppDelegate.
- (UIEdgeInsets)getSafeAreaInsets {
if (#available(iOS 11.0, *)) {
UIEdgeInsets insets = self.window.safeAreaInsets;
return insets;
} else {
return UIEdgeInsetsZero;
}
Call this in the viewDidLoad of the child view and check if the bottom edge is 0.
//classic devices without notch
if ([(AppDelegate*)[[UIApplication sharedApplication] delegate] getSafeAreaInsets].bottom == 0) {
self.labelHeaderContraint.constant = 32;
}

How can I hide a tableView when I drag it down to a certain point, Swift

I have a textField and when I tap it a tableView appear below. When I scroll the tableView down, say 25% of the height of the tableView I want to hide it. Is it possible ? I am using the scrollViewWillBeginDragging function but its not what I want.
func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
let translation = scrollView.panGestureRecognizer.translation(in: scrollView.superview!)
if translation.y > 550 {
self.animateTableView(shouldShow: false)
}
}
Use this UIScrollViewDelegate method:-
func scrollViewDidScroll(_ scrollView: UIScrollView) {
let bottomEdge: CGFloat = scrollView.contentOffset.y + scrollView.frame.size.height
let contentSize = scrollView.contentSize.height * 0.25
if bottomEdge >= contentSize {
/* Code to hide tableView */
}
}

Remove Collection view bounce issue when using pull to refresh

I have one collection view configured as follow
superCollectionView!.alwaysBounceHorizontal = false
superCollectionView!.alwaysBounceVertical = false
if #available(iOS 10.0, *) {
superCollectionView!.refreshControl = refreshControl
} else {
superCollectionView!.backgroundView = refreshControl
}
but bounce effect is still there.
I want to remove bounce from bottom...
If you want to remove bouncing only from the bottom (For letting the refreshControl to be available), I'd suggest to handle it in scroll​View​Did​Scroll:​ method to check if the scroll view contentOffset.y has been reached to bottom of the scroll view (logically, it is the content size of the scroll view minus the height of the visible frame of the scroll view), as follows:
Solution:
func scrollViewDidScroll(_ scrollView: UIScrollView) {
if scrollView.contentOffset.y >= scrollView.contentSize.height - scrollView.frame.size.height {
scrollView.setContentOffset(CGPoint(x: scrollView.contentOffset.x, y: scrollView.contentSize.height - scrollView.frame.size.height), animated: false)
}
}
Output:
After implementing scroll​View​Did​Scroll as mentioned above, it should be behaves like:
Also:
What about achieving the opposite?
Referring to the above description, preventing the top bouncing would be:
func scrollViewDidScroll(_ scrollView: UIScrollView) {
if scrollView.contentOffset.y < 0 {
scrollView.setContentOffset(CGPoint(x: scrollView.contentOffset.x, y: 0), animated: false)
}
}

Setting scrollView.delegate to scrollView and not UITableView

I've set up a scrollView inside my custom cell. Inside my tableViewController I've used...
UIScrollViewDelegate
...Which fill delegate when the "scrollView" has moved. Though, I only want it to react when the image scroll view is moved.
It currently reacts to both, when the tablView scrolls and image scrollview scrolls.
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
//other irrelevant cell setup....
scrollView = cell.imageScrollView
scrollView.pagingEnabled = true
scrollView.scrollEnabled = true
scrollView.delegate = self
scrollView.tag = indexPath.row
}
func scrollViewDidScroll(scrollView: UIScrollView) {
var pageWidth = scrollView.frame.size.width
var newPage = floor((scrollView.contentOffset.x - pageWidth / 2) / pageWidth) + 1
//prints: 0.0 , while tableView scrolls
//also, prints newPage when scrollView is moved
print(newPage)
}
I've tried to use sender but didn't know who to properly incorporate it. Any ideas? It seems that....
- (void)scrollViewDidScroll:(UIScrollView *)sender{}
... no longer exists.
You can try use isMemberOfClass(),like this:
if sender.isMemberOfClass(UITableView) {
// your logic
}
else sender.isMemberOfClass(UIScrollView)
{
// your logic
}

check if UIView is in UIScrollView visible state

What is the easiest and most elegant way to check if a UIView is visible on the current UIScrollView's contentView? There are two ways to do this, one is involving the contentOffset.y position of the UIScrollView and the other way is to convert the rect area?
If you're trying to work out if a view has been scrolled on screen, try this:
CGRect thePosition = myView.frame;
CGRect container = CGRectMake(scrollView.contentOffset.x, scrollView.contentOffset.y, scrollView.frame.size.width, scrollView.frame.size.height);
if(CGRectIntersectsRect(thePosition, container))
{
// This view has been scrolled on screen
}
Swift 5: in case that you want to trigger an event that checks that the entire UIView is visible in the scroll view:
extension ViewController: UIScrollViewDelegate {
func scrollViewDidScroll(_ scrollView: UIScrollView) {
if scrollView.bounds.contains(targetView.frame) {
// entire UIView is visible in scroll view
}
}
}
Implement scrollViewDidScroll: in your scroll view delegate and calculate manually which views are visible (e.g. by checking if CGRectIntersectsRect(scrollView.bounds, subview.frame) returns true.
updated for swift 3
var rect1: CGRect!
// initialize rect1 to the relevant subview
if rect1.frame.intersects(CGRect(origin: scrollView.contentOffset, size: scrollView.frame.size)) {
// the view is visible
}
I think your ideas are correct. if it was me i would do it as following:
//scrollView is the main scroll view
//mainview is scrollview.superview
//view is the view inside the scroll view
CGRect viewRect = view.frame;
CGRect mainRect = mainView.frame;
if(CGRectIntersectsRect(mainRect, viewRect))
{
//view is visible
}
José's solution didn't quite work for me, it was detecting my view before it came on screen. The following intersects code works perfect in my tableview if José's simpler solution doesn't work for you.
func scrollViewDidScroll(_ scrollView: UIScrollView) {
let viewFrame = scrollView.convert(targetView.bounds, from: targetView)
if viewFrame.intersects(scrollView.bounds) {
// targetView is visible
}
else {
// targetView is not visible
}
}
Solution that takes into account insets
public extension UIScrollView {
/// Returns `adjustedContentInset` on iOS >= 11 and `contentInset` on iOS < 11.
var fullContentInsets: UIEdgeInsets {
if #available(iOS 11.0, *) {
return adjustedContentInset
} else {
return contentInset
}
}
/// Visible content frame. Equal to bounds without insets.
var visibleContentFrame: CGRect {
bounds.inset(by: fullContentInsets)
}
}
if scrollView.visibleContentFrame.contains(view) {
// View is fully visible even if there are overlaying views
}