Here's a picture of a UILabel getting split between two lines:
I'm okay with it getting split, but it's getting split awkwardly. Is there any way to distribute the text more evenly between the two lines? I.e. to have three words (no pun intended) on each line in this case. The string is coming from user input, so I need a solution that works for any string (character limit is 40). Also, I'm doing this programatically. Thanks!
Add a linebreak \n to the text where you want the split to happen.
EDIT
Here is a solution that splits the string in roughly half, based on spaces:
var str = "Hello, label, here is some variable text"
let length = str.characters.count // 40
var splitRange = str.range(of: " ", options: String.CompareOptions.literal, range: str.index(str.startIndex, offsetBy: length / 2)..<str.endIndex, locale: nil) // finds first space after halfway mark
var firstLine = str.substring(to: splitRange!.lowerBound) // "Hello, label, here is"
var secondLine = str.substring(from: splitRange!.upperBound) // "some variable text"
Related
I want to split a paragraph with multiple sentences part by part. Specifically, I want to show 1st sentence in a Text widget and then remaining of the sentence in another Text widget. How can I achieve this?
var str = 'Taste the crunchy and spicy goodness of our potato chips! Our chips are sure to tantalize your taste buds, leaving you wanting more! And with their irresistible flavor, you won't be able to resist the temptation to indulge. So why not grab a bag today and experience the deliciousness of our potato chips? You won't regret';
var parts = str.split(' ');
var prefix = parts[0].trim();
var prefix1 = parts[1].trim();
print(prefix); // Taste
print(prefix1); // the
It prints out word by word but not sentence by sentence..
If your sentences are separated by periods (.), you can do something like this:
String str = "first. second. third.";
// get the position of the first period
int index = str.indexOf(".");
// get first sentence
var firstPart = str.substring(0, index + 1).trim();
// get the rest of sentences
var secondPart = str.substring(index + 1).trim();
update:
if you sentences can end with (. or ! or ?), you can use a RegEx as an argument to indexOf method
int index = str.indexOf(RegExp(r'[\.\?\!]'));
How can I get the NSRange of last line inside one line? (If cursor is at the end of it)
In the image example, I want to get the line range of the text in red box.
My code now get the range of whole line (green box):
var currentLineRange: NSRange {
let nsText = self.text as NSString
let currentLineRange = nsText.lineRange(for: self.selectedRange)
return currentLineRange
}
Notice that the range you want is a function of the width with which you have laid out the text, and the font of the text, among many other things. You cannot tell this from a String alone, as a String is just some Characters, and does not have those information. The lineRange method looks for new line characters in the string, which there evidently aren't, in your Lorem Ipsum paragraph, between "of" and "Lorem", hence that is not counted as a "line".
Assuming this is displayed in a UITextView (as that is what you have tagged the question with), you can use its layoutManager to get the range you want.
let selectedRange = textview.selectedRange
let glyphRange = textview.layoutManager.glyphRange(forCharacterRange: selectedRange, actualCharacterRange: nil)
// if the selection starts after the last glyph, glyphRange.lowerBound is not a valid glyph index
// so we need to use the last glyph in that case
let glyphIndex = glyphRange.lowerBound == textview.layoutManager.numberOfGlyphs ?
glyphRange.lowerBound - 1 : glyphRange.lowerBound
var effectiveGlyphRange = NSRange(location: 0, length: 0)
// this gets the *glyph* range of the line
textview.layoutManager.lineFragmentRect(forGlyphAt: glyphIndex, effectiveRange: &effectiveGlyphRange)
let effectiveCharRange = textview.layoutManager.characterRange(forGlyphRange: effectiveGlyphRange, actualGlyphRange: nil)
This code will get the range of the line (aka line fragment) containing the start of the selected range.
If you want to get a range of multiple lines when multiple lines are selected (similar to how lineRange would have behaved), it should be trivial to modify the code to also get the range of the line containing the end of the selected range, then combine the two ranges.
I would like to be able to get the range(s) of the paragraph(s) overlapping with the textView.selectedRange.
My goal is to change the alignment of the whole paragraph where the selection is.
I have tried the following but the ranges provided by enumerate attribute seem to all be of length 1, making my intersection check useless.
Is there a way to get a list of continuous ranges with the same paragraph style?
var alignment: NSTextAlignment
let contentRange = NSRange(location: 0, length: editor.contentLength)
editor.attributedText.enumerateAttribute(.paragraphStyle, in: contentRange, options: .longestEffectiveRangeNotRequired) { (paragraphStyle, range, _) in
//Check if attribute range contains cursor
if NSIntersectionRange(range, editor.selectedRange).length > 0 {
print(range)
if let paragraphStyle = paragraphStyle as? NSParagraphStyle {
let newStyle: NSMutableParagraphStyle = paragraphStyle.mutableParagraphStyle
newStyle.alignment = alignment
editor.addAttribute(.paragraphStyle, value: newStyle, at: range)
}
}
}
Many thanks
Edit
Thanks to #Larme, having no options does indeed get a continuous range:
editor.attributedText.enumerateAttribute(.paragraphStyle, in: contentRange, options: []) { (paragraphStyle, range, _) in
However, this will combine consecutive paragraphs with the same paragraph style in the same range.
i.e.
The user creates three paragraphs aligned to the left
The user wants to change the middle one to align right
They should be able to
a) select any portion of the paragraph
b) have the cursor at any position in the paragraph (selection length 0) to achieve the same result (affecting the whole paragraph)
The current check using enumarateAttribute will return a range grouping all three paragraphs together (as they all have the same paragraphStyle) and apply the new alignment to all of them.
The enumerateAttribute doesn't really get the range of a single paragraph it will return a consecutive range for all consecutive paragraphs with the same paragraphStyle
Is there another way to get the range of the paragraph corresponding to the selectedRange?
A bit late to the party, but hopefully this helps.
The way I approached this problem was by saving the selected range, pulling an NSString from my NSAttributedString, then calling NSString's paragraphRange. Once you have the paragraph range, then you can enumerate however you'd like!
//0: SAVE RANGE
let rangeOfNote = yourTextView.selectedRange
//1: CONVERT NSATTRIBUTEDSTRING TO NSSTRING
let composeText = yourTextView.attributedText.string as NSString
//2: CALL NSSTRING'S PARAGRAPH RANGE (for: NSRange)
let paragraphRange = composeText.paragraphRange(for: rangeOfNote)
//3: CHECK CORRECT
print(composeText.substring(with: paragraphRange), paragraphRange)
//4: Use range in enumerate attributes.
yourTextView.attributedText.enumerateAttributes(in: paragraphRange, ...)
This question already has answers here:
Number of words in a Swift String for word count calculation
(7 answers)
Closed 5 years ago.
Edit: there is already a question similar to this one but it's for numbers separated by a specific character (Get no. Of words in swift for average calculator). Instead this question is about to get the number of real words in a text, separated in various ways: a line break, some line breaks, a space, more than a space etc.
I would like to get the number of words in a string with Swift 3.
I'm using this code but I get imprecise result because the number is get counting the spaces and new lines instead of the effective number of words.
let str = "Architects and city planners,are \ndesigning buildings to create a better quality of life in our urban areas."
// 18 words, 21 spaces, 2 lines
let components = str.components(separatedBy: .whitespacesAndNewlines)
let a = components.count
print(a)
// 23 instead of 18
Consecutive spaces and newlines aren't coalesced into one generic whitespace region, so you're simply getting a bunch of empty "words" between successive whitespace characters. Get rid of this by filtering out empty strings:
let components = str.components(separatedBy: .whitespacesAndNewlines)
let words = components.filter { !$0.isEmpty }
print(words.count) // 17
The above will print 17 because you haven't included , as a separation character, so the string "planners,are" is treated as one word.
You can break that string up as well by adding punctuation characters to the set of separators like so:
let chararacterSet = CharacterSet.whitespacesAndNewlines.union(.punctuationCharacters)
let components = str.components(separatedBy: chararacterSet)
let words = components.filter { !$0.isEmpty }
print(words.count) // 18
Now you'll see a count of 18 like you expect.
I need to get the range of two words in a string, for example:
ยัฟิแก ไฟหก
(this is literally me typing PYABCD WASD) - it's a non-sensical test since I don't speak Thai.
//Find all the ranges of each word
var words: [String] = []
var ranges: [NSRange] = []
//Convert to nsstring first because otherwise you get stuck with Ranges and Strings.
let nstext = backgroundTextField.stringValue as NSString //contains "ยัฟิแก ไฟหก"
words = nstext.componentsSeparatedByString(" ")
var nstextLessWordsWeHaveRangesFor = nstext //if you have two identical words this prevents just getting the first word's range
for word in words
{
let range:NSRange = nstextLessWordsWeHaveRangesFor.rangeOfString(word)
Swift.print(range)
ranges.append(range)
//create a string the same length as word
var fillerString:String = ""
for i in 0..<word.characters.count{
//for var i=0;i<word.characters.count;i += 1{
Swift.print("i: \(i)")
fillerString = fillerString.stringByAppendingString(" ")
}
//remove duplicate words / letters so that we get correct range each time.
if range.length <= nstextLessWordsWeHaveRangesFor.length
{
nstextLessWordsWeHaveRangesFor = nstextLessWordsWeHaveRangesFor.stringByReplacingCharactersInRange(range, withString: fillerString)
}
}
outputs:
(0,6)
(5,4)
Those ranges are overlapping.
This causes problems down the road where I'm trying to use NSLayoutManager.enumerateEnclosingRectsForGlyphRange since the ranges are inconsistent.
How can I get the correct range (or in this specific case, non-overlapping ranges)?
Swift String characters describe "extended grapheme clusters", and NSString
uses UTF-16 code points, therefore the length of a string differs
depending on which representation you use.
For example, the first character "ยั" is actually the combination
of "ย" (U+0E22) with the diacritical mark " ั" (U+0E31).
That counts as one String character, but as two NSString characters.
As a consequence, indices change when you replace the word with
spaces.
The simplest solution is to stick to one, either String or NSString
(if possible). Since you are working with NSString, changing
for i in 0..<word.characters.count {
to
for i in 0..<range.length {
should solve the problem. The creation of the filler string
can be simplified to
//create a string the same length as word
let fillerString = String(count: range.length, repeatedValue: Character(" "))
Removing nstextLessWordsWeHaveRangesFor solves the issue (at the bottom starting with range.length <= nstextLessWordsWeHaveRangesFor.length). The modification of that variable is changing the range and giving unexpected output. Here is the result when the duplicate word removal is removed:
var words: [String] = []
let nstext = "ยัฟิแก ไฟหก" as NSString
words = nstext.componentsSeparatedByString(" ")
for word in words {
let range = nstext.rangeOfString(word)
print(range)
}
Output is: (0,6) and (7,4)