How to hide NSCollectionView Scroll indicator - swift

I have an NSCollectionView and I would like to hide the horizontal scroll indicators.
I've tried
collectionView.enclosingScrollView?.verticalScroller?.isHidden = true
But it is not working.
Thank you in advance.

hidden didn't work for me too.
The only way I found to hack this, is by changing inset:
(scrollViewCollectionView is of type NSScrollView, this example is while creating NSCollectionView programmatically)
scrollViewCollectionView.documentView?.enclosingScrollView?.scrollerInsets = NSEdgeInsets.init(top: 0, left: 0, bottom: 100, right: 0)
Please note: My NSCollectionView is horizontal, and less then 100 height, this is why this hack resolved in a hidden indicator.

Override hasHorizontalScroller or horizontalScroller to false and nil will cause NSScroller not to be displayed, and NSScrollView will not respond to scroll events.
This means that you can't scroll through NSScrollView's many scrolling methods.
When hasHorizontalScroller = true, horizontalScroller != nil will cause a drawing error.
class MyScrollView : NSScrollView {
// !!! Don't use this !!!
override var hasHorizontalScroller: Bool {
get {
// return false will cause NSScroller not to be displayed
// and NSScrollView will not respond to scroll events,
// this means that you can't scroll through NSScrollView's many scrolling methods.
false
}
set {
super.hasHorizontalScroller = newValue
}
}
// !!! Don't use this !!!
override var horizontalScroller: NSScroller? {
get {
// return nil will cause NSScroller not to be displayed,
// but it still occupies the drawing area of the parent view.
nil
}
set {
super.horizontalScroller = newValue
}
}
}
This is the way to hide NSScroller and respond to scroll events correctly. Only useful in versions above 10.7:
class HiddenScroller: NSScroller {
// #available(macOS 10.7, *)
// let NSScroller tell NSScrollView that its own width is 0, so that it will not really occupy the drawing area.
override class func scrollerWidth(for controlSize: ControlSize, scrollerStyle: Style) -> CGFloat {
0
}
}

Create an outlet for the ScrollView which contains the CollectionView as seen here. I've named mine #IBOutlet weak var collectionViewScrollView: NSScrollView!
in viewDidAppear() function add:
collectionViewScrollView.scrollerStyle = .legacy
collectionViewScrollView.verticalScroller?.isHidden = true - for vertical scroll
collectionViewScrollView.horizontalScroller?.isHidden = true - for horizontal scroll
For some reason, in my case it only works if I set the collectionViewScrollView.scrollerStyle to .legacy. Not sure why, but it works.

Setting "Show Vertical Scroller" or "Show Horizontal Scroller" in storyboard doesn't remove the scrollers without setting constrains (height and width) of Bordered Scroll View of Collection View. After I did that and unchecked "Show Vertical Scroller" and "Show Horizontal Scroller" in Attributes Panel in storyboard they disappeared.

I got same problem and just solve it. You can write your own custom NSScrollView and override 2 stored property: hasHorizontalScroller, horizontalScroller, and 1 function scrollWheel(with:). Here's my code:
class MyScrollView: NSScrollView {
override var hasHorizontalScroller: Bool {
get {
return false
}
set {
super.hasHorizontalScroller = newValue
}
}
override var horizontalScroller: NSScroller? {
get {
return nil
}
set {
super.horizontalScroller = newValue
}
}
//comment it or use super for scrroling
override func scrollWheel(with event: NSEvent) {}
}
And don't forget to set Border Scroll View class to MyScrollView in .xib or storyboard.
Enjoy it!

You can also achieve that via storyboard

I also meet the same problem. MCMatan is right. Please adjust the position of scroller to some place invisible.
scrollView.scrollerInsets = NSEdgeInsets.init(top: 0, left: 0, bottom: -10, right: 0)

for Swift 4 & 5 in UIKit:
for Horizontal:
collectionView.showsHorizontalScrollIndicator = false
For Vertical:
collectionView.showsVerticalScrollIndicator = false

In my case, the horizontal and vertical scroller of the collection scroll view are only hidden if do exactly as follow:
1. In Interface Builder.
1.a. Select Scroll View —> Attributes Inspector:
+ Uncheck Show Horizontal Scroller.
+ Uncheck Show Vertical Scroller.
+ Uncheck Automactically Hide Scroller.
1.b. Select Size Inspector:
+ Uncheck Automatically Adjust.
1.c. Select Clip View —> Size Inspector:
+ Uncheck Automatically Adjust.
2. In code do exactly as follow:
[self.scrollView setBackgroundColor:[NSColor clearColor]];
[self.scrollView setBorderType:NSNoBorder];
[self.scrollView setHasVerticalScroller:NO];
[self.scrollView setHasHorizontalScroller:NO];
[self.scrollView setAutomaticallyAdjustsContentInsets:NO];
[self.scrollView setContentInsets:NSEdgeInsetsMake(0.0, 0.0, -NSHeight([self.scrollView horizontalScroller].frame), -NSWidth([self.scrollView verticalScroller].frame))];
[self.scrollView setScrollerInsets:NSEdgeInsetsMake(0.0, 0.0, -NSHeight([self.scrollView horizontalScroller].frame), -NSWidth([self.scrollView verticalScroller].frame))];
[self.scrollView setScrollEnable:NO];
NSClipView *clipView = (NSClipView *)[self.scrollView documentView];
if ([clipView isKindOfClass:[NSClipView class]])
{
[clipView setAutomaticallyAdjustsContentInsets:NO];
[clipView setContentInsets:NSEdgeInsetsZero];
}
Then the NSCollectionView will fit to the Clip View as same width and height without the horizontal and vertical scrollers.

If someone still needs it, this one trick should work
collectionView.enclosingScrollView?.horizontalScroller?.alphaValue = 0.0

Related

Prevent cell content from "jumping" when applying constraint

I have a subclassed UICollectionViewCell and I want it to expand when tapped.
To achieve this, I put the title into a view ("titleStack") and the body into a separate view ("bodyStack"), and then put both of them into a container UIStackView ("mainStack"). I then constrain the contentView of the cell to the leading, trailing, and top edges of mainStack.
When the cell is selected, a constraint is applied that sets the bottom of the contentView's constraint to be the bottom of bodyStack. When it's unselected, I remove that constraint and instead apply one that sets the contentView's bottom constraint equal to titleStack's bottom constraint.
For the most part this works well, but when deselecting, there's this little jump, as you can see in this video:
What I would like is for titleStack to stay pinned to the top while the cell animates the shrinking portion, but it appears to jump to the bottom, giving it a sort of glitchy look. I'm wondering how I can change this.
I've pasted the relevant code below:
private func setUp() {
backgroundColor = .systemGray6
clipsToBounds = true
layer.cornerRadius = cornerRadius
setUpMainStack()
setUpConstraints()
updateAppearance()
}
private func setUpMainStack() {
contentView.constrain(mainStack, using: .edges, padding: 5, except: [.bottom])
mainStack.add([titleStack, bodyStack])
bodyStack.add([countryLabel, foundedLabel, codeLabel, nationalLabel])
}
private func setUpConstraints() {
titleStack.topAnchor.constraint(equalTo: contentView.topAnchor).isActive = true
closedConstraint =
titleStack.bottomAnchor.constraint(equalTo: contentView.bottomAnchor)
closedConstraint?.priority = .defaultLow // use low priority so stack stays pinned to top of cell
openConstraint =
bodyStack.bottomAnchor.constraint(equalTo: contentView.bottomAnchor)
openConstraint?.priority = .defaultLow
}
/// Updates the views to reflect changes in selection
private func updateAppearance() {
UIView.animate(withDuration: 0.3) {
self.closedConstraint?.isActive = !self.isSelected
self.openConstraint?.isActive = self.isSelected
}
}
Thanks so much!
I was able to solve this by simply showing and hiding my "bodyStack" as well as using "layoutIfNeeded." I removed closedConstraint and openConstraint and just gave it a normal bottom constraint.
The relevant code:
func updateAppearance() {
UIView.animate(withDuration: 0.3) {
self.bodyStack.isHidden = !self.isSelected
self.layoutIfNeeded()
}
}

iOS 11 iPhone X simulator UITabBar icons and titles being rendered on top covering eachother

Anyone having issue with the iPhone X simulator around the UITabBar component?
Mine seem to be rendering the icons and title on top of each other, I'm not sure if I'm missing anything, I also ran it in the iPhone 8 simulator, and one actual devices where it looks fine just as shown on the story board.
iPhone X:
iPhone 8
I was able to get around the problem by simply calling invalidateIntrinsicContentSize on the UITabBar in viewDidLayoutSubviews.
-(void)viewDidLayoutSubviews
{
[super viewDidLayoutSubviews];
[self.tabBar invalidateIntrinsicContentSize];
}
Note: The bottom of the tab bar will need to be contained to the bottom of the main view, rather than the safe area, and the tab bar should have no height constraint.
Answer provided by VoidLess fixes TabBar problems only partially. It fixes layout problems within the tabbar, but if you use viewcontroller that hides the tabbar, the tabbar is rendered incorrectly during animations (to reproduce it is best 2 have 2 segues - one modal and one push. If you alternate the segues, you can see the tabbar being rendered out of place). The code bellow fixes both problems. Good job apple.
class SafeAreaFixTabBar: UITabBar {
var oldSafeAreaInsets = UIEdgeInsets.zero
#available(iOS 11.0, *)
override func safeAreaInsetsDidChange() {
super.safeAreaInsetsDidChange()
if oldSafeAreaInsets != safeAreaInsets {
oldSafeAreaInsets = safeAreaInsets
invalidateIntrinsicContentSize()
superview?.setNeedsLayout()
superview?.layoutSubviews()
}
}
override func sizeThatFits(_ size: CGSize) -> CGSize {
var size = super.sizeThatFits(size)
if #available(iOS 11.0, *) {
let bottomInset = safeAreaInsets.bottom
if bottomInset > 0 && size.height < 50 && (size.height + bottomInset < 90) {
size.height += bottomInset
}
}
return size
}
override var frame: CGRect {
get {
return super.frame
}
set {
var tmp = newValue
if let superview = superview, tmp.maxY !=
superview.frame.height {
tmp.origin.y = superview.frame.height - tmp.height
}
super.frame = tmp
}
}
}
Objective-C code:
#implementation VSTabBarFix {
UIEdgeInsets oldSafeAreaInsets;
}
- (void)awakeFromNib {
[super awakeFromNib];
oldSafeAreaInsets = UIEdgeInsetsZero;
}
- (void)safeAreaInsetsDidChange {
[super safeAreaInsetsDidChange];
if (!UIEdgeInsetsEqualToEdgeInsets(oldSafeAreaInsets, self.safeAreaInsets)) {
[self invalidateIntrinsicContentSize];
if (self.superview) {
[self.superview setNeedsLayout];
[self.superview layoutSubviews];
}
}
}
- (CGSize)sizeThatFits:(CGSize)size {
size = [super sizeThatFits:size];
if (#available(iOS 11.0, *)) {
float bottomInset = self.safeAreaInsets.bottom;
if (bottomInset > 0 && size.height < 50 && (size.height + bottomInset < 90)) {
size.height += bottomInset;
}
}
return size;
}
- (void)setFrame:(CGRect)frame {
if (self.superview) {
if (frame.origin.y + frame.size.height != self.superview.frame.size.height) {
frame.origin.y = self.superview.frame.size.height - frame.size.height;
}
}
[super setFrame:frame];
}
#end
There is a trick by which we can solve the problem.
Just put your UITabBar inside a UIView.
This is really working for me.
Or you can follow this Link for more details.
override UITabBar sizeThatFits(_) for safeArea
extension UITabBar {
static let height: CGFloat = 49.0
override open func sizeThatFits(_ size: CGSize) -> CGSize {
guard let window = UIApplication.shared.keyWindow else {
return super.sizeThatFits(size)
}
var sizeThatFits = super.sizeThatFits(size)
if #available(iOS 11.0, *) {
sizeThatFits.height = UITabBar.height + window.safeAreaInsets.bottom
} else {
sizeThatFits.height = UITabBar.height
}
return sizeThatFits
}
}
I added this to viewWillAppear of my custom UITabBarController, because none of the provided answers worked for me:
tabBar.invalidateIntrinsicContentSize()
tabBar.superview?.setNeedsLayout()
tabBar.superview?.layoutSubviews()
I had the same problem.
If I set any non-zero constant on the UITabBar's bottom constraint to the safe area:
It starts working as expected...
That is the only change I made and I have no idea why it works but if anyone does I'd love to know.
Fixed by using subclassed UITabBar to apply safeAreaInsets:
class SafeAreaFixTabBar: UITabBar {
var oldSafeAreaInsets = UIEdgeInsets.zero
#available(iOS 11.0, *)
override func safeAreaInsetsDidChange() {
super.safeAreaInsetsDidChange()
if oldSafeAreaInsets != safeAreaInsets {
oldSafeAreaInsets = safeAreaInsets
invalidateIntrinsicContentSize()
superview?.setNeedsLayout()
superview?.layoutSubviews()
}
}
override func sizeThatFits(_ size: CGSize) -> CGSize {
var size = super.sizeThatFits(size)
if #available(iOS 11.0, *) {
let bottomInset = safeAreaInsets.bottom
if bottomInset > 0 && size.height < 50 {
size.height += bottomInset
}
}
return size
}
}
Moving the tab bar 1 point away from the bottom worked for me.
Of course you'll get a gap by doing so which you'll have to fill in some way, but the text/icons are now properly positioned.
Aha! It's actually magic!
I finally figured this out after hours of cursing Apple.
UIKit actually does handle this for you, and it appears that the shifted tab bar items are due to incorrect setup (and probably an actual UIKit bug). There is no need for subclassing or a background view.
UITabBar will "just work" if it is constrained to the superview's bottom, NOT to the bottom safe area.
It even works in Interface builder.
Correct Setup
In interface builder, viewing as iPhone X, drag a UITabBar out to where it snaps to the bottom safe area inset. When you drop it, it should look correct (fill the space all the way to the bottom edge).
You can then do an "Add Missing Constraints" and IB will add the correct constraints and your tab bar will magically work on all iPhones! (Note that the bottom constraint looks like it has a constant value equal to the height of the iPhone X unsafe area, but the constant is actually 0)
Sometimes it still doesn't work
What's really dumb is that you can actaully see the bug in IB as well, even if you add the exact constraints that IB adds in the steps above!
Drag out a UITabBar and don't snap it to the bottom safe area inset
Add leading, trailing and bottom constraints all to superview (not safe area)
Weirdly, this will fix itself if you do a "Reverse First And Second Item" in the constraint inspector for the bottom constraint. ¯_(ツ)_/¯
Solved for me by calling [tabController.view setNeedsLayout]; after dismissing the modal in completion block.
[vc dismissViewControllerAnimated:YES completion:^(){
UITabBarController* tabController = [UIApplication sharedApplication].delegate.window.rootViewController;
[tabController.view setNeedsLayout];
}];
The UITabBar is increasing in height to be above the home button/line, but drawing the subview in its original location and overlaying the UITabBarItem over the subview.
As a workaround you can detect the iPhone X and then shrink the height of the view by 32px to ensure the tab bar is displayed in the safe area above the home line.
For example, if you're creating your TabBar programatically replace
self.tabBarController = [[UITabBarController alloc] init];
self.window.rootViewController = self.tabBarController;
With this:
#define IS_IPHONEX (([[UIScreen mainScreen] bounds].size.height-812)?NO:YES)
self.tabBarController = [[UITabBarController alloc] init];
self.window.rootViewController = [[UIViewController alloc] init] ;
if(IS_IPHONEX)
self.window.rootViewController.view.frame = CGRectMake(self.window.rootViewController.view.frame.origin.x, self.window.rootViewController.view.frame.origin.y, self.window.rootViewController.view.frame.size.width, self.window.rootViewController.view.frame.size.height + 32) ;
[self.window.rootViewController.view addSubview:self.tabBarController.view];
self.tabBarController.tabBar.barTintColor = [UIColor colorWithWhite:0.98 alpha:1.0] ;
self.window.rootViewController.view.backgroundColor = [UIColor colorWithWhite:0.98 alpha:1.0] ;
NOTE: This could well be a bug, as the view sizes and tab bar layout are set by the OS. It should probably display as per Apple's screenshot in the iPhone X Human Interface Guidelines here: https://developer.apple.com/ios/human-interface-guidelines/overview/iphone-x/
My case was that I had set a custom UITabBar height in my UITabBarController, like this:
override func viewWillLayoutSubviews() {
var tabFrame = tabBar.frame
tabFrame.size.height = 60
tabFrame.origin.y = self.view.frame.size.height - 60
tabBar.frame = tabFrame
}
Removing this code was the solution for the TabBar to display correctly on iPhone X.
The simplest solution I found was to simply add a 0.2 pt space between the bottom of the tab bar and the bottom of the safeAreaLayoutGuide.bottomAnchor like so.
tabBar.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor, constant: -0.2)
I was having the same issue when I was trying to set the frame of UITabBar in my custom TabBarController.
self.tabBar.frame = CGRectMake(0, self.view.frame.size.height - kTabBarHeight, self.view.frame.size.width, kTabBarHeight);
When I just adjusted it to the new size the issue went away
if(IS_IPHONE_X){
self.tabBar.frame = CGRectMake(0, self.view.frame.size.height - kPhoneXTabBarHeight, self.view.frame.size.width, kPhoneXTabBarHeight);
}
else{
self.tabBar.frame = CGRectMake(0, self.view.frame.size.height - kTabBarHeight, self.view.frame.size.width, kTabBarHeight);
}
I encountered this today with a a UITabBar manually added to the view controller in the storyboard, when aligning to the bottom of the safe area with a constant of 0 I would get the issue but changing it to 1 would fix the problem. Having the UITabBar up 1 pixel more than normal was acceptable for my application.
I have scratched my head over this problem. It seems to be associated with how the tabBar is initialized and added to view hierarchy. I also tried above solutions like calling invalidateIntrinsicContentSize, setting the frame, and also bottomInsets inside a UITabBar subclass. They seem to work however temporarily and they break of some other scenario or regress the tab bar by causing some ambiguous layout issue. When I was debugging the issue I tried assigning the height constraints to the UITabBar and centerYAnchor, however neither fixed the problem. I realized in view debugger that the tabBar height was correct before and after the problem reproduced, which led me to think that the problem was in the subviews. I used the below code to successfully fix this problem without regressing any other scenario.
- (void) viewWillTransitionToSize:(CGSize)size withTransitionCoordinator:(id<UIViewControllerTransitionCoordinator>)coordinator
{
[super viewWillTransitionToSize:size withTransitionCoordinator:coordinator];
if (DEVICE_IS_IPHONEX())
{
[coordinator animateAlongsideTransition:^(id<UIViewControllerTransitionCoordinatorContext> _Nonnull context) {
for (UIView *view in self.tabBar.subviews)
{
if ([NSStringFromClass(view.class) containsString:#"UITabBarButton"])
{
if (#available (iOS 11, *))
{
[view.bottomAnchor constraintEqualToAnchor:view.superview.safeAreaLayoutGuide.bottomAnchor].active = YES;
}
}
}
} completion:^(id<UIViewControllerTransitionCoordinatorContext> _Nonnull context) {
[self.tabBar layoutSubviews];
}];
}
}
Assumptions: I am doing this only for iPhone X, since it doesn't seem to reproduce on any other device at the moment.
Is based on the assumption that Apple doesn't change the name of the UITabBarButton class in future iOS releases.
We're doing this on UITabBarButton only when means if apple adds more internal subviews in to UITabBar we might need to modify the code to adjust for that.
Please lemme know if this works, will be open to suggestions and improvements!
It should be simple to create a swift equivalent for this.
From this tutorial:
https://github.com/eggswift/ESTabBarController
and after initialization of tab bar writing this line in appdelegate class
(self.tabBarController.tabBar as? ESTabBar)?.itemCustomPositioning = .fillIncludeSeparator
Solves my problem of tab bar.
Hope its solves your problem
Thanks
Select tabbar and set "Save Area Relative Margins" checkbox in Inspector Editor like this:
I had the similar problem, at first it was rendered correctly but after setting up badgeValue on one of the tabBarItem it broke the layout.
What it worked for me without subclassing UITabBar was this, on my already created UITabBarController subclass.
-(void)viewDidLayoutSubviews {
[super viewDidLayoutSubviews];
NSLayoutYAxisAnchor *tabBarBottomAnchor = self.tabBar.bottomAnchor;
NSLayoutYAxisAnchor *tabBarSuperviewBottomAnchor = self.tabBar.superview.bottomAnchor;
[tabBarBottomAnchor constraintEqualToAnchor:tabBarSuperviewBottomAnchor].active = YES;
[self.view layoutIfNeeded];
}
I used tabBar superview to make sure that the constraints/anchors are on the same view hierarchy and avoid crashes.
Based on my understanding, since this seems to be a UIKit bug, we just need to rewrite/re-set the tab bar constraints so the auto layout engine can layout the tab bar again correctly.
If you have any height constraint for the Tab Bar try removing it .
Faced the same problem and removing this solved the issue.
I created new UITabBarController in my storyboard and pushed all view controllers to this new UITabBarConttoller. So, all work well in iPhone X simulator.
For iPhone you can do this, Subclass UITabBarController.
class MyTabBarController: UITabBarController {
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
if #available(iOS 11, *) {
self.tabBar.heightAnchor.constraint(equalToConstant: 40).isActive = true
self.tabBar.invalidateIntrinsicContentSize()
}
}
}
Goto Storyboard and allow Use Safe Area Layout Guide and change class of UITabbarController to MyTabBarController
P.S This solution is not tested in case of universal application and iPad.
try to change splash screen with #3x size is (3726 × 6624)
For me, remove [self.tabBar setBackgroundImage:] work, maybe it's UIKit bug
For me this fixed all the issues:
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
let currentHeight = tabBar.frame.height
tabBar.frame = CGRect(x: 0, y: view.frame.size.height - currentHeight, width: view.frame.size.width, height: currentHeight)
}
I was using a UITabBarController in the storyboard and at first it was working alright for me, but after upgrading to newer Xcode version it started giving me issues related to the height of the tabBar.
For me, the fix was to delete the existing UITabBarController from storyboard and re-create by dragging it from the interface builder objects library.
For those who write whole UITabBarController programmatically, you can use UITabBarItem.appearance().titlePositionAdjustment to adjust the title position
So in this case that you want add a gap between Icon and Title use it in viewDidLoad:
override func viewDidLoad() {
super.viewDidLoad()
// Specify amount to offset a position, positive for right or down, negative for left or up
let verticalUIOffset = UIOffset(horizontal: 0, vertical: hasTopNotch() ? 5 : 0)
UITabBarItem.appearance().titlePositionAdjustment = verticalUIOffset
}
detecting if device has Notch screen:
func hasTopNotch() -> Bool {
if #available(iOS 11.0, tvOS 11.0, *) {
return UIApplication.shared.delegate?.window??.safeAreaInsets.top ?? 0 > 20
}
return false
}
For me the solution was to select the Tab Bar in the view hierarchy, then go to:
Editor -> Resolve Auto Layout Issues, and under "Selected Views" (not "All views in view") choose "Add missing constraints".
I was having the same issue which was solved by setting the items of the tabBar after the tab bar was laid out.
In my case the issue happened when:
There is a custom view controller
A UITabBar is created in the initializer of the view controller
The tab bar items are set before view did load
In view did load the tab bar is added to the main view of the view controller
Then, the items are rendered as you mention.
I think this is a bug UIKit from iPhoneX.
because it works:
if (#available(iOS 11.0, *)) {
if ([UIApplication sharedApplication].keyWindow.safeAreaInsets.top > 0.0) {
self.tabBarBottomLayoutConstraint.constant = 1.0;
}
}

unable to scroll the textview in Swift

I have a TextView as shown below
I am not able to scroll the text view , I have added UITextViewDelegate in UIViewController class as well as set isUserInteractionEnabled property in textViewDidBeginEditing
func textViewDidBeginEditing(_ textView: UITextView) {
textView.backgroundColor = UIColor.lightGray
textView.isEditable = false
textView.isUserInteractionEnabled = true
textView.isScrollEnabled = true
}
What did I need to do?
Also, the scrolling is enabled in attribute inspector
This issue has occurred because the actual UITextView's size was more than screen size as shown below
The real answer is that the UITextView needs its content frame height inferior to its content size height to be able to scroll.
In your case, the content frame height is equal to the content size, so it doesn't scroll.
You just have to set the leading left, trailing, top space and bottom space to the View, but first make sure the Text View is smaller than the actual View (parent).
It starts to get complicated when you have a UITextView on a UIView which is either a view on a UIScrollView or directly on a UIScrollview.
The best thing to do, is to give each of them their own actions when scrolling.
Firstly, make sure you have all of your delegates for EVERYTHING set.
Make sure your UITextViews are not user restricted for what they don't need to be.
What I have to do in one of my published apps is this👇
func scrollViewWillBeginDragging(_ scrollView: UIScrollView)
{
if scrollView == textView1
{
// Do something if you actually want to, or just let textView1 scroll as intended
}
else if scrollView == textView2
{
// Do something if you actually want to, or just let textView2 scroll as intended
}
else if scrollView == zoomingScroll
{
// Do something if you need to or leave it
}
else
{
// Do something with all the other scrollable views if you need to
}
}

How to Make the scroll of a TableView inside ScrollView behave naturally

I need to do this app that has a weird configuration.
As shown in the next image, the main view is a UIScrollView. Then inside it should have a UIPageView, and each page of the PageView should have a UITableView.
I've done all this so far. But my problem is that I want the scrolling to behave naturally.
The next is what I mean naturally. Currently when I scroll on one of the UITableViews, it scrolls the tableview (not the scrollview). But I want it to scroll the ScrollView unless the scrollview cannot scroll cause it got to its top or bottom (In that case I'd like it to scroll the tableview).
For example, let's say my scrollview is currently scrolled to the top. Then I put my finger over the tableview (of the current page being shown) and start scrolling down. I this case, I want the scrollview to scroll (no the tableview). If I keep scrolling down my scrollview and it reaches the bottom, if I remove my finger from the display and put it back over the tebleview and scroll down again, I want my tableview to scroll down now because the scrollview reached its bottom and it's not able to keep scrolling.
Do you guys have any idea about how to implement this scrolling?
I'm REALLY lost with this. Any help will be greatly appreciate it :(
Thanks!
The solution to simultaneously handling the scroll view and the table view revolves around the UIScrollViewDelegate. Therefore, have your view controller conform to that protocol:
class ViewController: UIViewController, UIScrollViewDelegate {
I’ll represent the scroll view and table view as outlets:
#IBOutlet weak var scrollView: UIScrollView!
#IBOutlet weak var tableView: UITableView!
We’ll also need to track the height of the scroll view content as well as the screen height. You’ll see why later.
let screenHeight = UIScreen.mainScreen().bounds.height
let scrollViewContentHeight = 1200 as CGFloat
A little configuration is needed in viewDidLoad::
override func viewDidLoad() {
super.viewDidLoad()
scrollView.contentSize = CGSizeMake(scrollViewContentWidth, scrollViewContentHeight)
scrollView.delegate = self
tableView.delegate = self
scrollView.bounces = false
tableView.bounces = false
tableView.scrollEnabled = false
}
where I’ve turned off bouncing to keep things simple. The key settings are the delegates for the scroll view and the table view and having the table view scrolling being turned off at first.
These are necessary so that the scrollViewDidScroll: delegate method can handle reaching the bottom of the scroll view and reaching the top of the table view. Here is that method:
func scrollViewDidScroll(scrollView: UIScrollView) {
let yOffset = scrollView.contentOffset.y
if scrollView == self.scrollView {
if yOffset >= scrollViewContentHeight - screenHeight {
scrollView.scrollEnabled = false
tableView.scrollEnabled = true
}
}
if scrollView == self.tableView {
if yOffset <= 0 {
self.scrollView.scrollEnabled = true
self.tableView.scrollEnabled = false
}
}
}
What the delegate method is doing is detecting when the scroll view has reached its bottom. When that has happened the table view can be scrolled. It is also detecting when the table view reaches the top where the scroll view is re-enabled.
I created a GIF to demonstrate the results:
Modified Daniel's answer to make it more efficient and bug free.
#IBOutlet weak var scrollView: UIScrollView!
#IBOutlet weak var tableView: UITableView!
#IBOutlet weak var tableHeight: NSLayoutConstraint!
override func viewDidLoad() {
super.viewDidLoad()
//Set table height to cover entire view
//if navigation bar is not translucent, reduce navigation bar height from view height
tableHeight.constant = self.view.frame.height-64
self.tableView.isScrollEnabled = false
//no need to write following if checked in storyboard
self.scrollView.bounces = false
self.tableView.bounces = true
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 20
}
func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
let label = UILabel(frame: CGRect(x: 0, y: 0, width: tableView.frame.width, height: 30))
label.text = "Section 1"
label.textAlignment = .center
label.backgroundColor = .yellow
return label
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath)
cell.textLabel?.text = "Row: \(indexPath.row+1)"
return cell
}
func scrollViewDidScroll(_ scrollView: UIScrollView) {
if scrollView == self.scrollView {
tableView.isScrollEnabled = (self.scrollView.contentOffset.y >= 200)
}
if scrollView == self.tableView {
self.tableView.isScrollEnabled = (tableView.contentOffset.y > 0)
}
}
Complete project can be seen here:
https://gitlab.com/vineetks/TableScroll.git
After many trials and errors, this is what worked best for me. The solution has to solve two needs 1) determine who's scrolling property should be used; tableView or scrollView? 2) make sure that the tableView doesn't give authority to the scrollView until it has reached the top of it's table/content.
In order to see if the scrollview should be used for scrolling vs the tableview, i checked to see if the UIView right above my tableview was within frame. If the UIView is within frame, it's safe to say the scrollView should have authority to scroll. If the UIView is not within frame, that means that the tableView is taking up the entire window, and therefor should have authority to scroll.
func scrollViewDidScroll(_ scrollView: UIScrollView) {
if scrollView.bounds.intersects(UIView.frame) == true {
//the UIView is within frame, use the UIScrollView's scrolling.
if tableView.contentOffset.y == 0 {
//tableViews content is at the top of the tableView.
tableView.isUserInteractionEnabled = false
tableView.resignFirstResponder()
print("using scrollView scroll")
} else {
//UIView is in frame, but the tableView still has more content to scroll before resigning its scrolling over to ScrollView.
tableView.isUserInteractionEnabled = true
scrollView.resignFirstResponder()
print("using tableView scroll")
}
} else {
//UIView is not in frame. Use tableViews scroll.
tableView.isUserInteractionEnabled = true
scrollView.resignFirstResponder()
print("using tableView scroll")
}
}
hope this helps someone!
None of the answers here worked perfectly for me. Each one had it's owned nuanced problem (needing to do a repeated swipe when one scrollview hit it's bottom, or the scroll indicator not looking correct, etc), so figured I'd throw in another answer.
Ole Begemann has a great write up on doing this exactly https://oleb.net/blog/2014/05/scrollviews-inside-scrollviews/
Despite being an old post, the concepts still apply to the current APIs. Additionally, there is a maintained (Xcode 9 compatible) Objective-C implementation of his approach https://github.com/eyeem/OLEContainerScrollView
If you are facing problem with the nested scrolling issue , here tis the simplest solution for it .
go to your design screen
select your scroll view and then disable bounce on scroll
if your view uses table view inside scroll view then disable bounce on scroll of the table view as well
run and check it is solved
check how to disable bounce on scroll of a scroll view
check how to disable bounce on scroll of a tableview view
I was struggling with this problem, too. There is a very simple solution.
In interface builder:
create simple ViewController
add a simple View, it will be our header, and constrain it to superview
it's the red view on the example below
I have added 12px from top, left and right, and set fixed height to 128px
embed a PageViewController, making sure it is constrained to the superview, and not the header
Now, here comes the fun part: for each page you add, make sure its tableView has an offset from top. Thats it. You can do if with this code, for example (assuming you use UITableViewController as a page):
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
let tables = viewControllers.compactMap { $0 as? UITableViewController }
tables.forEach {
$0.tableView.contentInset = UIEdgeInsets(top: headerView.bounds.height, left: 0, bottom: 0, right: 0)
$0.tableView.contentOffset = CGPoint(x: 0, y: -headerView.bounds.height)
}
}
No messy scroll inside scroll inside table view, no mangling with delegates, no duplicated scrolls, perfectly natural behavior. If you can't see the header, it is probably because of the tableView background color. You have to set it to clear, for the header to be visible from under the tableView.
I think there are two options.
Since you know the size of the scroll view and the main view, you are unable to tell whether the scroll view hit the bottom or not.
if (scrollView.contentOffset.y >= (scrollView.contentSize.height - scrollView.frame.size.height)) {
// reach bottom
}
So when it hit; you basically set
[contentScrollView setScrollEnabled:NO];
and other way around for your tableView.
The other thing, which is more precise I think, is to add Gesture to your views.
UITapGestureRecognizer *tapRecognizer = [[UITapGestureRecognizer alloc]
initWithTarget:self action:#selector(respondToTapGesture:)];
// Specify that the gesture must be a single tap
tapRecognizer.numberOfTapsRequired = 1;
// Add the tap gesture recognizer to the view
[self.view addGestureRecognizer:tapRecognizer];
// Do any additional setup after loading the view, typically from a nib
So when you add Gesture, you can simply control the active view by changing setScrollEnabled in the respondToTapGesture.
I found an awesome library
MXParallaxHeader
In Storyboard just set UIScrollView class to MXScrollView then magic happens.
I used this class to handle my UIScrollView when I embed a UIPageViewController container view. even you can insert a parallax header view for more detail.
Also, this library provides Cocoapods and Carthage
I attached an image below which represent UIViewHierarchy.
MXScrollView Hierarchy
SWIFT 5
I had some trouble using Vineet's answer for when I could not guarantee the scrollView content offset (Y) due to various different screen sizes. To resolve this, I changed the first trigger event of when the tableView's scroll gets enabled.
func scrollViewDidScroll(_ scrollView: UIScrollView) {
if scrollView.bounds.contains(button.frame) {
tableView.isScrollEnabled = true
}
if scrollView == tableView {
self.tableView.isScrollEnabled = (tableView.contentOffset.y > 0)
}
}
The scrollView.bounds.contains will check if a given element's frame is FULLY within the scrollView's visible content. I set this to a button that I have below the tableView. You could set this to your tableVIew's frame instead if your only condition is that your tableView is fully visible.
I left the original implementation of when to disable the tableView's scroll and it works very well.
I tried the solution marked as the correct answer, but it was not working properly. The user need to click two times on the table view for scroll and after that I was not able to scroll the entire screen again. So I just applied the following code in viewDidLoad():
tableView.addGestureRecognizer(UISwipeGestureRecognizer(target: self, action: #selector(tableViewSwiped)))
scrollView.addGestureRecognizer(UISwipeGestureRecognizer(target: self, action: #selector(scrollViewSwiped)))
And the code below is the implementation of the actions:
func tableViewSwiped(){
scrollView.isScrollEnabled = false
tableView.isScrollEnabled = true
}
func scrollViewSwiped(){
scrollView.isScrollEnabled = true
tableView.isScrollEnabled = false
}
One easy trick, if you want to achieve it is replacing parent scrollview with normal container view.
Adding a pan gesture on container view, you can play with top constraint of first view to assign negative values. You can keep a check of page View's origin if it achieves to top you can start assigning that value on content offset of the pageView's child view. Until user achieves the table view in a state of top most view in container view, you can keep page tableView's scrolling disabled and allow scrolling manually by setting content offset.
So initially the page view height will be collapsed (or say out of screen) or less at bottom. Later on scrolling down it will expand to take more space.
Gesture will automatically stop responding if out of frames say on nav bar or other view outside container view.
Gestures are a key to user interactive transitions used in many apps. You can mimic scroll for a certain time with it.
In my case I'm using constraint for height like that:
self.heightTableViewConstraint.constant = self.tableView.contentSize.height
self.scrollView.contentInset.bottom = self.tableView.contentSize.height
Below code works great for me
As I wanted to show some header after some scroll and table view supposed to scroll
And in ViewDidLoad add
override func viewDidLoad() {
super.viewDidLoad()
mainScrollView.delegate = self
}
Change 265 to whatever number you want to stop upper scroll
extension AccountViewController: UIScrollViewDelegate {
func scrollViewDidScroll(_ scrollView: UIScrollView) {
print(notebookTableView.contentOffset.y)
if notebookTableView.contentOffset.y < 265 {
if notebookTableView.contentOffset.y > 0 {
mainScrollView.setContentOffset(notebookTableView.contentOffset, animated: false)
} else {
mainScrollView.setContentOffset(CGPoint(x: 0.0, y: 0.0), animated: false)
}
} else {
mainScrollView.setContentOffset(CGPoint(x: 0.0, y: 265), animated: false)
}
}
}
CGFloat tableHeight = 0.0f;
YourArray =[response valueForKey:#"result"];
tableHeight = 0.0f;
for (int i = 0; i < [YourArray count]; i ++) {
tableHeight += [self tableView:self.aTableviewDoc heightForRowAtIndexPath:[NSIndexPath indexPathForRow:i inSection:0]];
}
self.aTableviewDoc.frame = CGRectMake(self.aTableviewDoc.frame.origin.x, self.aTableviewDoc.frame.origin.y, self.aTableviewDoc.frame.size.width, tableHeight);
Maybe brute-force, but working perfectly if cell heights are the same: by the way, I use auto layout.
for the tableView (or collectionView or whatever), set an arbitrary height in storyboard, and make an outlet to class. Wherever appropriate, (viewDidLoad() or...) set the tableView's height big enough so that tableView doesn't need to scroll. (need to know the number of rows in advance) Then only the outer scrollView will scroll nicely.

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
}