How to Save the Attributed String (Text) into File (Swift, Cocoa)

How can I write NSAttributedString in rtf file?

You can use NSAttributedString's data(from:) method to convert your attributed string into rtf data.

extension NSAttributedString {
func rtf() throws -> Data {
try data(from: .init(location: 0, length: length),
documentAttributes: [.documentType: NSAttributedString.DocumentType.rtf,
.characterEncoding: String.Encoding.utf8])
}
}


let textView = UITextView()
textView.attributedText = .init(string: "abc",
attributes: [.font: UIFont(name: "Helvetica", size: 16)!])
do {
let rtfURL = try FileManager.default.url(for: .documentDirectory, in: .userDomainMask, appropriateFor: nil, create: false).appendingPathComponent("RichTextDocument.rtf")
try textView.attributedText.rtf().write(to: rtfURL)
print("saved")
} catch {
print(error)
}

Convert NSAttributedString into Data for storage

You need to specify what kind of document data you would like to convert your attributed string to:



.txt    // Plain Text Document Type (Simple Text)
.html // HTML Text Document Type (Hypertext Markup Language)
.rtf // RTF Text Document Type (Rich text format document)
.rtfd // RTFD Text Document Type (Rich text format document with attachment)

update Xcode 10.2 • Swift 5 or later

let textView = UITextView()
textView.attributedText = .init(string: "abc",
attributes: [.font: UIFont(name: "Helvetica", size: 16)!])
if let attributedText = textView.attributedText {
do {
let htmlData = try attributedText.data(from: .init(location: 0, length: attributedText.length),
documentAttributes: [.documentType: NSAttributedString.DocumentType.html])
let htmlString = String(data: htmlData, encoding: .utf8) ?? ""
print(htmlString)
} catch {
print(error)
}
}

Expanding on that:

extension NSAttributedString {

convenience init(data: Data, documentType: DocumentType, encoding: String.Encoding = .utf8) throws {
try self.init(attributedString: .init(data: data, options: [.documentType: documentType, .characterEncoding: encoding.rawValue], documentAttributes: nil))
}

func data(_ documentType: DocumentType) -> Data {
// Discussion
// Raises an rangeException if any part of range lies beyond the end of the receiver’s characters.
// Therefore passing a valid range allow us to force unwrap the result
try! data(from: .init(location: 0, length: length),
documentAttributes: [.documentType: documentType])
}

var text: Data { data(.plain) }
var html: Data { data(.html) }
var rtf: Data { data(.rtf) }
var rtfd: Data { data(.rtfd) }
}

Usage:

let textView = UITextView()
textView.attributedText = .init(string: "abc", attributes: [.font: UIFont(name: "Helvetica", size: 16)!])
if let textData = textView.attributedText?.text {
let text = String(data: textData, encoding: .utf8) ?? ""
print(text) // abc
}
if let htmlData = textView.attributedText?.html {
let html = String(data: htmlData, encoding: .utf8) ?? ""
print(html) // /* <!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01//EN" ...
}

This will print

abc
/* <!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<meta http-equiv="Content-Style-Type" content="text/css">
<title></title>
<meta name="Generator" content="Cocoa HTML Writer">
<style type="text/css">
p.p1 {margin: 0.0px 0.0px 0.0px 0.0px; font: 16.0px Helvetica}
span.s1 {font-family: 'Helvetica'; font-weight: normal; font-style: normal; font-size: 16.00pt}
</style>
</head>
<body>
<p class="p1"><span class="s1">abc</span></p>
</body>
</html>
*/

Cocoa: How to save NSAttributedString to JSON

NSAttributedString has two properties:

  • the string
  • an array of attribute "runs"

Each "run" has:

  • an integer range that it applies to
  • a dictionary of key/value attributes

It would be very easy to represent that as JSON, using enumerateAttributesInRange:options:usingBlock:.

Something like:

{
"string" : "Hello World",
"runs" : [
{
"range" : [0,3],
"attributes" : {
"font" : {
"name" : "Arial",
"size" : 12
}
}
},
{
"range" : [3,6],
"attributes" : {
"font" : {
"name" : "Arial",
"size" : 12
},
"color" : [255,0,0]
}
},
{
"range" : [9,2],
"attributes" : {
"font" : {
"name" : "Arial",
"size" : 12
}
}
}
]
}

EDIT: here's an example implementation:

// create a basic attributed string
NSMutableAttributedString *attStr = [[NSMutableAttributedString alloc] initWithString:@"Hello World" attributes:@{NSFontAttributeName: [NSFont fontWithName:@"Arial" size:12]}];
[attStr addAttribute:NSForegroundColorAttributeName value:[NSColor redColor] range:NSMakeRange(3, 6)];

// build array of attribute runs
NSMutableArray *attributeRuns = [NSMutableArray array];
[attStr enumerateAttributesInRange:NSMakeRange(0, attStr.length) options:0 usingBlock:^(NSDictionary *attrs, NSRange range, BOOL *stop) {
NSArray *rangeArray = @[[NSNumber numberWithUnsignedInteger:range.location],
[NSNumber numberWithUnsignedInteger:range.length]];

NSMutableDictionary *runAttributes = [NSMutableDictionary dictionary];
[attrs enumerateKeysAndObjectsUsingBlock:^(id attributeName, id attributeValue, BOOL *stop) {

if ([attributeName isEqual:NSFontAttributeName]) { // convert font values into a dictionary with the name and size
attributeName = @"font";
attributeValue = @{@"name": [(NSFont *)attributeValue displayName],
@"size": [NSNumber numberWithFloat:[(NSFont *)attributeValue pointSize]]};

} else if ([attributeName isEqualToString:NSForegroundColorAttributeName]) { // convert foreground colour values into an array with red/green/blue as a number from 0 to 255
attributeName = @"color";
attributeValue = @[[NSNumber numberWithInteger:([(NSColor *)attributeValue redComponent] * 255)],
[NSNumber numberWithInteger:([(NSColor *)attributeValue greenComponent] * 255)],
[NSNumber numberWithInteger:([(NSColor *)attributeValue blueComponent] * 255)]];

} else { // skip unknown attributes
NSLog(@"skipping unknown attribute %@", attributeName);
return;
}


[runAttributes setObject:attributeValue forKey:attributeName];
}];

// save the attributes (if there are any)
if (runAttributes.count == 0)
return;

[attributeRuns addObject:@{@"range": rangeArray,
@"attributes": runAttributes}];
}];

// build JSON output
NSDictionary *jsonOutput = @{@"string": attStr.string,
@"runs": attributeRuns};
NSData *jsonData = [NSJSONSerialization dataWithJSONObject:jsonOutput options:NSJSONWritingPrettyPrinted error:NULL];

NSLog(@"%@", [[NSString alloc] initWithData:jsonData encoding:NSUTF8StringEncoding]);
exit(0);

Saving custom attributes in NSAttributedString

The normal way of saving an NSAttributedString is to use RTF, and RTF data is what the -dataFromRange:documentAttributes:error: method of NSAttributedString generates.

However, the RTF format has no support for custom attributes. Instead, you should use the NSCoding protocol to archive your attributed string, which will preserve the custom attributes:

//asssume attributedString is your NSAttributedString
//encode the string as NSData
NSData* stringData = [NSKeyedArchiver archivedDataWithRootObject:attributedString];
[stringData writeToFile:pathToFile atomically:YES];

//read the data back in and decode the string
NSData* newStringData = [NSData dataWithContentsOfFile:pathToFile];
NSAttributedString* newString = [NSKeyedUnarchiver unarchiveObjectWithData:newStringData];

Read and write a String from text file

For reading and writing you should use a location that is writeable, for example documents directory. The following code shows how to read and write a simple string. You can test it on a playground.

Swift 3.x - 5.x

let file = "file.txt" //this is the file. we will write to and read from it

let text = "some text" //just a text

if let dir = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first {

let fileURL = dir.appendingPathComponent(file)

//writing
do {
try text.write(to: fileURL, atomically: false, encoding: .utf8)
}
catch {/* error handling here */}

//reading
do {
let text2 = try String(contentsOf: fileURL, encoding: .utf8)
}
catch {/* error handling here */}
}

Swift 2.2

let file = "file.txt" //this is the file. we will write to and read from it

let text = "some text" //just a text

if let dir = NSSearchPathForDirectoriesInDomains(NSSearchPathDirectory.DocumentDirectory, NSSearchPathDomainMask.AllDomainsMask, true).first {
let path = NSURL(fileURLWithPath: dir).URLByAppendingPathComponent(file)

//writing
do {
try text.writeToURL(path, atomically: false, encoding: NSUTF8StringEncoding)
}
catch {/* error handling here */}

//reading
do {
let text2 = try NSString(contentsOfURL: path, encoding: NSUTF8StringEncoding)
}
catch {/* error handling here */}
}

Swift 1.x

let file = "file.txt"

if let dirs : [String] = NSSearchPathForDirectoriesInDomains(NSSearchPathDirectory.DocumentDirectory, NSSearchPathDomainMask.AllDomainsMask, true) as? [String] {
let dir = dirs[0] //documents directory
let path = dir.stringByAppendingPathComponent(file);
let text = "some text"

//writing
text.writeToFile(path, atomically: false, encoding: NSUTF8StringEncoding, error: nil);

//reading
let text2 = String(contentsOfFile: path, encoding: NSUTF8StringEncoding, error: nil)
}

RTF file to attributed string

Rather than looking to see if the operation worked/failed in the debugger, you’d be much better off writing the code to handle the failure appropriately:

if let attributedText = NSAttributedString(fileURL: fileURL, options: [NSDocumentTypeDocumentAttribute:NSRTFTextDocumentType], documentAttributes: nil, error: &error) {
textView.attributedText = attributedText
}
else if let error = error {
println(error.localizedDescription)
}

Swift 4

do {
let attributedString = try NSAttributedString(url: fileURL, options: [NSAttributedString.DocumentReadingOptionKey.documentType: NSAttributedString.DocumentType.rtf], documentAttributes: nil)
} catch {
print("\(error.localizedDescription)")
}


Related Topics



Leave a reply



Submit