Autoscrolling tableview content based on keyboard appearance - ios

In my main view controller, I present a popup which is a UITableviewcontroller class and has a resizable textview as one of the cells. Now as the content grows and text view expands the typing content goes beyond the keyboard and it's not visible on the screen. In order to resolve this issue, I calculated the cursor position and keyboard position and based on that adjusted the tableviews content offset so that when typing starts the offset adjust to show the typing content above the keyboard. It seems to work but as per my logic now the issue is when there's a large content and if the cursor is at the bottom and if you scroll back to top and start typing while the cursor remains at bottom, it doesn't scroll to there right away as I have just adjusted a 20pt space to content offset. I'm not sure how to calculate the content offset of tableview based on the cursor point. Below is my code so far. Any help is appreciated.
-(void)adjustTextScroll:(UITextView *)textView
{
UITextRange *selectedTextRange = textView.selectedTextRange;
CGRect windowRect = CGRectZero;
if (selectedTextRange != nil)
{
CGRect caretRect = [textView caretRectForPosition:selectedTextRange.end];
windowRect = [textView convertRect:caretRect toView:nil];
}
//Checks if current cursor position is behind keyboard position
if (CGRectGetMinY(windowRect) > (keyboardYpos - 50)) // 50 added for space difference margin from keyboard
{
CGPoint contentOffset = self.tableView.contentOffset;
contentOffset.y += 20 ;
self.tableView.contentOffset = contentOffset;
}
}
//Keyboard notification
- (void)keyboardWasShown:(NSNotification *)notification
{
// Get the size of the keyboard.
CGSize keyboardSize = [[[notification userInfo] objectForKey:UIKeyboardFrameBeginUserInfoKey] CGRectValue].size;
CGFloat height = MIN(keyboardSize.height,keyboardSize.width);
CGFloat mainViewHeight = MAX([[UIScreen mainScreen]bounds].size.width,[[UIScreen mainScreen] bounds].size.height);
keyboardYpos = mainViewHeight - height;
}
- (void)textViewDidChange:(UITextView *)textView{
[self adjustTextScroll:textView]
}

Use TPKeyboardAvoiding and let this take care of all scroll issues. I myself have worked on quite a number of apps which uses this. Works like a charm.

Related

Find bounds of UIViewController's view not obscured by UINavigationBar, UITabBar, (etc)

I can configure my UIViewController's edgesForExtendedLayout so that it will extend underneath content such as the navigation bar or tab bar. If I do this, is there some way to determine the frame that is not obscured?
As a possible alternative, is there a way for a UIViewController to determine the default contentInset to apply to a UIScrollView it contains?
Use case
I have zoomable UIScrollView containing an image.
When it is fully zoomed out I want to adjust the content inset too allow the content to stay centred (details here). However, my modified insets don't take in to account the insets that the UIViewController applies automatically so that its content isn't obscured by navigation bars, etc.
I also need to compute the minimum zoom for the content – that at which the whole image will be visible and not obscured. To compute this, I need to know the size of the unobscured part of the content view.
You need this
-(CGRect) unobscuredBounds
{
CGRect bounds = [self.view bounds];
return UIEdgeInsetsInsetRect(bounds, [self defaultContentInsets]);
}
-(UIEdgeInsets) defaultContentInsets
{
const CGFloat topOverlay = self.topLayoutGuide.length;
const CGFloat bottomOverlay = self.bottomLayoutGuide.length;
return UIEdgeInsetsMake(topOverlay, 0, bottomOverlay, 0);
}
You could put this in a category for easy reusability.
These methods correctly handle the changes that occur when the view resizes after a rotation – the change to the UINavigationBar size is correctly handled.
Centring Content
To use this to centre content by adjusting insets, you'd do something like this:
-(void) scrollViewDidZoom:(UIScrollView *)scrollView
{
[self centerContent];
}
- (void)centerContent
{
const CGSize contentSize = self.scrollView.contentSize;
const CGSize unobscuredBounds = [self unobscuredBounds].size;
const CGFloat left = MAX(0, (unobscuredBounds.width - contentSize.width)) * 0.5f;
const CGFloat top = MAX(0, (unobscuredBounds.height - contentSize.height)) * 0.5f;
self.scrollView.contentInset = UIEdgeInsetsMake(top, left, top, left);
}
Your content insets will now reflect the default insets that they need (to avoid being covered up) and will also have the insets they need to be nicely centred.
Handling Rotation & Zoom
You probably also want to perform centring when animating between landscape and portrait. At the same time, you might want to adjust your minimum zoom scale so that your content will always fit. Try out something like this:
-(void) willAnimateRotationToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation duration:(NSTimeInterval)duration
{
[self centerContent];
const bool zoomIsAtMinimum = self.scrollView.zoomScale == self.scrollView.minimumZoomScale;
self.scrollView.minimumZoomScale = [self currentMinimumScale];
if(zoomIsAtMinimum)
{
self.scrollView.zoomScale = self.scrollView.minimumZoomScale;
}
}
-(CGFloat) currentMinimumScale
{
const CGFloat currentScale = self.scrollView.zoomScale;
const CGSize scaledContentSize = self.scrollView.contentSize;
const CGSize scrollViewSize = [self unobscuredBounds].size;
CGFloat scaleToFitWidth = currentScale * scrollViewSize.width / scaledContentSize.width;
CGFloat scaleToFitHeight = currentScale * scrollViewSize.height / scaledContentSize.height;
return MIN(scaleToFitWidth, scaleToFitHeight);
}
The willAnimateRotationToInterfaceOrientation:… method is called within the view animation block, so the changes that it applies will lead to nice smooth animated changes as you switch from landscape to portrait.

Unable to identify the touches in customview embedded in UIScrollView due to resetting the contentoffset

I have a UIScrollView which holds a custom drawing view. The drawing view is used to draw a large content (10000x10000 pixels). Since I cannot embed huge view inside scrollview [due memory limitations], I have created a custom view which is almost twice the size of the scrollView. I have used the StreetScroller [apple sample] logic to implement the same. This works fine but there is a problem with identifying touches. You can download the updated sample from here https://github.com/praveencastelino/SampleApps/tree/master/StreetScroller
Since the contentOffset of scrollview is reset to center whenever it moves 'X amount of pixels' from its center. Hence, the scrollView content offset is different from what we actually need. This what we do in scrollView.
- (void)recenterIfNecessary
{
CGPoint currentOffset = [self contentOffset];
CGFloat contentHeight = [self contentSize].height;
CGFloat contentWidth = [self contentSize].width;
CGPoint centerOffset,distanceFromCenter;
centerOffset.y = (contentHeight - [self bounds].size.height) / 2.0;
distanceFromCenter.y = fabs(currentOffset.y - centerOffset.y);
centerOffset.x = (contentWidth - [self bounds].size.width) / 2.0;
distanceFromCenter.x = fabs(currentOffset.x - centerOffset.x);
if (distanceFromCenter.y > (contentHeight / 6.0))
{
self.contentOffset = CGPointMake(currentOffset.x, centerOffset.y);
[_labelContainerView didResetByVerticalDistancePoint:CGPointMake(currentOffset.x, centerOffset.y - currentOffset.y) visibleFrame:[self bounds]];
}
if (distanceFromCenter.x > (contentWidth / 6.0))
{
self.contentOffset = CGPointMake(centerOffset.x, currentOffset.y);
[_labelContainerView didResetByHorizontalDistancePoint: CGPointMake(centerOffset.x - currentOffset.x, currentOffset.y) visibleFrame:[self bounds]];
}
}
Whenever scrollview resets the center, the custom view notified and it tracks the virtual content offset.
-(void)didResetByVerticalDistancePoint:(CGPoint)distance visibleFrame:(CGRect)frame
{
_contentOffsetY += distance.y;
NSLog(#"_contentOffsetY %f",_contentOffsetY);
[self setNeedsDisplay];
}
However, I wanted to calculate the virtual content offset whenever scroll view scrolls [At present the content offset is calculated only when we reset content offset of scrollview to center]. This would eventually help me in handling touches.
Also, I need a way to restrict the bounds of the scrollview from scrolling infinitely. I want to display only the content and avoid the scrolling if user tries to scroll it further.

UITableView Not Scrolling after contentInset

i'm working on a project where i have a tableview and a uitextfield.
I'm applying the following method when the uitextfield gain/loose the focus :
-(void)enableInset {
CGFloat offSet = -30.0f;
UIEdgeInsets inset = UIEdgeInsetsMake(placesMapView.frame.size.height - offSet, 0.0f, 0.0f, 00.f);
// Updating the tableView position.
placesTableView.contentInset = inset;
placesTableView.contentOffset = CGPointMake(0.0f, -(placesMapView.frame.size.height - offSet));
placesTableView.scrollIndicatorInsets = inset;
}
and
- (void)disableInset {
CGFloat offset = self.navigationController.navigationBar.frame.size.height + [UIApplication sharedApplication].statusBarFrame.size.height;
UIEdgeInsets inset = UIEdgeInsetsMake(offset, 0.0f, 0.0f, 00.f);
placesTableView.contentInset = inset;
placesTableView.contentOffset = CGPointMake(0.0f, -offset);
placesTableView.scrollIndicatorInsets = inset;
}
The enableInset method is called in viewDidLayoutSubviews.
then when i call disableInset and enableInset, the UITableView can not be scrolled anymore.
What did i did wrong ? Any idea where i can look for some answer ?
EDIT :
If it can help, i added the project on github :
https://github.com/Loadex/PlaceViewer
To re-produce the bug :
Scroll the list, tap on the search bar, hit cancel, try to scroll again the list.
Weirdly click on the filter button, when the UIActionSheet is dismissed, the scroll is working again.
While looking for a solution to your problem i noticed that the bottom part of the contentInset of your placesTableView kept changing through the different states. It was 0 when in the initial state where you could see the map, and the tableView was behaving as expected. It got set to 216 when the keyboard came up after tapping the search field. I figured this was some automated communication between the tableView and the keyboard (through Notifications or something you did in PlacesQueryTableViewController). This is fine because we want the bottom inset to be set to the top of the keyboard when it appears. Now, here comes the buggy part. When I tapped the cancel button, the contentInset.bottom got set to -216.
I can't quite explain why this happens, but I suspect it has something to do with how that automatic change of the inset is implemented. I suspect that it does something like tableView.contentInset.bottom -= heightOfKeyboard, and that probably happens when the animation is finished, and not before. The source of your problem is that you change that bottom of contentInset before the animation is done, and thus before that automatic change has happened. So you're setting the bottom to 0 as soon as the user taps cancel. Then the system comes in and reduces it by the height of the keyboard, which turns out to be 216. That's what I think is happening anyway.
To fix this problem, avoid changing the bottom part of the contentInset and just change the top part. placesTableView.contentInset.top is readOnly, but if you do it like in the code below, you can get around that. I have just changed two lines of code in each method, the ones that have to do with the inset. Hopefully you see what I did.
-(void)enableInset {
NSLog(#"Enabling insets");
// Setting the tableView to overlay the map view
CGFloat offSet = [placestableViewController tableView:placestableViewController.tableView heightForRowAtIndexPath:nil] - 30.0f;
UIEdgeInsets inset = placesTableView.contentInset; // UIEdgeInsetsMake(placesMapView.frame.size.height - offSet, 0.0f, 0.0f, 0.0f);
inset.top = placesMapView.frame.size.height - offSet;
// Updating the tableView position.
placesTableView.contentInset = inset;
placesTableView.contentOffset = CGPointMake(0.0f, -(placesMapView.frame.size.height - offSet));
placesTableView.scrollIndicatorInsets = inset;
placesMapView.hidden = NO;
[placestableViewController loadObjects];}
- (void)disableInset {
NSLog(#"Disable insets");
CGFloat offset = self.navigationController.navigationBar.frame.size.height + [UIApplication sharedApplication].statusBarFrame.size.height;
UIEdgeInsets inset = placesTableView.contentInset;// UIEdgeInsetsMake(offset, 0.0f, 0.0f, 0.0f);
inset.top = offset;
placesTableView.contentInset = inset;
placesTableView.contentOffset = CGPointMake(0.0f, -offset);
placesTableView.scrollIndicatorInsets = inset;
// Hidding the map while in search
placesMapView.hidden = YES;}
.
BTW, if you want to know how I found the contentInset values at the different states, it's quite simple. What I did was to set myself as the delegate of placesTableView in - (void)viewDidLoad like this placesTableView.delegate = self;. I also had to change the #interfacestatement to #interface KrackMapViewController () <UITableViewDelegate> to say that we conform to the UITableViewDelegate. Now, here's the trick: UITableViewDelegate conforms to UIScrollViewDelegate. That means we can implement methods of the scroll view delegate. The one that is particularly interesting is this one:
- (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView
{
NSLog(#"Did scroll with insetTop: %f, insetBottom: %f, contentOffset: %f", placesTableView.contentInset.top,
placesTableView.contentInset.bottom,
placesTableView.contentOffset.y);
}
That lets us know when you start dragging the tableView, and in there simply NSLog out the different parameters.
I hope this was helpful. Let me know if this works for you.
After a few research here is what I noticed:
The TableView when the keyboard is released is not scrolling because the tableview seems to believe that it is displayed on the entire screen. I tried to add more data in the tableview and we can see that the view is scrolling a little.
What I believe happened is that when the keyboard is hidden, some automatic calls are done and messing with what I set in my enableInset method. Here is my working solution:
I registered for the hideKeyboard event:
[[NSNotificationCenter defaultCenter] addObserver:self
selector:#selector(keyboardDidHide:)
name:UIKeyboardDidHideNotification
object:nil];
and in the callback I called enableInset:
- (void)keyboardDidHide: (NSNotification *) notif{
[self enableInset];
}
And the view is scrolling back again.
Any explanation about this are welcome.

Strange Behavior in Scrolling UIScrollView to UITextView hidden by keyboard

I have looked at a number of posts here on scrolling and unhiding a UITextField and believed that the same code should work for a UITextView, but that seems not to be the case. The first issue I encountered was that the sample app I have is an iPad app supporting landscape orientation only. The keyboard size returned from the notification had the height and width of the keyboard reversed.
Next, while I can get the scrollview to scroll the textview, it does not reveal all of it and in fact the amount of the textview that is shown is dependent on where I tap in the textview. It is more like it is scrolling to where the cursor will be which is not what I want.
Here is the code I am using. It was taken from an example, the only real change is that a UITextView is used instead of a UITextField. If the only thing I do is to replace the textview with a textfield it works fine.
- (void)keyboardWasShown:(NSNotification*)aNotification
{
NSDictionary* info = [aNotification userInfo];
CGSize kbSize = [[info objectForKey:UIKeyboardFrameBeginUserInfoKey] CGRectValue].size;
UIEdgeInsets contentInsets = UIEdgeInsetsMake(0.0, 0.0, kbSize.width, 0.0);
_myscrollview.contentInset = contentInsets;
_myscrollview.scrollIndicatorInsets = contentInsets;
CGRect aRect = self.view.frame;
aRect.size.height -= kbSize.width;
DebugLog(#"textview = %#", _textview);
if (!CGRectContainsPoint(aRect, _textview.frame.origin) ) {
CGPoint scrollPoint = CGPointMake(0.0, _textview.frame.origin.y-kbSize.width);
[_myscrollview setContentOffset:scrollPoint animated:YES];
}
}
If you are just wanting to scroll to the top of your UITextView you can do that by
[textview scrollRectToVisible:CGRectMake(0,0,1,1) animated:YES];
Try messing with the below method. Perhaps set it to the height of the textView. Can you post a screenshot of the issue?
- (void)textViewDidChangeSelection:(UITextView *)textView
{
[textView setSelectedRange:NSMakeRange(0, 0)];
}
Apple has provided the correct method here (check the Listing 4-1). It works for both UITextField and UITextView. We do not need to edit the range or anything else. It should be the scrollview that needs handling. The one marked as 'Answered' may work, but we better follow the creator's method!

iOS scrollView setContentOffset "shimmy"

I have a scrollView with multiple textFields, which tracks the active field and makes sure it is visible when the keyboard pops up. It all works well, but when I tab from the 3rd to 4th textField, I get a little up and down "shimmy" before the textField ends up in the right place. Any suggestions?
-(void)keyboardDidShow:(NSNotification *)notification
{
if (keyboardIsShown)return;
NSDictionary* info=[notification userInfo];
// get keyboard size
CGSize keyboardSize=[[info objectForKey:UIKeyboardFrameBeginUserInfoKey]CGRectValue].size;
//Set scrollview insets to make room for keyboard
UIEdgeInsets contentInsets=UIEdgeInsetsMake(0.0, 0.0, keyboardSize.height, 0.0);
scrollView.contentInset=contentInsets;
scrollView.scrollIndicatorInsets=contentInsets;
//scroll the active text field into view
CGRect viewFrame=self.view.frame;
viewFrame.size.height-=keyboardSize.height;
int fieldHeight=self.currentTextField.bounds.size.height;
CGFloat navHeight=self.navigationController.navigationBar.frame.size.height;
CGPoint viewPoint=CGPointMake(0.0, self.currentTextField.frame.origin.y+fieldHeight);
if (!CGRectContainsPoint(viewFrame, viewPoint)) {
//scroll to make sure active field is showing
CGPoint scrollPoint=CGPointMake(0.0, viewPoint.y-keyboardSize.height+navHeight);//+navHeight
[scrollView setContentOffset:scrollPoint animated:YES];
}
}
-(void)showActiveField
{
//this makes sure that activeField shows when selecting another field after initial keyboard show
int fieldHeight=self.currentTextField.bounds.size.height;
CGPoint viewPoint=CGPointMake(0.0, self.currentTextField.frame.origin.y+fieldHeight);
CGRect viewFrame=self.view.frame;
int inset=scrollView.contentInset.bottom;
if (!CGRectContainsPoint(viewFrame, viewPoint)) {
//scroll to make sure active field is showing
CGPoint scrollPoint=CGPointMake(0.0, viewPoint.y-inset);
[scrollView setContentOffset:scrollPoint animated:YES];
}
}
Where do you set keyboardIsShown? Don't you want to do that Right after you check if it is already set?
And then: is the 4th field near the end of the scrollview and you have bounce scroll set?

Resources