Using NSDataDetector to just like Apple's Notes app - ios

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.

Related

Can you save attributed strings to Cloud Firestore?

I'm trying to create a collaborative note-taking app, where the notes are saved as NSAttributedStrings in the iOS app since they contain both images and text.
Can I save a NSAttributedString to Cloud Firestore? If I can, how do I convert the NSAttributedString to a format that's readable by an Android device and website? If it's not possible, what format do I save a note in that contains both images and text (similar to Evernote) that will work across platforms?
EDIT: Are there any markup languages (e.g. HTML, XML) that can be stored in Firestore?
As you can see from the documentation, Firestore strings can only store UTF-8 characters, so you will not be able to store it natively.
An NSAttributedString is not simply "readable" by Android. As far as I can see, there is no direct Android equivalent. The closest thing is a Spannable, but it's not really very close.
I suggest abandoning the idea that you can store OS-specific data in Firestore for use across client platforms. You will have a much easier time if you reduce your data down to the primitive types that Firestore can store.
As Doug mentioned in his answer, trying to use NSAttributedString cross platform is challenging as there is no direct Android equivalent so it's probably best to keep the data as primitives.
...But the short answer is: Yes, you can store an NSAttributedString in Firestore because Firestore supports NSData objects.
If you really want to go cross platform with your string style, one thought is to understand that an NSAttributed string is a string with a dictionary of key: value pairs that define the strings look. So you could store primitives in Firestore and then use the appropriate platforms functions to re-assemble the string.
So the string could be stored in Firestore like this
string_0 (a document)
text: "Hello, World"
attrs:
font: "Helvetica"
size: "12"
color: "blue"
You could then read that in as create an attributed string based on those attributes.
That being said, I can get you 1/2 way there on the iOS/macOS side if you really want to store an NSAttributed string in Firestore.
Here's a function that creates an NSAttributedString, archives it and stores the data in Firestore.
func storeAttributedString() {
let quote = "Hello, World"
let font = NSFont.boldSystemFont(ofSize: 20)
let color = NSColor.blue
let intialAttributes: [NSAttributedString.Key: Any] = [
.font: font,
.foregroundColor: color,
]
let attrString = NSAttributedString(string: quote, attributes: intialAttributes)
let archivedData: Data = try! NSKeyedArchiver.archivedData(withRootObject: attrString, requiringSecureCoding: false)
let dict: [String: Any] = [
"attrString": archivedData
]
let attrStringCollection = self.db.collection("attr_strings")
let doc = attrStringCollection.document("string_0")
doc.setData(dict)
}
then to read it back, here's the function that reads it and displays the attributed string an a macOS NSTextField.
func readAttributedString() {
self.myField.allowsEditingTextAttributes = true //allows rich text
let attrStringCollection = self.db.collection("attr_strings")
let doc = attrStringCollection.document("string_0")
doc.getDocument(completion: { snapshot, error in
if let err = error {
print(err.localizedDescription)
return
}
guard let snap = snapshot else { return }
let archivedData = snap.get("attrString") as! Data
let unarchivedData: NSAttributedString? = try! NSKeyedUnarchiver.unarchiveTopLevelObjectWithData(archivedData) as? NSAttributedString
self.myField.attributedStringValue = unarchivedData!
})
}

Apple Watch input values via scribble only

I'm working on WatchKit App. In this app, there are some fields that the user should fill it,
I searched how to deal with input fields in iWatch, and I found the following code:
presentTextInputController(withSuggestions: ["1"], allowedInputMode: WKTextInputMode.plain) { (arr: [Any]?) in
if let answers = arr as? [String] {
if let answer = answers[0] as? String {
self.speechLabel.setText(answer)
}
}
}
and this code gives me two choices: Diction and scribble, i.e
In my App, I want to support only the scribble not both of them,
I tried to pass withSuggestions parameter as nil, but the app direct me to dictiation, not to scribble.
Is there a way to let the user only use scribble?

Calculating word count from a url in swift

I'm creating a reading list app, and I'd like to pass the read time of a user added link to a table cell in their reading list - and the only way to get that number is from that page's word count. I've found a few solutions, namely Parsehub, Parse and Mercury but they seem to be geared more towards use cases that need more advanced things to be scraped from a url. Is there a simpler way in Swift to calculate word count of a url?
First of all, you need to parse the HTML. HTML can only be parsed reliably with dedicated HTML parser. Please don't use Regular Expressions or any other search method to parse HTML. You may read it why from this link. If you are using swift, you may try Fuzi or Kanna. After you get the body text with any one of the library, you have to remove extra white spaces and count the words. I have written some basic code with Fuzi library for you to get started.
import Fuzi
// Trim
func trim(src:String) -> String {
return src.trimmingCharacters(in: CharacterSet.whitespacesAndNewlines)
}
// Remove Extra double spaces and new lines
func clean(src:String) ->String {
return src.replacingOccurrences(
of: "\\s+",
with: " ",
options: .regularExpression)
}
let htmlUrl = URL(fileURLWithPath: ((#file as NSString).deletingLastPathComponent as NSString).appendingPathComponent("test.html"))
do {
let data = try Data(contentsOf: htmlUrl)
let document = try HTMLDocument(data: data)
// get body of text
if let body = document.xpath("//body").first?.stringValue {
let cleanBody = clean(src: body)
let trimmedBody = trim(src:cleanBody)
print(trimmedBody.components(separatedBy: " ").count)
}
} catch {
print(error)
}
If you are fancy, you may change my global functions to String extension or you can combine them in a single function. I wrote it for clarity.

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.

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

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)
}

Resources