Cocoa: How to re-read file content after file has been changed from another app using NSDocument? - swift

I have a document based cocoa app that opens an .md file to display the markdown content in a nice format. If I change the .md file in another app like textedit, I want to reload the views in my app.
Here's what I have working so far:
import Cocoa
class Document: NSDocument {
var fileContent = "Nothing yet :("
override init() {
// Add your subclass-specific initialization here.
super.init()
}
override class var autosavesInPlace: Bool {
return false
}
override func makeWindowControllers() {
// Returns the Storyboard that contains your Document window.
let storyboard = NSStoryboard(name: NSStoryboard.Name("Main"), bundle: nil)
let windowController = storyboard.instantiateController(withIdentifier: NSStoryboard.SceneIdentifier("Document Window Controller")) as! NSWindowController
self.addWindowController(windowController)
}
override func data(ofType typeName: String) throws -> Data {
throw NSError(domain: NSOSStatusErrorDomain, code: unimpErr, userInfo: nil)
}
override func read(from data: Data, ofType typeName: String) throws {
fileContent = (try String(data: data, encoding: .utf8))!
}
// this fn is called every time textEdit changes the file content.
override func presentedItemDidChange() {
// Here is the PROBLEM:
// HOW do I access the new file content?
}
}
Here is the problem
presentedItemDidChange() is called every time textEdit makes a change. That works great. But I can't for the life of me figure out how then to access the new file content, so I can reassign fileContent = newContent. Any thoughts?

I would call for the document readFromURL:ofType:error: as described here.

Related

How do I set the text of a label from outside of viewDidAppear?

I'm writing a Mac (Swift) application on Xcode which gets data from a command and asynchronously changes the stringValue of some text in the window. I already figured out the asynchronous part from here, but I can't seem to figure out how to actually change the text, since Xcode seems to require it to be in viewDidAppear. Unfortunately I can't put the function which runs the command in viewDidAppear since it is called by another file and needs to be a public func (as far as I know).
Here are a couple of methods I tried:
1. Call a function inside viewDidAppear which changes the text:
self.viewDidAppear().printText("testing!") // this part is where the "New Output" line is on the attached link above
...
override func viewDidAppear() {
func printText(_ string: String) {
textLabel.stringValue = string
}
}
Result: Value of tuple type '()' has no member 'printText' (on the first line)
2. Change an already-declared variable to the current message, then use Notification Center to tell viewDidAppear to change the text.
var textToPrint = "random text" // directly inside the class
let nc = NotificationCenter.default // directly inside the class
...
self.textToPrint = "testing!" // in place of the "New Output" line in the link above
self.nc.post(name: Notification.Name("printText"), object: nil) // in place of the "New Output" line in the link above
...
#objc func printText2() { // directly inside the class
textLabel.stringValue = textToPrint // directly inside the class
} // directly inside the class
...
override func viewDidAppear() {
nc.addObserver(self, selector: #selector(printText2), name: Notification.Name("printText"), object: nil)
}
For this one, I had to put printText2 outside of viewDidAppear because apparently selectors (for Notification Center) only work if you do that.
Result: NSControl.stringValue must be used from main thread only (on textLabel.stringValue line).
Also, the text never changes.
So I need to either somehow change the label's text directly from the asynchronous function, or to have viewDidAppear do it (also transmitting the new message).
...................................................................
Extra project code requested by Upholder of Truth
import Cocoa
class VC_image: NSViewController, NSWindowDelegate {
#IBOutlet var textLabel: NSTextField!
public func processImage(_ path: String) { // this function is called by another file
previewImage()
}
public func previewImage() {
if let path = Bundle.main.path(forResource: "bashscript", ofType: "sh") {
let task3 = Process()
task3.launchPath = "/bin/sh"
task3.arguments = [path]
let pipe3 = Pipe()
task3.standardOutput = pipe3
let outHandle = pipe3.fileHandleForReading
outHandle.readabilityHandler = { pipe3 in
if let line = String(data: pipe3.availableData, encoding: String.Encoding.utf8) {
// Update your view with the new text here
let messageToPrint = line.components(separatedBy: " ")
if (messageToPrint.count == 6) {
DispatchQueue.main.async {
self.textLabel.stringValue = "testing!"
}
}
} else {
print("Error decoding data: \(pipe3.availableData)")
}
}
task3.launch()
}
}
}

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.

UIDocumentPickerViewController allows selecting a file the first time the app is run, but not subsequently

My app, when run, creates a json document (using a UIDocument subclass) in its document directory. And then, when opening up the UIDocumentPickerViewController to select a file, if the app has written a new file, the behaviour is as expected.
However, if I run the app again (and overwrite the last created file), the delegate method didPickDocumentsAt doesn't get called, unless I browse around for a few seconds.
What am I missing here?
#IBAction func showDocumentPicker() {
let documentPicker = UIDocumentPickerViewController(documentTypes: [kUTTypeJSON as String], in: .import)
documentPicker.allowsMultipleSelection = false
documentPicker.delegate = self
self.present(documentPicker, animated: true, completion: nil)
} //this function is in the initial definition of the class and is connected to a UIBarButton
extension BudgetExportViewController: UIDocumentPickerDelegate {
func documentPicker(_ controller: UIDocumentPickerViewController, didPickDocumentsAt urls: [URL]) {
print("selected document: \(urls.first)")
print("555555555555555555555555555555")
//document = BudgetExportDocument(fileURL: urls.first!)
// CFURLStartAccessingSecurityScopedResource(urls.first! as CFURL)
// let documentData = try? Data.init(contentsOf: urls.first!)
// let json = try? JSONDecoder().decode(BudgetExportData.self, from: documentData!)
// budgetThisMonth = json
// print("Budgetthismonth")
// print(budgetThisMonth)
// CFURLStopAccessingSecurityScopedResource(urls.first! as CFURL)
}
}
Apparently, the problem was the fact that I was overwriting the same file so many times. Now, if a file with the same name exists, it won't write over it and the UIDocumentPickerViewController behaviour is as expected.

How to extract a zip file and get the extracted components in a Share Extension in Swift

I need to do the following-
I have another app in which i will export the users config(.txt) and contacts(.vcf) in a zip format.
In the second app i have a share extension to get the exported zip and in the share extension, i need to extract the zip file and get both the txt and vcf files and then upload them to a parse server.
I have done till opening the exported zip in the share extension. but i could not get the zip extracted.
I couldn't get the answer in internet.
Here is my ShareViewController
import UIKit
import Social
import Parse
import MobileCoreServices
import SSZipArchive
class ShareViewController: SLComposeServiceViewController {
var requird_data : NSData!
var path : URL!
override func viewDidLoad() {
super.viewDidLoad()
//Parse.setApplicationId("cGFyc2UtYXBwLXdob3N1cA==", clientKey: "")
initUI()
getURL()
textView.delegate = self
textView.keyboardType = .numberPad
}
// override func viewWillAppear(_ animated: Bool) {
// super.viewWillAppear(true)
//
// }
func initUI()
{
navigationController?.navigationBar.titleTextAttributes = [NSForegroundColorAttributeName: UIColor.white]
title = "upup"
navigationController?.navigationBar.tintColor = .white
navigationController?.navigationBar.backgroundColor = UIColor(red:0.97, green:0.44, blue:0.12, alpha:1.00)
placeholder = "Please enter your Phone number"
}
private func getURL() {
let extensionItem = extensionContext?.inputItems.first as! NSExtensionItem
let itemProvider = extensionItem.attachments?.first as! NSItemProvider
let zip_type = String(kUTTypeZipArchive)
if itemProvider.hasItemConformingToTypeIdentifier(zip_type) {
itemProvider.loadItem(forTypeIdentifier: zip_type, options: nil, completionHandler: { (item, error) -> Void in
guard let url = item as? NSURL else { return }
print("\(item.debugDescription)")
OperationQueue.main.addOperation {
self.path = url as URL
SSZipArchive.unzipFile(atPath: url.path!, toDestination: url.path!)
}
})
} else {
print("error")
}
}
override func isContentValid() -> Bool {
// Do validation of contentText and/or NSExtensionContext attachments here
return true
}
override func didSelectPost() {
// This is called after the user selects Post. Do the upload of contentText and/or NSExtensionContext attachments.
// Inform the host that we're done, so it un-blocks its UI. Note: Alternatively you could call super's -didSelectPost, which will similarly complete the extension context.
self.extensionContext!.completeRequest(returningItems: [], completionHandler: nil)
}
override func configurationItems() -> [Any]! {
// To add configuration options via table cells at the bottom of the sheet, return an array of SLComposeSheetConfigurationItem here.
return []
}
override func textView(_ textView: UITextView, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool
{
let length = ((textView.text)?.characters.count)! + text.characters.count - range.length
let allowedset : CharacterSet = CharacterSet(charactersIn: "0123456789+").inverted as CharacterSet
let filtered = (text.components(separatedBy: allowedset)).joined(separator: "")
return (length<17) && (text == filtered)
}
}
I use SSZipAchive to extract the file. Link : https://github.com/ZipArchive/ZipArchive
I ran the application in the Xcode 9 beta 1. I used the new Files app from simulator to share the zip.
Below is my Share Extensions Info.Plist
I am newbie to share extension so i don't know much about it. All the code above are from bits and pieces from the following tutorials and a little googling.
1.https://www.appcoda.com/ios8-share-extension-swift/
2.https://hackernoon.com/how-to-build-an-ios-share-extension-in-swift-4a2019935b2e
Please guide me.
I use swift 3.
I found out the solution. It was my mistake to give the destination file path for the extracted items to be the same as the source files path. After changing it to the app's documents directory i got it working.
let documentsPath = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true)[0]
SSZipArchive.unzipFile(atPath: url.path!, toDestination: documentsPath)

How to add simple save-load functions to document-based Swift 3 app?

I'm studying Swift (for just a hobby) at the moment, and trying to figure out how I'm supposed to and save and load functions to document.swift file in document-based swift app? I'd like to know how to save and load simple txt-files. I'm using NSTextView, so I guess I have to change that to NSString?
Here are those functions at the moment:
override func data(ofType typeName: String) throws -> Data {
// Insert code here to write your document to data of the specified type. If outError != nil, ensure that you create and set an appropriate error when returning nil.
// You can also choose to override fileWrapperOfType:error:, writeToURL:ofType:error:, or writeToURL:ofType:forSaveOperation:originalContentsURL:error: instead.
throw NSError(domain: NSOSStatusErrorDomain, code: unimpErr, userInfo: nil)
}
override func read(from data: Data, ofType typeName: String) throws {
// Insert code here to read your document from the given data of the specified type. If outError != nil, ensure that you create and set an appropriate error when returning false.
// You can also choose to override readFromFileWrapper:ofType:error: or readFromURL:ofType:error: instead.
// If you override either of these, you should also override -isEntireFileLoaded to return false if the contents are lazily loaded.
throw NSError(domain: NSOSStatusErrorDomain, code: unimpErr, userInfo: nil)
}
In you document.swift file these functions below will load and set your text views string value.
Hope this helps!
var content = ""
override func makeWindowControllers() {
//Make the window controller
let storyboard = NSStoryboard(name: "Main", bundle: nil)
let windowController = storyboard.instantiateController(withIdentifier: "Document Window Controller") as! NSWindowController
self.addWindowController(windowController)
//Access the view controller
let vc = windowController.contentViewController as! ViewController
//Set the text view string to the variable that was loaded
vc.textView.string = content
}
override func data(ofType typeName: String) throws -> Data {
//Access the current view controller
if let vc = self.windowControllers[0].contentViewController as? ViewController {
//Get the textView's String Value; or call a function that will create a string of what needs to be saved
return vc.textView.string?.data(using: String.Encoding.utf8) ?? Data()
}else {
throw NSError(domain: NSOSStatusErrorDomain, code: unimpErr, userInfo: nil)
}
}
override func read(from url: URL, ofType typeName: String) throws {
do {
//Set a string variable to the loaded string
//We can't directly set the text views string value because this function is called before the makeWindowControllers() so now text view is created yet.
content = try String(contentsOf: url, encoding: String.Encoding.utf8)
}catch {
throw NSError(domain: NSOSStatusErrorDomain, code: unimpErr, userInfo: nil)
}
}