Detect moment when newline starts in UITextView - ios

I try to detect when carriage goes at new line in UITextView. I can detect it by comparison total later width with UITextView width:
CGSize size = [textView.text sizeWithAttributes:textView.typingAttributes];
if(size.width > textView.bounds.size.width)
NSLog (#"New line");
But it dose not work proper way because -sizeWithAttributes:textView returns only width of letters without indentation width. Help please solve this.

This is how I would do it:
Get the UITextPosition of the last character.
Call caretRectForPosition on your UITextView.
Create a CGRect variable and initially store CGRectZero in it.
In your textViewDidChange: method, call caretRectForPosition: by passing the UITextPosition.
Compare it with the current value stored in the CGRect variable. If the new y-origin of the caretRect is greater than the last one, it means a new line has been reached.
Sample code:
CGRect previousRect = CGRectZero;
- (void)textViewDidChange:(UITextView *)textView{
UITextPosition* pos = yourTextView.endOfDocument;//explore others like beginningOfDocument if you want to customize the behaviour
CGRect currentRect = [yourTextView caretRectForPosition:pos];
if (currentRect.origin.y > previousRect.origin.y){
//new line reached, write your code
}
previousRect = currentRect;
}
Also, you should read the documentation for UITextInput protocol reference here. It is magical, I'm telling you.
Let me know if you have any other issues with this.

answer of #n00bProgrammer in Swift-4 with more precise line break detection.
#n00bProgrammer answer is perfect except one thing it reacts differently when the user starts typing in a first line, it presents that Started New Line too.
Overcoming issue, here is the refined code
var previousRect = CGRect.zero
func textViewDidChange(_ textView: UITextView) {
let pos = textView.endOfDocument
let currentRect = textView.caretRect(for: pos)
self.previousRect = self.previousRect.origin.y == 0.0 ? currentRect : self.previousRect
if currentRect.origin.y > self.previousRect.origin.y {
//new line reached, write your code
print("Started New Line")
}
self.previousRect = currentRect
}

For Swift use this
previousRect = CGRectZero
func textViewDidChange(textView: UITextView) {
var pos = textView.endOfDocument
var currentRect = textView.caretRectForPosition(pos)
if(currentRect.origin.y > previousRect?.origin.y){
//new line reached, write your code
}
previousRect = currentRect
}

You can use the UITextViewDelegate
- (BOOL)textView:(UITextView *)textView shouldChangeTextInRange:(NSRange)range replacementText: (NSString *)text
{
BOOL newLine = [text isEqualToString:#"\n"];
if(newLine)
{
NSLog(#"User started a new line");
}
return YES;
}

Swift 3
The accepted answer and the swift version works fine, but here is a Swift 3 version for the lazy people out there.
class CustomViewController: UIViewController, UITextViewDelegate {
let textView = UITextView(frame: .zero)
var previousRect = CGRect.zero
override func viewDidLoad(){
textView.frame = CGRect(
x: 20,
y: 0,
width: view.frame.width,
height: 50
)
textView.delegate = self
view.addSubview(textView)
}
func textViewDidChange(_ textView: UITextView) {
let pos = textView.endOfDocument
let currentRect = textView.caretRect(for: pos)
if previousRect != CGRect.zero {
if currentRect.origin.y > previousRect.origin.y {
print("new line")
}
}
previousRect = currentRect
}
}

SWIFT 4
If you don't want to use previousRect. Let's try this:
func textViewDidChange(_ textView: UITextView) {
let pos = textView.endOfDocument
let currentRect = textView.caretRect(for: pos)
if (currentRect.origin.y == -1 || currentRect.origin.y == CGFloat.infinity){
print("Yeah!, I've gone to a new line")
//-1 for new line with a char, infinity is new line with a space
}
}

SWIFT 5
Lets not overcomplicate things.
func textView(_ textView: UITextView, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool {
if text == "\n" {
// return pressed
}
}

You need to get the height of the text, not the width. Use either sizeWithFont:constrainedToSize:lineBreakMode: (if you need to support iOS 6 or earlier) or use boundingRectWithSize:options:attributes:context: if you only support iOS 7.

Related

Swift UITextView Delegate

I am having a problem and have searched all across StackO and did not see a solution.
I have a UITextview extension with TextViewDelegate that I call inside of my VC so that i can have a placeholder label. The problem is i now need to add a func that checks for remaining chars in that same textView which i am able to get to work properly. But i cant grab a label to present it on the VC from that extension. I have been trying delegates but since it is a delegate itself i cant use my normal methods. What is the best route to go about this? Thank You for your help!
Here is the code. The placeholder label code is left out since it will make everything longer and I do not feel its needed for a solution. But I can add if necessary. And i can not move this code straight into VC as i need this extension to stay like this.
extension UITextView: UITextViewDelegate {
/// When the UITextView change, show or hide the label based on if the UITextView is empty or not
public func textViewDidChange(_ textView: UITextView) {
if let placeholderLabel = self.viewWithTag(100) as? UILabel {
placeholderLabel.isHidden = !self.text.isEmpty
}
checkRemainingChars(textView: textView)
}
func checkRemainingChars(textView: UITextView) {
let allowedChars = 140
if let charsInTextField = textView.text?.count {
let charsInLabel = charsInTextField
let remainingChars = charsInLabel
if remainingChars <= allowedChars {
//Need to grab this label
charsLeftLabel.textColor = UIColor.lightGray
}
if remainingChars >= 120 {
//Need to grab this label
charsLeftLabel.textColor = UIColor.orange
}
if remainingChars >= allowedChars {
//Need to grab this label
charsLeftLabel.textColor = UIColor.red
}
//This prints fine
print("Remaining chars is \(remainingChars)/140")
//Need to grab this label
charsLeftLabel.text = String(remainingChars)
}
}
Thanks again.

Emojis breaking my code in UItextView Swift 4

func textView(_ textView: UITextView, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool {
if range.length + range.location > commentView.text!.count{
return false
}
let newLength = (commentView.text?.count)! + text.count - range.length
let i = charCount - newLength
if i < 30 {
charCountLabel.textColor = UIColor.red
} else {
charCountLabel.textColor = UIColor(r: 79, g: 79, b: 79)
}
charCountLabel.text = "\(i)"
return newLength < charCount
}
The above code is a character counter for a UITextView, yet when I enter a single emoji into the UITextView the editing stops, why is that?? and how would I integrate a fix
CommentView : UItextView
charCount : Int
charCountLabel : UIlabel
sc of the debugger
upon stepping though the thread I get this when I try to send another character :
further in thread
EDIT
upon going through the debugger I have found that the second emoji or any char is causing the "I" var to be some super long number same with the "newLength" ... any one got any ideas?
I tried running your code in a test project and hit several issues. I assumed you initialized 'charCount' with 0 to begin, but this results in 'i' being -1 when you type the first character, which then returns false for every character after that.
If you're simply trying to implement a text length counter there are easier ways to do it. The two methods below populate the proper character count in the counter label when adding/deleting regular text and emoji characters.
First method I'd try is implementing the textView delegate func textViewDidChange(_ textView: UITextView). This will update the label count after every character you type. You could also set your text color here if you want.
func textViewDidChange(_ textView: UITextView) {
// only want to update character count label for commentView
guard textView == commentView, let string = textView.text else {
return
}
// update counter label text with the current text count of the textview
charCountLabel.text = "\(string.count)"
}
The second method is to use the textView delegate you were using. Here's some code I got working in a test project. There are probably better ways than this but this will get you going.
#IBOutlet weak var commentView: UITextView!
#IBOutlet weak var charCountLabel: UILabel!
let minCount = 30
let maxCount = 120
func textView(_ textView: UITextView, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool {
// only want to update character count label for commentView
guard textView == commentView, let string = textView.text else {
return true
}
// get current text + new character being entered
var newStr = string+text
// check if this is a backspace
let isDeleting = (range.length > 0) && text.isEmpty
if isDeleting == true {
// trim last character
// you may want to drop based on the range.length, however you'll need
// to determine if the character is an emoji and adjust the number of characters
// as range.length returns a length > 1 for emojis
newStr = String(newStr.dropLast())
}
// set text color based on whether we're over the min count
charCountLabel.textColor = newStr.count < minCount ? .red : .blue
// set the character count in the counter label
charCountLabel.text = "\(newStr.count)"
// if we're less than the max count allowed, return true
return newStr.count < maxCount
}

Swift 3 - Detecting new lines in UITextView

I'm trying to count (real-time) new lines on UITextView, I found the below method that works well but I actually need to add more features but I don't know how to do.
func textViewDidChange(_ textView: UITextView) {
let pos = textView.endOfDocument
let currentRect = textView.caretRect(for: pos)
if previousRect != CGRect.zero {
if currentRect.origin.y > previousRect.origin.y {
//increase the counter
counter += 1
}
}
previousRect = currentRect
}
Ok so, the code works fine, but I need to:
decrease the counter counter -= 1 when the user delete a line
actually this code increase the counter when new line is detected, instead I need to increase the counter when the return button on the keyboard is pressed giving the user the possibility to exceed the frame width of the text view without increase the counter
I don't know how to do that, do you have any suggestions?
EDIT (Visual Example)
the output here is 4
I solved the problem by myself, I will post the code below if someone need it (check comments in the code):
func textViewDidChange(_ textView: UITextView) {
let pos = textView.endOfDocument
let currentRect = textView.caretRect(for: pos)
if previousRect != CGRect.zero {
if currentRect.origin.y > previousRect.origin.y {
// Array of Strings
var currentLines = textView.text.components(separatedBy: "\n")
// Remove Blank Strings
currentLines = currentLines.filter{ $0 != "" }
//increase the counter counting how many items inside the array
counter = currentLines.count
}
}
previousRect = currentRect
}
You could listen to every individual character change and just check if it's the new line or not with this delegate function https://developer.apple.com/reference/uikit/uitextviewdelegate/1618630-textview

How to allow user to enter multi-line text Swift (Like Facebook or Twitter posts)

I know I can use a text view but its awkward with the keyboard on smaller iPhones. And it looks odd being able to scroll and write in a box.
I want to have something like a Twitter post box or Facebook post box or contacts (extra information box). One that expands as the user writes more.
Can someone point me in the right direction?
Thanks
Firstly, make your view controller implement UITextViewDelegate,
class YourViewController: UIViewController, UITextViewDelegate {
#IBOutlet weak var textView: UITextView!
...
}
In the viewDidLoad method, set the delegete,
self.textView.delegate = self
Finally you should implement the textViewDidChange method.
func textViewDidChange(textView: UITextView) {
let textViewFixedWidth: CGFloat = self.textView.frame.size.width
let newSize: CGSize = self.textView.sizeThatFits(CGSizeMake(textViewFixedWidth, CGFloat(MAXFLOAT)))
var newFrame: CGRect = self.textView.frame
var textViewYPosition = self.textView.frame.origin.y
var heightDifference = self.textView.frame.height - newSize.height
if (abs(heightDifference) > 20) {
newFrame.size = CGSizeMake(fmax(newSize.width, textViewFixedWidth), newSize.height)
newFrame.offset(dx: 0.0, dy: 0)
}
self.textView.frame = newFrame
}
You just need to have a text view - which resizes every time the text changes. I am not sure on swift, but on obj-C, override the -textViewDidChange method and resize using :
#pragma mark - UITextViewDelegate
- (void)textViewDidChange:(UITextView *)textView {
[self resizeTextView];
}
- (void)resizeTextView {
[self.textView sizeToFit];
self.textView.frame = CGRectMake(self.textView.x, self.textView.y, __device_width, self.textView.h);
}
There should probably be something similar in Swift.

Scroll UITextView To Bottom

I am making a an app that has a UITextView and a button.
When I click the button some text will add in the UITextView.
But when clicking the button, I wan't to scroll down to the bottom of the text field so the user can see the last text added.
How to make the UITextView to scroll down to the bottom?
I tried:
int numLines = LogTextView.contentSize.height / LogTextView.font.lineHeight+1;
NSLog(#"%d",numLines);
NSUInteger length = self.LogTextView.text.length;
self.LogTextView.selectedRange = NSMakeRange(0, length);
but it will not work...
I also tried:
self.LogTextView.contentSize=CGSizeMake(length,0);
You can use the following code if you are talking about UITextView:
-(void)scrollTextViewToBottom:(UITextView *)textView {
if(textView.text.length > 0 ) {
NSRange bottom = NSMakeRange(textView.text.length -1, 1);
[textView scrollRangeToVisible:bottom];
}
}
SWIFT 4:
func scrollTextViewToBottom(textView: UITextView) {
if textView.text.count > 0 {
let location = textView.text.count - 1
let bottom = NSMakeRange(location, 1)
textView.scrollRangeToVisible(bottom)
}
}
Try this if you have problem on iOS 7 or above. See this SO answer.
- (void)scrollTextViewToBottom:(UITextView *)textView {
NSRange range = NSMakeRange(textView.text.length, 0);
[textView scrollRangeToVisible:range];
// an iOS bug, see https://stackoverflow.com/a/20989956/971070
[textView setScrollEnabled:NO];
[textView setScrollEnabled:YES];
}
With Swift 3
let bottom = self.textView.contentSize.height - self.textView.bounds.size.height
self.textView.setContentOffset(CGPoint(x: 0, y: bottom), animated: true)
Swift 5
extension UITextView {
func simple_scrollToBottom() {
let textCount: Int = text.count
guard textCount >= 1 else { return }
scrollRangeToVisible(NSRange(location: textCount - 1, length: 1))
}
}
// Usage
textView.simple_scrollToBottom()
Make a range, specifying encoding, to the last character, then scroll to that range
Something other than utf8 might be appropriate depending on your content
let range = NSMakeRange(self.textView.text.lengthOfBytes(using: .utf8), 0);
self.textView.scrollRangeToVisible(range);
You have to implement a delegate method. The code below checks whether a newline has been entered and, if so, scrolls to the bottom of the textView:
- (BOOL)textView:(UITextView *)textView shouldChangeTextInRange:(NSRange)range replacementText:(NSString *)text
{
if ([text isEqualToString:#"\n"]) {
textView.contentOffset = CGPointMake(0.0, textView.contentSize.height);
}
return YES;
}
This works for me! :D
CGPoint bottomOffset = CGPointMake(0, self.textView.contentSize.height - self.textView.bounds.size.height);
[self.description1 setContentOffset:bottomOffset animated:YES];
As a generic approach for scrolling to bottom, it can be done on a UIScrollView.
extension UIScrollView {
func scrollToBottom() {
let contentHeight = contentSize.height - frame.size.height
let contentoffsetY = max(contentHeight, 0)
setContentOffset(CGPoint(x: 0, y: contentoffsetY), animated: true)
}
}
This will work on all descendants of UIScrollView like UITextView, UITableView etc..
textView.scrollRangeToVisible(NSRange(..<textView.text.endIndex, in: textView.text))
This solution does a couple of notable things slightly different:
Utilizes the String.Index interface (likely more performant than e.g. .count)
Uses a PartialRangeUpTo which avoids an explicit range start position, reducing the code to a clean one-liner
The Swift version of #Hong Duan answer
func scrollTextViewToBottom(textView: UITextView) {
if textView.text.count > 0 {
let location = textView.text.count - 1
let bottom = NSMakeRange(location, 1)
textView.scrollRangeToVisible(bottom)
// an iOS bug, see https://stackoverflow.com/a/20989956/971070
textView.isScrollEnabled = false
textView.isScrollEnabled = true
}
}

Resources