The app needs to create a URL to pass into a UIDocumentInteractionController that will present the user with options to share the file. It's a PDF that is downloaded over the network and confirmed as being in place.
let documentsPath = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!
let filePath = documentsPath.appendingPathComponent("documents/\(self.filename!)").path
let fileURL = URL(fileURLWithPath: filePath)
This is where the PDF is stored after being downloaded.
let documentController: UIDocumentInteractionController = UIDocumentInteractionController.init(url: fileURL)
documentController.uti = "com.adobe.pdf"
documentController.presentOpenInMenu(from: CGRect(x: 0, y: 0, width: self.container.frame.width, height: self.container.frame.height), in: self.container, animated: true)
This opens up the menu with the sharing options but when trying to select an app to open the PDF, the following error occurs:
Could not instantiate class NSURL. Error: Error Domain=NSCocoaErrorDomain Code=4864 "The URL archive of type “public.url” contains invalid data." UserInfo={NSDebugDescription=The URL archive of type “public.url” contains invalid data.}
After having confirmed the document is where it should be, when printing the fileURL, it's the following:
file:///var/mobile/Containers/Data/Application/3296B736-4DFB-4F62-9B05-C800D574982B/Documents/documents/77351848-68816600-1626168959.pdf
I have searched high and low for an answer to this and I believe it's more linked to the URL than the UIDocumentInteractionController itself.
Any advice would be enormously appreciated.
Swift 5 Implementation(Xcode 12.5):
class VC1: UIViewController {
var pdfURL: URL?
var documentInteractionController:UIDocumentInteractionController!
override func viewDidLoad() {
super.viewDidLoad()
}
#IBAction func downloadAction(_ sender: Any) {
self.downloadPDF()
}
func downloadPDF() {
guard let url = URL(string: "https://file-examples-com.github.io/uploads/2017/10/file-sample_150kB.pdf") else { return }
let urlSession = URLSession(configuration: .default, delegate: self, delegateQueue: OperationQueue())
let downloadTask = urlSession.downloadTask(with: url)
downloadTask.resume()
}
func showPDF(url: URL) {
documentInteractionController = UIDocumentInteractionController(url: url)
documentInteractionController.delegate = self
DispatchQueue.main.async { [self] in
documentInteractionController.presentPreview(animated: true)
}
}
}
extension VC1: UIDocumentInteractionControllerDelegate {
func documentInteractionControllerViewControllerForPreview(_ controller: UIDocumentInteractionController) -> UIViewController {
return self
}
func documentInteractionControllerDidEndPreview(_ controller: UIDocumentInteractionController) {
print("Dismissed!!!")
documentInteractionController = nil
}
}
extension VC1: URLSessionDownloadDelegate {
func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL) {
guard let url = downloadTask.originalRequest?.url else { return }
let documentsPath = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask)[0]
let destinationURL = documentsPath.appendingPathComponent(url.lastPathComponent)
try? FileManager.default.removeItem(at: destinationURL)
do {
try FileManager.default.copyItem(at: location, to: destinationURL)
self.pdfURL = destinationURL
showPDF(url: destinationURL)
} catch let error {
print("Error: \(error.localizedDescription)")
}
}
}
Related
this is my first time trying to add an AR preview into an app (also my first time using the file system). I have been trying to implement a solution similar to that explained here https://developer.apple.com/forums/thread/126377 however one key difference is that my usdz model is not in my main bundle as it is generated and downloaded from an external source at run time. I was wondering if it is possible to display a file stored in the apps documents or cache directory and how it is done.
The file is downloaded and stored in the caches directory as follows:
class ModelFetcher: NSObject{
var modelUrl: URL?
func generateModel() {
guard let url = URL(string: "http://127.0.0.1:5000/model.usdz") else {return}
let urlSession = URLSession(configuration: .default, delegate: self, delegateQueue: OperationQueue())
var request = URLRequest(url: url)
request.httpMethod = "POST"
let downloadTask = urlSession.downloadTask(with: request)
downloadTask.resume()
}
}
extension ModelFetcher: URLSessionDownloadDelegate {
func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL) {
print("File Downloaded Location- ", location)
guard let url = downloadTask.originalRequest?.url else {
return
}
let docsPath = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask)[0]
let destinationPath = docsPath.appendingPathComponent(url.lastPathComponent)
try? FileManager.default.removeItem(at: destinationPath)
do {
try FileManager.default.copyItem(at: location, to: destinationPath)
self.modelUrl = destinationPath
print("File moved to: \(modelUrl!.absoluteURL)")
} catch let error {
print("Copy Error: \(error.localizedDescription)")
}
}
}
I then try to display the model using ARQuicklookPreview as follows:
import SwiftUI
import QuickLook
import ARKit
struct ARQuickLookView: UIViewControllerRepresentable {
var allowScaling: Bool = true
func makeCoordinator() -> ARQuickLookView.Coordinator {
Coordinator(self)
}
func makeUIViewController(context: Context) -> QLPreviewController {
let controller = QLPreviewController()
controller.dataSource = context.coordinator
return controller
}
func updateUIViewController(_ controller: QLPreviewController,
context: Context) {
// nothing to do here
}
class Coordinator: NSObject, QLPreviewControllerDataSource {
let parent: ARQuickLookView
let destinationPath = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask)[0].appendingPathComponent("model.usdz")
private lazy var fileURL: URL = destinationPath
init(_ parent: ARQuickLookView) {
self.parent = parent
super.init()
}
func numberOfPreviewItems(in controller: QLPreviewController) -> Int {
return 1
}
func previewController(
_ controller: QLPreviewController,
previewItemAt index: Int
) -> QLPreviewItem {
let fileURL = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask)[0].appendingPathComponent("model.usdz")
print(fileURL)
let item = ARQuickLookPreviewItem(fileAt: fileURL)
print(item)
item.allowsContentScaling = parent.allowScaling
return item
}
}
}
struct ARQuickLookView_Previews: PreviewProvider {
static var previews: some View {
ARQuickLookView()
}
}
However, I receive the error "Unhandled item type 13: contentType is: (null) #PreviewItem" in the console and in the UI it reads unsupported file type, I have performed tests to make sure the file is in the location of the URL and I can even open it in preview on my mac so it's not like the file format or location is wrong.
Any help on displaying a model from a location other than the main bundle would be helpful, or perhaps a workaround to first move the file then display it.
Edit: After changing the preview controller function to
let fileURL = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask)[0].appendingPathComponent("model.usdz")
return fileUrl as QLPreviewItem
Now in the UI it just says "model universal scene description Package"
And in the console I still get an error however now it reads: Unhandled item type 13: contentType is: com.pixar.universal-scene-description-mobile #PreviewItem
Which doesn't make sense as that it one of the only types it supports
Thanks,
Louis
Problem with downloading files.
I'm trying to download a file form url and push it into open/share modal. But da data downloads as Data and if I try saving it to File app it saves a file called Data.
I just need to download and share the file with the original extension. Like file.extension.
Here's the code use. I used Alamofire pod here:
AF.download(url).responseData { response in
if let preset = response.value {
let shareArray = [preset]
let activityViewController = UIActivityViewController(activityItems: shareArray , applicationActivities: nil)
activityViewController.popoverPresentationController?.sourceView = self.view
self.present(activityViewController, animated: true, completion: nil)
}
}
Also tried this code but the app crashed:
if let url = URL(string: downloadURL!) {
let task = URLSession.shared.downloadTask(with: url) { localURL, urlResponse, error in
if let localURL = localURL {
let shareArray = [localURL]
let activityViewController = UIActivityViewController(activityItems: shareArray , applicationActivities: nil)
activityViewController.popoverPresentationController?.sourceView = self.view
self.present(activityViewController, animated: true, completion: nil)
}
}
task.resume()
}
The issue there is that you are trying to share the temporary file returned. It has a dynamic UTI (Unified Type Identifier). You need to get the url response suggested file name and rename the file.
import UIKit
import PlaygroundSupport
PlaygroundPage.current.needsIndefiniteExecution = true
extension URL {
var typeIdentifier: String? { (try? resourceValues(forKeys: [.typeIdentifierKey]))?.typeIdentifier }
}
let url = URL(string: "https://i.stack.imgur.com/varL9.jpg")!
URLSession.shared.downloadTask(with: url) { location, response, error in
guard let location = location,
let httpURLResponse = response as? HTTPURLResponse,
httpURLResponse.statusCode == 200 else { return }
let fileName = httpURLResponse.suggestedFilename ?? httpURLResponse.url?.lastPathComponent ?? url.lastPathComponent
let destination = FileManager.default.temporaryDirectory.appendingPathComponent(fileName)
do {
if FileManager.default.fileExists(atPath: destination.path) {
try FileManager.default.removeItem(at: destination)
}
print("location", location.typeIdentifier ?? "no UTI") // location dyn.ah62d4rv4ge81k5pu
try FileManager.default.moveItem(at: location, to: destination)
print("destination", destination.typeIdentifier ?? "no UTI") // destination public.jpeg
} catch {
print(error)
}
}.resume()
To complete the answer from #leo Dabus, there is a need to modify the destination URL further and append the file extension you desire by using
FileManager.default.moveItem(at:to:)
https://developer.apple.com/documentation/foundation/filemanager/1414750-moveitem
func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL) {
// create destination URL with the original pdf name
print("fileDownload: urlSession")
guard let url = downloadTask.originalRequest?.url else { return }
print("fileDownload: urlSession \(url)")
let documentsPath = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0]
let destinationURL = documentsPath.appendingPathComponent(url.lastPathComponent)
// delete original copy
try? FileManager.default.removeItem(at: destinationURL)
// append a new extension type
let newURL = destinationURL.deletingPathExtension().appendingPathExtension("pdf")
// copy from temp to Document
try? FileManager.default.moveItem(at: destinationURL, to: newURL)
do {
try FileManager.default.copyItem(at: location, to: newURL)
myViewDocumentsmethod(PdfUrl:destinationURL)
print("fileDownload: downloadLocation", destinationURL)
} catch let error {
print("fileDownload: error \(error.localizedDescription)")
}
}
I have the following code that allows me to download a PDF file from a URL, it works correctly:
class ViewController: UIViewController {
#IBOutlet weak var progressView: UIProgressView!
override func viewDidLoad() {
let _ = DownloadManager.shared.activate()
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
DownloadManager.shared.onProgress = { (progress) in
OperationQueue.main.addOperation {
self.progressView.progress = progress
}
}
}
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
DownloadManager.shared.onProgress = nil
}
#IBAction func startDownload(_ sender: Any) {
let url = URL(string: "https://d0.awsstatic.com/whitepapers/KMS-Cryptographic-Details.pdf")!
let task = DownloadManager.shared.activate().downloadTask(with: url)
task.resume()
}
}
The file is currently going to: file:///Users/cybermac/Library/Developer/CoreSimulator/Devices/CAEC75D0-423A-4FB2-B0D6-9E7CADB190A1/data/Containers/Data/Application/8B5CBFC8-7058-48DB-A1C4-872302A80610/Library/Caches/com.apple.nsurlsessiond/Downloads/com.example.DownloadTaskExample/CFNetworkDownload_Q7OVlf.tmp
How do I save it in /Documents/?
Something like this: file:///Users/cybermac/Library/Developer/CoreSimulator/Devices/CAEC75D0-423A-4FB2-B0D6-9E7CADB190A1/data/Containers/Data/Application/64370B29-2C01-470F-AE76-17EF1A7BC918/Documents/
The idea is that the file saved in that directory can be used to read it offline (with PDFKit or webKit). It will only be deleted if the application is deleted.
You need to move the file to your custom location after the download. Implement URLSessionDownloadDelegate and you will receive the location of your downloaded file.
Delegate method:
func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL)
Code to move the file:
do {
let documentsURL = try
FileManager.default.url(for: .documentDirectory,
in: .userDomainMask,
appropriateFor: nil,
create: false)
let savedURL = documentsURL.appendingPathComponent("yourCustomName.pdf")
try FileManager.default.moveItem(at: location, to: savedURL)
} catch {
print ("file error: \(error)")
}
To learn more refer to this repo.
This snippet was downloaded your Remote PDF from Server and "Save" into your Photo Library
func drawPDFfromURL(url: URL) -> UIImage? {
guard let document = CGPDFDocument(url as CFURL) else { return nil }
guard let page = document.page(at: 1) else { return nil }
let pageRect = page.getBoxRect(.mediaBox)
let renderer = UIGraphicsImageRenderer(size: pageRect.size)
let img = renderer.image { ctx in
UIColor.white.set()
ctx.fill(pageRect)
ctx.cgContext.translateBy(x: 0.0, y: pageRect.size.height)
ctx.cgContext.scaleBy(x: 1.0, y: -1.0)
ctx.cgContext.drawPDFPage(page)
}
return img
}
step 2
UIImageWriteToSavedPhotosAlbum(drawPDFfromURL(url: url!) ?? UIImage(), nil, nil, nil)
class ViewController: UIViewController {
let quickLookController = QLPreviewController()
override func viewDidLoad() {
super.viewDidLoad()
quickLookController.dataSource = self
quickLookController.delegate = self
}
#IBAction func buttonAction(_ sender: Any) {
present(quickLookController, animated: true, completion: nil)
quickLookController.reloadData()
quickLookController.refreshCurrentPreviewItem()
}
}
extension ViewController: QLPreviewControllerDataSource,QLPreviewControllerDelegate {
func numberOfPreviewItems(in controller: QLPreviewController) -> Int {
return 1
}
func previewController(_ controller: QLPreviewController, previewItemAt index: Int) -> QLPreviewItem {
let path = Bundle.main.path(forResource: "AppCoda-Ppt.ppt", ofType: nil)
let url = NSURL(fileURLWithPath: path!)
return url
}
}
This is a bug in iOS 11.2. Please file it at bugreport.apple.com if you want to be kept up to date on its status. A workaround is not to store your ppt file in your app bundle. Use a different location somewhere in your container, such as your Application Support directory.
Answer from #Thomas is correct (THANK YOU!) For Swift 3 you can do something like this in the previewItemAt:index method to cache the file on demand:
func previewController(_ controller: QLPreviewController, previewItemAt index: Int) -> QLPreviewItem {
guard let CacheDirURL = try? FileManager.default.url(for: .cachesDirectory, in: .userDomainMask, appropriateFor: nil, create: true) else {
print("Can't get cache dir URL")
return NSURL(fileURLWithPath: "FILE_NOT_FOUND")
}
let fileUrl = CacheDirURL.appendingPathComponent("cachedFileName.pdf")
if !FileManager.default.fileExists(atPath: fileUrl.path) {
if let sourceUrl = Bundle.main.url(forResource: "Pioneer_UAS_Policy_Aug_2016", withExtension: "pdf") {
print("Copying file from bundle \(sourceUrl.path)")
try? FileManager.default.copyItem(at: sourceUrl, to: fileUrl)
}
}
return NSURL(fileURLWithPath: fileUrl.path)
}
You have to copy the file to the i.e. temporary directory and read the file from there.
I download and display PDF from my swift application using this code:
var docController:UIDocumentInteractionController!
let pdfUrl = NSURL(string: "ENTER_URL_OF_PDF")
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
downloadDoc(pdfUrl: pdfUrl!)
}
#IBAction func buttonAction(_ sender: AnyObject) {
docController.presentOptionsMenu(from: self.view.frame, in: self.view, animated: true)
}
func downloadDoc(pdfUrl : NSURL) {
let urlTest = self.pdfUrl!.absoluteString
let pdfUrl = NSURL(string: urlTest!)
if(pdfUrl != nil){
let pdfRequest: NSURLRequest = NSURLRequest(url: pdfUrl! as URL)
NSURLConnection.sendAsynchronousRequest(pdfRequest as URLRequest, queue: OperationQueue.main) {(response, data, error) in
let httpResponse = response as? HTTPURLResponse
if(httpResponse?.statusCode == 200 && error == nil){
let documentsUrl = FileManager.default.urls(for: FileManager.SearchPathDirectory.documentDirectory, in: FileManager.SearchPathDomainMask.userDomainMask).first as! NSURL
if let fileName = self.pdfUrl!.lastPathComponent {
let destinationUrl = documentsUrl.appendingPathComponent(fileName)
if let data = data {
do {
try data.write(to: destinationUrl!, options: .atomic)
} catch {
print(error)
}
self.docController = UIDocumentInteractionController(url: destinationUrl!)
}
}
}
}
}
}
The problem I'm facing is that when I try with a PDF containing grater than 3 pages, I got this error:
fatal error: unexpectedly found nil while unwrapping an Optional value
and my app crash. When I searched about the max size that can be downloaded I read in man SO posts that there is no limit!
So why I got this problem?