String.anotherIndex causes "fatal error: cannot increment endIndex" - ios

I try to get the distance between a string's startIndex and another index, but get the following error in the first loop iteration. The code actually works with most string, but with some it crashes.
fatal error: cannot increment endIndex
let content = NSMutableAttributedString(string: rawContent, attributes: attrs)
var startIndex = content.string.characters.startIndex
while true {
let searchRange = startIndex..<rawContent.characters.endIndex
if let range = rawContent.rangeOfString("\n", range: searchRange) {
let index = rawContent.characters.startIndex.distanceTo(range.startIndex)
startIndex = range.startIndex.advancedBy(1)
rawContent.replaceRange(range, with: "*")
content.addAttribute(
NSForegroundColorAttributeName,
value: UIColor.redColor(),
range: NSMakeRange(index, 1))
}
else {
break
}
}
content.replaceCharactersInRange(NSMakeRange(0, content.length), withString: rawContent)
content is NSMutableAttributedString and when the app crashes the variables have the following values:
range.startIndex: 164
content.string.characters.startIndex: 0
content.string.characters.endIndex: 437,
content.string.characters.count: 435
I don't understand why the error message says about increasing endIndex when I'm trying to calculate the distance from the startIndex and anotherIndex is less than the string length.

The cause for the error is that you are mixing Range<String.Index> and NSRange APIs. The first is counting in Characters and the second in UTF–16 code units. If you start with:
import Cocoa
let content = NSMutableAttributedString(string: "♥️♥️\n")
... then your code enters an infinite loop (this refers to #Tapani's original question and I haven't checked if this is still the case after his changes; the central problem remains the same though)! This is because:
NSString(string: "♥️♥️\n").length // 5
"♥️♥️\n".characters.count // 3
... so that you end up replacing (part of) the second heart with a space, leaving the new line in place, which in turn keeps you in the loop.
One way to avoid these problems is to do something along the lines of:
let content = NSMutableAttributedString(string: "♥️♥️\n")
let newLinesPattern = try! NSRegularExpression(pattern: "\\n", options: [])
let length = (content.string as NSString).length
let fullRange = NSMakeRange(0, length)
let matches = newLinesPattern.matchesInString(content.string, options: [], range: fullRange)
for match in matches.reverse() {
content.replaceCharactersInRange(match.range, withAttributedString: NSAttributedString(string: " "))
}
content.string // "♥️♥️ "
If you copy and paste this into a playground and study the code (e.g. Alt-Click on method names to popup their API), you'll see that this code works exclusively with NSRange metrics. It is also much safer as we are looping through the matched ranges in reverse so you can replace them with substrings of different length. Moreover, the use of NSRegularExpression makes this a more general solution (plus you can store the patterns and reuse them elsewhere). Finally, replacing with NSAttributedString, in addition to replaceCharactersInRange being NSRange based, also gives you a greater control in the sense that you can either keep the existing attributes (get them from the range you are replacing) or add others...

Your code worked fine for me, but you should be error checking. And since you are searching the entire content.string anyway why add the complexity of setting the search range?
Something like this would be simpler:
if let anotherIndex = content.string.rangeOfString("\n")
{
let index = content.string.startIndex.distanceTo(anotherIndex.startIndex)
}

Related

UILabel text is not updated even in MainThread

UILabel.text is not updated inside the main thread. labelOne is updated but labelTwo which is to show translated word is not updated. When I print translatedWord it prints right string to console but UILabel is not updated.
datatask = session.dataTask(with: request, completionHandler: {data, response, error in
if error == nil {
let receivedData = try? JSONSerialization.jsonObject(with: data!, options: []) as? [String: Any]
DispatchQueue.main.async {
self.labelOne.text = wordTobeTranslated
let data = "\(String(describing: receivedData!["text"]))"
let all = data.components(separatedBy: "(")
let afterAll = all[2]
let last = afterAll.components(separatedBy:")" )
self.translatedWord = last[0]
self.importantWords.append(last[0])
self.labelTwo.text = self.translatedWord
print(self.translatedWord)
}
}
})
datatask?.resume()
Some tips to debug your issue:
Try to set some hard coded string to labelTwo and check whether it
is displaying. (or)
Update any value for labelTwo in Storyboard and check whether is is
displaying.
If string is displayed from any of the above steps, then labelTwo is configured correctly in storyboard.
You can also try self.labelTwo.layoutIfNeeded() method after updating the text to force update the UI.
If none of the steps helps you, check the Font color of UILabel. If it is same as background color, it would not be seen.
Looking at your code sample, it would appear that you’re trying to retrieve:
describing: receivedData!["text"]
from:
\(String(describing: receivedData!["text"]))
The problem is that the \(...) in a String literal results in string interpolation where the expression inside \( and ) will be evaluated and that’s what will be place in the string. And the String(describing: ...) will interpret the value and return a string representation. So, let’s say that receivedData!["text"] contained the word “Foo”. Then
let data = "\(String(describing: receivedData!["text"]))"
Would result in data containing the string, Optional("Foo").
If you want to remove that Optional(...) part, you should either unwrap the optional or use a nil-coalescing operator, ??. And frankly, rather than using string interpolation at all, I’d just do:
let data = String(describing: receivedData?["text"] ?? "")
I know this is an old question, but for new searchers: Try setting number of lines of UILabel to Zero. It might be a constraints issue.

Swift: Replace Strings to NSTextAttachment in UITextView after loading styled RTF

In my iOS/swift project I am loading a RTF document to a UITextView with the code below. The RTF itself contains styled text like "... blah blah [ABC.png] blah blah [DEF.png] blah..." wich is loaded to the UITextView just fine.
Now, I want to replace all occurrences of [someImage.png] with an actual image as NSTextAttachment. How can I do that?
I am aware of the possibility to embed images in RTF documents, but I can not do that in this project.
if let rtfPath = Bundle.main.url(forResource: "testABC", withExtension: "rtf")
{
do
{
//load RTF to UITextView
let attributedStringWithRtf = try NSAttributedString(url: rtfPath, options: [.documentType: NSAttributedString.DocumentType.rtf], documentAttributes: nil)
txtView.attributedText = attributedStringWithRtf
//find all "[ABC.png]" and replace with image
let regPattern = "\\[.*?\\]"
//now...?
}
}
Here is something that you could do.
Note: I'm not a Swift Developper, more an Objective-C one, so there may be some ugly Swift code (the try!, etc.). But it's more for the logic on using NSRegularExpression (which I used in Objective-C since it's shared in CocoaTouch)
So the main line directives:
Find where are the images placeholders.
Create a NSAttributeString/NSTextAttachment from it.
Replace the placeholder with the previous attributed string.
let regPattern = "\\[((.*?).png)\\]"
let regex = try! NSRegularExpression.init(pattern: regPattern, options: [])
let matches = regex.matches(in: attributedStringWithRtf.string, options: [], range: NSMakeRange(0, attributedStringWithRtf.length))
for aMatch in matches.reversed()
{
let allRangeToReplace = attributedStringWithRtf.attributedSubstring(from: aMatch.range(at: 0)).string
let imageNameWithExtension = attributedStringWithRtf.attributedSubstring(from: aMatch.range(at: 1)).string
let imageNameWithoutExtension = attributedStringWithRtf.attributedSubstring(from: aMatch.range(at: 2)).string
print("allRangeToReplace: \(allRangeToReplace)")
print("imageNameWithExtension: \(imageNameWithExtension)")
print("imageNameWithoutExtension: \(imageNameWithoutExtension)")
//Create your NSAttributedString with NSTextAttachment here
let myImageAttribute = ...
attributedStringWithRtf.replaceCharacters(in: imageNameRange, with: myImageAttributeString)
}
So what's the idea?
I used a modify pattern. I hard-wrote "png", but you could change it. I added some () to get easily the interesting parts. I thought that you may wanted to retrieve the name of the image, with or without the .png, that's why I got all theses print(). Maybe because you saved it in your app, etc. If you need to add the extension as a group, you may want to add it into parenthesis in your regPattern and check what aMatch.range(at: ??) to call. for using Bundle.main.url(forResource: imageName, withExtension: imageExtension)
I used the matches.reversed() because if you modify the length of the "match" with a replacement of different length, the previous ranges will be off. So starting from the end could do the trick.
Some code to transform a UIImage into NSAttributedString through NSTextAttachment: How to add images as text attachment in Swift using nsattributedstring

Detect and open a Postal Address in Swift(iOS) using NSDataDetector

I currently have a UITextView with Address detection active, this detects any addresses in this field and makes them into a clickable link that opens up Apple Maps with the address as the focus. I really like this functionality but my users would rather have a button to click on instead of the inline link.
With this in mind I have researched into NSDataDetector and managed to get this functionality working for emailing and phone numbers but I am stuck on Addresses. The code I am using to detect the address is below:
let types: NSTextCheckingType = [.Address]
let detector = try! NSDataDetector(types: types.rawValue)
let matches = detector.matchesInString(input, options: [], range: NSMakeRange(0, input.characters.count))
for match in matches {
return NSURL(string: "\((input as NSString).substringWithRange(match.range))")
}
However the NSURL fails to be created, I believe the syntax must be wrong, it works for phone numbers as below:
let types: NSTextCheckingType = [.PhoneNumber]
let detector = try! NSDataDetector(types: types.rawValue)
let matches = detector.matchesInString(input, options: [], range: NSMakeRange(0, input.characters.count))
for match in matches {
return NSURL(string: "telprompt://\((input as NSString).substringWithRange(match.range))")
}
If the address is valid it returns the components of the address(street, city etc.) in the matches object. I was hoping the NSURL could be used with address just like phone numbers but I may be wrong so is there an alternate way I could use the results from the detector and display them on Apple Maps manually?
Or as a side note could I hide the UITextView and trigger the link touch programatically based on a button touch?
Any more information can be provided as needed,
thank you in advance for any help.

Using NSDataDetector to just like Apple's Notes app

I'm trying to find several different data types including Dates, Addresses, Phone numbers, and Links. I'm already able to find them but I want to be able to format them by underlining and changing their color. This is my code so far.
func detectData() {
let text = self.textView.text
let types: NSTextCheckingType = .Date | .Address | .PhoneNumber | .Link
var error: NSError?
let detector = NSDataDetector(types: types.rawValue, error: &error)
var dataMatches: NSArray = [detector!.matchesInString(text, options: nil, range: NSMakeRange(0, (text as NSString).length))]
for match in dataMatches {
I was thinking I should first get each result out of the loop then
1) turn them into strings 2)format them.
First question. How will I put my formatted string back into my UITextView at the same place?
Second question. I'm thinking about creating a switch like so
switch match {
case match == NSTextCheckingType.date
but now that I have a specific type of NSTextCheckingType, what do I have to do to make them have the functionality I want? (e.g. call a phone number, open up maps for an address, create a event for a date)
To do what Notes does you just need to set the dataDetectorTypes property on your text view. That's all! No NSDataDetector involved.

create an attributed string out of plain (Android formatted) text in swift for iOS

I am reading strings out of a Localizable.strings which contains something like that which is basically what you have in an strings.xml of an Android app
"testShort" = "A <b>short</b>\ntest with another<b>bold text</b>";
The bold and and line feed are the only two formatting attributes I have in my texts. I am trying to develop a method like this for days now without success:
func ConvertText(inputText: String) -> NSAttributedString {
// here comes the conversion to a representation with Helvetica 14pt and Helvetica-Bold 14pt including line feeds.
}
My final goal is to display the text in an UITextView's attributedText.
Being kinda new to Swift and iOS without knowing Objective-C I found its very difficult to do String manipulations as they are quite different and complex and all examples are in Objective-C. What makes it even harder is that most API methods are not available or different in Swift than in Objective-C...
Here is what I tried so far for the method body with the help of a lot of other posts here:
var test = inputText.dataUsingEncoding(NSUnicodeStringEncoding, allowLossyConversion: true)!
attrStr = NSAttributedString(
data: test,
options: [NSDocumentTypeDocumentAttribute: NSHTMLTextDocumentType],
documentAttributes: nil,
error: nil)!
return attrStr
The main issues here are that \n isn't converted and the font is very small and different.
Next I tried to manually bold a part of a text. It seem to work like that:
var newText = NSMutableAttributedString(string: inputText)
newText.addAttribute(NSFontAttributeName, value: UIFont(name: "Helvetica-Bold", size: 14.0)!, range: NSRange(location:2,length:4))
Now I tried to search for the attributes in the text, deleting them and use the addAttribute kinda like that
// Start of bold text
var range = newText.rangeOfString("<b>")!
// End of bold text
var range2 = newText.rangeOfString("</b>")!
// replacing attributes
newText = newText.stringByReplacingCharactersInRange(range, withString: "")
newText = newText.stringByReplacingCharactersInRange(range2, withString: "")
// creating a range for bold => error "'String.Index' is not convertible to 'int'"
// how to do such a thing
var boldRange = NSMakeRange(range.startIndex, range2.endIndex -3)
// setting bold
newText.addAttribute(NSFontAttributeName, value: UIFont(name: "Helvetica-Bold", size: 14.0)!, range: boldRange)
This whole range thing is my main issue at the moment as its quite different to a simple position in the string.
This issue is a great example for the lack of (or well hidden) documentation:
The addAttribute wants an NSRange, the rangeOfString seems to deliver a generic Range according to an error message I get - but there is no info about it.
The Search Documentation button in Xcode on rangeOfString() leads to NSString.
Searching in there for rangeOfString()says it returns NSRange. Clicking on that leads to the info of a type alias for _NSRange which in turn has two NSUInteger properties named location and length. Where is the startIndex and endIndex property I see in XCode? Very confusing...
Would be great if you can give me some snippets or hints where I'm wrong here or even the method body as I'm still hoping its not too difficult if you know iOS and Swift well. I'm aiming for iOS 7.1 support but if its way easier with iOS 8 only its fine as well.
Regarding your first method with NSAttributedString:
The \n character in HTML is just ordinary white space. To get a line break you
would have to replace it by <br /> first.
The font attributes can be controlled by a HTML <span>, see Parsing HTML into NSAttributedText - how to set font?.
This gives (now updated for Swift 2):
func convertText(inputText: String) -> NSAttributedString {
var html = inputText
// Replace newline character by HTML line break
while let range = html.rangeOfString("\n") {
html.replaceRange(range, with: "<br />")
}
// Embed in a <span> for font attributes:
html = "<span style=\"font-family: Helvetica; font-size:14pt;\">" + html + "</span>"
let data = html.dataUsingEncoding(NSUnicodeStringEncoding, allowLossyConversion: true)!
let attrStr = try? NSAttributedString(
data: data,
options: [NSDocumentTypeDocumentAttribute: NSHTMLTextDocumentType],
documentAttributes: nil)
return attrStr!
}
Regarding your second method:
There are two different rangeOfString() methods, one for (Swift) String and one
for (Foundation) NSString. The String method returns a Range<String.Index>
and the NSString method returns an NSRange.
Converting between these two is possible but complicated. The reason is that in
a String each "extended grapheme cluster" counts as one character, whereas in
NSString each UTF-16 unit is counted. An extended grapheme cluster can be
one or more UTF-16 unit ("😄" is two UTF-16 units, "🇩🇪" is four).
The addAttribute() method accepts only an NSRange. The easiest method to solve
this problem is to convert the Swift string to NSString and work with NSRange
only. Then your method could look like this:
func convertText(inputText: String) -> NSAttributedString {
let attrString = NSMutableAttributedString(string: inputText)
let boldFont = UIFont(name: "Helvetica-Bold", size: 14.0)!
var r1 = (attrString.string as NSString).rangeOfString("<b>")
while r1.location != NSNotFound {
let r2 = (attrString.string as NSString).rangeOfString("</b>")
if r2.location != NSNotFound && r2.location > r1.location {
let r3 = NSMakeRange(r1.location + r1.length, r2.location - r1.location - r1.length)
attrString.addAttribute(NSFontAttributeName, value: boldFont, range: r3)
attrString.replaceCharactersInRange(r2, withString: "")
attrString.replaceCharactersInRange(r1, withString: "")
} else {
break
}
r1 = (attrString.string as NSString).rangeOfString("<b>")
}
return attrString
}

Resources