How to convert character index from layoutManager to String scale in swift?
this is the code I'm using:
let touchPoint: CGPoint = gesture.locationOfTouch(0, inView: self.definitionLabel)
let index = layoutManager.characterIndexForPoint(touchPoint, inTextContainer: textContainer, fractionOfDistanceBetweenInsertionPoints: nil)
please don't tell me to use advanceBy() function on the first index of the string characterset since characters like ò count two in the scale of layoutManager but swift string counts theme once.
The index returned from the NSLayoutManager is "NSString based",
i.e. it is the number of UTF-16 code units from the start of the string
to the character at the given point. (So ò actually counts as
one, but Emojis 😀 count two and flags 🇧🇪 even count four.)
To convert that index to a valid Swift String index, you can use
the same approach as in https://stackoverflow.com/a/30404532/1187415:
let text = ... // the stored text
let i16 = text.utf16.startIndex.advancedBy(index, limit: text.utf16.endIndex)
// i16 is a String.UTF16View.Index
if let strIndex = String.Index(i16, within: text) {
// strIndex is a String.CharacterView.Index
let char = text[strIndex]
print(char)
}
Updated for Swift 5
Martin R's answer gave me the rough outline; here's working Swift 5 version. Mine is for a UITextView with Attributed Text but should work just as well with regular String and for UITextField and UILabel.
func handleTap(_ sender: UIGestureRecognizer) {
guard let textView = sender.view as? UITextView else { return }
guard let plaintext = textView.attributedText?.string else { return }
//guard let plaintext = textView.text else { return }
let location = sender.location(in: textView)
let charIndex = textView.layoutManager.characterIndex(for: location, in: textView.textContainer,
fractionOfDistanceBetweenInsertionPoints: nil)
if let strIndex = plaintext.utf16.index(plaintext.utf16.startIndex, offsetBy: charIndex, limitedBy: plaintext.utf16.endIndex) {
let char = plaintext[strIndex]
print("Character tapped was \(char)")
}
}
let textTap = UITapGestureRecognizer(target: self, action: #selector(handleTap))
textView.addGestureRecognizer(textTap)
Related
I am wanting to find the current paragraph that the user is typing in(where the caret is). Example: Here it would be in the 2nd Paragraph.
I know I can separate paragraphs using: let components = textView.text.components(separatedBy: "\n") but I am unsure how I would run a check for the current editing paragraph. Any ideas?
Here is one approach...
Get the Y position of the caret (insertion point). Then, loop through an enumeration of the paragraphs in the textView, comparing their bounding rects to the caret position:
extension UITextView {
func boundingFrame(ofTextRange range: Range<String.Index>?) -> CGRect? {
guard let range = range else { return nil }
let length = range.upperBound.encodedOffset-range.lowerBound.encodedOffset
guard
let start = position(from: beginningOfDocument, offset: range.lowerBound.encodedOffset),
let end = position(from: start, offset: length),
let txtRange = textRange(from: start, to: end)
else { return nil }
return selectionRects(for: txtRange).reduce(CGRect.null) { $0.union($1.rect) }
}
}
// return value will be Zero-based index of the paragraphs
// if the textView has no text, return -1
#objc func getParagraphIndex(in textView: UITextView) -> Int {
// this will make sure the the text container has updated
theTextView.layoutManager.ensureLayout(for: theTextView.textContainer)
// make sure we have some text
guard let str = theTextView.text else { return -1 }
// get the full range
let textRange = str.startIndex..<str.endIndex
// we want to enumerate by paragraphs
let opts:NSString.EnumerationOptions = .byParagraphs
var caretYPos = CGFloat(0)
if let selectedTextRange = theTextView.selectedTextRange {
caretYPos = theTextView.caretRect(for: selectedTextRange.start).origin.y + 4
}
var pIndex = -1
var i = 0
// loop through the paragraphs, comparing the caret Y position to the paragraph bounding rects
str.enumerateSubstrings(in: textRange, options: opts) {
(substring, substringRange, enclosingRange, b) in
// get the bounding rect for the sub-rects in each paragraph
if let boundRect = self.theTextView.boundingFrame(ofTextRange: substringRange) {
if caretYPos > boundRect.origin.y && caretYPos < boundRect.origin.y + boundRect.size.height {
pIndex = i
b = true
}
i += 1
}
}
return pIndex
}
Usage:
let paraIndex = getParagraphIndex(in myTextView)
I am getting data from Json and displaying it in table view how to check whether the number is float or double or integer in swift 3 if it is float how to get the no.of digits after decimal can anyone help me how to implement this in swift 3 ?
if specialLoop.attributeCode == "special_price" {
let attributeString: NSMutableAttributedString = NSMutableAttributedString(string: "$ \((arr.price))")
attributeString.addAttribute(NSStrikethroughStyleAttributeName, value: 1, range: NSMakeRange(0, attributeString.length))
let specialPrice = specialLoop.value.replacingOccurrences(of: ".0000", with: "0")
print(specialPrice)
cell.productPrice.text = "$ \(specialPrice)"
cell.specialPriceLabel.isHidden = false
cell.specialPriceLabel.attributedText = attributeString
break
}
else {
cell.specialPriceLabel.isHidden = true
let price = arr.price
print(price)
cell.productPrice.text = "$ \( (price))0"
}
You can use (if let)
let data = [String: Any]()
if let value = data["key"] as? Int {
} else if let value = data["key"] as? Float {
} else if let value = data["key"] as? Double {
}
as describe below, you can find a type of any object (whether custom class or built-in class like - String, Int, etc.).
class demo {
let a: String = ""
}
let demoObj = demo()
print(type(of: demoObj))
--> Output: "demo.Type"
Relatively new to Swift programming.
I get an dictionary from which I need to get the user name (first and last name) and display it in table view cell along with other data.
On iPhone 5 and below as the screen width is only 320, the requirement is to just display the first name and first character of the last name.
E.g: "Howard Mark" to "Howard M"
I am sure that there is an elegant way to extract the required string.
struct ScreenSize
{
static let SCREEN_WIDTH = UIScreen.main.bounds.size.width
static let SCREEN_HEIGHT = UIScreen.main.bounds.size.height
static let SCREEN_MAX_LENGTH = max(ScreenSize.SCREEN_WIDTH, ScreenSize.SCREEN_HEIGHT)
static let SCREEN_MIN_LENGTH = min(ScreenSize.SCREEN_WIDTH, ScreenSize.SCREEN_HEIGHT)
}
struct DeviceType
{
static let IS_IPHONE_5_OR_LESS = (UIDevice.current.userInterfaceIdiom == .phone && ScreenSize.SCREEN_MAX_LENGTH <= 568.0)
}
func functionCall (referralData:[String: Any?])
{
var myMutableString = NSMutableAttributedString()
if var nameString = referralData[KEY_FULL_NAME]
{
if DeviceType.IS_IPHONE_5_OR_LESS
{
var a = nameString as? NSString
var arr = a?.components(separatedBy: " ")
let newStr = arr?[0] as? String
nameString = newStr
if((arr?.count)! > 1)
{
if let secondString = arr?[1]
{
let newStr2 = newStr as? NSString
let secondChar = newStr2?.character(at: 0)
let stringchar:String! = String(secondChar!)
nameString = "\(newStr!) \(stringchar)"
}
}
print("iPhone 5 or less")
}
else
{
print("Greater than iPhone 5")
}
}
}
I don't think anyone should be thinking in Swift 3 any more. Here's a Swift 4 example:
var name = "Veeblefetzer Rumplestiltskin"
if let r = name.range(of: " ", options: .backwards, range: name.startIndex..<name.endIndex) {
name = String(name[name.startIndex..<name.index(r.upperBound, offsetBy: 1)])
}
name // "Veeblefetzer R"
maybe like this?
let fullName = "Dominik Pich"
var parts = fullName.components(separatedBy: " ")
if parts.count > 0,
let lastNameInitial = parts.last?.prefix(1) {
parts[parts.count-1] = String(lastNameInitial)
}
let truncatedName = parts.joined(separator: " ")
//use
print("fullName: \(fullName)")
print("truncatedName: \(truncatedName)")
I'd wrap it in a nice String extension - e.g. a computed property truncateLast
The most helpful answer I've found when using Swift's built in substring command is here: https://stackoverflow.com/a/39677331/5495979
Although, using the substring command still requires you to first obtain a range or an index, so it still requires a couple commands to implement.
So Using my preferred method from the SO answer referenced above I would just grab the index of the first character and use the substring command and cast it to a string:
let index = secondString.index(secondString.startIndex, offsetBy: 0)
let stringChar = String(secondString.substring(to: index))
I also wanted to let you know that there appears to be a logic error in your code when obtaining the first letter of the last name. You unwrap what should be the last name in your string components array with if let secondString = arr?[1] but then don't assign the unwrapped string stored in secondString to newStr2 before parsing newStr2 for the first character in the string. As it appears now when you parse the first character of the string with let secondChar = newStr2?.character(at: 0) you will actually be parsing the first character from the first name (since when assigning newStr2 with let newStr2 = newStr as? NSString you are actually assigning the fist entry from the array of name strings since newStr is only assinged with let newStr = arr?[0] as? String)
Long time user, first time poster, so my apologies if I make any errors in presenting my question. I have been working on this for hours and I've decided it is time to ask the experts. I have also searched through every similar question that has been "answered" and work, which leads me to believe they are outdated.
I am attempting to grab the tapped word from a UITextview that would be used later in the code. For example, there is a paragraph of words in the text view:
"The initial return on time investment is much smaller, due to him trading his upfront cost for sweat-equity in the company, but the potential long-term payout is much greater".
I would want to be able to tap on a word, e.g. 'investment', and run it through another function to define it. However simply tapping the word, crashes the program, and I do not receive the word tapped.
I implemented a tap gesture recognizer:
let tap = UITapGestureRecognizer(target: self, action: #selector(tapResponse(_:)))
tap.delegate = self
tvEditor.addGestureRecognizer(tap)
and then wrote the function: 2
func tapResponse(recognizer: UITapGestureRecognizer) {
let location: CGPoint = recognizer.locationInView(tvEditor)
let position: CGPoint = CGPointMake(location.x, location.y)
let tapPosition: UITextPosition = tvEditor.closestPositionToPoint(position)!
let textRange: UITextRange = tvEditor.tokenizer.rangeEnclosingPosition(tapPosition, withGranularity: UITextGranularity.Word, inDirection: 1)!
let tappedWord: String = tvEditor.textInRange(textRange)!
print("tapped word : %#", tappedWord)
}
Ideally, this should take the location from the tapped part of the Textview, take the position by taking the .x & .y, and then looking through the Textview at the point closest to the position, finding the Range enclosing the position with granularity (to return the word), and setting the contents as a String, which I am currently just printing to the console. However, on tapping the word, I receive this crash.3
along with "fatal error: unexpectedly found nil while unwrapping an Optional value" in the console.
Any help would be greatly appreciated. I may just be missing something simple, or it could be much more complicated.
Whenever the tapped received at the blank spaces between the words, tapPosition returned by the TextView can be nil nil.
Swift has new operator called optional ? which tells the compiler that the variable may have nil value. If you do not use ? after the variable name indicates that the variable can never have nil value.
In Swift, using ! operator means you are forcing the compiler to forcefully extract the value from the optional variable. So, in that case, if the value of the variable is nil, it will crash on forcing.
So, what is actually happening is
You are creating the variable let tapPosition: UITextPosition, let textRange: UITextRange and let tappedWord: String are not optional
return type of the method myTextView.closestPositionToPoint(position), tvEditor.textInRange(textRange) are optional variable UITextPosition?, String?
Assigning a value of optional variable to non optional variable requires !
The method is returning nil and you are forcing it to get the value ! lead to CRASH
What you can do
Before forcing any optional variable, just be sure that it has some value using
if variable != nil
{
print(variable!)
}
Correct method would be as
func tapResponse(recognizer: UITapGestureRecognizer) {
let location: CGPoint = recognizer.locationInView(myTextView)
let position: CGPoint = CGPointMake(location.x, location.y)
let tapPosition: UITextPosition? = myTextView.closestPositionToPoint(position)
if tapPosition != nil {
let textRange: UITextRange? = myTextView.tokenizer.rangeEnclosingPosition(tapPosition!, withGranularity: UITextGranularity.Word, inDirection: 1)
if textRange != nil
{
let tappedWord: String? = myTextView.textInRange(textRange!)
print("tapped word : ", tappedWord)
}
}
}
Swift 3.0 Answer - Working as of July 1st, 2016
In my ViewDidLoad() -
I use text from a previous VC, so my variable "theText" is already declared. I included a sample string that has been noted out.
//Create a variable of the text you wish to attribute.
let textToAttribute = theText // or "This is sample text"
// Break your string in to an array, to loop through it.
let textToAttributeArray = textToAttribute.componentsSeparatedByString(" ")
// Define a variable as an NSMutableAttributedString() so you can append to it in your loop.
let attributedText = NSMutableAttributedString()
// Create a For - In loop that goes through each word your wish to attribute.
for word in textToAttributeArray{
// Create a pending attribution variable. Add a space for linking back together so that it doesn't looklikethis.
let attributePending = NSMutableAttributedString(string: word + " ")
// Set an attribute on part of the string, with a length of the word.
let myRange = NSRange(location: 0, length: word.characters.count)
// Create a custom attribute to get the value of the word tapped
let myCustomAttribute = [ "Tapped Word:": word]
// Add the attribute to your pending attribute variable
attributePending.addAttributes(myCustomAttribute, range: myRange)
print(word)
print(attributePending)
//append 'attributePending' to your attributedText variable.
attributedText.appendAttributedString(attributePending) ///////
print(attributedText)
}
textView.attributedText = attributedText // Add your attributed text to textview.
Now we will add a tap gesture recognizer to register taps.
let tap = UITapGestureRecognizer(target: self, action: #selector(HandleTap(_:)))
tap.delegate = self
textView.addGestureRecognizer(tap) // add gesture recognizer to text view.
Now we declare a function under the viewDidLoad()
func HandleTap(sender: UITapGestureRecognizer) {
let myTextView = sender.view as! UITextView //sender is TextView
let layoutManager = myTextView.layoutManager //Set layout manager
// location of tap in myTextView coordinates
var location = sender.locationInView(myTextView)
location.x -= myTextView.textContainerInset.left;
location.y -= myTextView.textContainerInset.top;
// character index at tap location
let characterIndex = layoutManager.characterIndexForPoint(location, inTextContainer: myTextView.textContainer, fractionOfDistanceBetweenInsertionPoints: nil)
// if index is valid then do something.
if characterIndex < myTextView.textStorage.length {
// print the character index
print("Your character is at index: \(characterIndex)") //optional character index.
// print the character at the index
let myRange = NSRange(location: characterIndex, length: 1)
let substring = (myTextView.attributedText.string as NSString).substringWithRange(myRange)
print("character at index: \(substring)")
// check if the tap location has a certain attribute
let attributeName = "Tapped Word:" //make sure this matches the name in viewDidLoad()
let attributeValue = myTextView.attributedText.attribute(attributeName, atIndex: characterIndex, effectiveRange: nil) as? String
if let value = attributeValue {
print("You tapped on \(attributeName) and the value is: \(value)")
}
}
}
In addition to #AmitSingh answer, this is updated Swift 3.0 version:
func didTapTextView(recognizer: UITapGestureRecognizer) {
let location: CGPoint = recognizer.location(in: textView)
let position: CGPoint = CGPoint(x: location.x, y: location.y)
let tapPosition: UITextPosition? = textView.closestPosition(to: position)
if tapPosition != nil {
let textRange: UITextRange? = textView.tokenizer.rangeEnclosingPosition(tapPosition!, with: UITextGranularity.word, inDirection: 1)
if textRange != nil
{
let tappedWord: String? = textView.text(in: textRange!)
print("tapped word : ", tappedWord!)
}
}
}
The other code is the same as his.
Hope it helps!
Yesterday I was working on getting and setting the cursor position in a UITextField. Now I am trying to get the character just before the cursor position. So in the following example, I would want to return an "e".
func characterBeforeCursor() -> String
Notes
I didn't see any other SO questions that were the same of this, but maybe I missed them.
I wrote this question first and when I find an answer I will post both the question and the answer at the same time. Of course, better answers are welcomed.
I said "character" but String is fine.
If the cursor is showing and the position one place before it is valid, then get that text. Thanks to this answer for some hints.
func characterBeforeCursor() -> String? {
// get the cursor position
if let cursorRange = textField.selectedTextRange {
// get the position one character before the cursor start position
if let newPosition = textField.position(from: cursorRange.start, offset: -1) {
let range = textField.textRange(from: newPosition, to: cursorRange.start)
return textField.text(in: range!)
}
}
return nil
}
The result of
if let text = characterBeforeCursor() {
print(text)
}
is "e", as per your example.
You can also use this:
NSInteger endOffset = [textfld offsetFromPosition:textfld.beginningOfDocument toPosition:range1.end];
NSRange offsetRange = NSMakeRange(endOffset-1, 1);
NSString *str1 = [textfld.text substringWithRange:offsetRange];
NSLog(#"str1= %#",str1);
In swift you can use
let range1 : UITextRange = textField.selectedTextRange!
let endoffset : NSInteger = textField.offsetFromPosition(textField.beginningOfDocument, toPosition: range1.end)
let offsetRange : NSRange = NSMakeRange(endoffset-1, 1)
let index: String.Index = (textField.text?.startIndex.advancedBy(offsetRange.location))!
let str1 : String = (textField.text?.substringFromIndex(index))!
let index1 : String.Index = str1.startIndex.advancedBy(1)
let str2: String = str1.substringToIndex(index1)
print(str2)