Get Current Paragraph Index - ios

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)

Related

Swift: String firstIndex after character

I'm trying to detect the parentheses on string for example: foo(bar)baz(blim) to and reverse the content inside of the parentheses but I'm getting out of bounce range on my implementation:
func reverseInParentheses(inputString: String) -> String {
var tmpStr = inputString
var done = false
while !done {
if let lastIndexOfChar = tmpStr.lastIndex(of: "(") {
let startIndex = tmpStr.index(lastIndexOfChar, offsetBy:1)
if let index = tmpStr.firstIndex(of: ")") {
let range = startIndex..<index
let strToVerse = String(tmpStr[range])
let reversedStr = reverseStr(str: strToVerse)
tmpStr = tmpStr.replacingOccurrences(of: "(" + strToVerse + ")", with: reversedStr)
}
} else {
done = true
}
}
return tmpStr
}
How can I get the tmpStr.firstIndex(of: ")") after the startIndex any of you knows how can do that?
how can I get the tmpStr.firstIndex(of: ")") after the startIndex?
One way to do this is to "cut" the string at startIndex, and get the second half. Then use firstIndex(of:) on the substring. Since Substrings are just "views" onto the original strings from which they are cut from, firstIndexOf still returns indices of the original string.
let string = "foo(bar)baz(blim)"
if let lastIndexOfChar = string.lastIndex(of: "(") {
let startIndex = string.index(after: lastIndexOfChar)
let substring = string[startIndex..<string.endIndex] // cut off the first part of the string.
// now you have a "Substring" object
if let indexAfterOpenBracket = substring.firstIndex(of: ")") {
// prints "blim", showing that the index is indeed from the original string
print(string[startIndex..<indexAfterOpenBracket])
}
}
You can write this as an extension:
extension StringProtocol {
func firstIndex(of char: Character, after index: Index) -> Index? {
let substring = self[index..<endIndex]
return substring.firstIndex(of: char)
}
}
Now if you call tmpStr.firstIndex(of: ")", after: startIndex) in your reverseInParentheses, it should work.
You can iterate your string keeping an index as reference to compare it to the endIndex. So every time you successfully find a range you do a new search starting after the end index. Btw you should not use replacingOccurrences because it might replace words not inside parentheses as well. You can use RangeReplaceableCollection replaceSubrange and pass the reversed substring to that method.
To find the first index after character you can extend collection and return the index after the firstIndex of the element if found:
extension Collection where Element: Equatable {
func firstIndex(after element: Element) -> Index? {
guard let index = firstIndex(of: element) else { return nil }
return self.index(after: index)
}
}
Your method should look something like this:
func reverseInParentheses(inputString: String) -> String {
var inputString = inputString
var startIndex = inputString.startIndex
while startIndex < inputString.endIndex,
let start = inputString[startIndex...].firstIndex(after: "("),
let end = inputString[start...].firstIndex(of: ")") {
inputString.replaceSubrange(start..<end, with: inputString[start..<end].reversed())
startIndex = inputString.index(after: end)
}
return inputString
}
let str = "foo(bar)baz(blim)"
reverseInParentheses(inputString: str) // "foo(rab)baz(milb)"
Or extending StringProtocol and constraining Self to RangeReplaceableCollection:
extension StringProtocol where Self: RangeReplaceableCollection {
var reversingSubstringsBetweenParentheses: Self {
var startIndex = self.startIndex
var source = self
while startIndex < endIndex,
let start = source[startIndex...].firstIndex(after: "("),
let end = source[start...].firstIndex(of: ")") {
source.replaceSubrange(start..<end, with: source[start..<end].reversed())
startIndex = index(after: end)
}
return source
}
}
let str = "foo(bar)baz(blim)"
str.reversingSubstringsBetweenParentheses // "foo(rab)baz(milb)"
Updated answer of Leo Dabus in case there is written some more text after the last parentheses.
func reverseInParentheses(_ str: String) -> String {
var str = str
var startIndex = str.startIndex
Var lastIndexOfChar = str.lastIndex(of: ")") ?? startIndex
while startIndex < lastIndexOfChar {
let start = str[startIndex...].firstIndex(after: "("),
let end = str[start...].firstIndex(of: ")") {
str.replaceSubrange(start..<end, with: str[start..<end].reversed())
startIndex = str.index(after: end)
}
return str
}

Emoji skin-tone detect

Following this post I tried to update the code from Swift 2.0 to Swift 5.0 to check which emojis have skin tones available or not and other variations already present.
My updated code in detail:
extension String {
var emojiSkinToneModifiers: [String] {
return [ "🏻", "🏼", "🏽", "🏾", "🏿" ]
}
var emojiVisibleLength: Int {
var count = 0
enumerateSubstrings(in: startIndex..<endIndex, options: .byComposedCharacterSequences) { (_, _, _, _) in
count = count + 1
}
return count
}
var emojiUnmodified: String {
if self.count == 0 {
return ""
}
let range = String(self[..<self.index(self.startIndex, offsetBy: 1)])
return range
}
var canHaveSkinToneModifier: Bool {
if self.count == 0 {
return false
}
let modified = self.emojiUnmodified + self.emojiSkinToneModifiers[0]
return modified.emojiVisibleLength == 1
}
}
And use this with an array:
let emojis = [ "πŸ‘", "πŸ‘πŸΏ", "🐸" ]
for emoji in emojis {
if emoji.canHaveSkinToneModifier {
let unmodified = emoji.emojiUnmodified
print(emoji)
for modifier in emoji.emojiSkinToneModifiers {
print(unmodified + modifier)
}
} else {
print(emoji)
}
}
The output:
πŸ‘πŸ‘πŸ»πŸ‘πŸΌπŸ‘πŸ½πŸ‘πŸΎπŸ‘πŸΏ πŸ‘πŸΏπŸ‘πŸΏπŸ»πŸ‘πŸΏπŸΌπŸ‘πŸΏπŸ½πŸ‘πŸΏπŸΎπŸ‘πŸΏπŸΏ 🐸🐸🏻🐸🏼🐸🏽🐸🏾🐸🏿
assigns variations to emojis that do not have them or that already is instead of: πŸ‘πŸ‘πŸ»πŸ‘πŸΌπŸ‘πŸ½πŸ‘πŸΎπŸ‘πŸΏ πŸ‘πŸΏ 🐸
I suppose enumerateSubstringsInRange is incorrect and self.characters.count now became self.count easy and correct to count one emoji (composed) compared to before Swift 4 but maybe not useful in this case. What am I not seeing wrong?
Thanks
A "hack" would be to compare the visual representation of a correct emoji (like "🐸") and a wanna-be emoji (like "🐸🏽").
I've modified your code here and there to make it work:
extension String {
static let emojiSkinToneModifiers: [String] = ["🏻", "🏼", "🏽", "🏾", "🏿"]
var emojiVisibleLength: Int {
var count = 0
let nsstr = self as NSString
let range = NSRange(location: 0, length: nsstr.length)
nsstr.enumerateSubstrings(in: range,
options: .byComposedCharacterSequences)
{ (_, _, _, _) in
count = count + 1
}
return count
}
var emojiUnmodified: String {
if isEmpty {
return self
}
let string = String(self.unicodeScalars.first!)
return string
}
private static let emojiReferenceSize: CGSize = {
let size = CGSize(width : CGFloat.greatestFiniteMagnitude,
height: CGFloat.greatestFiniteMagnitude)
let rect = ("πŸ‘" as NSString).boundingRect(with: size,
options: .usesLineFragmentOrigin,
context: nil)
return rect.size
}()
var canHaveSkinToneModifier: Bool {
if isEmpty {
return false
}
let modified = self.emojiUnmodified + String.emojiSkinToneModifiers[0]
let size = (modified as NSString)
.boundingRect(with: CGSize(width : CGFloat.greatestFiniteMagnitude,
height: .greatestFiniteMagnitude),
options: .usesLineFragmentOrigin,
context: nil).size
return size == String.emojiReferenceSize
}
}
Let's try it out:
let emojis = [ "πŸ‘", "πŸ‘πŸΏ", "🐸" ]
for emoji in emojis {
if emoji.canHaveSkinToneModifier {
let unmodified = emoji.emojiUnmodified
print(unmodified)
for modifier in String.emojiSkinToneModifiers {
print(unmodified + modifier)
}
} else {
print(emoji)
}
print("\n")
}
And voila!
πŸ‘
πŸ‘πŸ»
πŸ‘πŸΌ
πŸ‘πŸ½
πŸ‘πŸΎ
πŸ‘πŸΏ
πŸ‘
πŸ‘πŸ»
πŸ‘πŸΌ
πŸ‘πŸ½
πŸ‘πŸΎ
πŸ‘πŸΏ
🐸

Gradually and randomly visualize string

I am currently working on a simple program to gradually and randomly visualize a string in two iteration. Right now I have managed to get the first iteration but I'm not sure how to do the second one. If someone could give any example or advice I would be very grateful. My code looks like this:
let s = "Hello playground"
let factor = 0.25
let factor2 = 0.45
var n = s.filter({ $0 != " " }).count // # of non-space characters
var m = lrint(factor * Double(n)) // # of characters to display
let t = String(s.map { c -> Character in
if c == " " {
// Preserve space
return " "
} else if Int.random(in: 0..<n) < m {
// Replace
m -= 1
n -= 1
return c
} else {
// Keep
n -= 1
return "_"
}
})
print(t) // h_l__ _l_______d
To clarify, I want to use factor2 in the second iteration to print something that randomly add letters on top of t that looks something like this h_l_o pl_g_____d.
Replacing Characters
Starting from #MartinR's code, you should remember the indices that have been replaced. So, I am going to slightly change the code that replaces characters :
let s = "Hello playground"
let factor = 0.25
let factor2 = 0.45
var n = s.filter({ $0 != " " }).count // # of non-space characters
let nonSpaces = n
var m = lrint(factor * Double(n)) // # of characters to display
var indices = Array(s.indices)
var t = ""
for i in s.indices {
let c = s[i]
if c == " " {
// Preserve space
t.append(" ")
indices.removeAll(where: { $0 == i })
} else if Int.random(in: 0..<n) < m {
// Keep
m -= 1
n -= 1
t.append(c)
indices.removeAll(where: { $0 == i })
} else {
// Replace
n -= 1
t.append("_")
}
}
print(t) //For example: _e___ ______ou_d
Revealing Characters
In order to do that, we should calculate the number of characters that we want to reveal:
m = lrint((factor2 - factor) * Double(nonSpaces))
To pick three indices to reveal randomly, we shuffle indices and then replace the m first indices :
indices.shuffle()
var u = t
for i in 0..<m {
let index = indices[i]
u.replaceSubrange(index..<u.index(after: index), with: String(s[index]))
}
indices.removeSubrange(0..<m)
print(u) //For example: _e__o _l__g_ou_d
I wrote StringRevealer struct, that handle all revealing logic for you:
/// Hide all unicode letter characters as `_` symbol.
struct StringRevealer {
/// We need mapping between index of string character and his position in state array.
/// This struct represent one such record
private struct Symbol: Hashable {
let index: String.Index
let position: Int
}
private let originalString: String
private var currentState: [Character]
private let charactersCount: Int
private var revealed: Int
var revealedPercent: Double {
return Double(revealed) / Double(charactersCount)
}
private var unrevealedSymbols: Set<Symbol>
init(_ text: String) {
originalString = text
var state: [Character] = []
var symbols: [Symbol] = []
var count = 0
var index = originalString.startIndex
var i = 0
while index != originalString.endIndex {
let char = originalString[index]
if CharacterSet.letters.contains(char.unicodeScalars.first!) {
state.append("_")
symbols.append(Symbol(index: index, position: i))
count += 1
} else {
state.append(char)
}
index = originalString.index(after: index)
i += 1
}
currentState = state
charactersCount = count
revealed = 0
unrevealedSymbols = Set(symbols)
}
/// Current state of text. O(n) conplexity
func text() -> String {
return currentState.reduce(into: "") { $0.append($1) }
}
/// Reveal one random symbol in string
mutating func reveal() {
guard let symbol = unrevealedSymbols.randomElement() else { return }
unrevealedSymbols.remove(symbol)
currentState[symbol.position] = originalString[symbol.index]
revealed += 1
}
/// Reveal random symbols on string until `revealedPercent` > `percent`
mutating func reveal(until percent: Double) {
guard percent <= 1 else { return }
while revealedPercent < percent {
reveal()
}
}
}
var revealer = StringRevealer("Hello Ρ‚ΠΎΠ²Π°Ρ€ΠΈΡ‰! πŸ‘‹")
print(revealer.text())
print(revealer.revealedPercent)
for percent in [0.25, 0.45, 0.8] {
revealer.reveal(until: percent)
print(revealer.text())
print(revealer.revealedPercent)
}
It use CharacterSet.letters inside, so most of languages should be supported, emoji ignored and not-alphabetic characters as well.

KeyboardExtension adjustTextPosition issues with emojis

I'm helping build a keyboardextension and I've recently run into an issue with Swift 4 and emojis. The new UTF-16 emoji support for Swift 4 is really nice but there is an issue with adjustTextPosition in UIInputViewController.
If we call adjustTextPosition to step over an emoji it will simply not step far enough, it seems like the characteroffset used by UIInputViewController doesn't match the character count used by the system.
To test simply write a text with emojis and whenever some key is clicked call:
super.textDocumentProxy.adjustTextPosition(byCharacterOffset: 1)
What can be observed is that we have to click it more than what is to be expected.
Swift 5, it seems that the following code works well on iOS 12.
let count: Int = String(text).utf16.count
textDocumentProxy.adjustTextPosition(byCharacterOffset: count)
Adjusting caret position measured in grapheme clusters (Swift Characters):
func adjustCaretPosition(offset: Int) {
guard let textAfterCaret = textDocumentProxy.documentContextAfterInput else { return }
if let offsetIndex = offset > 0 ? textAfterCaret.index(textAfterCaret.startIndex, offsetBy: offset, limitedBy: textAfterCaret.endIndex) : textBeforeCaret.index(textBeforeCaret.endIndex, offsetBy: offset, limitedBy: textAfterCaret.startIndex),
let offsetIndex_utf16 = offsetIndex.samePosition(in: offset > 0 ? textAfterCaret.utf16 : textBeforeCaret.utf16)
{
let offset = offset > 0 ? textAfterCaret.utf16.distance(from: textAfterCaret.utf16.startIndex, to: offsetIndex_utf16) : textBeforeCaret.utf16.distance(from: textBeforeCaret.utf16.endIndex, to: offsetIndex_utf16)
textDocumentProxy.adjustTextPosition(byCharacterOffset: offset)
}
else {
textDocumentProxy.adjustTextPosition(byCharacterOffset: offset)
}
}
UPD: A messy hack, to try and fix Safari inconsistency. Since there's no way to discriminate between safari and not safari, to act based on result looks like the only solution.
func adjustCaretPosition(offset: Int) {
// for convenience
let textAfterCaret = textDocumentProxy.documentContextAfterInput ?? ""
let textBeforeCaret = textDocumentProxy.documentContextBeforeInput ?? ""
if let offsetIndex = offset > 0 ? textAfterCaret.index(textAfterCaret.startIndex, offsetBy: offset, limitedBy: textAfterCaret.endIndex) : textBeforeCaret.index(textBeforeCaret.endIndex, offsetBy: offset, limitedBy: textAfterCaret.startIndex),
let offsetIndex_utf16 = offsetIndex.samePosition(in: offset > 0 ? textAfterCaret.utf16 : textBeforeCaret.utf16)
{
// part of context before caret adjustment
let previousText = offset > 0 ? textAfterCaret : textBeforeCaret
// what we expect after adjustment
let expectedText = offset > 0 ? String(textAfterCaret[offsetIndex..<textAfterCaret.endIndex]) : String(textBeforeCaret[textBeforeCaret.startIndex..<offsetIndex])
// offset in UTF-16 characters
let offset_utf16 = offset > 0 ? textAfterCaret.utf16.distance(from: textAfterCaret.utf16.startIndex, to: offsetIndex_utf16) : textBeforeCaret.utf16.distance(from: textBeforeCaret.utf16.endIndex, to: offsetIndex_utf16)
// making adjustment
textDocumentProxy.adjustTextPosition(byCharacterOffset: offset)
// part of context after caret adjustment
let compareText = offset > 0 ? textAfterCaret : textBeforeCaret
// rollback if got unwanted results
// then adjust by grapheme clusters offset
if compareText != "", expectedText != compareText, compareText != previousText {
textDocumentProxy.adjustTextPosition(byCharacterOffset: -offset_utf16)
textDocumentProxy.adjustTextPosition(byCharacterOffset: offset)
}
}
else {
// we probably stumbled upon a textDocumentProxy inconsistency, i.e. context got divided by an emoji
// adjust by grapheme clusters offset
textDocumentProxy.adjustTextPosition(byCharacterOffset: offset)
}
}
try this
let correctedOffset = adjust(offset: offset)
textDocumentProxy.adjustTextPosition(byCharacterOffset: correctedOffset)
private func adjust(offset: Int) -> Int {
if offset > 0, let after = textDocumentProxy.documentContextAfterInput {
let offsetStringIndex = after.index(after.startIndex, offsetBy: offset)
let chunk = after[..<offsetStringIndex]
let characterCount = chunk.utf16.count
return characterCount
} else if offset < 0, let before = textDocumentProxy.documentContextBeforeInput {
let offsetStringIndex = before.index(before.endIndex, offsetBy: offset)
let chunk = before[offsetStringIndex...]
let characterCount = chunk.utf16.count
return -1*characterCount
} else {
return offset
}
}

Delete characters in range of string

I have the following string I would like to edit:
var someString = "I wan't this text {something I don't want}"
I would like to remove all the text contained in the two braces, no matter how long that text is. I have been using the follow code to remove a section of a String when I know the range:
extension String {
mutating func deleteCharactersInRange(range: NSRange) {
let mutableSelf = NSMutableString(string: self)
mutableSelf.deleteCharactersInRange(range)
self = mutableSelf
}
}
However, I do not know the range in my problem. Any ideas?
Working with strings and ranges can be quite challenging when mixing NSString and NSRange with Swift's String and Range.
Here is a pure Swift solution.
var someString = "I wan't this text {something I don't want}"
let rangeOpenCurl = someString.rangeOfString("{")
let rangeCloseCurl = someString.rangeOfString("}")
if let startLocation = rangeOpenCurl?.startIndex,
let endLocation = rangeCloseCurl?.endIndex {
someString.replaceRange(startLocation ..< endLocation, with: "")
}
With a RegEx pattern to match anything enclosed with curly brackets:
var sourceString: String = "I wan\'t this text {something I don't want}"
let destinationString = sourceString.stringByReplacingOccurrencesOfString("\\{(.*?)\\}", withString: "", options: .RegularExpressionSearch)
print(destinationString)
This will print "I wan't this text " without the double quotes.
extension String {
func getCurlyBraceRanges() -> [NSRange] {
var results = [NSRange]()
var leftCurlyBrace = -1
for index in 0..<self.characters.count {
let char = self[self.startIndex.advancedBy(index)]
if char == Character("{") {
leftCurlyBrace = index
} else if char == Character("}") {
if leftCurlyBrace != -1 {
results.append(NSRange(location: leftCurlyBrace, length: index - leftCurlyBrace + 1))
leftCurlyBrace = -1
}
}
}
return results
}
mutating func deleteCharactersInRange(range: NSRange) {
let mutableSelf = NSMutableString(string: self)
mutableSelf.deleteCharactersInRange(range)
self = String(mutableSelf)
}
mutating func deleteCharactersInRanges(ranges: [NSRange]) {
var tmpString = self
for i in (0..<ranges.count).reverse() {
tmpString.deleteCharactersInRange(ranges[i])
print(tmpString)
}
self = tmpString
}
}
var testString = "I wan't this text {something I don't want}"
testString.deleteCharactersInRanges(testString.getCurlyBraceRanges())
Output: "I wan't this text "

Resources