Document-Based Application - ios

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.

Related

Using CoreData as dynamic Widget Intents in SwiftUI

I've followed along the Apple Developer Code-Along videos (https://developer.apple.com/news/?id=yv6so7ie) as well as looked at this repo with different Widget types (https://github.com/pawello2222/WidgetExamples) in search on how to populate a dynamic intent with items from Core Data.
My question is: How can I used the saved Core Data objects from the user to have them as "filters" or options in widget settings?
My Core Data model is called Favourite and it is a Class Definition CodeGen file.
I have added the Intent target to my project and can get it to appear in the Widget settings, but when I tap in to "Choose" the list is empty. However, in my Core Data there are 3 saved items.
I have tried doing simple things like print(CoreDM.shared.getAllFavourites()) to see if I am even retrieving them all, but not listing in the settings, but the console prints out:
<NextDeparturesIntent: 0x283490bd0> {
favourite = <null>;
}
At this point I'm stuck on understanding on how I can get my Favourites visible and then usable. It seems everything else is hooked up and working or ready but the retrieval.
I have also tried re-adding into the Info.plist of the intent the IntentSupported of the intent's name: NextDeparturesIntentHandling:but that had no success.
Files
Core Data Model - Favourite - FavouritesCDModel
In my Core Data model I have more options but for this example:
UUID
beginName
finishName
Widget Intent - NextDepartures.intentdefinition
This is what has been set up as followed by Apple's Code Along videos:
app-name-intent - IntentHandler.swift
import Intents
class IntentHandler: INExtension, NextDeparturesIntentHandling {
let coreDM = CoreDataManager.shared
func provideFavouriteOptionsCollection(
for intent: NextDeparturesIntent,
with completion: #escaping (INObjectCollection<FavouriteRoutes>?, Error?) -> Void
) {
dump( coreDM.getAllFavourites() ) // <--- debug line
let favouriteRoutes = coreDM.getAllFavourites().map {
FavouriteRoutes(identifier: $0.uniqueId, display: $0.departingStopName!)
}
let collection = INObjectCollection(items: favouriteRoutes)
completion(collection, nil)
}
override func handler(for intent: INIntent) -> Any {
return self
}
}
CoreDataManager
import CoreData
final class CoreDataManager {
static let shared = CoreDataManager()
private init() {}
private let persistentContainer: NSPersistentContainer = {
let container = NSPersistentContainer(name: "FavouritesCDModel")
container.loadPersistentStores(completionHandler: { description, error in
if let error = error as NSError? {
fatalError("Core Data Store failed \(error.localizedDescription)")
}
})
return container
}()
var managedObjectContext: NSManagedObjectContext {
persistentContainer.viewContext
}
func getAllFavourites() -> [Favourite] {
let fetchRequest: NSFetchRequest<Favourite> = Favourite.fetchRequest()
do {
return try managedObjectContext.fetch(fetchRequest)
} catch {
return []
}
}
}
I didn't realise that the app, widget, and other targets are all sandboxed.
I incorrectly assumed everything within the same app ecosystem would be allowed access to the same items.
In order to get the above code to work is adding the file to the App Groups and FileManager.
CoreDataManager
Inside the persistentContainer add in the storeURL and descriptions:
let storeURL = FileManager.appGroupContainerURL.appendingPathComponent("COREDATAFILE.sqlite")
container.persistentStoreDescriptions = [NSPersistentStoreDescription(url: storeURL)]
FileManager+Ext
Create a FileManager extension for the container url:
extension FileManager {
static let appGroupContainerURL = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: "group.com.domain.app")!
}
Info.plist
Make sure that the Info.plist files have access to the app group
Signing and Capabilities
Make sure you add the App Groups capability to each target that needs it, and add it in App Store Connect

iOS Action Extension, share PDF from Safari fails

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.

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.

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