Swift 3 - Detecting new lines in UITextView - ios

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

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
}

Method for word determination within UITextView.text and modifying word with prefix using Swift 2.2, iOS 9.3 & Xcode 7?

Using a TextView for user message entry it is editable and selectable during user editing.
One of the buttons below the field, toggles the TextView between editing and hash-tagging mode.
When toggled to tag, the TextView has it's editable & selectable properties disabled, and I have a function to detect taps and returns the character position within the text.
I need to determine the word, if any, tapped on, and modify the word in the UITextView.text by prefixing it with a # unless it already has two hashes it which case it removes the hashes.
I'm using regular expressions for the logic.
I have not been able to find a high level method for determining the word of the character tapped on.
I have searched through the Apple's Dev. Lib. and sites like raywenderlich and Grok Swift, but cannot find the method I am sure must be there.
I could implement by testing if the current charcter is a valid word divider if not then decrement character index and test until the word boundary is determined. At which point, I return to the prior index and test for the # character, in the case it is a #, I would test the next character and in the case it is not a #, I would add the # character to the start of the word.
Is there a function within UIKit, TextKit, or a method of UITextView or NSTextStorage, that will return the word of the character tapped and NSRange of that word?
Also what would be the correct method for adding the # to the TextView's text?
[textView:shouldChangeTextInRange:replacementText or textView.textStorage:replaceCharactersInRange:withString:]
I have worked commercially on PC, PlayStation and GameBoy, but this is the first time developing an app and using the iPhone/Mac platform, so I could really use advice.
for detecting the # you need to call the code inside the delegate shouldChangeCharactersInRange
let stringprocess = stringfordetecting.text
let tok = stringprocess!.componentsSeparatedByString(" ")
for item in tok
{
let demo = String(item)
if demo.hasPrefix("#")
{
let range = (stringfordetecting.text! as NSString).rangeOfString(item)
//add code
}
else
{
//add code
}
for detecting the tapped character index add a guesture to the textview
let tapGesture = UITapGestureRecognizer(target: self, action: "textTapped:")
tapGesture.headline = indexPath
tapGesture.numberOfTapsRequired = 1
textview2.addGestureRecognizer(tapGesture)
func textTapped(recognizer: MyTapGestureRecognizer){
let textView: UITextView = recognizer.view as! UITextView
var layoutManager: NSLayoutManager = textView.layoutManager
var location: CGPoint = recognizer.locationInView(textView)
let position: CGPoint = CGPointMake(location.x, location.y)
location.x -= textview2.textContainerInset.left
location.y -= textview2.textContainerInset.top
var charIndex: Int
charIndex = layoutManager.characterIndexForPoint(location, inTextContainer: textview2.textContainer, fractionOfDistanceBetweenInsertionPoints: nil)
if charIndex < textview2.textStorage.length
{
print(charIndex)
}
}
for detecting the tapped character in a textview inside the tapguesture recogniser function
func textTapped(recognizer: MyTapGestureRecognizer){
let textView: UITextView = recognizer.view as! UITextView
var layoutManager: NSLayoutManager = textView.layoutManager
var location: CGPoint = recognizer.locationInView(textView)
let position: CGPoint = CGPointMake(location.x, location.y)
location.x -= cell.messageLabel.textContainerInset.left
location.y -= cell.messageLabel.textContainerInset.top
var charIndex: Int
charIndex = layoutManager.characterIndexForPoint(location, inTextContainer: cell.messageLabel.textContainer, fractionOfDistanceBetweenInsertionPoints: nil)
if charIndex < cell.messageLabel.textStorage.length {
let stringprocess = textview.text
let tok = stringprocess.componentsSeparatedByString(" ")
// let attributedString1 = NSMutableAttributedString(string:stringcheck as String)
for item in tok
{
let demo = String(item)
if demo.hasPrefix("#") {
let range = (stringcheck as NSString).rangeOfString(item)
var i = range.location
while i <= range.location+range.length
{
if i == charIndex
{
print(demo)
}
i++
}
}
}
}

Detect moment when newline starts in UITextView

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.

scrollViewDidEndDecelerating being called for a simple touch

I'm implementing UIScrollViewDelegate and doing a lot of stuff with it. However, I just found an annoying issue.
I expect scrollViewDidEndDecelerating: to be called always after scrollViewWillBeginDecelerating:. However, if I simply touch my ScrollView (actually I'm touching a button inside the scrollView), scrollViewDidEndDecelerating: gets called and scrollViewWillBeginDecelerating: was not called.
So how can I avoid scrollViewDidEndDecelerating: being called when I simply press a button inside my UIScrollView?
Thank you!
Create a member BOOL called buttonPressed or similar and initialise this to false in your init: method.
Set the BOOL to true whenever your button/s are hit, and then perform the following check:
-(void)scrollViewDidEndDecelerating: (UIScrollView*)scrollView
{
if (!buttonPressed)
{
// resume normal processing
}
else
{
// you will need to decide on the best place to reset this variable.
buttonPressed = NO;
}
}
I had the same problem I fixed it by making a variable that hold the current page num and compare it with the local current page variable if they r equal then don't proceed.
var currentPage : CGFloat = 0.0
var oldPage : CGFloat = 0.0
func scrollViewDidEndDecelerating(_ scrollView: UIScrollView){
// Test the offset and calculate the current page after scrolling ends
let pageWidth:CGFloat = scrollView.frame.width
let currentPage:CGFloat = floor((scrollView.contentOffset.x-pageWidth/2)/pageWidth)+1
// Change the indicator
print("that's the self.currentPage \(self.currentPage) and that's the current : \(currentPage)")
guard self.currentPage != currentPage else { return }
oldPage = self.currentPage
self.currentPage = currentPage;
print("that's the old \(oldPage) and that's the current : \(currentPage)")
//Do something
}

Resources