iOS Swift: How to access selected text in WKWebView - swift

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.

Related

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!

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

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.

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)

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.