How can I save the attributed string (text) into file (swift, cocoa)? - swift

I have NSTextView and I can have text as nsattributedstring. I can save text into .txt file using NSSavePanel, as plain text, but not as formatted text.
#IBAction func saveDNA(sender: AnyObject)
{
let saveDNAtoFile: NSSavePanel = NSSavePanel()
saveDNAtoFile.canSelectHiddenExtension = true
saveDNAtoFile.runModal()
do
{
let exportedFileURL = saveDNAtoFile.URL
let textDNA = self.inputDnaFromUser.string
if exportedFileURL != nil
{
try textDNA!.writeToURL(exportedFileURL!, atomically: false, encoding: NSUTF8StringEncoding)
}
} catch
{
}
}
How can I save the attributedstring (text) into file using NSSavePanel, to be able later to open this file to have all made before formatting in the text? What I should change in the code above, if I can use NSSavePanel for this ?

One day out ... Ok, I have figured out the code for Swift 2 (note this - options: NSFileWrapperWritingOptions.Atomic). Below. I am sure it will save time for beginners like me, more time to write necessary and more interesting algorithms, than this standard functionality.
#IBAction func saveDNA(sender: AnyObject)
{
let saveDNAtoFile: NSSavePanel = NSSavePanel()
saveDNAtoFile.canSelectHiddenExtension = true
saveDNAtoFile.runModal()
do
{
let exportedFileURL = saveDNAtoFile.URL
let textDNA = inputDnaFromUser.textStorage
if exportedFileURL != nil
{
let range = NSRange(0..<textDNA!.length)
let textTSave = try textDNA!.fileWrapperFromRange(range, documentAttributes: [NSDocumentTypeDocumentAttribute:NSRTFTextDocumentType])
try textTSave.writeToURL(exportedFileURL!, options: NSFileWrapperWritingOptions.Atomic, originalContentsURL: nil)
}
} catch
{
}
}

AppKit and UIKit add many methods to NSAttributedString for serializing and deserializing. Formerly they were documented separately, but now they are part of the unified NSAttributedString documentation.
There are too many methods to list here, but in the documentation you will find methods to convert NSAttributedString to/from several formats including Rich Text Format (RTF), HTML (starting in macOS 10.15 and iOS 13), Markdown (starting in macOS 12 and iOS 15), and others. You can also convert to/from Data, in which case you specify the format by setting the appropriate documentType in the documentAttributes dictionary. The conversions to/from Data support a few formats for which there are no dedicated methods.

Related

Select Text in Webkit applications via macOS accessibility API

I need to select text in a WebKit view of another application (Apple Mail) using accessibility APIs.
For regular text fields, I do something like this:
func selectText(withRange range: CFRange) throws {
var range = range
guard let newValue: AXValue = AXValueCreate(AXValueType.cfRange, &range) else { return }
AXUIElementSetAttributeValue(self, kAXSelectedTextRangeAttribute as CFString, newValue)
}
However, in the composing window of Apple Mail every text seems to be of type Static Text which doesn't come with the necessary AXSelectedTextRange
It has AXSelectedTextMarkerRange, though, which requires an AXTextMarker. I just don't get how to create one of these. I have no trouble reading the text from a user created selection using this here, but I'm unable to select text via the accessibility APIs.
Thanks to the hint from Willeke I was able to figure it out. It is indeed possible to do it using AXTextMarkerForIndex. Knowing that it's actually pretty straightforward.
Here's my code:
func getTextMarker(forIndex index: CFIndex) throws -> AXTextMarker? {
var textMarker: AnyObject?
guard AXUIElementCopyParameterizedAttributeValue(self,"AXTextMarkerForIndex" as CFString, index as AnyObject, &textMarker) == .success else { return nil }
return textMarker as! AXTextMarker
}
func selectStaticText(withRange range: CFRange) throws {
guard let textMarkerStart = try? getTextMarker(forIndex: range.location) else { return }
guard let textMarkerEnd = try? getTextMarker(forIndex: range.location + range.length) else { return }
let textMarkerRange = AXTextMarkerRangeCreate(kCFAllocatorDefault, textMarkerStart, textMarkerEnd)
AXUIElementSetAttributeValue(self, "AXSelectedTextMarkerRange" as CFString, textMarkerRange)
}

NSTextAttachment contents property nil after reading from pasteboard

I have a custom NSTextView subclass that displays one or more NSTextAttachment objects that represent tokens (similar to NSTokenField).
The problem is that the contents property on NSTextAttachment is always nil after reading the NSAttributedString from the pasteboard, even though I see this data is being saved to the pasteboard.
Create the text attachment and insert it into the text storage
let attachmentData = "\(key):\(value)".data(using: .utf8)
let attachment = NSTextAttachment(data: attachmentData, ofType: kUTTypeUTF8PlainText as String)
attachment.attachmentCell = CustomAttachmentCell(key: key, value: value)
...
textStorage.replaceCharacters(in: range, with: NSAttributedString(attachment: attachment))
CustomAttachmentCell is a standard NSTextAttachmentCell subclass that only handles custom drawing. It draws the text attachment within the text view as intended and uses standard CoreGraphics API calls to draw a background and text for the attachment's visual representation.
class CustomAttachmentCell: NSTextAttachmentCell {
let key: String
let value: String
override func draw(withFrame cellFrame: NSRect, in controlView: NSView?) {
drawKey(withFrame: cellFrame, in: controlView)
drawValue(withFrame: cellFrame, in: controlView)
}
}
Configure NSTextViewDelegate methods to handle writing the attachment to the pasteboard
// If the previous method is not used, this method and the next allow the textview to take care of attachment dragging and pasting, with the delegate responsible only for writing the attachment to the pasteboard.
// In this method, the delegate should return an array of types that it can write to the pasteboard for the given attachment.
func textView(_ view: NSTextView, writablePasteboardTypesFor cell: NSTextAttachmentCellProtocol, at charIndex: Int) -> [NSPasteboard.PasteboardType] {
return [(kUTTypeFlatRTFD as NSPasteboard.PasteboardType)]
}
// In this method, the delegate should attempt to write the given attachment to the pasteboard with the given type, and return success or failure.
func textView(_ view: NSTextView, write cell: NSTextAttachmentCellProtocol, at charIndex: Int, to pboard: NSPasteboard, type: NSPasteboard.PasteboardType) -> Bool {
guard type == kUTTypeFlatRTFD as NSPasteboard.PasteboardType else { return false }
guard let attachment = cell.attachment else { return false }
return pboard.writeObjects([NSAttributedString(attachment: attachment)])
}
After right-clicking the text attachment in the text view and choosing "cut" from the context menu, I can see the RTFD data in the pasteboard using the Clipboard Viewer.app:
rtfd............
...Attachment.........TXT.rtf....M...÷...........exampleKey:exampleValue....E.......
...Attachment....TXT.rtf..........Ñ`¶...........².Ñ`¶...............ï...{\rtf1\ansi\ansicpg1252\cocoartf1671\cocoasubrtf600
{\fonttbl\f0\fswiss\fcharset0 Helvetica;}
{\colortbl;\red255\green255\blue255;}
{\*\expandedcolortbl;;}
\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\partightenfactor0
\f0\fs24 \cf0 {{\NeXTGraphic Attachment \width640 \height640 \appleattachmentpadding0 \appleembedtype0 \appleaqc
}¬}\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\partightenfactor0
\cf0 \
\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\partightenfactor0
\cf0 {{\NeXTGraphic Attachment \width640 \height640 \appleattachmentpadding0 \appleembedtype0 \appleaqc
}¬}}
Now when I paste back into the text view and textStorageWillProcessEditing() is called. I'm expecting to be able to retrieve the text attachment contents property at this point, but it is always nil. Am I missing some critical piece in this chain?
override func textStorageWillProcessEditing(_ notification: Notification) {
guard let attributedString = notification.object as? NSAttributedString else { return }
attributedString.enumerateAttribute(.attachment, in: NSRange(location: 0, length: attributedString.length), options: [], using: { (value, range, stop) in
if let attachment = value as? NSTextAttachment {
// attachment.contents is nil here, I was expecting a Data object representing the UTF-8 string "exampleKey:exampleValue"
}
})
}
Alternatively I've tried overriding some of the pasteboard-related methods in my NSTextView subclass to do the same thing (read/write rftd data).
Overrode readablePasteboardTypes() to reorder so that the richest data (e.g. com.apple.flat-rtfd / NeXT RTFD pasteboard type) are first in the array.
Overrode preferredPasteboardType(from: restrictedToTypesFrom:) to return (kUTTypeFlatRTFD as NSPasteboard.PasteboardType)
Overrode readSelection(from:type:)
When overriding the methods above I see the same behavior where I can:
Read NSAttributedString from the pasteboard
Enumerate the NSTextAttachment objects contained within the attributed string
The enumerated NSTextAttachment objects contents property is always set to nil instead of the Data representing the UTF-8 string "exampleKey:exampleValue" that was written to the pasteboard inside the NSTextAttachment which is inside the NSAttributedString.
It appears that this is quite an esoteric problem as all the sample code and search results I've found all either use NSFileWrapper or NSImage/UIImage to back their NSTextAttachment rather than the NSTextAttachment(data:ofType:) initializer.
Thanks!

iOS Swift: How to access selected text in WKWebView

I would like to be able to use a menu button to copy selected text from a web page in WKWebView to the pasteboard. I would like to get the text from the pasteboard into a text view in a second view controller. How do I access and copy the selected text in the WKWebView?
Swift 4
You can access the general pasteboard with the following line:
let generalPasteboard = UIPasteboard.general
In the view controller, you can add an observer to observe when something is copied to the pasteboard.
override func viewDidLoad() {
super.viewDidLoad()
// https://stackoverflow.com/questions/35711080/how-can-i-edit-the-text-copied-into-uipasteboard
NotificationCenter.default.addObserver(self, selector: #selector(pasteboardChanged(_:)), name: UIPasteboard.changedNotification, object: generalPasteboard)
}
override func viewDidDisappear(_ animated: Bool) {
NotificationCenter.default.removeObserver(UIPasteboard.changedNotification)
super.viewDidDisappear(animated)
}
#objc
func pasteboardChanged(_ notification: Notification) {
print("Pasteboard has been changed")
if let data = generalPasteboard.data(forPasteboardType: kUTTypeHTML as String) {
let dataStr = String(data: data, encoding: .ascii)!
print("data str = \(dataStr)")
}
}
In the above pasteboardChanged function, I get the data as HTML in order to display the copied as formatted text in a second controller in a WKWebView. You must import MobileCoreServices in order to reference the UTI kUTTypeHTML. To see other UTI's, please see the following link: Apple Developer - UTI Text Types
import MobileCoreServices
In your original question, you mentioned you want to put the copied content into a second textview. If you want to keep the formatting, you will need to get the copied data as RTFD then convert it to an attributed string. Then set the textview to display the attributed string.
let rtfdStringType = "com.apple.flat-rtfd"
// Get the last copied data in the pasteboard as RTFD
if let data = pasteboard.data(forPasteboardType: rtfdStringType) {
do {
print("rtfd data str = \(String(data: data, encoding: .ascii) ?? "")")
// Convert rtfd data to attributedString
let attStr = try NSAttributedString(data: data, options: [NSAttributedString.DocumentReadingOptionKey.documentType: NSAttributedString.DocumentType.rtfd], documentAttributes: nil)
// Insert it into textview
print("attr str = \(attStr)")
copiedTextView.attributedText = attStr
}
catch {
print("Couldn't convert pasted rtfd")
}
}
Because I don't know your exact project or use case so you may need to alter the code a little but I hope I provided you with pieces you need for project. Please comment if there's anything I missed.

Is it possible to save NSData(obtained from NSKeyedArchiver) as NSString and then read back as NSData?

I want to save some information to video metadata. Now I can save the text, that is String object.
// this works well
let metaItem = AVMutableMetadataItem()
metaItem.key = AVMetadataCommonKeySource as NSCopying & NSObjectProtocol
metaItem.keySpace = AVMetadataKeySpaceCommon
metaItem.value = String("some text") as! NSCopying & NSObjectProtocol
So instead of just String I'd like to serialize custom object:
class ARTRMetadata: NSObject, NSCoding {
// ...
required init(coder aDecoder: NSCoder) {
//...
}
func encode(with aCoder: NSCoder) {
//...
}
}
I tried to convert Data to String, it crashed, now I stucked at writing/reading that Data to .txt file:
static func saveMetadataObjectAsText(memento: ARTRMetadata)->String {
let tempFilepath = NSTemporaryDirectory().appending("someFile2.txt")
FileManager.default.createFile(atPath: tempFilepath, contents: nil, attributes: nil)
if NSKeyedArchiver.archiveRootObject(memento, toFile: tempFilepath) {}
else { print("archiveRootObject toFile: FAILURE") }
do {
let contentsFeedToMetadataItem = try String(contentsOfFile: tempFilepath)
//let contentsFeedToMetadataItem = try String(contentsOfFile: tempFilepath, encoding: String.Encoding.utf8) // The file “someFile2.txt” couldn’t be opened using text encoding Unicode (UTF-8).
return contentsFeedToMetadataItem
}
catch { print(error) }
return "ERROR in contentsFeedToMetadataItem"
}
Now it crashes because "The file “someFile2.txt” couldn’t be opened because the text encoding of its contents can’t be determined."
I suppose the problem is that NSData obtained from NSKeyedArchiver is not valid NSString. If I am correct, how to dump the data as text? And then restore it with the same bytes (for NSKeyedUnarchiver)?
Thanks in advance!
Why do you want to save data as text file? Even if you could save Data as string (indeed you can if you encode it with base64) it's not human readable anyway – well there might be a very few people who can read base64 fluently.
Long story short, save Data directly to disk and read it back. Data provides appropriate API.
By the way: archiveRootObject(toFile writes Data anyway, so read try Data(contentsOfFile: tempFilepath) and return that.

Create CSV file in Swift and write to file

I have an app I've made that has a UITableView with todoItems as an array for it. It works flawlessly and I have an export button that creates a CSV file from the UITableView data and emails it out:
// Variables
var toDoItems:[String] = []
var convertMutable: NSMutableString!
var incomingString: String = ""
var datastring: NSString!
// Mail alert if user does not have email setup on device
func showSendMailErrorAlert() {
let sendMailErrorAlert = UIAlertView(title: "Could Not Send Email", message: "Your device could not send e-mail. Please check e-mail configuration and try again.", delegate: self, cancelButtonTitle: "OK")
sendMailErrorAlert.show()
}
// MARK: MFMailComposeViewControllerDelegate Method
func mailComposeController(controller: MFMailComposeViewController, didFinishWithResult result: MFMailComposeResult, error: NSError?) {
controller.dismissViewControllerAnimated(true, completion: nil)
}
// CSV Export Button
#IBAction func csvExport(sender: AnyObject) {
// Convert tableView String Data to NSMutableString
convertMutable = NSMutableString();
for item in toDoItems
{
convertMutable.appendFormat("%#\r", item)
}
print("NSMutableString: \(convertMutable)")
// Convert above NSMutableString to NSData
let data = convertMutable.dataUsingEncoding(NSUTF8StringEncoding, allowLossyConversion: false)
if let d = data { // Unwrap since data is optional and print
print("NSData: \(d)")
}
//Email Functions
func configuredMailComposeViewController() -> MFMailComposeViewController {
let mailComposerVC = MFMailComposeViewController()
mailComposerVC.mailComposeDelegate = self
mailComposerVC.setSubject("CSV File Export")
mailComposerVC.setMessageBody("", isHTML: false)
mailComposerVC.addAttachmentData(data!, mimeType: "text/csv", fileName: "TodoList.csv")
return mailComposerVC
}
// Compose Email
let mailComposeViewController = configuredMailComposeViewController()
if MFMailComposeViewController.canSendMail() {
self.presentViewController(mailComposeViewController, animated: true, completion: nil)
} else {
self.showSendMailErrorAlert() // One of the MAIL functions
}
}
My question is how do I create the same CSV file, but instead of emailing, save it to file? I'm new to programming and still learning Swift 2. I understand that the section of code (data!, mimeType: "text/csv", fileName: "TodoList.csv") creates the file as an attachment. I've looked online for this and trying to understand paths and directories is hard for me. My ultimate goal is to have another UITableView with a list of these 'saved' CSV files listed. Can someone please help? Thank you!
I added the following IBAction to my project:
// Save Item to Memory
#IBAction func saveButton(sender: UIBarButtonItem) {
// Convert tableView String Data to NSMutableString
convertMutable = NSMutableString();
for item in toDoItems
{
convertMutable.appendFormat("%#\r", item)
}
print("NSMutableString: \(convertMutable)")
// Convert above NSMutableString to NSData
let data = convertMutable.dataUsingEncoding(NSUTF8StringEncoding, allowLossyConversion: false)
if let d = data { // Unwrap since data is optional and print
print("NSData: \(d)")
}
let path = NSSearchPathForDirectoriesInDomains(.DocumentDirectory, .UserDomainMask, true)[0] as NSString
func writeToFile(_: convertMutable, path: String, atomically useAuxiliaryFile: Bool, encoding enc: UInt) throws {
}
}
I was struggling to find a decent simple answer to this for ages.
Here is the best way that I've found to create a csv file and even the directory you want it to be it and write to it.
//First make sure you know your file path, you can get it from user input or whatever
//Keep the path clean of the name for now
var filePath = "/Users/Johnson/Documents/NEW FOLDER/"
//then you need to pick your file name
let fileName = "AwesomeFile.csv"
//You should probably have some data to put in it
//You can even convert your array to a string by appending all of it's elements
let fileData = "This,is,just,some,dummy,data"
// Create a FileManager instance this will help you make a new folder if you don't have it already
let fileManager = FileManager.default
//Create your target directory
do {
try fileManager.createDirectory(atPath: filePath!, withIntermediateDirectories: true, attributes: nil)
//Now it's finally time to complete the path
filePath! += fileName!
}
catch let error as NSError {
print("Ooops! Something went wrong: \(error)")
}
//Then simply write to the file
do {
// Write contents to file
try fileData.write(toFile: filePath!, atomically: true, encoding: String.Encoding.utf8)
print("Writing CSV to: \(filePath!)")
}
catch let error as NSError {
print("Ooops! Something went wrong: \(error)")
}
PS. Just noticed that question is from year ago, but I hope it helps a struggling newbie like myself when they inevitably stumble upon it like I did.
convertMutable can be easily written to disk with either fun writeToFile(_ path: String, atomically useAuxiliaryFile: Bool, encoding enc: UInt) throws or func writeToURL(_ url: NSURL, atomically useAuxiliaryFile: Bool, encoding enc: UInt) throws. All you have to do is create a path or URL to write the string out to. If you are using iCloud things will be more challenging but for locally stored files you can use let path = NSSearchPathForDirectoriesInDomains(.DocumentDirectory, .UserDomainMask, true)[0] as NSString to get the root path of the documents directory.
Update: Based on you first comment below here is some added info:
The first issue is that it appears as though you are looking for code you can just paste int your project without really understanding what it does. My appologies if I'm wrong, but if I'm right this is not a good route to take as you will have many issues down the road when things change.
At the bottom of your last code section you are trying to create a function inside a function which is not going to do what you want. The above mentioned functions are the declarations of two NSString functions not functions that you need to create. As NSMutableString is a subclass of NSString you can use those functions on your convertMutable variable.
Another thing you need to deal with is creating a name for the file you want to save, currently you have pasted in the line above that gets the Documents directory but does not have a file name. You will need to devise a way to create a unique filename each time you save a CSV file and add that name to the end of path. Then you can use writeToFile… or writeToURL… to write the string to the desired location.
If you find you don't fully comprehend the code you are adding then consider getting a book or finding classes about Swift (Coursera.org has a class that may be of use). There are plenty of resources out there learn the basics of software development and Swift, it will take effort and time but it will be worth it in the end if this is something you want to pursue.