How can I convert a UITextRange object to an NSRange? I've seen plenty of SO posts about going the other direction but that's the opposite of what I need. I'm using the UITextRange selectedTextRange which is a property of a UITextView. It returns a UITextRange but I need a range.
You need something like this:
- (NSRange) selectedRangeInTextView:(UITextView*)textView
{
UITextPosition* beginning = textView.beginningOfDocument;
UITextRange* selectedRange = textView.selectedTextRange;
UITextPosition* selectionStart = selectedRange.start;
UITextPosition* selectionEnd = selectedRange.end;
const NSInteger location = [textView offsetFromPosition:beginning toPosition:selectionStart];
const NSInteger length = [textView offsetFromPosition:selectionStart toPosition:selectionEnd];
return NSMakeRange(location, length);
}
Here's a Swift extension based on the answer by Vitaly S.
extension UITextInput {
var selectedRange: NSRange? {
guard let range = self.selectedTextRange else { return nil }
let location = offsetFromPosition(beginningOfDocument, toPosition: range.start)
let length = offsetFromPosition(range.start, toPosition: range.end)
return NSRange(location: location, length: length)
}
}
Swift 4
extension UITextInput {
var selectedRange: NSRange? {
guard let range = selectedTextRange else { return nil }
let location = offset(from: beginningOfDocument, to: range.start)
let length = offset(from: range.start, to: range.end)
return NSRange(location: location, length: length)
}
}
UITextView has a property
#property(nonatomic) NSRange selectedRange;
Swift 5
extension UITextInput
{
var selectedRange: NSRange?
{
if let selectedRange = self.selectedTextRange
{
return NSMakeRange(self.offset(from: self.beginningOfDocument, to: selectedRange.start),
self.offset(from: selectedRange.start, to: selectedRange.end))
}
else
{
return nil
}
}
}
Related
For example string = "Hello #manan123 and #man"
i am doing tagging functionality in textview and i want when user press backspace if word contains # then whole word will delete not single character.
but i am facing problem while two tagged word have same characters. So in above example when i am trying to delete last word #man then #manan123 also convert to the an123.
Here is my code on textview delegate method
func textView(_ textView: UITextView, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool {
if text == "" {
if let selectedRange = textView.selectedTextRange {
let cursorOffset = textView.offset(from: textView.beginningOfDocument, to: selectedRange.start)
if let myText = textView.text {
let index = myText.index(myText.startIndex, offsetBy: cursorOffset)
let substring = myText[..<index]
if let lastword = substring.components(separatedBy: " ").last {
if lastword.hasPrefix("#") {
//Check complete word
let completeLastword = myText.components(separatedBy: " ").filter{$0.contains(lastword)}.last
textView.text = myText.replacingOccurrences(of: completeLastword!, with: "")
return false
}
}
}
}
}
return true
}
Hi I have updated your code please check it.
if text == "" {
if let selectedRange = textView.selectedTextRange {
let cursorOffset = textView.offset(from: textView.beginningOfDocument, to: selectedRange.start)
if let myText = textView.text {
let index = myText.index(myText.startIndex, offsetBy: cursorOffset)
let substring = myText[..<index]
if let lastword = substring.components(separatedBy: " ").last {
if lastword.hasPrefix("#") {
var newText = myText
let start = newText.index(myText.startIndex, offsetBy: cursorOffset - lastword.count);
let end = newText.index(myText.startIndex, offsetBy: (cursorOffset - lastword.count) + lastword.count);
newText.replaceSubrange(start..<end, with: "")
textView.text = newText
return false
}
}
}
}
}
Hope this helps.
I have an extension which identify and return the word before the cursor:
extension UITextView {
var currentWord : String? {
let beginning = beginningOfDocument
if let start = position(from: beginning, offset: selectedRange.location),
let end = position(from: start, offset: selectedRange.length) {
let textRange = tokenizer.rangeEnclosingPosition(end, with: .word, inDirection: 1)
if let textRange = textRange {
return text(in: textRange)
}
}
return nil
}
I am using UItextGranularity.word and works fine.
However my issue is this:
If at the beginning of the word i have an # it won't be returned.
so if i have #jon the currentword will be jon.
Is there a way to include the # so to have the complete word with the special Character?
Thank you
It doesn't seem like you can do it with UITextInputTokenizer. You can try this solution:
var currentWord: String? {
let regex = try! NSRegularExpression(pattern: "\\S+$")
let textRange = NSRange(location: 0, length: selectedRange.location)
if let range = regex.firstMatch(in: text, range: textRange)?.range {
return String(text[Range(range, in: text)!])
}
return nil
}
I have created a UITextfield where i want to add 4 digit password, but once I enter the first digit whole placeholder disappear. how can I keep next three placeholder letters in UITextfield while entering next password letters.
here is my screenshot:
I wanted to try SeanLintern88's solution because it sounded like a little challange. And it is in the case where the text field should have spaces between the underscore.
textField.text = "_ _ _ _"
This is the solution I came with and while it was fun to write, this is something I do not recommend using in real projects. Better try the 4 separate text fields approach :)
extension ViewController: UITextFieldDelegate {
func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
guard let text = textField.text else { return false }
var location = range.location
//is deleting
if string == "" {
guard let indexToReplace = text.index(text.startIndex, offsetBy: location, limitedBy: text.endIndex) else {
return false
}
let isCharDeleted = location % 2 == 0
//place " " or "_" depending on the position that was deleted
let stringToReplaceWith = isCharDeleted ? "_" : " "
let charachter = stringToReplaceWith[stringToReplaceWith.startIndex]
textField.text?.remove(at: indexToReplace)
textField.text?.insert(charachter, at: indexToReplace)
var newCursorPositionOffset = location
//deletetion occured on space " "
if !isCharDeleted {
guard let previousIndex = text.index(text.startIndex, offsetBy: location-1, limitedBy: text.endIndex) else {
return false
}
//delete the previous charachter
textField.text?.remove(at: previousIndex)
let dash = "_"
let char = dash[dash.startIndex]
textField.text?.insert(char, at: previousIndex)
//correct cursor position
newCursorPositionOffset -= 1
}
//move cursor position
let newPosition = textField.position(from: textField.beginningOfDocument, offset: newCursorPositionOffset)
textField.selectedTextRange = textField.textRange(from: newPosition!, to: newPosition!)
return false
}
//is typing
if range.location + 1 <= text.characters.count,
let end = text.index(text.startIndex, offsetBy: location+1, limitedBy: text.endIndex),
let start = text.index(text.startIndex, offsetBy: location, limitedBy: text.endIndex) {
textField.text = textField.text?.replacingOccurrences(of: "_", with: string, options: .caseInsensitive, range: Range(uncheckedBounds: (lower: start, upper: end)))
//correct the cursor position if placed on " " index
if range.location % 2 != 0 {
location -= 1
}
}
//skip " " and move cursor to the next "_"
if location+2 < text.characters.count {
let newPosition = textField.position(from: textField.beginningOfDocument, offset: location+2)
textField.selectedTextRange = textField.textRange(from: newPosition!, to: newPosition!)
}
return false
}
func textFieldDidBeginEditing(_ textField: UITextField) {
let newPosition = textField.beginningOfDocument
textField.selectedTextRange = textField.textRange(from: newPosition, to: newPosition)
}
}
PS: Probably it can be made shorter with better choosing of functions for string operations, but still a pretty .. unfriendly solution.
Conclusion: DON'T DO THIS AT HOME :D
Instead of using placeHolder, just override the shouldChangeCharactersInRange method and append the characters to the string until the string is 4 characters long, you can also use attributed string if you want the _ to look different to the dots.
Swift 4
This is a modified version of #Denislava Shentova's answer that I believe simplifies into less lines of code, fixes issues, and makes the code more readable.
Not Fully Tested
import UIKit
class YourClass: UIViewController {
//It is also a good idea to deny users the ability to paste into this textField.
//set up textField
let textField : UITextField = {
let textField = UITextField()
// These are 'long dashes' you can replace them with whatever you would like.
// These look best IMO.
textField.text = "——————" //No spaces needed!
textField.textColor = .black
textField.textAlignment = .center
textField.tintColor = .clear //this will hide the cursor.
return textField
}()
override func viewDidLoad() {
super.viewDidLoad()
textField.delegate = self
//This sets the spacing or 'Kern' of the textfield. Adjust the value: 10.0 and the fontSize to get the desired output.
textField.defaultTextAttributes.updateValue(10.0, forKey: NSAttributedStringKey.kern.rawValue)
}
}
extension YourClass : UITextFieldDelegate {
func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
//get the current text of the textField.
guard let text = textField.text else { return false }
//handle backspace event.
if string == "" {
guard let indexToReplace = text.index(text.startIndex, offsetBy: range.location, limitedBy: text.endIndex) else { return false }
textField.text?.remove(at: indexToReplace)
textField.text?.insert("—", at: indexToReplace)
//adjust cursor position
if let newPostion = textField.position(from: textField.beginningOfDocument, offset: range.location) {
textField.selectedTextRange = textField.textRange(from: newPostion, to: newPostion)
return false
}
}
//handle character entered event.
if range.location + 1 <= text.count,
let end = text.index(text.startIndex, offsetBy: range.location + 1, limitedBy: text.endIndex),
let start = text.index(text.startIndex, offsetBy: range.location, limitedBy: text.endIndex) {
textField.text = textField.text?.replacingOccurrences(of: "—", with: string, options: .caseInsensitive, range: Range(uncheckedBounds: (lower: start, upper: end)))
}
//adjust cursor position.
if range.location + 1 < text.count {
if let newPosition = textField.position(from: textField.beginningOfDocument, offset: range.location + 1){
textField.selectedTextRange = textField.textRange(from: newPosition, to: newPosition)
}
}
return false
}
//make sure to start at the begining of the textField.
func textFieldDidBeginEditing(_ textField: UITextField) {
let newPosition = textField.beginningOfDocument
textField.selectedTextRange = textField.textRange(from: newPosition, to: newPosition)
}
}
I have a text view with a string:
#"The best football player in the world is #OPTION, and the best basketball player is #OPTION?"
This is an example what I have to do. I want to replace #OPTION with a dropdown list, which means with custom view. It depends on the question, it can be only one #OPTION or more. Thanks in advance.
Edited:
The string is from API and it appears in UITextView or UILabel
After understanding your needs, The main issue you are facing is not knowing where to add the select lists.
I have created 2 categories for your case, for UILabel and for UITextView,
following these posts which contain the relevant answers for that:
How do I locate the CGRect for a substring of text in a UILabel?
Get X and Y coordinates of a word in UITextView
These categories find the CGRect for a string inside, which is where you should position your pickers.
The down-part of this for UILabel, is that it doesn't handle wordWrap line breaking mode well, and therefore it won't find the correct location, to handle this correctly, you should add line breaks when needed in case you use the UILabel.
UILabel:
extension UILabel
{
func rectFor(string str : String, fromIndex: Int = 0) -> (CGRect, NSRange)?
{
// Find the range of the string
guard self.text != nil else { return nil }
let subStringToSearch : NSString = (self.text! as NSString).substring(from: fromIndex) as NSString
var stringRange = subStringToSearch.range(of: str)
if (stringRange.location != NSNotFound)
{
guard self.attributedText != nil else { return nil }
// Add the starting point to the sub string
stringRange.location += fromIndex
let storage = NSTextStorage(attributedString: self.attributedText!)
let layoutManager = NSLayoutManager()
storage.addLayoutManager(layoutManager)
let textContainer = NSTextContainer(size: self.frame.size)
textContainer.lineFragmentPadding = 0
textContainer.lineBreakMode = .byWordWrapping
layoutManager.addTextContainer(textContainer)
var glyphRange = NSRange()
layoutManager.characterRange(forGlyphRange: stringRange, actualGlyphRange: &glyphRange)
let resultRect = layoutManager.boundingRect(forGlyphRange: glyphRange, in:textContainer)
return (resultRect, stringRange)
}
return nil
}
}
Usage for infinite searching all available substring (I recommend adding it in viewDidLayoutSubviews() in case you use auto-layout:
var lastFoundIndex : Int = 0
while let result = self.label.rectFor(string: "#OPTION", fromIndex: lastFoundIndex)
{
let view : UIView = UIView(frame: result.0)
view.backgroundColor = UIColor.red
self.label.addSubview(view)
lastFoundIndex = result.1.location + 1
}
And the same one for UITextView:
extension UITextView
{
func rectFor(string str : String, fromIndex: Int = 0) -> (CGRect, NSRange)?
{
// Find the range of the string
guard self.text != nil else { return nil }
let subStringToSearch : NSString = (self.text! as NSString).substring(from: fromIndex) as NSString
var stringRange = subStringToSearch.range(of: str)
if (stringRange.location != NSNotFound)
{
guard self.attributedText != nil else { return nil }
// Add the starting point to the sub string
stringRange.location += fromIndex
// Find first position
let startPosition = self.position(from: self.beginningOfDocument, offset: stringRange.location)
let endPosition = self.position(from: startPosition!, offset: stringRange.length)
let resultRange = self.textRange(from: startPosition!, to: endPosition!)
let resultRect = self.firstRect(for: resultRange!)
return (resultRect, stringRange)
}
return nil
}
}
Usage:
var lastFoundTextIndex : Int = 0
while let result = self.textView.rectFor(string: "#OPTION", fromIndex: lastFoundTextIndex)
{
let view : UIView = UIView(frame: result.0)
view.backgroundColor = UIColor.red
self.textView.addSubview(view)
lastFoundTextIndex = result.1.location + 1
}
In your case, textview gives the best results and uses included methods for that, the sample code uses a label & a text view, initialized in the code:
self.label.text = "The best football player in the world is\n#OPTION, and the best basketball player\n is #OPTION?"
self.textView.text = "The best football player in the world is #OPTION, and the best basketball player is #OPTION?"
And the output just adds views on top of the "#OPTION" strings:
Hope this helps
EDIT - Added Objective-C Variation:
Create 2 extensions - 1 for UITextView and 1 for UILabel:
UILabel:
#interface UILabel (UILabel_SubStringRect)
- (NSDictionary*)rectForString:(NSString*)string fromIndex:(int)index;
#end
#import "UILabel+SubStringRect.h"
#implementation UILabel (UILabel_SubStringRect)
- (NSDictionary*)rectForString:(NSString*)string fromIndex:(int)index
{
if (string != nil)
{
NSString* subStringToSearch = [self.text substringFromIndex:index];
NSRange stringRange = [subStringToSearch rangeOfString:string];
if (stringRange.location != NSNotFound)
{
if (self.attributedText != nil)
{
stringRange.location += index;
NSTextStorage* storage = [[NSTextStorage alloc] initWithAttributedString:self.attributedText];
NSLayoutManager* layoutManager = [NSLayoutManager new];
[storage addLayoutManager:layoutManager];
NSTextContainer* textContainer = [[NSTextContainer alloc] initWithSize:self.frame.size];
textContainer.lineFragmentPadding = 0;
textContainer.lineBreakMode = NSLineBreakByWordWrapping;
[layoutManager addTextContainer:textContainer];
NSRange glyphRange;
[layoutManager characterRangeForGlyphRange:stringRange actualGlyphRange:&glyphRange];
CGRect resultRect = [layoutManager boundingRectForGlyphRange:glyphRange inTextContainer:textContainer];
return #{ #"rect" : [NSValue valueWithCGRect:resultRect], #"range" : [NSValue valueWithRange:stringRange] };
}
}
}
return nil;
}
#end
UITextView:
#interface UITextView (SubStringRect)
- (NSDictionary*)rectForString:(NSString*)string fromIndex:(int)index;
#end
#import "UITextView+SubStringRect.h"
#implementation UITextView (SubStringRect)
- (NSDictionary*)rectForString:(NSString*)string fromIndex:(int)index
{
if (string != nil)
{
NSString* subStringToSearch = [self.text substringFromIndex:index];
NSRange stringRange = [subStringToSearch rangeOfString:string];
if (stringRange.location != NSNotFound)
{
if (self.attributedText != nil)
{
stringRange.location += index;
UITextPosition* startPosition = [self positionFromPosition:self.beginningOfDocument offset:stringRange.location];
UITextPosition* endPosition = [self positionFromPosition:startPosition offset:stringRange.length];
UITextRange* resultRange = [self textRangeFromPosition:startPosition toPosition:endPosition];
CGRect resultRect = [self firstRectForRange:resultRange];
return #{ #"rect" : [NSValue valueWithCGRect:resultRect], #"range" : [NSValue valueWithRange:stringRange] };
}
}
}
return nil;
}
#end
Usage Sample - UILabel:
int lastFoundIndex = 0;
NSDictionary* resultDict = nil;
do
{
resultDict = [self.label rectForString:#"#OPTION" fromIndex:lastFoundIndex];
if (resultDict != nil)
{
NSLog(#"result: %#", resultDict[#"rect"]);
UIView* view = [[UIView alloc] initWithFrame:[resultDict[#"rect"] CGRectValue]];
[view setBackgroundColor:[UIColor redColor]];
[self.label addSubview:view];
lastFoundIndex = (int)[resultDict[#"range"] rangeValue].location + 1;
}
} while (resultDict != nil);
UITextView:
int lastFoundTextIndex = 0;
NSDictionary* resultTextDict = nil;
do
{
resultTextDict = [self.textview rectForString:#"#OPTION" fromIndex:lastFoundTextIndex];
if (resultTextDict != nil)
{
NSLog(#"result: %#", resultTextDict[#"rect"]);
UIView* view = [[UIView alloc] initWithFrame:[resultTextDict[#"rect"] CGRectValue]];
[view setBackgroundColor:[UIColor redColor]];
[self.textview addSubview:view];
lastFoundTextIndex = (int)[resultTextDict[#"range"] rangeValue].location + 1;
}
} while (resultTextDict != nil);
I want to get ride of the white spaces in front and at the end of my NSAttributedString(Trimming it). I can't simply convert it to string and do trimming because there are images(attachments) in it.
How can i do it?
Create extension of NSAttributedString as below.
extension NSAttributedString {
public func attributedStringByTrimmingCharacterSet(charSet: CharacterSet) -> NSAttributedString {
let modifiedString = NSMutableAttributedString(attributedString: self)
modifiedString.trimCharactersInSet(charSet: charSet)
return NSAttributedString(attributedString: modifiedString)
}
}
extension NSMutableAttributedString {
public func trimCharactersInSet(charSet: CharacterSet) {
var range = (string as NSString).rangeOfCharacter(from: charSet as CharacterSet)
// Trim leading characters from character set.
while range.length != 0 && range.location == 0 {
replaceCharacters(in: range, with: "")
range = (string as NSString).rangeOfCharacter(from: charSet)
}
// Trim trailing characters from character set.
range = (string as NSString).rangeOfCharacter(from: charSet, options: .backwards)
while range.length != 0 && NSMaxRange(range) == length {
replaceCharacters(in: range, with: "")
range = (string as NSString).rangeOfCharacter(from: charSet, options: .backwards)
}
}
}
and use in viewController where you want to use. like this
let attstring = NSAttributedString(string: "this is test message. Please wait. ")
let result = attstring.attributedStringByTrimmingCharacterSet(NSCharacterSet.whitespaceAndNewlineCharacterSet())
This works even with emoji in the text
extension NSAttributedString {
/** Will Trim space and new line from start and end of the text */
public func trimWhiteSpace() -> NSAttributedString {
let invertedSet = CharacterSet.whitespacesAndNewlines.inverted
let startRange = string.utf16.description.rangeOfCharacter(from: invertedSet)
let endRange = string.utf16.description.rangeOfCharacter(from: invertedSet, options: .backwards)
guard let startLocation = startRange?.upperBound, let endLocation = endRange?.lowerBound else {
return NSAttributedString(string: string)
}
let location = string.utf16.distance(from: string.startIndex, to: startLocation) - 1
let length = string.utf16.distance(from: startLocation, to: endLocation) + 2
let range = NSRange(location: location, length: length)
return attributedSubstring(from: range)
}
}
USAGE
let attributeString = NSAttributedString(string: "\n\n\n Hi 👋 👩👩👧👩👩👦👦👩👩👧👧👨👨👦👩👦👨👨👧👧👨👨👦👦👨👨👧👦👩👧👦👩👦👦👩👧👧👨👦 buddy. ")
let result = attributeString.trimWhiteSpace().string // Hi 👋 👩👩👧👩👩👦👦👩👩👧👧👨👨👦👩👦👨👨👧👧👨👨👦👦👨👨👧👦👩👧👦👩👦👦👩👧👧👨👦 buddy.
Swift 4 and above
extension NSMutableAttributedString {
func trimmedAttributedString() -> NSAttributedString {
let invertedSet = CharacterSet.whitespacesAndNewlines.inverted
let startRange = string.rangeOfCharacter(from: invertedSet)
let endRange = string.rangeOfCharacter(from: invertedSet, options: .backwards)
guard let startLocation = startRange?.upperBound, let endLocation = endRange?.lowerBound else {
return NSAttributedString(string: string)
}
let location = string.distance(from: string.startIndex, to: startLocation) - 1
let length = string.distance(from: startLocation, to: endLocation) + 2
let range = NSRange(location: location, length: length)
return attributedSubstring(from: range)
}
}
use:
let string = "This is string with some space in the end. "
let attributedText = NSMutableAttributedString(string: string).trimmedAttributedString()
It turns out that Unicode strings are hard hahaha! The other solutions posted here are a great starting point, but they crashed for me when using non-latin strings.
Whenever using indexes or ranges in Swift Strings, we need to use String.Index instead of plain Int. Creating an NSRange from a Range<String.Index> has to be done with NSRange(swiftRange, in: String).
That being said, this code builds on the other answers, but makes it unicode-proof:
public extension NSMutableAttributedString {
/// Trims new lines and whitespaces off the beginning and the end of attributed strings
func trimmedAttributedString() -> NSAttributedString {
let invertedSet = CharacterSet.whitespacesAndNewlines.inverted
let startRange = string.rangeOfCharacter(from: invertedSet)
let endRange = string.rangeOfCharacter(from: invertedSet, options: .backwards)
guard let startLocation = startRange?.lowerBound, let endLocation = endRange?.lowerBound else {
return NSAttributedString(string: string)
}
let trimmedRange = startLocation...endLocation
return attributedSubstring(from: NSRange(trimmedRange, in: string))
}
}
I made a swift 3 implementation, just in case anyone is interested:
/**
Trim an attributed string. Can for example be used to remove all leading and trailing spaces and line breaks.
*/
public func attributedStringByTrimmingCharactersInSet(set: CharacterSet) -> NSAttributedString {
let invertedSet = set.inverted
let rangeFromStart = string.rangeOfCharacter(from: invertedSet)
let rangeFromEnd = string.rangeOfCharacter(from: invertedSet, options: .backwards)
if let startLocation = rangeFromStart?.upperBound, let endLocation = rangeFromEnd?.lowerBound {
let location = string.distance(from: string.startIndex, to: startLocation) - 1
let length = string.distance(from: startLocation, to: endLocation) + 2
let newRange = NSRange(location: location, length: length)
return self.attributedSubstring(from: newRange)
} else {
return NSAttributedString()
}
}
Swift 3.2 Version:
extension NSAttributedString {
public func trimmingCharacters(in characterSet: CharacterSet) -> NSAttributedString {
let modifiedString = NSMutableAttributedString(attributedString: self)
modifiedString.trimCharacters(in: characterSet)
return NSAttributedString(attributedString: modifiedString)
}
}
extension NSMutableAttributedString {
public func trimCharacters(in characterSet: CharacterSet) {
var range = (string as NSString).rangeOfCharacter(from: characterSet)
// Trim leading characters from character set.
while range.length != 0 && range.location == 0 {
replaceCharacters(in: range, with: "")
range = (string as NSString).rangeOfCharacter(from: characterSet)
}
// Trim trailing characters from character set.
range = (string as NSString).rangeOfCharacter(from: characterSet, options: .backwards)
while range.length != 0 && NSMaxRange(range) == length {
replaceCharacters(in: range, with: "")
range = (string as NSString).rangeOfCharacter(from: characterSet, options: .backwards)
}
}
}
Following code will work for your requirement.
var attString: NSAttributedString = NSAttributedString(string: " this is att string")
let trimmedString = attString.string.stringByTrimmingCharactersInSet(NSCharacterSet.whitespaceCharacterSet())