UITextView is not scrolled to top when loaded - ios

When I have text that does not fill the UITextView, it is scrolled to the top working as intended. When there is more text than will fit on screen, the UITextView is scrolled to the middle of the text, rather than the top.
Here are some potentially relevant details:
In viewDidLoad to give some padding on top and bottom of UITextView:
self.mainTextView.textContainerInset = UIEdgeInsetsMake(90, 0, 70, 0);
The UITextView uses auto layout to anchor it 20px from top, bottom and each side of the screen (done in IB) to allow for different screen sizes and orientations.
I can still scroll it with my finger once its loaded.
EDIT
I found that removing the auto layout constraints and then fixing the width only seems to fix the issue, but only for that screen width.

add the following function to your view controller class...
Swift 3
override func viewDidLayoutSubviews() {
self.mainTextView.setContentOffset(.zero, animated: false)
}
Swift 2.1
override func viewDidLayoutSubviews() {
self.mainTextView.setContentOffset(CGPointZero, animated: false)
}
Objective C
- (void)viewDidLayoutSubviews {
[self.mainTextView setContentOffset:CGPointZero animated:NO];
}

UITextView is a subclass of UIScrollView, so you can use its methods. If all you want to do is ensure that it's scrolled to the top, then wherever the text is added try:
[self.mainTextView setContentOffset:CGPointZero animated:NO];
EDIT: AutoLayout with any kind of scrollview gets wonky fast. That setting a fixed width solves it isn't surprising. If it doesn't work in -viewDidLayoutSubviews then that is odd. Setting a layout constraint manually may work. First create the constraints in IB:
#property (weak, nonatomic) IBOutlet NSLayoutConstraint *textViewWidthConstraint;
#property (weak, nonatomic) IBOutlet NSLayoutConstraint *textViewHeightConstraint;
then in the ViewController
-(void)updateViewConstraints {
self.textViewWidthConstraint.constant = self.view.frame.size.width - 40.0f;
self.textViewHeightConstraint.constant = self.view.frame.size.height - 40.0f;
[super updateViewConstraints];
}
May still be necessary to setContentOffset in -viewDidLayoutSubviews.
(Another method would be to create a layout constraint for "'equal' widths" and "'equal' heights" between the textView and its superView, with a constant of "-40". It's only 'equal' if the constant is zero, otherwise it adjusts by the constant. But because you can only add this constraint to a view that constraints both views, you can't do this in IB.)
You may ask yourself, if I have to do this, what's the point of AutoLayout? I've studied AutoLayout in depth, and that is an excellent question.

Swift
self.textView.scrollRangeToVisible(NSMakeRange(0, 0))
Objective-C
[self.textView scrollRangeToVisible:(NSMakeRange(0, 0))];

i had same issue! Reset to suggested constrains and just put (y offset)
#IBOutlet weak var textContent: UITextView!
override func viewDidLoad() {
textContent.scrollsToTop = true
var contentHeight = textContent.contentSize.height
var offSet = textContent.contentOffset.x
var contentOffset = contentHeight - offSet
textContent.contentOffset = CGPointMake(0, -contentOffset)
}

For iOS9 and later the textview even on viewWillAppear: is coming with CGRect(0,0,1000,1000). In order for this to work you have to call in viewWillAppear:
[self.view setNeedsLayout];
[self.view layoutIfNeeded];
// * Your code here
After that the textview will have correct CGRect data and you can perform any scrolling operation you may need.

The problem with putting code in viewDidLayoutSubviews and viewWillLayoutSubviews is that these methods are called a lot (during device rotation, resizing views etc ...). If you're reading something from text view, and you rotate the device, you expect that the part of the content you're viewing stays on screen. You do not expect that it scrolls back to top.
Instead of scrolling the content to top, try to keep text view's scrollEnabled property set to NO (false), and turn it back on in viewDidAppear.

If you don't wanna mess with constraints:
override func updateViewConstraints() {
super.updateViewConstraints()
}
override func viewDidLayoutSubviews() {
self.textLabel.setContentOffset(CGPointZero, animated: false)
}

This is an interesting bug. In our project, this is only occurring on devices with an iPhone 5-size screen. It appears that the textview contentOffset changes at some point during the view controller lifecycle. In viewDidLoad and viewWillAppear the textview's contentOffset is 0,0, and by viewDidAppear it's changed. You can see it happening in viewWillLayoutSubviews. Constraints appear to be set up correctly.
This will ensure you don't call a scrolling method unless it's needed:
if textView.contentOffset.y > 0 {
textView.contentOffset = CGPoint(x: 0, y: 0)
// Or use scrollRectToVisible, scrollRangeToVisible, etc.
}

Swift
override func viewDidLoad() {
super.viewDidLoad()
textView.isScrollEnabled = false
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
textView.isScrollEnabled = true
}

For me this works in a different way, I tried all things mentioned above but none of the worked in func viewWillAppear(_ animated: Bool). Which eventually makes textView scrolled up, and in func viewDidAppear(_ animated: Bool) it would scroll after screen appeared.
Below worked for me but got some constraint related issue with keyboard up and down.
override func viewDidLayoutSubviews() {
self.textView.setContentOffset(.zero, animated: false)
}
Below worked as expectation:
override func viewDidLoad() {
super.viewDidLoad()
self.textView.scrollsToTop = true
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
self.view.layoutIfNeeded()
self.textView.setContentOffset(.zero, animated: false)
}

David Rectors answer in Objective C:
#import "TopTextView.h"
#implementation TopTextView
bool scrolled = NO;
- (void) layoutSubviews
{
[super layoutSubviews];
if (!scrolled) {
[self setContentOffset:CGPointMake(0, 0) animated:NO];
scrolled = YES;
}
}
#end

It seems like a terrible idea to handle this issue in code in the view controller because: A. The view controller isn't making any mistake or doing anything wrong, and B, if you have more than one view controller with a wrongly scrolled text view, you end up with redundant code. The solution should be to write code that exists in the text view class. My solution works with Interface Builder where I simply select a custom class for the UITextView and use this class:
import Foundation
import UIKit
class TopTextView: UITextView {
var scrolled = false
override func layoutSubviews() {
super.layoutSubviews()
if scrolled { return }
setContentOffset(.zero, animated: false)
scrolled = true
}
}
This worked for me. I happen to have a view controller with a child view with a UITextView as a child of that view, not with a UITextView as the child of the view controller. I don't know how well this works if the text view is under top or bottom bars but since no edge insets are touched, this should work.

In my case I had to do it like this:
textView.setContentOffset(CGPoint(x: 0, y: -self.textView.adjustedContentInset.top), animated: false)
because the texview was underneath the navigation bar and had an adjusted inset

Related

Nested scrollview with Vertical scrolling for both [duplicate]

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.

UITextView starts at Bottom or Middle of the text

I'll get right to it. I have a UItextView placed in my view that when needs to scroll to see all the text (when a lot of text is present in the textView) the textView starts in the middle of the text sometimes and the bottom of the text other times.
Editing is not enabled on the textView. I need a way to force the textView to start at the top, every time. I saw some questions somewhat like this where other people used a content offset, but I do not really know how that works or if it would even be applicable here.
Thanks for your help.
That did the trick for me!
Objective C:
[self.textView scrollRangeToVisible:NSMakeRange(0, 0)];
Swift:
self.textView.scrollRangeToVisible(NSMakeRange(0, 0))
Swift 2 (Alternate Solution)
Add this override method to your ViewController
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
textView.setContentOffset(CGPointZero, animated: false)
}
Swift 3 & 4 (syntax edit)
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
textView.contentOffset = .zero
}
All of the answers above did not work for me. However, the secret turns out to be to implement your solution within an override of viewDidLayoutSubviews, as in:
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
welcomeText.contentOffset = .zero
}
HTH :)
In Swift 2
You can use this to make the textView start from the top:
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
myTextView.setContentOffset(CGPointZero, animated: false)
}
Confirmed working in Xcode 7.2 with Swift 2
Try this below code -
if ( [self respondsToSelector:#selector(setAutomaticallyAdjustsScrollViewInsets:)]){
self.automaticallyAdjustsScrollViewInsets = NO;
}
Or you can also set this property by StoryBoard -
Select ViewController then select attributes inspector now unchecked Adjust Scroll View Insets.
For Swift >2.2, I had issues with iOS 8 and iOS 9 using above methods as there are no single answer that works so here is what I did to make it work for both.
override func viewWillAppear(animated: Bool) {
super.viewWillAppear(animated)
if #available(iOS 9.0, *) {
textView.scrollEnabled = false
}
self.textView.scrollRangeToVisible(NSMakeRange(0, 0))
}
override func viewDidAppear(animated: Bool) {
super.viewDidAppear(animated)
if #available(iOS 9.0, *) {
textView.scrollEnabled = true
}
}
Update your UINavigationBar's translucent property to NO:
self.navigationController.navigationBar.translucent = NO;
This will fix the view from being framed underneath the navigation bar and status bar.
If you have to show and hide the navigation bar, then use below code in your viewDidLoad
if ([self respondsToSelector:#selector(edgesForExtendedLayout)])
self.edgesForExtendedLayout = UIRectEdgeNone; // iOS 7 specific
Hope this helps.
Xcode 7.2 7c68; IOS 9.1
My ViewController which contains UITextView is complicated, and changed a lot during the project (IDE version changed maybe 2~3 times too).
I've tried all above solutions, if you encounter the same issue, be PATIENT.
There are three possible 'secret codes' to solve:
textView.scrollEnabled = false
//then set text
textView.scrollEnabled = true
textView.scrollRangeToVisible(NSMakeRange(0, 0))
textView.setContentOffset(CGPointZero, animated: false)
And there are two places you can put those codes in:
viewDidLoad()
viewDidLayoutSubviews()
Combine them, you'll get 3*2=6 solutions, the correct combination depends on how complicated you ViewController is (Believe me, after delete just a view above textView, I need to find a new combination).
And I found that:
When put 'secret codes' in viewDidLayoutSubviews(), but textView.text = someStrings in viewDidLoad(), the content in textView will 'shake' sometimes. So, put them in the same place.
Last word: try ALL combinations, this is how I solve this stupid bug more than three times during two months.
With a lot of testing, i found must add below in viewWillLayoutSubviews() function to make sure the UITextView show up from the very beginning:
override func viewWillLayoutSubviews() {
super.viewWillLayoutSubviews()
textViewTerms.scrollRangeToVisible(NSMakeRange(0, 0))
}
UITextView scrolling seems to be a problem to a lot of people. Gathering from the answers here around (especially this) and the Apple Developer documentation, using some of my own wit, here is a solution that works for me. You can modify the code to suit your needs.
My use case is as follows: the same UITextView is used for different purposes, displaying varying content in different circumstances. What I want is that when the content changes, the old scroll position is restored, or at times, scrolled to the end. I don't want too much animation when this is done. Especially I don't want the view to animate like all the text was new. This solution first restores the old scroll position without animation, then scrolls to the end animated, if so desired.
What you need to do (or should I say can do) is extend UITextView as follows:
extension UITextView {
func setText(text: String, storedOffset: CGPoint, scrollToEnd: Bool) {
self.text = text
let delayInSeconds = 0.001
let popTime: dispatch_time_t = dispatch_time(DISPATCH_TIME_NOW, Int64(delayInSeconds * Double(NSEC_PER_SEC)))
dispatch_after(popTime, dispatch_get_main_queue(), {
self.setContentOffset(storedOffset, animated: false)
if scrollToEnd && !text.isEmpty {
let popTime: dispatch_time_t = dispatch_time(DISPATCH_TIME_NOW, Int64(delayInSeconds * Double(NSEC_PER_SEC)))
dispatch_after(popTime, dispatch_get_main_queue(), {
self.scrollRangeToVisible(NSMakeRange(text.lengthOfBytesUsingEncoding(NSUTF8StringEncoding) - 1, 0))
})
}
})
}
}
What this does is it updates the text, then uses a stored value of the UITextView.contentOffset property (or anything you pass as a parameter), and sets the offset of the view accordingly. If desired, after this, it scrolls to the end of the new, potentially changed content.
I'm new to iOS programming and I don't know why it works so well it does, if someone has some information on this it would be nice to know. Also the approach may not be perfect so I'm open to improvement ideas as well.
And of course thanks to NixonsBack for posting the answer behind the link above.
My first post :), cheers!
Put this one line of code in ViewDidLoad
self.automaticallyAdjustsScrollViewInsets = false
The following code should give you effect you want.
[self.scrollView setContentOffset:CGPointMake(0, -self.scrollView.contentInset.top) animated:YES];
You'll need to replace "self.scrollView" with the name of your scroll view. You should put this code in after you've set the text of the scroll view.
This worked for me:
override func viewWillLayoutSubviews() {
super.viewWillLayoutSubviews()
textView.scrollRectToVisible(CGRect(origin: CGPointZero, size: CGSizeMake(1.0, 1.0)), animated: false)
}
This worked for me with Xcode 8.3.3:
-(void)viewWillLayoutSubviews
{
[super viewWillLayoutSubviews];
[self.txtQuestion scrollRangeToVisible:NSMakeRange(0, 0)];
}
Create an outlet for your UITextView in the ViewController.swift file. In the ViewDidLoad section put the following:
Swift:
self.textView.contentOffset.y = 0
I have tried:
self.textView.scrollRangeToVisible(NSMakeRange(0, 0))
I translated zeeple's answer to MonoTouch/Xamarin (C#).
public override void ViewDidLayoutSubviews()
{
base.ViewDidLayoutSubviews();
myForm.SetContentOffset(new CoreGraphics.CGPoint(0,0), animated: false);
}
I had to implement two answers here to get my view working as I want:
From Juan David Cruz Serrano:
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
textView.setContentOffset(CGPoint.zero, animated: false)
}
And from Murat Yasar:
automaticallyAdjustsScrollViewInsets = false
This gave a UITextView that loads with the scroll at the very top and where the insets are not changed once scrolling starts. Very strange that this is not the default behaviour.
To force the textView to start at the top every time, use the following code:
Swift 4.2:
override func viewDidLayoutSubviews() {
textView.setContentOffset(CGPoint(x: 0, y: 0), animated: false)
}
Objective-C:
- (void)viewDidLayoutSubviews {
[self.yourTextView setContentOffset:CGPointZero animated:NO];
}
Swift 4.2 & Swift 5
set content offset both before and after setting the text. (on the main Thread)
let animation = false //or whatever you want
self.mainTextView.setContentOffset(.zero, animated: animation)
self.mainTextView.attributedText = YOUR_ATTRIBUTED_TEXT
self.mainTextView.setContentOffset(.zero, animated: animation)
In my case I was loading a textView in a Custom tableview cell. Below is what I did to make sure the text in a textview loads at the top of the text in my textview in my custom cell.
1.) In storyboard, set the textview ScrollEnabled = false by unchecking the button.
2.) You set the isScrollEnabled to true on the textview after the view loads. I set mine in a small delay like below:
override func awakeFromNib() {
super.awakeFromNib()
let when = DispatchTime.now() + 1
DispatchQueue.main.asyncAfter(deadline: when){
self.textView.isScrollEnabled = true
}
}
Regardless, if you are in my situation or not, try setting scrollEnabled to false and then when the view loads, set scrollEnabled to true.

UITableViewWrapperView and UITableView size differs with autolayout

I am building a chat. Everything seem to be quite ok but I bumped into sort of 'buggy' problem.
i got UIViewController with UITextView bar for entering message and UITableView.
They are in this constraint: "V:|-(64)-[chatTable][sendMessageBar]-(keyboard)-|".
When the keyboard is not out - the constant of this constraint is 0. and after keyboard is out - i increase the constant to keyboard height.
when the keyboard is not out:
self.table.contentSize = (375.0,78.5)
self.table.bounds = (0.0,-490.0,375.0,568.5)
self.table.frame = (0.0,64.0,375.0,568.5)
self.table.subviews[0].frame (UITableViewWrapperView) = (0.0,0.0,375.0,568.5)
self.table.subviews[0].frame (UITableViewWrapperView) = (0.0,0.0,375.0,568.5)
and when the keyboard comes out:
self.table.contentSize = (375.0,78.5)
self.table.bounds = (0.0,-274.0,375.0,352.5
self.table.frame = (0.0,64.0,375.0,352.5)
self.table.subviews[0].frame (UITableViewWrapperView) = (0.0,-137.5,375.0,137.5)
self.table.subviews[0].frame (UITableViewWrapperView) = (0.0,0.0,375.0,137.5)
So the UITableViewWrapperView, after I increase constraints constant, differs in size to its superview - UITableView. Is there a way to fix this ? I would assume that UITableViewWrapperView would change its frame and bounds according to UITableView but it does not.
Any ideas where is the problem or how could I work around it ?
ADDING:
After some more research - it seems that it happens somewhere between viewWillLayoutSubviews and viewDidLayoutSubviews. It is kinda weird tho:
override func viewWillLayoutSubviews() {
println("WrapperView Frame :991: \(self.table.subviews[0].frame)") \\ WrapperView Frame :991: (0.0,0.0,375.0,568.5)
super.viewWillLayoutSubviews()
println("WrapperView Frame :992: \(self.table.subviews[0].frame)") \\ WrapperView Frame :992: (0.0,0.0,375.0,568.5)
}
override func viewDidLayoutSubviews() {
println("WrapperView Frame :6: \(self.table.subviews[0].frame)") \\ WrapperView Frame :6: (0.0,-137.5,375.0,137.5)
super.viewDidLayoutSubviews()
println(">> viewDidLayoutSubviews")
}
So it seems that something happens there that messes up the UITableViewWrapperView
The following fixed it for me:
func fixTableViewInsets() {
let zContentInsets = UIEdgeInsetsZero
tableView.contentInset = zContentInsets
tableView.scrollIndicatorInsets = zContentInsets
}
override func viewWillLayoutSubviews() {
super.viewWillLayoutSubviews()
fixTableViewInsets()
}
I discovered that at viewWillAppear() that the insets were all 0. But at viewDidAppear(), they had been modified to apparently offset for navigation bar, etc. This makes the UITableViewWrapperView different from the UITableView.
I changed the insets in its own routine so that it was easier to experiment with calling it from different places. The viewWillLayoutSubviews() let it get changed before being presented - placing the change in viewDidAppear() caused the table to jerk.
I ran into this today and while the fix suggested by #anorskdev works nicely, it seems that the root cause of the issue is the automaticallyAdjustsScrollViewInsets property of UIViewController, which is true by default. I turned it off in my storyboard and the problem went away. Look for the "Adjust Scroll View Insets" checkbox in the View Controller inspector and make sure it's unchecked.
It seems that it is a bug (fighting with this bug took all day for me)
Finally this workaround helped:
for (UIView *subview in tableView.subviews)
{
if ([NSStringFromClass([subview class]) isEqualToString:#"UITableViewWrapperView"])
{
subview.frame = CGRectMake(0, 0, tableView.bounds.size.width, tableView.bounds.size.height);
}
}
After small investigation I have found this solution with setting all the safeAreaInsets and layoutMargins on the UITableView to zero:
Swift 4 snipset:
class CustomTableView: UITableView {
override var safeAreaInsets: UIEdgeInsets {
get {
return .zero
}
}
override var layoutMargins: UIEdgeInsets {
get {
return .zero
}
set {
super.layoutMargins = .zero
}
}
}
The main problem is safeAreaInsets introduced in tvOS 11.0 - the UITableViewWrapperView just took the properties from the parent view (UITableView) and renders the content with safeAreaInsets.
I was facing the same issue on tvOS 11.3, and neither of suggestions related with zero insets or scroll disable did the job, except looping through tableView's subviews and setting the UITableViewWrapperView's frame to the tableView's frame.
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
for view in tableView.subviews {
if String(describing: type(of: view)) == "UITableViewWrapperView" {
view.frame = CGRect(x: 0, y: 0, width: tableView.bounds.size.width, height: tableView.bounds.size.height)
}
}
}
In iOS 11 UITableViewWrapperView has gone, so this problem may occur only on later iOS versions. I faced it on iOS10 when I pushed custom UIViewController in UINavigationController stack.
So, the solution is to override property automaticallyAdjustsScrollViewInsets in custom view controller like this:
override var automaticallyAdjustsScrollViewInsets: Bool {
get {
return false
}
set {
}
}
Objective C version of this answer given by anorskdev
- (void) viewWillLayoutSubviews {
[super viewWillLayoutSubviews];
[tableView setContentInset:UIEdgeInsetsZero];
[tableView setScrollIndicatorInsets:UIEdgeInsetsZero];
}
edit: Turning off automaticallyAdjustsScrollViewInsets on the hosting ViewController, as suggested by Steve Roy in this answer, also worked and is the one I went with, as it seems cleaner to disable the behaviour rather than correcting it afterwards.

How to scroll a UIScrollView to the bottom in viewWillAppear, when using autolayout,without a visual animation

Here is the problem:
I have a bunch of views that I put in a UIScrollView. The size and position of those subviews are defined by constraints. This works perfectly, and scrolling works too. So far so good.
However, I want my scrollview to scroll all the way to the bottom when I show the viewcontroller on screen for the first time, and this is where trouble starts. In order to know where the bottom is I need to know the position and size of the lowest element in my subviews. Should be easy too (since I have the reference to that UIView somewhere): get the frame of the UIView and voila.
I want to scroll the scrollview to the bottom before it appears on screen (so basically, in viewWillAppear:), but the constraints only get evaluated after viewWillAppear and before viewDidAppear: is called.
Getting the UIView frame in viewWillAppear gives me a zero sized CGRect. Doing the same in viewDidAppear gives me the correct CGRect. But viewDidAppear is too late for me, since the scrollview is on screen already so you see the content moving up.
Does anyone have a good solution for this? I tried putting the code in viewDidLayoutSubviews but that doesn't work either.
The problem with viewWillAppear is that it is called before the layout happens. You have to adjust the scroll view's content offset after it is laid out - in viewDidLayoutSubviews method. in Here is my solution:
#property (nonatomic, assign) BOOL shouldScrollToBottom;
- (void)viewDidLoad
{
[super viewDidLoad];
_shouldScrollToBottom = YES;
}
- (void)viewDidLayoutSubviews
{
[super viewDidLayoutSubviews];
// Scroll table view to the last row
if (_shouldScrollToBottom)
{
_shouldScrollToBottom = NO;
[_scrollView setContentOffset:CGPointMake(0, CGFLOAT_MAX)];
}
}
I was running into this problem too! The view didn't scroll to the desired page before viewDidAppear(). It had multiple subviews to be layed out by viewDidLayoutSubviews(), so doing it once in that function, like Thomas suggested, didn't work for me either. However, the viewDidLayoutSubviews() approach works, if you use a little trick:
You need to attempt to scroll to the desired point every time viewDidLayoutSubviews() is called during the creation of the view. Then, after the view is displayed, you don't want this to be called anymore. So you add a control variable that is changed in viewDidAppear(), after the initial layout. Here's the full example:
var needsFirstScroll = true
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
if(needsFirstScroll) {
var frame = CGRect() // placeholder, do your frame calculation here
scrollView.scrollRectToVisible(frame, animated: false)
}
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
needsFirstScroll = false
}
This is what I did and it works for me (cell height computed by autolayout):
class MessagesTableViewController: UITableViewController
{
var shouldScrollToBottom = false
override func viewDidLoad()
{
super.viewDidLoad()
...
shouldScrollToBottom = true
}
override func viewDidLayoutSubviews()
{
super.viewDidLayoutSubviews()
if(shouldScrollToBottom == true)
{
shouldScrollToBottom = false
goToBottom()
}
}
private func goToBottom()
{
if(data.count == 0)
{
return
}
let indexPath = NSIndexPath(forRow: tableView.numberOfRowsInSection(0)-1, inSection: 0)
tableView.scrollToRowAtIndexPath(indexPath, atScrollPosition: .Bottom, animated: false)
}
}
The solution I finally came up with was to:
Store all subviews of the scrollview in 1 contentview and not directly as child of the scrollview
observe when the bounds of that contentview change
each time they change do a scrollRectToVisible to the bottom of the scrollview
disable the auto-scroll-to-bottom as soon as a scrollViewWillBeginDragging delegate call is detected so the auto-scrolling stops when the user starts moving the scrollview
I found a solution to this problem, this might be useful to future visitors. You need to load the tableView in viewWillAppear before trying to scroll.
- (void)viewWillAppear:(BOOL)animated
{
[super viewWillAppear:animated];
[self.tableView reloadData];
[self.tableView scrollToRowAtIndexPath:[NSIndexPath indexPathForRow:[self.tableView.numberOfRowsInSection:0] inSection:0] atScrollPosition:UITableViewScrollPositionBottom animated:animated];
}

Disable flashing scroll bar of a UITableViewController?

I have a UITableViewController, and I'd like to make it not flash the vertical scroll bar when I go back from a push action segue on one of it's cells (popping the view controller and going back to the UITableViewController).
It seems that, if the table has many rows (mine has around 20 with 60 points height each, so bigger than the screen), when I go back, it always flashes the vertical scroll bar once to show where it is in the table. However, I don't want that to happen, but I do want to keep the scrollbar around so it shows when the user scrolls. Therefore, disabling it completely is not an option.
Is this default behavior and can I disable it temporarily?
There is a simpler solution that doesn't require avoiding using a UITableViewController subclass.
You can override viewDidAppear: as stated by http://stackoverflow.com/users/2445863/yonosoytu, but there is no need to refrain from calling [super viewDidAppear:animated]. Simply disable the vertical scrolling indicator before doing so, and then enable it back afterwards.
- (void)viewDidAppear:(BOOL)animated {
self.tableView.showsVerticalScrollIndicator = NO;
[super viewDidAppear:animated];
self.tableView.showsVerticalScrollIndicator = YES;
}
If you're using Interface Builder, you can disable the Shows Vertical Indicator option on the tableView for your UIViewController and enable it in code as shown above.
To get Cezar's answer to work for iOS10 I had to include a (sizeable) delay before re-enabling the scroll indicator. This looks a bit strange if someone tries to scroll before the second is up, so you can re-enable the scroll indicator as soon as someone scrolls.
override func viewDidAppear(_ animated: Bool) {
tableView.showsVerticalScrollIndicator = false
super.viewDidAppear(animated)
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
self.tableView.showsVerticalScrollIndicator = true
}
}
override func scrollViewDidScroll(_ scrollView: UIScrollView) {
if !tableView.showsVerticalScrollIndicator {
tableView.showsVerticalScrollIndicator = true
}
}
Actually, on thinking about it, you don't even need the delay, just do this:
override func viewDidAppear(_ animated: Bool) {
tableView.showsVerticalScrollIndicator = false
super.viewDidAppear(animated)
}
override func scrollViewDidScroll(_ scrollView: UIScrollView) {
if !tableView.showsVerticalScrollIndicator {
tableView.showsVerticalScrollIndicator = true
}
}
Update: Please, look at Cezar’s answer below, which gives a nice workaround without any of the drawbacks of my proposals.
According to the documentation it is a behaviour of UITableViewController:
When the table view has appeared, the controller flashes the table view’s scroll indicators. The UITableViewController class implements this in the superclass method viewDidAppear:.
So I think you have two options:
You can avoid using UITableViewController and start using a naked UIViewController. Rebuilding the functionality of UITableViewController from UIViewController is not that hard (you can follow this old article as reference).
Override viewDidAppear: and don’t call [super viewDidAppear:animated]. The problem here is that you don’t know what else does UITableViewController do when viewDidAppear: is called, so you might break something.

Resources