iOS Action Extension, share PDF from Safari fails - ios

I have an Action Extension to which I'm trying to share PDF-files.
I'm using the boilerplate code for ActionRequestHandler.swift that was autogenerated for me:
func beginRequest(with context: NSExtensionContext) {
// Do not call super in an Action extension with no user interface
self.extensionContext = context
for item in context.inputItems as! [NSExtensionItem] {
if let attachments = item.attachments {
for itemProvider in attachments {
...
...
}
}
}
}
Working from other apps
When exporting from every application except Safari, this is what I get:
This is all ok, I can verify that it's an pdf by checking the com.adobe.pdf and then I use the public.file-url to fetch the shared file.
Failing from Safari
But when exporting from Safari (doesn't matter if I choose "Automatic" or "Pdf" for file type), I instead only get com.apple.property-list:
Further info
Both dropbox and OneDrive works, so it's doable in some sort of way.
Also I realised that sharing an PDF from a url that's protected by some sort of login doesn't work with "Public.file-url" since that URL wont be accessible from inside swift-code.
That leads me to think that the java-script preprocessor might be the way to go? Fetch the pdf-contents with JS and pass it on to code?
Question
How do I use the com.apple.property-list to fetch the file?
Or is some config I did faulty, since I get this property-list instead of the pdf/url combo?

While I didn't manage to figure out a solution to the original question, I did manage to solve the problem.
When adding an Action Extension, one gets to choose Action type:
Presents user interface
No user interface
I choosed No user interfacesince that was what I wanted.
That gave me an Action.js file and ActionRequestHandler.swift:
class ActionRequestHandler: NSObject, NSExtensionRequestHandling {
...
}
These files seem to work around a system where the Action.js is supposed to fetch/manipulate the source page and then send information to the backing Swift code. As stated in my original question, when sharing a PDF from Safari, no PDF-URL gets attached.
A working solution
If I instead choose Presents user interface, I got another setup, ActionViewController.swift:
class ActionViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
// Get the item[s] we're handling from the extension context.
for item in self.extensionContext!.inputItems as! [NSExtensionItem] {
for provider in item.attachments! {
if provider.hasItemConformingToTypeIdentifier(kUTTypePDF as String) {
provider.loadItem(forTypeIdentifier: kUTTypePDF as String, options: nil, completionHandler: { (pdfUrl, error) in
OperationQueue.main.addOperation {
if let pdfUrl = pdfUrl as? URL {
// pdfUrl now contains the path to the shared pdf data
}
}
}
}
}
This file / solution works as expected, the extensionContext gets populated with one attachment that conforms to kUTTypePDF as expected.
Why this works, while the "no gui"-approach doesn't, I have no idea. Bug or feature?
I have not found any documentation of how/why this is supposed to work in Apple's developer section, the "share extension" documentation is very light.

Related

Can't unarchive a file sent by watch

I have a class containing data that is being produced on the Apple Watch. I use the following method to archive the class, store the data in a file and then send the file to the iPhone.
func send(file counter: CounterModel) {
let session = WCSession.default
let fm = FileManager.default
let documentsDirectory = fm.urls(for: .documentDirectory, in: .userDomainMask).first!
let transferStore = documentsDirectory.appendingPathComponent("transferfile").appendingPathExtension("cnt")
do {
let counterData = try NSKeyedArchiver.archivedData(
withRootObject: counter,
requiringSecureCoding: false
)
try counterData.write(to: transferStore)
if session.activationState == .activated {
session.transferFile(transferStore, metadata: nil)
}
} catch {
print("Oops")
}
}
Sending the file to the iPhone works fine, the delegate method is being called and the file is received. However, I can't unarchive the data and get the error message "The data couldn’t be read because it isn’t in the correct format." The delegate is simple:
func session(_ session: WCSession, didReceive file: WCSessionFile) {
do {
let contents = try Data(contentsOf: file.fileURL)
if let newValue = try NSKeyedUnarchiver.unarchiveTopLevelObjectWithData(contents) as? CounterModel {
listOfCounters.append(newValue)
} else {
print("The content could not be decoded.")
}
} catch {
print("Failed to retrieve the file with error \(error.localizedDescription).")
}
}
Apparently, I'm doing something wrong. The un-archiving of the data on the iPhone works, so this is not the problem. Perhaps the file send has another format, but I can't get any information on that.
I opened the problem as a ticket to DTS and got the following answer:
The culprit is that your Model class has a different (full) class name in different targets. A Swift class has a module name, which by default is tied to the target name. When your Model class is compiled for your WatchKit extension, its full name is “TagetName_watchkit_extension.Model”; when it is compiled for your iOS app, it becomes “TargetName.Model”.
When your WatchKit extension archives an object Model, it uses “Target_watchkit_extension.Model” as the class name, which is not recognized by your iOS app, and triggers the failure.
You can use #objc to give your Model class a full name, which prevents the compiler from adding the module name, like below:
#objc(Model)
class Model: NSObject, NSCoding, ObservableObject {
I implemented this advice and it worked. However, on my MacBook I got an error message from the preview, that stated, that I needed to change some methods of my model with a prefix of "#objc dynamic". This might, however, happen, because DTS at Apple, didn't get this error.
The response on the problem was:
“#objc dynamic” is required for KVO (key-value observation) support. Since a “#Published" variable relies on KVO as well, adding that does sound reasonable for me.
This solved my problem and I'm happy.

Action Extensions - how do I know when a host app support modification in place?

I'm following a tutorial to create a simple Action Extension for text. Action extension is triggered as a modal overlay from the “Share…” button in the systemwide text selection context menu (or using the Action button in apps that handle simple text and support it).
The modified are made on a modal overlay handle in a target of your app and at the end of the editing you send back the edited content to the host app (if you want to) and the text is replaced with the edited content. If you send it to an hosted app that don't support modification in place the action don't have effect. My question is: how do I know when an host app supports modification in place?
Here is the code I'm using (derived from this online tutorial).
Get the items from extension:
let textItem = self.extensionContext!.inputItems[0]
as! NSExtensionItem
let textItemProvider = textItem.attachments![0] // the extension supports text based content so the kUTTypeText UTI is used to perform this test
if textItemProvider.hasItemConformingToTypeIdentifier(kUTTypeText as String) {
textItemProvider.loadItem(forTypeIdentifier: // if the host app has data of the required type, it can be loaded into the extension
kUTTypeText as String,
options: nil,
completionHandler: { (result, error) in
self.receivedText = result as? String // string containing the text loaded from the host app
if self.receivedText != nil {
DispatchQueue.main.async {
self.processedText = self.receivedText!
}
}
})
}
Returning edited content to the host app:
func returnEditedContent() {
let returnProvider =
NSItemProvider(item: processedText as NSSecureCoding?,
typeIdentifier: kUTTypeText as String) // create a new NSItemProvider instance
let returnItem = NSExtensionItem() // a new NSExtensionItem instance is created
returnItem.attachments = [returnProvider]
self.extensionContext!.completeRequest(returningItems: [returnItem], completionHandler: nil)
}
Cancel button:
#IBAction func cancelButtonPressed(_ sender: Any) {
self.extensionContext!.cancelRequest(withError: DismissExtension.cancelByUser)
}
I searched for a solution in various resources without finding it. Here are some documents:
Apple - Understand Action Extensions, Apple Human Interface Guidelines - Sharing and Actions, Medium - Simple Text Action Extension Swift 3

How to use my view controllers and other class in the Share Extension ? iOS | Swift 4

I am creating a chatting application. User can share the images from other application to my application. I have added Share Extension to show my app in the native share app list. I'm also getting the selected data in didSelectPost Method. From here I want to show the list of the users to whom the image can be forwarded. For this, I'm using an already created view controller in the main app target.
override func didSelectPost() {
// This is called after the user selects Post. Do the upload of contentText and/or NSExtensionContext attachments.
if let content = self.extensionContext!.inputItems[0] as? NSExtensionItem {
let contentType = kUTTypeImage as String
// Verify the provider is valid
if let contents = content.attachments as? [NSItemProvider] {
for attachment in contents {
if attachment.hasItemConformingToTypeIdentifier(contentType) {
attachment.loadItem(forTypeIdentifier: contentType, options: nil) { (data, error) in
let url = data as! URL
let imageData = try! Data(contentsOf: url)
// Here I'm navigating to my viewcontroller, let's say: ForwardVC
}
}
}
}
}
I don't want to recreate the same screen in Share Extension. Apart from this view controllers, I have many more classes and wrappers that I want to use within the share extension. Like, SocketManager, Webservices, etc. Please suggest me your approach to achieve the same.
P.S.: I've tried setting multiple targets to required viewControllers and using same pods for Share Extention. In this approach, I'm facing a lot of issues as many of the methods and pods are not extention compliant. Also, is it the right way to do this.

Document-Based Application

Xcode 9 / iOS 11:
My app creates pdf-files which are rendered with data from a core data-database. The pdf's are created from a WebView and are stored in the documents directory. Everything works so far. I can open the pdf-files in Apple's Files App and even edit them there. I would like to implement the DocumentBrowserViewController in my app. I used the template from Xcode 9. I'm not able to show the created pdf-files in the DocumentViewController like in Apple's Files App. I can pick the documents but the DocumentViewController shows only the file name and a done-button. Which is the easiest way to show existing pdf's in this DocumentViewController from the template? Where do I have to implement the relevant code?
Code from Xcode9-template:
class DocumentViewController: UIViewController {
#IBOutlet weak var documentNameLabel: UILabel!
var document: UIDocument?
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
// Access the document
document?.open(completionHandler: { (success) in
if success {
// Display the content of the document, e.g.:
self.documentNameLabel.text = self.document?.fileURL.lastPathComponent
// --> How to show/load the pdf here? Do I have to create a UIView here first?
} else {
// Make sure to handle the failed import appropriately, e.g., by presenting an error message to the user.
}
})
}
and here's the subclass of UIDocument from the Xcode 9-template:
class Document: UIDocument {
var pdfData: PreviewViewController?
override func contents(forType typeName: String) throws -> Any {
// Encode your document with an instance of NSData or NSFileWrapper
return Data()
}
override func load(fromContents contents: Any, ofType typeName: String?) throws {
// --> do I have to load the pdf here?
}
}
I'm not very experienced in app-development. I was watching Paul Hagerty's course cs193p on iTunesU about persistence with UIDocument. But he's saving the model of the app and I have a pdf. Is there an easy way to do show the pdf's? I tried different ways to load the pdf but I'm not sure if I have to create a UIView first. The nice feature in Files App is that you can edit or send the pdf via email.
A great resource for UIDocumentBrowserViewController is the WWDC video from 2017:
Building Great Document-based Apps in iOS 11 - WWDC 2017
To answer your question in your comment in func load. Yes you get your pdf here as Data:
override func load(fromContents contents: Any, ofType typeName: String?) throws {
guard let data = contents as? Data else { return }
}
But what you should do is in your DocumentViewController is to load the fileURL which is a property of UIDocument into a UIWebView which can easily display pdf files. Have a look here: https://stackoverflow.com/a/38792456/4089350
Then in your document.open function pass the fileURL to your UIWebView.

Quick Look Preview Extension iOS preparePreviewOfFile(at:completionHandler:)

I'm trying to write a simple Quick Look Preview Extension for my UIDocument-based iOS app.
The problem is that in my implementation of preparePreviewOfFile(at:completionHandler:) my attempt to open the UIDocument based on the URL I'm being handed is failing. I instantiate my document with the file URL and call open(completionHandler:) but I'm not getting any data, and I'm seeing a console message that the file coordinator has crashed.
All of this works fine in my actual app; it's just the Quick Look Preview Extension implementation that's having trouble. Is there something special I have to do to open a UIDocument from inside a Quick Look Preview Extension? Apple doesn't provide any sample code; in WWDC 2017 video 229 they just gloss over the whole thing.
EDIT: Curiouser and curiouser. I created a simplified testbed app that displays a Quick Look preview with UIDocumentInteractionController, along with my custom Quick Look Preview Extension. On the Simulator, the preview works! On the device, it doesn't. It looks like, when I tell my document to open, its load(fromContents:ofType) is never even called; instead, we are getting a pair of error messages like this:
The connection to service named com.apple.FileCoordination was invalidated.
A process invoked one of the -[NSFileCoordinator coordinate...] methods but filecoordinationd crashed. Returning an error.
I was able to work around the issue by not calling open on my UIDocument. Instead, I call read directly, on a background thread, like this:
func preparePreviewOfFile(at url: URL, completionHandler handler: #escaping (Error?) -> Void) {
DispatchQueue.global(qos: .background).async {
let doc = MyDocument(fileURL: url)
do {
try doc.read(from: url)
DispatchQueue.main.async {
// update interface here!
}
handler(nil)
} catch {
handler(error)
}
}
}
I have no idea if that's even legal. You'd think that just reading the document straight in, without the use of a file coordinator, would be Bad. But it does seem to work!
I found yet another workaround, using NSFileCoordinator and calling load manually to get the UIDocument to process the data:
let fc = NSFileCoordinator()
let intent = NSFileAccessIntent.readingIntent(with: url)
fc.coordinate(with: [intent], queue: .main) { err in
do {
let data = try Data(contentsOf: intent.url)
let doc = MyDocument(fileURL: url)
try doc.load(fromContents: data, ofType: nil)
self.lab.text = doc.string
handler(nil)
} catch {
handler(error)
}
}
Again, whether that's legal, I have no idea, but I feel better about it than calling read directly, because at least I'm passing through a file coordinator.

Resources