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!
})
}
Related
I was doing localization of an app in which I face an issue regarding dialects of a country's language. My main question is, is there any provision of adding custom language.
Eg:
Suppose there are two languages:
PL for Poland
UK for Ukraine
I need to support pl-uk i.e Poland Ukraini
Adding a pl-UK.lproj would have made sense if this dialect could be chosen from the system preferences, which is not the case. If you have a local setting, I'm afraid there's no other solution than managing the localisations yourself - and it won't work for Interface Builder files.
The simplest is to store all the pl-UK differences in a separate file (it can be a .strings that you store into the pl.lproj folder (that you localise in Polish Polish - to respect the semantics of the system). Then in a custom function, you load those strings:
func localize(_ string : String, comment: comment) {
guard !isUkrainianPolish else {
return NSLocalizedString(string, comment: comment)
}
// retrieve the cache and check if a key with string exists
if let url = Bundle.main.url(forResource: "localizable_pl_UK" /* or any other name*/, withExtension: "strings", subdirectory: nil, localization:"pl"),
let data = try? Data(contentsOf: url),
let plist = (try? PropertyListSerialization.propertyList(from: data, options: [], format: nil)) as? [String:String] {
// cache the dictionary where you want
return plist[string] ?? NSLocalizedString(string, comment: comment)
}
}
Depending on the organisation of your code, you can implement the function in a singleton or the class that handle localizations.
If this seems like a dupe, sorry. I'm trying to ask a very specific question, and not sure my searching has really led me to the right place. Anyway, here's the setup. Take a picture on the iPhone camera, turn it into base64 string data, shove it up the wire to a Node API, turn that into a file to shove onto S3. Pretty straight forward.
General disclaimers apply; I'd prefer to use a B64 string in JSON for simplicity and universality, and I'll withhold further comments on the silliness of form-encoded uploads :)
Here's my very simple Swift code to produce B64, turn it back into an image, and display it as a proof that the stuff works - at least in Apple land.
Note: "redButton" is one of the assets in my app. I switched to that for the sake of sending much smaller packets at the API for testing, but the results remain. Thanks.
func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [String : Any]) {
// build data packet
imagePicker.dismiss(animated: true, completion: nil)
//let image:UIImage = (info[UIImagePickerControllerOriginalImage] as? UIImage)!
let image:UIImage = UIImage(named: "redButton")!
showAlert( title: "size", msg: String( describing: image.size ) )
let data = UIImageJPEGRepresentation(image, 0.5)
let b64 = data!.base64EncodedData()//.base64EncodedString()//options: Data.Base64EncodingOptions.lineLength64Characters)
let b64String = b64.base64EncodedString()
debugPrint( "len=" + String( describing: b64String.lengthOfBytes(using: String.Encoding.utf8)))
let newDataString = Data.init(base64Encoded: b64String )
let newData = Data.init(base64Encoded: newDataString! )
let newImage = UIImage.init(data: newData!)
tmpImage.image = newImage
}
That all works. I see the image in the little UIImage.
So at least fully in the Apple camp, the img->b64->img works.
However...
When I copy the actual resulting blob of b64-encoded string data, and manually paste it into the source attribute, marked up with the data stuff, it does NOT display the expected image, and in fact, just shows a broken image in the browser.
ie...
<html>
<body>
<img src="data:image/jpg;base64,LzlqLzRBQVFTa1pKUmdBQkFRQUFTQUJJ (brevity)...">
</body>
</html>
So, am I doing something wrong in my proofing in the HTML page? Am I expecting the wrong results from what Apple calls base64 string data? Am I just plain missing something painfully obvious that my sleep-deprived brain is missing?
It eventually gets sent to the server in an HTTP POST call, per normal means, as a dictionary, turned into json via the json encoding stuff in newer Swift.
var params = ["image": [ "content_type": "image/jpg", "filename":"test.jpg", "file_data": b64String] ]
And for the sake of compeleteness, here's the Node code where I reconstitute this data into a binary bit, and from here I shove it up to the S3 system, and in every case, the file is not recognized as a proper JPG file.
var b64 = req.body.image.file_data;
var base64data = new Buffer(b64, 'base64'); // according to all the new-node version docs
I'm on the home stretch of a crunch-time product that we're shoving at investors next week, and apparently this is a critical feature to show off for that meeting.
Tell me I'm missing something painfully obvious, and that I'm stupid. I welcome it, please. It can't not be just something stupid, right?
Thanks!
Here:
let b64 = data!.base64EncodedData()
let b64String = b64.base64EncodedString()
you encode the given data twice. It should be just
let b64String = data!.base64EncodedString()
Your “in Apple land” test works because
let newDataString = Data.init(base64Encoded: b64String )
let newData = Data.init(base64Encoded: newDataString! )
also decodes the Base64 twice. That would now be just
let newData = Data(base64Encoded: b64String)
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
I'm currently working on a iOS app developed in Swift and I need to store some user-created content on the device but I can't seem to find a simple and quick way to store/receive the users content on the device.
Could someone explain how to store and access local storage?
The idea is to store the data when the user executes an action and receive it when the app starts.
The simplest solution for storing a few strings or common types is UserDefaults.
The UserDefaults class provides convenience methods for accessing common types such as floats, doubles, integers, Boolean values, and URLs.
UserDefaults lets us store objects against a key of our choice, It's a good idea to store these keys somewhere accessible so we can reuse them.
Keys
struct DefaultsKeys {
static let keyOne = "firstStringKey"
static let keyTwo = "secondStringKey"
}
Setting
let defaults = UserDefaults.standard
defaults.set("Some String Value", forKey: DefaultsKeys.keyOne)
defaults.set("Another String Value", forKey: DefaultsKeys.keyTwo)
Getting
let defaults = UserDefaults.standard
if let stringOne = defaults.string(forKey: DefaultsKeys.keyOne) {
print(stringOne) // Some String Value
}
if let stringTwo = defaults.string(forKey: DefaultsKeys.keyTwo) {
print(stringTwo) // Another String Value
}
Swift 2.0
In Swift 2.0 UserDefaults was called NSUserDefaults and the setters and getters were named slightly differently:
Setting
let defaults = NSUserDefaults.standardUserDefaults()
defaults.setObject("Some String Value", forKey: DefaultsKeys.keyOne)
defaults.setObject("Another String Value", forKey: DefaultsKeys.keyTwo)
Getting
let defaults = NSUserDefaults.standardUserDefaults()
if let stringOne = defaults.stringForKey(DefaultsKeys.keyOne) {
print(stringOne) // Some String Value
}
if let stringTwo = defaults.stringForKey(DefaultsKeys.keyTwo) {
print(stringTwo) // Another String Value
}
For anything more serious than minor config you should consider using a more robust persistent store:
CoreData
Realm
SQLite
They Say Use NSUserDefaults
When I was implementing long term (after app close) data storage for the first time, everything I read online pointed me towards NSUserDefaults. However, I wanted to store a dictionary and, although possible, it was proving to be a pain. I spent hours trying to get type-errors to go away.
NSUserDefaults is Also Limited in Function
Further reading revealed how the read/write of NSUserDefaults really forces the app to read/write everything or nothing, all at once, so it isn't efficient. Then I learned that retrieving an array isn't straight forward. I realized that if you're storing more than a few strings or booleans, NSUserDefaults really isn't ideal.
It's also not scalable. If you're learning how to code, learn the scalable way. Only use NSUserDefaults for storing simple strings or booleans related to preferences. Store arrays and other data using Core Data, it's not as hard as they say. Just start small.
Update: Also, if you add Apple Watch support, there's another potential consideration. Your app's NSUserDefaults is now automatically sent to the Watch Extension.
Using Core Data
So I ignored the warnings about Core Data being a more difficult solution and started reading. Within three hours I had it working. I had my table array being saved in Core Data and reloading the data upon opening the app back up! The tutorial code was easy enough to adapt and I was able to have it store both title and detail arrays with only a little extra experimenting.
So for anyone reading this post who's struggling with NSUserDefault type issues or whose need is more than storing strings, consider spending an hour or two playing with core data.
Here's the tutorial I read:
http://www.raywenderlich.com/85578/first-core-data-app-using-swift
If you didn't check "Core Data"
If you didn't check "Core Data"when you created your app, you can add it after and it only takes five minutes:
http://craig24.com/2014/12/how-to-add-core-data-to-an-existing-swift-project-in-xcode/
http://blog.zeityer.com/post/119012600864/adding-core-data-to-an-existing-swift-project
How to Delete from Core Data Lists
Delete Data from Coredata Swift
Okey so thanks to #bploat and the link to http://www.codingexplorer.com/nsuserdefaults-a-swift-introduction/
I've found that the answer is quite simple for some basic string storage.
let defaults = NSUserDefaults.standardUserDefaults()
// Store
defaults.setObject("theGreatestName", forKey: "username")
// Receive
if let name = defaults.stringForKey("username")
{
print(name)
// Will output "theGreatestName"
}
I've summarized it here http://ridewing.se/blog/save-local-data-in-swift/
Using NSCoding and NSKeyedArchiver is another great option for data that's too complex for NSUserDefaults, but for which CoreData would be overkill. It also gives you the opportunity to manage the file structure more explicitly, which is great if you want to use encryption.
For Swift 4.0, this got easier:
let defaults = UserDefaults.standard
//Set
defaults.set(passwordTextField.text, forKey: "Password")
//Get
let myPassword = defaults.string(forKey: "Password")
Swift 5+
None of the answers really cover in detail the default built in local storage capabilities. It can do far more than just strings.
You have the following options straight from the apple documentation for 'getting' data from the defaults.
func object(forKey: String) -> Any?
//Returns the object associated with the specified key.
func url(forKey: String) -> URL?
//Returns the URL associated with the specified key.
func array(forKey: String) -> [Any]?
//Returns the array associated with the specified key.
func dictionary(forKey: String) -> [String : Any]?
//Returns the dictionary object associated with the specified key.
func string(forKey: String) -> String?
//Returns the string associated with the specified key.
func stringArray(forKey: String) -> [String]?
//Returns the array of strings associated with the specified key.
func data(forKey: String) -> Data?
//Returns the data object associated with the specified key.
func bool(forKey: String) -> Bool
//Returns the Boolean value associated with the specified key.
func integer(forKey: String) -> Int
//Returns the integer value associated with the specified key.
func float(forKey: String) -> Float
//Returns the float value associated with the specified key.
func double(forKey: String) -> Double
//Returns the double value associated with the specified key.
func dictionaryRepresentation() -> [String : Any]
//Returns a dictionary that contains a union of all key-value pairs in the domains in the search list.
Here are the options for 'setting'
func set(Any?, forKey: String)
//Sets the value of the specified default key.
func set(Float, forKey: String)
//Sets the value of the specified default key to the specified float value.
func set(Double, forKey: String)
//Sets the value of the specified default key to the double value.
func set(Int, forKey: String)
//Sets the value of the specified default key to the specified integer value.
func set(Bool, forKey: String)
//Sets the value of the specified default key to the specified Boolean value.
func set(URL?, forKey: String)
//Sets the value of the specified default key to the specified URL.
If are storing things like preferences and not a large data set these are perfectly fine options.
Double Example:
Setting:
let defaults = UserDefaults.standard
var someDouble:Double = 0.5
defaults.set(someDouble, forKey: "someDouble")
Getting:
let defaults = UserDefaults.standard
var someDouble:Double = 0.0
someDouble = defaults.double(forKey: "someDouble")
What is interesting about one of the getters is dictionaryRepresentation, this handy getter will take all your data types regardless what they are and put them into a nice dictionary that you can access by it's string name and give the correct corresponding data type when you ask for it back since it's of type 'any'.
You can store your own classes and objects also using the func set(Any?, forKey: String) and func object(forKey: String) -> Any? setter and getter accordingly.
Hope this clarifies more the power of the UserDefaults class for storing local data.
On the note of how much you should store and how often, Hardy_Germany gave a good answer on that on this post, here is a quote from it
As many already mentioned: I'm not aware of any SIZE limitation
(except physical memory) to store data in a .plist (e.g.
UserDefaults). So it's not a question of HOW MUCH.
The real question should be HOW OFTEN you write new / changed
values... And this is related to the battery drain this writes will
cause.
IOS has no chance to avoid a physical write to "disk" if a single
value changed, just to keep data integrity. Regarding UserDefaults
this cause the whole file rewritten to disk.
This powers up the "disk" and keep it powered up for a longer time and
prevent IOS to go to low power state.
Something else to note as mentioned by user Mohammad Reza Farahani from this post is the asynchronous and synchronous nature of userDefaults.
When you set a default value, it’s changed synchronously within your
process, and asynchronously to persistent storage and other processes.
For example if you save and quickly close the program you may notice it does not save the results, this is because it's persisting asynchronously. You might not notice this all the time so if you plan on saving before quitting the program you may want to account for this by giving it some time to finish.
Maybe someone has some nice solutions for this they can share in the comments?
Swift 3.0
Setter :Local Storage
let authtoken = "12345"
// Userdefaults helps to store session data locally
let defaults = UserDefaults.standard
defaults.set(authtoken, forKey: "authtoken")
defaults.synchronize()
Getter:Local Storage
if UserDefaults.standard.string(forKey: "authtoken") != nil {
//perform your task on success }
For Swift 3
UserDefaults.standard.setValue(token, forKey: "user_auth_token")
print("\(UserDefaults.standard.value(forKey: "user_auth_token")!)")
For someone who'd not prefer to handle UserDefaults for some reasons, there's another option - NSKeyedArchiver & NSKeyedUnarchiver. It helps save objects into a file using archiver, and load archived file to original objects.
// To archive object,
let mutableData: NSMutableData = NSMutableData()
let archiver: NSKeyedArchiver = NSKeyedArchiver(forWritingWith: mutableData)
archiver.encode(object, forKey: key)
archiver.finishEncoding()
return mutableData.write(toFile: path, atomically: true)
// To unarchive objects,
if let data = try? Data(contentsOf: URL(fileURLWithPath: path)) {
let unarchiver = NSKeyedUnarchiver(forReadingWith: data)
let object = unarchiver.decodeObject(forKey: key)
}
I've write an simple utility to save/load objects in local storage, used sample codes above. You might want to see this.
https://github.com/DragonCherry/LocalStorage
NsUserDefaults saves only small variable sizes.
If you want to save many objects you can use CoreData as a native solution, or I created a library that helps you save objects as easy as .save() function. It’s based on SQLite.
SundeedQLite
Check it out and tell me your comments
This gives a great explanation for how to do this in Swift 5: https://www.hackingwithswift.com/example-code/system/how-to-save-user-settings-using-userdefaults
Summary:
To set a value:
let defaults = UserDefaults.standard
defaults.set("value", forKey: "key")
To get a String value:
let key = defaults.object(forKey: "StringKey") as? [String] ?? [String]()
To get integer value:
let key = defaults.integer(forKey: "IntegerKey")
I found this answer and it enabled me to save data, but since Swift 4.1 there has been a much easier way to do this using appstorage.
#AppStorage("studentNames") var studentName: String = "Put name here"
Each item must be unique, but using String you can store a large variety of data in here.
I've made a video tutorial to help you do this: http://youtube.com/watch?v=nLsJD6yL9Ps
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
}