Automatic adjustment of UIScrollView content offset with custom UIControl - ios

When a UITextField is added to a UIScrollview the scroll view automatically adjusts its contentOffset so that the view will not be obscured by the keyboard.
I have a custom UIControl which also presents a keyboard when it becomes the first responder by assigning its inputView property. The same scrolling behavior does not work. Is there a way to configure a UIControl such that a scroll view will keep it visible when the keyboard is presented?
My guess is that it could be possible by overriding a property defined in one of the protocols UITextField and other classes which this behavior conform to. But these can be a bit of a maze. Also note, the issue here has nothing to do with the scroll view's contentInset property. The scroll view can scroll to show the custom control, it just doesn't do it automatically when the control becomes the first responder.

It looks like this is handled by an internal private method that Apple utilizes [UIFieldEditor scrollSelectionToVisible] as noted on this blog: http://sugarrushva.my03.com/712423-disable-uiscrollview-scrolling-when-uitextfield-becomes-first-responder.html
It appears to do this by stepping back up through the view hierarchy and if it finds a parent UIScrollView, it scrolls the view to bring the UITextField into visible view. You'll need to implement the scrolling manually on your custom control when it becomes first responder, or handle it by introspecting the parent views.

I was pointed in the right direction by #markflowers.
Based on that, here's what I've written into the control to get the desired behavior:
- (BOOL)becomeFirstResponder {
if ([super becomeFirstResponder]) {
[self scrollParentViewToFrame];
return YES;
}
return NO;
}
- (void)scrollParentViewToFrame {
UIScrollView *scrollView = self.parentScrollView;
CGRect frame = [scrollView convertRect:self.bounds fromView:self];
[self.parentScrollView scrollRectToVisible:frame animated:YES];
}
- (UIScrollView *)parentScrollView {
return (UIScrollView *) [self closestParentWithClass:[UIScrollView class]];
}
Note that the frame attribute is not used in case the control is not a direct descendant of the scroll view. Instead convert the bounds to the scroll view's coordinate space.
The scroll adjustment is also needs to be performed after [super becomeFirstResponder] is called for it to interact properly with keyboard notifications that are being used to adjust the insets of the scroll view.
I defined the method to search for the closest parent scroll view in a UIView category which made it easier to recursively search up the hierarchy.
- (UIView *)closestParentWithClass:(Class)class {
if ([self isKindOfClass:class]) {
return self;
}
// Recursively searches up the view hierarchy, returns nil if a view
// has no superview.
return [self.superview closestParentWithClass:class];
}

Related

InputAccessoryView covers the bottom bar

Any idea how to get an inputAccessoryView to anchor to the tab bar rather than the bottom of the screen?
I have created a UIViewController and overridden the following methods:
-(BOOL)canBecomeFirstResponder {
return YES;
}
-(UIView *)inputAccessoryView {
CGRect frame = CGRectMake(0, 0, self.view.frame.size.width, 44);
self.keyboardInputAccessoryView =[[BRKeyboardInputBarView alloc] initWithFrame:frame leftButtonTitle:#"Left" andRightButtonTitle:#"Send"];
[self.keyboardInputAccessoryView setDelegate:self];
[self.keyboardInputAccessoryView setTranslatesAutoresizingMaskIntoConstraints:NO];
[self.keyboardInputAccessoryView removeFromSuperview];
return self.keyboardInputAccessoryView;
}
View controller with inputAccessoryView covering the tab bar
By the looks of it the view controller adds the view to the window rather than the current view controllers view, which would explain its positioning. However if I remove the line:
[self.keyboardInputAccessoryView removeFromSuperview];
I get a crash when I tap in the textview of my accessory view:
The view hierarchy is not prepared for the constraint:<NSLayoutConstraint:0x7fa0c2ca5f80 BRKeyboardInputBarView:0x7fa0c2d6fad0.bottom == UIInputSetContainerView:0x7fa0c295a2c0.bottom>
So I guess what I am asking is what is the correct way to add a keyboard accessory view so that it plays nicely with auto layout and avoids the crash, but also anchors itself to the view and not the window?
What you are seeing is the right behaviour.
The results you are seeing is because of the fact that UIViewController is a UIResponder subclass. By overriding the inputAccessoryView and returning an instance of a view, UIViewController will take care of placing that view at the bottom of the screen and animating it appropriately when keyboard appears or disappears.
If you want to add this bar on top of your keyboard, then you need to set the property inputAccessoryView of a textField/textView to your custom view.

Determine if button can be seen by user

I have an interesting requirement. I have a webview that expands in size when the user swipes up. This works well, but now I am trying to detect if the user has scrolled up to the top, so that I can minimize it again.
I am trying to do this by placing an image behind the webview, if the user scrolls past the top of the webview, the bounce effect takes place and the underlying image becomes visible. I was trying to use the "hidden" property thinking that the image is hidden when under the webview, but visible when the webview has been pulled down. This however, doesnt seem to work properly.
Anyone have any ideas on how to detect if a button/image is visible to the user?
Because the UIWebView implements UIScrollViewDelegate, it declares conformity to that protocol, you can use the ScrollViewDidScroll delegate method.
First make sure that your UIWebView is not inside a UIScrollView
Important: You should not embed UIWebView or UITableView objects in
UIScrollView objects. If you do so, unexpected behavior can result
because touch events for the two objects can be mixed up and wrongly
handled.
Instead, you can access the UIScrollView through the UIWebView properties since we now know that UIWebView is based on a UIScrollView. Your view controller can implement the UIScrollViewDelegate.
#interface MyViewController : UIViewController<UIScrollViewDelegate>
#end
Then you have to set the scrollView property inside your webview to the UIScrollViewDelegate like so:
- (void)viewDidLoad
{
[super viewDidLoad];
// Set the scrollView property's delegate protocol to self. This is so that this view controller will receive the delegate methods being fired when we interact with the scrollView.
webView.scrollView.delegate = self;
}
We're only interested in one of the ScrollView's delegate method - scrollViewDidScroll. Then you can detect when the scrollView has been scrolled inside your webview and ultimately have a simple mathematics equation that checks if the scrollView has been scrolled to the top:
- (void)scrollViewDidScroll:(UIScrollView *)scrollView
{
if(scrollView.contentOffset.y <= 0.0){
NSLog(#"TOP REACHED so do the chicken dance");
}
}
Look for contentOffset of scroll view of Web view if it's Y==0 then it means that user has scrolled up to the top.
CGRect visibleRect;
visibleRect.origin = webView.scrollView.contentOffset;
if(visibleRect.origin.y == 0)
{
//t means that user has scrolled up to the top
}

Custom UIView widget with UIScrollView not scrolling

I am trying to develop a new custom UIView (to allow for horizontal date selection). I want to do all the UI design in XIB files.
The custom UI view contains a scrollview and then two 'week' views. The idea is that as the scrolling occurs, I will move the two 'week' views in place and reconfigure them to the right dates to create an 'infinite' scroll for date selections.
I can load the UIView, which then loads scrollview and week views (all designed in a XIB).
My DatePickerView class, derived from the UIView class does an addSubview of the scroll view (which contains the two week views). The scroll view is 320 wide and the contentSize is set to 640 wide. UserInteraction is enabled. Horizonal Scrolling is enabled.
This all works and displays on the screen. The week views each contain 7 buttons. I can press them and they get the touch. However, the scrollview does not seem to want to scroll.
I set my custom view to be a UIScrollViewDelegate. No calls occur to scrollViewDidScroll.
For each of the week views, I have a 'container' view and then the buttons. I added the following to the container view (again derived from a UIView).
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
DDLogInfo(#"Began. Next Responder: %#", self.nextResponder);
[self.nextResponder touchesBegan:touches withEvent:event];
}
(and comparable ones for the other touch events, touchesMoved, touchesEnded, touchesCancelled),
I print out the nextResponder, which is the UIScrollView, so I know that I am sending the touch to the view, but I never see the scrollview want to scroll.
Is my method of passing the touchEvents up the responder chain correct?
Is there anything else I need to configure to get the scrolling to work?
Any help is appreciated.
Charlie
If I understand correctly, you want infinite scroll with just three pages of scroll view. I achieved it with similar effects in my calendar view project.
You can checkout from here DPCalendar
In a nutshell, I created a view like
#interface DPCalendarMonthlyView : UIScrollView
And initial it like this
self.showsHorizontalScrollIndicator = NO;
self.clipsToBounds = YES;
self.contentInset = UIEdgeInsetsZero;
self.pagingEnabled = YES;
self.delegate = self;
I create three views like this
[self.pagingViews addObject:[self singleMonthViewInFrame:self.bounds]];
[self.pagingViews addObject:[self singleMonthViewInFrame:CGRectMake(self.bounds.size.width, 0, self.bounds.size.width, self.bounds.size.height)]];
[self.pagingViews addObject:[self singleMonthViewInFrame:CGRectMake(self.bounds.size.width * 2, 0, self.bounds.size.width, self.bounds.size.height)]];
Then I set the content size and also scroll it to the middle
[self setContentSize:CGSizeMake(self.bounds.size.width * 3, self.bounds.size.height)];
[self scrollRectToVisible:((UIView *)[self.pagingViews objectAtIndex:1]).frame animated:NO];
In the scrollview delegate function, i need to do
- (void)scrollViewDidEndDecelerating:(UIScrollView *)sender
{
//If scroll right
if(self.contentOffset.x > self.frame.size.width)
{
//do something if scroll right
} else if(self.contentOffset.x < self.frame.size.width)
{
//do something else if scroll left
} else {
return;
}
//scroll back to the middle
[self scrollRectToVisible:((UICollectionView *)[self.pagingViews objectAtIndex:1]).frame animated:NO];
}
Hopefully it is useful to you.
For those that follow down this path, I figured this out and it was a silly error. I forgot to turn off AutoLayout. I keep forgetting that Apple put autoLayout as an enable/disable option under the 'document'-level of a NIB (so I forget to look there).
Turned it off and it works as designed. Looks like autoLayout was causing the views to be rearranged to not need to be scrolled, or something equivalent.

How to reset my UIScrollView's position after returning from a modal transition?

I have a simple view containing a long view with many buttons, with the whole thing being in a UIScrollView. The scroller works well, and I can scroll to the bottom and click a button. Every button triggers a modal seque to another view. That new view is then dismissed by user interaction, causing the original UIScrollView's view to load again.
Here's the problem: If I click on a button toward the top of the UIScrollView, I enter the modal segue, dismiss the new view, and return to the UIScrollView's view without a problem. But, if I click on one of the buttons toward the bottom of the UIScrollView, when I return seque out and then transition back, my scrolling is all messed up. I can only see the area beneath my scroller, and can't scroll back up to the top anymore!
I'm pretty sure there must be some way to reset the UIScrollView's starting and ending points upon ViewWillAppear, but I can't figure it out. Any help is appreciated!
Also, FYI, I simply added the UIScrollView through interface builder, and haven't implemented or synthesized it anywhere yet.
try this code:
- (void)viewWillAppear:(BOOL)animated
{
[yourscrollview setContentOffset:CGPointZero animated:YES];
}
Please note: the bug this question and answer is about appears to be fixed in iOS 7. The rest of this answer is only relevant to iOS 6 (and probably earlier).
The behaviour being exhibited here is a bug in the UIScrollView class. As noted by the OP, after returning from a modally presented UIViewController to a scene containing a UIScrollView, the UIScrollView takes whatever point it's currently scrolled to and starts behaving as though that is its origin. That means that if you'd scrolled down your scroll view before modally presenting another View Controller, you can't scroll back up upon returning to the scene with the scroll view.
The same thing happens when you remove the Scroll View from the view hierarchy and re-add it, even without changing its window.
You can work around this by setting the contentOffset of the scroll view back to {0,0} before it gets displayed again after dismissing the modal View Controller. If you actually want to preserve the point the user had scrolled to before they triggered the modal, then after the UIScrollView is redisplayed you can set the contentOffset back to whatever it was before you reset it.
Here's a UIScrollView subclass that fixes the bug without resetting the scroll view to the top whenever you return from a modal:
#interface NonBuggedScrollView : UIScrollView
#end
#implementation NonBuggedScrollView {
CGPoint oldOffset;
}
-(void)willMoveToWindow:(UIWindow *)newWindow {
oldOffset = self.contentOffset;
self.contentOffset = CGPointMake(0,0);
}
-(void)willMoveToSuperview:(UIView *)newSuperview {
oldOffset = self.contentOffset;
self.contentOffset = CGPointMake(0,0);
}
-(void)didMoveToWindow {
self.contentOffset = oldOffset;
}
-(void)didMoveToSuperview {
self.contentOffset = oldOffset;
}
#end
If you'd rather do this in a UIViewController than in a UIScrollView subclass, change the content offset in the viewWillAppear: and viewDidAppear methods.
If you don't want to preserve where the user's scroll position when they return from a modal, and just want to scroll the UIScrollView back to the top, as the OP asked for, then all you need is the even simpler:
#interface NonBuggedScrollView : UIScrollView
#end
#implementation NonBuggedScrollView
-(void)willMoveToWindow:(UIWindow *)newWindow {
self.contentOffset = CGPointMake(0,0);
}
-(void)willMoveToSuperview:(UIView *)newSuperview {
self.contentOffset = CGPointMake(0,0);
}
#end
First, thanks for the approved answer above. Someone mentioned that it was no longer applicable but I have a scrolling view inside of table view cell and it needs to be reset when the cell is reused.
Here is the solution in Swift.
#IBOutlet var scrollView: UIScrollView!
// many lines of code later inside a function of some sort...
scrollView.setContentOffset(CGPointMake(0.0, 0.0), animated: false)
To solve this problem i use this code:
-(void) viewWillAppear:(BOOL)animated{
[super viewWillAppear:animated];
[self.scrollview scrollRectToVisible:CGRectMake(0, 0, 1, 1)
animated:NO];
}
You can change the starting and ending points by calling scrollRectToVisible:animated:. But I'm not sure if this fixes your problem.
Use below code snippet to restore the scroll position for a UIScrollview
Declare "scrollPosition" variable as CGPoint type.
- (void)viewDidDisappear:(BOOL)animated
{
[super viewDidDisappear:animated];
//get the current offset
scrollPosition = scrollView.contentOffset;
//set current view to the beginning point
self.scrollView.contentOffset = CGPointZero;
}
- (void)viewDidLayoutSubviews
{
[super viewDidLayoutSubviews];
//retrieve the previous offset
self.scrollView.contentOffset = scrollPosition;
}

Scroll to top of UITableView by tapping status bar

I know there's tons of code out there to scroll a tableview to the top, but I want to do this when the top status bar is tapped, just like in Apple's native apps. Is this possible?
You get this for free, but you should check that the scrollsToTop attribute of your UITableView is YES.
When this does NOT work is when you have a UIScrollView (or descendant class like UITextView) object embedded inside another UIScrollView class (like UITableView). In this case, set scrollsToTop on the embedded UIScrollView class to NO. Then the tap-the-status-bar behavior will work.
If you came from Google and need a complete checklist:
Check that you've set scrollsToTop=YES (per Mark's suggestion) on your UITableView
Make sure that you've set scrollsToTop=NO on all OTHER UITableViews / UIScrollViews / UITextViews in your window, so that they're not intercepting the click. I've found myself printing out all the views in my window many times to debug this...
Make sure that your table view is at 0/0 (x/y coordinates) within the window - this is how the system knows that it should pass the message
Using the information given in other answers, I added the following code to my UITableViewController get it to work:
- (void)viewDidLoad
{
[super viewDidLoad];
for (UITextView *view in self.view.subviews) {
if ([view isKindOfClass:[UITextView class]]) {
view.scrollsToTop = NO;
}
}
self.tableView.scrollsToTop = YES;
}
This looks through all the views in the UITableViewController's hierarchy and turns off scrollsToTop on all the UITextViews that were intercepting the touch event. Then, ensured the tableView was still going to receive the touch.
You can mod this to iterate through other UITableViews / UIScrollViews / UITextViews that may be intercepting as well.
Hope this helps!
I had the same problem but fixed by following steps:
Set scrollsToTop = YES for tableview you wanted to scroll to top.
set scrollsToTop = NO for all other tableview or collection view or scrollview.
If any of your tableview cell has collection view . Make sure you set scrollsToTop to NO for the collection view as well.
If your view controller/ navigation controller is added as a subview on another view controller, Make sure you set it as a child Controller.
I know this is quite an old one but hope this can help. Following what #MarkGranoff said, the scrollsToTop doesn't work if more than one UIScrollView, or its subclasses, has got it set to YES (default value), a sanity check is probably worth to check who's actually messing up with this behaviour. The simple method below loop over the subviews of your view and logs the scrollsToTop value of all the UIScrollView in your view. Preferably to be called in your viewDidAppear method.
- (void)checkForScrollViewInView:(UIView *)view {
for (UIView *subview in [view subviews]) {
if ([subview isKindOfClass:[UIScrollView class]]) {
NSLog(#"scrollsToTop enabled: %i in scroll view %#", ((UIScrollView *)subview).scrollsToTop, subview);
}
if (subview.subviews.count > 0) {
[self checkForScrollViewInView:subview];
}
}
}
This is just a debug code indeed. Once you find the scrollsToTop value for each one of the UIScrollView subclasses just make sure only one is set to YES.
Like Mark said, you can only have one subclass of UIScrollView (usually the table view) that has the scrollsToTop property set to TRUE. Likely you have others, typically UITextView in your view. Just set their scrollsToTop property to FALSE and you're good to go.
On UIScrollView header file:
// When the user taps the status bar, the scroll view beneath the touch which is closest to the status bar will be scrolled to top, but only if its scrollsToTop property is YES, its delegate does not return NO from shouldScrollViewScrollToTop, and it is not already at the top.
// On iPhone, we execute this gesture only if there's one on-screen scroll view with scrollsToTop == YES. If more than one is found, none will be scrolled.
For example if you have a table view and scroll view like tags like this
you should make something like this in viewDidLoad
self.tableView.scrollsToTop = true
self.tagsView.scrollsToTop = false
There can be multiple UIScrollView descendants loaded eg.: having a UIPageViewcontroller on each page containing UITableViews.
The scrollsToTop property is true by default.
So in addition to handling nested UIScrollViews' scrollsToTop property, you should do the following:
//When the view is loaded disable scrollsToTop, this view may not be the visible one
override func viewDidLoad() {
super.viewDidLoad()
...
tableView.scrollsToTop = false
}
//Now it's time to enable scrolling, the view is guaranteed to be visible
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
tableView.scrollsToTop = true
}
//Do not forget to disable scrollsToTop, making other visible UIScrollView descendant be able to be scrolled to top
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
self.tableView.scrollsToTop = false
}
This way only one top level UITableView's scrollsToTop will be set to true.

Resources