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

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.

Related

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.

Xcode - Access persistent data from simulator (NSCoder)

I am storing persistent data in my application using NSCoder like below:
class UserNotification: NSObject, NSCoding {
var message: String!
init(message: String) {
self.message = message
}
required convenience init(coder aDecoder: NSCoder) {
let message = aDecoder.decodeObject(forKey: "message") as! String
self.init(message: message)
}
func encode(with aCoder: NSCoder) {
aCoder.encode(message, forKey: "message")
}
}
This is how they are loaded into the application:
let defaults = UserDefaults.standard
if (defaults.object(forKey: "userNotifications") != nil) {
let decoded = defaults.object(forKey: "userNotifications") as! Data
let decodedUserNotifications = NSKeyedUnarchiver.unarchiveObject(with: decoded as Data) as! [UserNotification]
userNotifications = decodedUserNotifications
}
I also have included a way for the user to clear all the saved data. For clearing, I have implemented:
let newListData = NSKeyedArchiver.archivedData(withRootObject: userNotifications)
let defaults = UserDefaults.standard
defaults.set(newListData, forKey: "userNotifications")
defaults.synchronize()
All user notification objects are stored in a globally accessible array:
userNotifications = [UserNotification]()
I would like to make sure that all the persistent data is removed once the user has cleared them. I was looking through the folder of my simulator (~/Library/Developer/CoreSimulator/Devices) in order to find the persistent data and to see if it gets removed, but I have not managed to find it anywhere.
Everything seems to work when running the application (saving data, erasing it etc.) but I would like to make sure that all data get erased correctly on the device. There are some thumbnails being saved and therefore I want to make sure it really becomes erased and not taking up unnecessary space.
So my question is where the data is stored when using NSCoder? I am able to find locally stored images from using FileManager, but the encoded data is nowhere to be found.

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.

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

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.

Handling NSCoding Errors in Swift

How should errors related to NSCoding be handled in Swift?
When an object is initialized using init?(coder:) it may fail to be initialized if the data is invalid. I'd like to catch these errors and appropriately handle them. Why is init?(coder:) not defined as a throwing function in Swift?
NSCoding defines it as Optional:
init?(coder aDecoder: NSCoder)
So it is certainly possible to detect errors.
90% of "why does Swift...?" questions can be answered with "because Cocoa." Cocoa does not define initWithCoder: as returning an error, so it does not translate to throws in Swift. There would be no way to cleanly bridge it to existing code. (NSCoding goes back NeXTSTEP. We've built a lot of software without returning an NSError there. Doesn't mean it might not be nice sometimes, but "couldn't init" has traditionally been enough.)
Check for nil. That means that something failed. That is all the information that is provided.
I've never in practice had to check too deeply that the entire object graph was correct. If it isn't, you're incredibly likely to get other errors anyway, and remember that NSKeyedUnarchiver will raise an ObjC exception (!!!) if it fails to decode. Unless you wrap this in an ObjC #catch, you're going to crash anyway. (And yes, that's pretty crazy, but still true.)
But if I wanted to be extremely careful and make sure that things I expected to be in the archive were really in the archive (even if they were nil), I might do it this way (untested; it compiles but I haven't made sure it really works):
import Foundation
enum DecodeError: ErrorType {
case MissingProperty(String)
case MalformedProperty(String)
}
extension NSCoder {
func encodeOptionalObject(obj: AnyObject?, forKey key: String) {
let data = obj.map{ NSKeyedArchiver.archivedDataWithRootObject($0) } ?? NSData()
self.encodeObject(data, forKey: key)
}
func decodeRequiredOptionalObjectForKey(key: String) throws -> AnyObject? {
guard let any = self.decodeObjectForKey(key) else {
throw DecodeError.MissingProperty(key)
}
guard let data = any as? NSData else {
throw DecodeError.MalformedProperty(key)
}
if data.length == 0 {
return nil // Found nil
}
// But remember, this will raise an ObjC exception if it's malformed data!
guard let prop = NSKeyedUnarchiver.unarchiveObjectWithData(data) else {
throw DecodeError.MalformedProperty(key)
}
return prop
}
}
class MyClass: NSObject, NSCoding {
static let propertyKey = "property"
let property: String?
init(property: String?) {
self.property = property
}
required init?(coder aDecoder: NSCoder) {
do {
property = try aDecoder.decodeRequiredOptionalObjectForKey(MyClass.propertyKey) as? String
} catch {
// do something with error if you want
property = nil
super.init()
return nil
}
super.init()
}
func encodeWithCoder(aCoder: NSCoder) {
aCoder.encodeOptionalObject(property, forKey: MyClass.propertyKey)
}
}
As I said, I've never actually done this in a Cocoa program. If anything were really corrupted in the archive, you're almost certainly going to wind up raising an ObjC exception, so all this error checking is likely overkill. But it does let you distinguish between "nil" and "not in the archive." I just encode the property individually as an NSData and then encode the NSData. If it's nil, I encode an empty NSData.