I've searched all over S.O. for a simple solution to get the cursor's current position, then scroll to it (assuming the keyboard is visible). Most seem overly complicated and/or ineffective in certain situations. How can I make scrolling work every time, whether the cursor is below a keyboard or not?
1) Be sure your UITextView's contentInsets are properly set & your textView is already firstResponder before doing this. The contentInset property tells the textView where the visible area is for the user. If the keyboard is visible, be sure the textView.contentInset.bottom property has been set to the top border of the keyboard otherwise the textView may scroll to a space not visible, behind the keyboard.
See this S.O. post for more information: What's the UIScrollView contentInset property for?
2) After my the insets are ready to go, and the textView is firstResponder, I call the following function:
private func scrollToCursorPositionIfBelowKeyboard() {
let caret = textView.caretRectForPosition(textView.selectedTextRange!.start)
let keyboardTopBorder = textView.bounds.size.height - keyboardHeight!
// Remember, the y-scale starts in the upper-left hand corner at "0", then gets
// larger as you go down the screen from top-to-bottom. Therefore, the caret.origin.y
// being larger than keyboardTopBorder indicates that the caret sits below the
// keyboardTopBorder, and the textView needs to scroll to the position.
if caret.origin.y > keyboardTopBorder {
textView.scrollRectToVisible(caret, animated: true)
}
}
Optional: If you just want to scroll to the cursor's current position (assuming the textView is currently firstResponder and contentInset has been properly set before now), just call:
private func scrollToCursorPosition() {
let caret = textView.caretRectForPosition(textView.selectedTextRange!.start)
textView.scrollRectToVisible(caret, animated: true)
}
Extra Information: In order to set the textView scroll bars to the proper height, modify the scrollIndicatorInsets by doing something like:
// This is not relative to the coordinate plane. You simply set the `.bottom` property
// as if it were a normal height property. The textView does the rest for you.
textView.contentInset.bottom = keyboardHeight
textView.scrollIndicatorInsets = textView.contentInset // Matches textView's visible space.
Related
I have a UIScrollView with a couple of subviews, one of them is a UIStackView that can be hidden/displayed when a button is pressed.
When the UIStackView is hidden I want to disable scrolling on the UIScrollView since the content will never have enough height that justifies scrolling being enabled. To achieve that, I'm using:
isScrollEnabled.toggle()
Problem is, when doing this, the whole view gets dragged up, as if I was scrolling down, causing certain elements to be hidden behind the navigationBar and the statusBar.
I tried to fix that using the following piece of code:
if let navigationBarHeight = vcDelegate?.navigationController?.navigationBar.frame.height {
if let statusBarHeight = window?.windowScene?.statusBarManager?.statusBarFrame.height {
let totalHeight = navigationBarHeight + statusBarHeight
setContentOffset(.init(x: 0, y: -totalHeight), animated: true)
}
}
And that does actually work correctly when the contentOffset.y is >= a certain value that I can't precisely specify (because, for some reason, printing the value of contentOffset.y only shows a value that is != 0 when I scroll all the way to the bottom of the view.)
When the contentOffset.y is < than that mystery value, disabling the scroll will cause the following behavior:
The view will be dragged up
The view will be dragged down (because I call the setContentOffset function)
My bottom line question is: why does the view get dragged up when I disable scrolling? Could it be a constraint issue, some "background" offset change that I should be aware of or something else?
So I'm pretty puzzled right now because my UIScrollView is acting pretty weirdly.
Basically, I have a UIScrollView that is about twice the height of an iPhone 6 that I am using to display some graphs using iOS Charts and so I've set it to be able to scroll and bounce vertically, but neither scroll nor bounce horizontally. All of the graphs, and some additional UITextFields and UILabels are embedded on a separate "body view" that is aligned with the frame of the UIScrollView as seems to be common practice. Thus, the hierarchy of my views looks like this:
This worked well until I noticed today that when I press a specific UITextField on this UIScrollView, which triggers a UIPickerView, all of the sudden my scroll view starts to allow horizontal bouncing. This behavior does not occur for the two other UITextField's on the body view when they are tapped.
I've checked all of the code that is being triggered by tapping on the affected text field, and nothing is directly editing the frames or bounds of any UI objects. In fact, the only function called when the text field is tapped on is the textFieldDidBeginEditing. I've attached the code for this function below, but I am fairly certain it is not the problem.
My next suspicion was that the UIPickerView popping up has been messing with the dimensions of my scroll view and/or it's embedded view. I'm not quite sure if this is possible/probable, but this whole thing has left me pretty stumped.
Here's my code:
func textFieldDidBeginEditing(_ textField: UITextField) {
if textField == overallTimeframeTextField {
...
// Not the problematic text field
} else if textField == subjectTimeframeTextField {
...
// Also not the problematic text field
} else { // Affected text field
// Set the text of the text field
if textField.text == "" {
// This is executed in this scenario
textField.text = subjectPickerData[0]
} else {
...
}
}
}
Here is a short GIF outlining my issue. You can see me scrolling down the page, where I am not able to bounce horizontally, and then once I tag on the text field, all of the sudden the scroll view allows the bounces.
GIF
I'm pretty lost with this issue, so any help would be appreciated.
Edit
To clarify, I explicitly declare my scrollView's content size to be equal the desired height and the width of the screen that the user is on. I then set the bodyView's width to equal the same value. This is done in viewDidAppear with the following code:
// Fit the content to the screen
scrollView.contentSize = CGSize(width: UIScreen.main.bounds.width, height: 1200)
bodyView.frame.size.width = UIScreen.main.bounds.width
I also have constraints which force the scrollview and body view to both have the same width as the UIViewController's default child view (the parent of the scroll view in my hierarchy).
One interesting thing that I've noticed is that when I print the width of my scroll view and my body view when the views load, I receive the following output for iPhone 6:
385.0
385.0
This is correct as that is the width of an iPhone 6. However, when I tap on the text field, and then print the same values, I get this output:
385.0
384.0
So for some reason, my body view is one point smaller than my scroll view. I've tried setting the body view's width to be equal to the scroll view when I tap on the text field, like I do in the viewDidAppear function, but this had no effect.
In terms of the UIPickerView, I initialize a pickerview with my class instance variables like so:
var subjectPickerView = UIPickerView()
I then assign this picker view to be the input view for the text field in viewDidLoad:
textField.inputView = subjectPickerView
So I'm not sure if this makes the picker view a subview of the scroll view, but it's just replacing the keyboard in this scenario.
Thanks to #AchmadJP's comment, I tried explicitly creating an equal widths constraint between my scroll view and my body view. This seems to have solved the issue.
The reason I had not done this previously was that the body view's leading space, trailing space, top space and bottom space were constrained to be the same as those of the scroll view. Theoretically, this should have meant that the widths were equal at all times, but apparently, that is not the case.
For anyone else with the same problem, you can see this answer for the solution.
In the storyboard, I have a label and many other objects in a UIScrollView. (The UIScrollView is in the view).
I want to set the navigation bar's title to the title of the label when the label scrolls past it.
I've tried this code but it doesn't work:
func scrollViewDidScroll(_ scrollView: UIScrollView) {
if(self.lbl.frame.origin.x==60) {
self.navigationController!.navigationBar.topItem!.title = lbl.text
}
}
What should i do? I am new to Swift.
First off, I suspect you're actually trying to find the label's y value (i.e. vertical offset) as opposed to its x value, i.e. self.lbl.frame.origin.x==60.
Secondly you don't want the label's frame since that CGRect represents the label's position within the scrollview (which won't change); instead, you want the label's position within the superview.
That said, try this:
func scrollViewDidScroll(_ scrollView: UIScrollView) {
if(scrollView.convert(lbl.frame.origin, to: self.view).y <= 60) {
navigationController!.navigationBar.topItem!.title = lbl.text
}
}
There are several problems with your code:
self.lbl.frame.origin.x==60 What are the chances that this will ever be true? That's like flipping a coin and hoping it lands on its edge. The top of the label will never be exactly at a certain point.
The label's origin will never change; scrolling a scroll view doesn't change the frame of its subviews. You need to look at the scroll view's content offset (i.e. how far is it scrolled).
You need to complete your tests. What should happen when the user scrolls back the other way? Also, do you want to change the navigation item title repeatedly, every instant the user is scrolling, or just once?
I'm building a view that's very similar to the messages app - I have a subview at the bottom of the page with a UITextView in it and as the user types and reaches the end of the line the text view as well as the view containing it should expand upward.
The way I have it working is that in the textViewDidChange: method I call my layout function, and that does
CGFloat textViewWidth = 200;
CGFloat textViewHeight = [self.textView sizeThatFits:CGSizeMake(textViewWidth, 2000)].height;
[self resizeParentWithTextViewSize:CGSizeMake(textViewWidth, textViewHeight)];
// And then the resize parent method eventually calls
textView.frame = CGRectMake(10, 10, textViewWidth, textViewHeight);
The problem is that when typing at the end of line and the view expands, I end up with an arbitrary contentOffset.y of something like 10.5 on the text view so the text is all shifted up to the top of the view. Weirdly, it's alternating on every other line, so expanding the first time leaves the y content offset shifted up, then at the next line it's close to zero, then back to 10.5 on the next line, etc. (not sure if that's helpful or just a strange artifact of my values). I can set it back to zero afterwards but it looks terrible because there's a brief flash where the text has the offset value and then it gets shifted back to the middle.
I've read that it's usually better to use content insets for scroll views rather than changing the frame, but I don't get how to do that because I do need to change the frame size as well.
How can I resize the UITextView without this happening? I think I can get by with setting the text view not to be scrollable and that fixes the issue, but I'd like to understand what's going on.
The problem is that UITextView's scroll animation and your frame setting action were happened at the same time.
UITextView internally scrolls the texts you currently typing to visible when typed one more character at the end of the line or typed the new line character. But the scroll animation does not need because you are expanding the textview. Unfortunately we can't control textview's internal scroll action so the text scrolls to the top of the expanded textview weirdly. And that weird scroll makes unnecessary bottom padding too.
You can avoid this weird action very simply with overriding UITextView's setContentOffset:animated: like this.
Objective-C
- (void)setContentOffset:(CGPoint)contentOffset animated:(BOOL)animated {
[super setContentOffset:contentOffset animated:NO];
}
Swift
override func setContentOffset(_ contentOffset: CGPoint, animated: Bool) {
super.setContentOffset(contentOffset, animated: false)
}
This code avoids the auto sizing UITextView's unnecessary scroll animations and you can expand the size of the text view freely.
Setting textView.scrollable = NO lets me resize the text view without any strange offsets, that's the only way I've been able to figure out. And I guess it's not too much of a limitation for common scenarios, if you want the text view to be scrollable you probably don't need to resize it on the fly since the user can scroll around as the content changes.
I confronted the same issue: changing the UITextView's frame to fit its content had a side effect on the scroll position being wrong. The UITextView scrolled even when the contentSize was fitting the bounds.
I ended up with setting scrollEnabled to true and with rolling the content offset back if the UITextView is not actually scrollable
override var contentOffset: CGPoint {
didSet {
if iOS8 {
// here the contentOffset may become non zero whereas it shouldn't be
if !isContentScrollable && contentOffset.y > 0 {
contentOffset.y = 0
}
}
}
}
var isContentScrollable: Bool {
let result = ceil(contentSize.height) > ceil(height)
return result
}
Actually, I faced the same issue and found that actually this happens only when UITextView has any Autolayout constraints. If you will try to use UITextView without applying any Constraint then this will not happen and will work fine. but, as you apply Auto layout constraints it automatically scrolls to bottom. To deal with this I just add method
-(void)viewDidLayoutSubviews{
[super viewDidLayoutSubviews];
self.textView.contentOffset = CGPointZero;
}
I have a view that contains a UIScrollView that is designed to scroll horizontally. (So the UIScrollView is wider than the Self.View width)
Inside the UIScrollView I have a dozen UILables that I let the user cycle through adding values programmatically, when the next UILable is programmatically selected, if it is out of bounds I change the UIScrollViews offset to make sure the UILabel appears just to the left of the right side of the view.
This is how I am currently checking the UILabel position and then adjusting the offset of the UILabel.
if (positionLabel.frame.origin.x > self.view.frame.size.width)
{
[axisContainerScrollView setContentOffset:CGPointMake(positionLabel.frame.origin.x+40 - self.view.frame.size.width, axisContainerScrollView.frame.origin.y)
animated:YES];
}
else if (positionLabel.frame.origin.x > self.view.frame.size.width)
{
axisContainerScrollView setContentOffset:CGPointMake(0.0, axisContainerScrollView.frame.origin.y)
animated:YES];
}
So positionLabel origin brings back the value of its position inside the UIScrollView then I change the offset of the axisContainerScrollView. The only problem with this is that if I scroll the view across and select a UILabel whose offset is already inside the view if send the label back across to the right..
I would like to adjust this if statement so that if the UILabel is inside the bounds of self.view then I don't want to change the offset.
You could convert the coordinate space of positionLabel.frame to be the same as self.view, then you can use CGRectContainsRect function to make the comparison you're wanting to do.
something like -
// convert label frame
CGRect comparisonFrame = [axisContainerScrollView convertRect:positionLabel.frame toView:self.view];
// check if label is contained in self.view
BOOL isContainedInView = CGRectContainsRect(self.view.frame, comparisonFrame);
Now you have a BOOL, you can place in any conditional, that you can use to check.
Have you looked at [UIScrollView scrollRectToVisible:animated:]? It may be a more elegant method to do what you want. I'm pretty sure it won't try to scroll if the rectangle is already visible.