I'm trying to use drag and drop to read an xlsx spreadsheet in my iOS app, thanks to some help I've got to the stage where I can get the data and have written this NSItemProviderReading class to access it:
class ExcelDocument:NSObject, NSItemProviderReading {
let data:Data?
required init(excelData:Data, typeIdentifier:String) {
data = excelData
}
static var readableTypeIdentifiersForItemProvider: [String] {
return ["org.openxmlformats.spreadsheetml.sheet"]
}
static func object(withItemProviderData data: Data, typeIdentifier: String) throws -> Self {
return self.init(excelData: data, typeIdentifier: typeIdentifier)
}
func write(toFile: String) -> URL?{
let fileManager = FileManager.default
do {
let documentDirectory = try fileManager.url(for: .documentDirectory, in: .userDomainMask, appropriateFor:nil, create:false)
let fileURL = documentDirectory.appendingPathComponent(toFile)
try self.data?.write(to: fileURL)
return fileURL
} catch {
print(error)
}
return nil
}
}
All this runs fine, and I can open and read the file written by the self.data?.write line, but the structure of the xlsx file seems to have been lost - there is no [Content_Types].xml or .rels, so utilities that try and read the file (I'm using XlsxReaderWriter) throw an error.
Given that Office Open XML is zipped XML I would have thought that the byte stream would work and unzipping would recreate the structure, but it seems not...
Any ideas?
I have solved it....
The problem was that I was retrieving the drop item with the NSItemProvider I created above..
func dropInteraction(_ interaction: UIDropInteraction, performDrop session: UIDropSession) {
session.loadObjects(ofClass: ExcelDocument.self) { excelItems in
if let workbooks = excelItems as? [ExcelDocument], let book = workbooks.first, let excelData = book.data {
what I should have been doing (and what works) is:
func dropInteraction(_ interaction: UIDropInteraction, performDrop session: UIDropSession) {
session.items[0].itemProvider.loadFileRepresentation(forTypeIdentifier: "org.openxmlformats.spreadsheetml.sheet") { url, error in
if url != nil {
(read the excel data from the URL)
}
}
I stumbled across session.loadFileRepresentation as a new function in iOS 11 in the Apple documentation - I've not seen it mentioned in any of the examples for drag and drop in iOS 11. Anyway, it's all working now
Related
I am trying to build and iOS app with similar behaviour to Pages / Numbers / Keynote. Each of these apps is a Document Based App, where the user is first presented with a UIDocumentBrowserViewController where the user choses a document to open in the app. In Numbers for example a user can select a .numbers file and it will open, or a user can select a .csv and it will import this csv file into a numbers file which is saved along side the original csv in the same location.
In my app I want the user to select a .csv file, and then I'll import it into my own document format (called .pivot) and save this alongside the csv file (just like numbers.) This works fine in the simulator but when I run my code on a device I get an error when calling save(to:for:completionHandler:) on my custom Pivot document.
My document browser code is as follows.
class DocumentBrowserViewController: UIDocumentBrowserViewController, UIDocumentBrowserViewControllerDelegate {
override func viewDidLoad() {
super.viewDidLoad()
delegate = self
allowsDocumentCreation = false
allowsPickingMultipleItems = false
}
func documentBrowser(_ controller: UIDocumentBrowserViewController, didPickDocumentsAt documentURLs: [URL]) {
guard let sourceURL = documentURLs.first else { return }
if sourceURL.pathExtension == "csv" {
// Create a CSV document so we can read the CSV data
let csvDocument = CSVDocument(fileURL: sourceURL)
csvDocument.open { _ in
guard let csv = csvDocument.csvData else {
fatalError("CSV is nil upon open")
}
// Create the file at the same location as the csv, with the same name just a different extension
var pivotURL = sourceURL.deletingLastPathComponent()
let pivotFilename = sourceURL.lastPathComponent .replacingOccurrences(of: "csv", with: "pivot")
pivotURL.appendPathComponent(pivotFilename, isDirectory: false)
let model = PivotModel()
model.csv = csv
let document = PivotDocument(fileURL: pivotURL)
document.model = model
document.save(to: pivotURL, for: .forCreating, completionHandler: { success in
// `success` is false here
DispatchQueue.main.async {
self.performSegue(withIdentifier: "presentPivot", sender: self)
}
})
}
}
}
}
My first UIDocument subclass to load a csv file is as follows.
import SwiftCSV // This is pulled in using SPM and works as I expect, so is unlikely causing this problem
class CSVDocument: UIDocument {
var csvData: CSV?
override func contents(forType typeName: String) throws -> Any {
return Data()
}
override func load(fromContents contents: Any, ofType typeName: String?) throws {
guard let data = contents as? Data else {
fatalError("No file data")
}
guard let string = String(data: data, encoding: .utf8) else {
fatalError("Cannot load data into string")
}
csvData = try CSV(string: string)
}
}
My second UIDocument subclass for my custom Pivot document is as follows. By overriding the handleError() function I can see the save fails with an error in the NSCocoaErrorDomain, with code of 513.
class PivotDocument: UIDocument {
var model: PivotModel!
var url: URL!
override func contents(forType typeName: String) throws -> Any {
let encoder = JSONEncoder()
return try encoder.encode(model)
}
override func load(fromContents contents: Any, ofType typeName: String?) throws {
guard let data = contents as? Data else {
fatalError("File contents are not Data")
}
let decoder = JSONDecoder()
model = try decoder.decode(PivotModel.self, from: data)
}
override func handleError(_ error: Error, userInteractionPermitted: Bool) {
let theError = error as NSError
print("\(theError.code)") // 513
print("\(theError.domain)") // NSCocoaErrorDomain
print("\(theError.localizedDescription)") // “example.pivot” couldn’t be moved because you don’t have permission to access “CSVs”.
super.handleError(error, userInteractionPermitted: userInteractionPermitted)
}
}
The fact that this works in the simulator (where my user has access to all the file system) but doesn't on iOS (where user and app permissions are different) makes me think I have a permission problem. Do I need to declare some entitlements in my Xcode project for example?
Or am I just misusing the UIDocument API and do I need to find a different implementation?
I found the function I was looking for that replicates the functionality of the iWork apps!
UIDocumentBrowserViewController has this function importDocument(at:nextToDocumentAt:mode:completionHandler:). From the docs:
Use this method to import a document into the same file provider and directory as an existing document.
For example, to duplicate a document that's already managed by a file provider:
Create a duplicate of the original file in the user's temporary directory. Be sure to give it a unique name.
Call importDocument(at:nextToDocumentAt:mode:completionHandler:), passing in the temporary file's URL as the documentURL parameter and the original file's URL as the neighborURL parameter.
So documentBrowser(_:didPickDocumentsAt:) is now:
let pivotFilename = sourceURL.lastPathComponent .replacingOccurrences(of: "csv", with: "pivot")
let path = FileManager.default.temporaryDirectory.appendingPathComponent(pivotFilename)
if FileManager.default.createFile(atPath: path.path, contents: nil, attributes: nil) {
self.importDocument(at: path, nextToDocumentAt: sourceURL, mode: .copy) { (importedURL, errorOrNil) in
guard let pivotURL = importedURL else {
fatalError("No URL for imported document. Error: \n \(errorOrNil?.localizedDescription ?? "NO ERROR")")
}
let model = PivotModel()
model.csv = csv
let document = PivotDocument(fileURL: pivotURL)
document.model = model
DispatchQueue.main.async {
self.performSegue(withIdentifier: "presentPivot", sender: self)
}
}
}
else {
fatalError("Could not create local pivot file in temp dir")
}
No more permissions errors. Hope this helps someone else in the future.
I created a stop button that can collect data, which will be saved to the defined path after clicking the stop button. However, if I want to continue collecting after clicking the stop button, the data will be added to the original text file. (This makes senses as I only know how to define one path)
My question is: Would it be possible to ask the user and input a new file name and save as a new text file after each stop so that the data is not added to the original file?
Below is what I have for one defined path and stacking up the data:
#IBAction func stopbuttonTapped(_ btn: UIButton) {
do {
let username:String = user_name.text!
fpsTimer.invalidate() //turn off the timer
let capdata = captureData.map{$0.verticesFormatted}.joined(separator:"") //convert capture data to string
let dir: URL = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).last! as URL
let url = dir.appendingPathComponent("testing.txt") //name the file
try capdata.appendLineToURL(fileURL: url as URL)
let result = try String(contentsOf: url as URL, encoding: String.Encoding.utf8)
}
catch {
print("Could not write to file")
}
}
And the extension I use for string and data:
extension String {
func appendLineToURL(fileURL: URL) throws {
try (self).appendToURL(fileURL: fileURL)
}
func appendToURL(fileURL: URL) throws {
let data = self.data(using: String.Encoding.utf8)!
try data.append(fileURL: fileURL)
}
func trim() -> String
{
return self.trimmingCharacters(in: CharacterSet.whitespaces)
}
}
extension Data {
func append(fileURL: URL) throws {
if let fileHandle = FileHandle(forWritingAtPath: fileURL.path) {
defer {
fileHandle.closeFile()
}
fileHandle.seekToEndOfFile()
fileHandle.write(self)
}
else {
try write(to: fileURL, options: .atomic)
}
}
}
Do I need to set a default file name (maybe texting.txt) and then popped up a user input for saving the text file? (That's where I am not too sure how to integrate to what I already have). I thank you so much for your time and suggestions in advance.
You could generate unique names.
For example:
let url = dir.appendingPathComponent("testing-\(Date.timeIntervalSinceReferenceDate).txt")
or
let url = dir.appendingPathComponent("testing-\(UUID().uuidString).txt")
so this might be a trivial question, but I can't get it to work.
I want to save a pdf file to CoreData after I dropped it onto a view (using IOS' Drag&Drop feature)
func dropInteraction(_ interaction: UIDropInteraction, performDrop session: UIDropSession) {
session.loadObjects(ofClass: ComicBookPDFDocument.self) { (pdfItems) in
let items = pdfItems as! [ComicBookPDFDocument]
// "Cannot assign value of type 'ComicBookPDFDocument' to type 'NSData?'"
self.file.data = items[0]
}
}
ComicBookPDFDocument just subclasses PDFDocument to make it conforming to NSItemProviderReading:
final class ComicBookPDFDocument: PDFDocument, NSItemProviderReading {
public static var readableTypeIdentifiersForItemProvider: [String] {
return [kUTTypePDF as String]
}
public static func object(withItemProviderData data: Data, typeIdentifier: String) throws -> ComicBookPDFDocument {
return ComicBookPDFDocument(data: data)!
}
}
However, I get this compiler error from XCode:
Cannot assign value of type 'ComicBookPDFDocument' to type 'NSData?'
How can I save the pdf data from a PDFDocument? I couldn't find anything on the internet or the documentation.
Thanks for any help
Okay, I don't know how I missed that, but here it is:
items[0].dataRepresentation()
You do one thing,
Try to save PDF into the Document Directory and save its path in the Core-Data.
Here is the code to save to Document directory and fetch from document direcory
class PDCache: NSObject {
static let sharedInstance = PDCache()
func saveData(obj: Data, fileName: String){
let filename = getDocumentsDirectory().appendingPathComponent("\(fileName).pdf")
do{
try obj.write(to: filename, options: .atomic)
} catch{
print(error.localizedDescription)
}
}
func getData(fileName: String) -> URL?{
let fileManager = FileManager.default
let filename = getDocumentsDirectory().appendingPathComponent("\(fileName).pdf")
if fileManager.fileExists(atPath: filename.path){
return URL(fileURLWithPath: filename.path)
}
return nil
}
private func getDocumentsDirectory() -> URL {
let paths = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)
return paths[0]
}
}
To save the data use this
let data = Data()
self.saveData(obj: data, fileName: "myPdfFile")
and to get the file url use this
let pdfUrl = self.getData(fileName: "myPdfFile")
Try this and let me know if it works for you.
I'm trying to open a .pdf file after download which is downloaded with Alamofire. But I've seen only using a "webview". Thus the application consumes lots of memory and is not viable.
What I want is to open it with the native device application. Any suggestions? Thank you.
Edit: This is my code for download file:
var localPath: NSURL?
Alamofire.download(.GET, url, destination: { (temporaryURL, response) in
let directoryURL = NSFileManager.defaultManager().URLsForDirectory(.DocumentDirectory, inDomains: .UserDomainMask)[0]
let pathComponent = response.suggestedFilename
localPath = directoryURL.URLByAppendingPathComponent(pathComponent!)
return localPath!
})
.response { (request, response, _, error) in
if error != nil
{
// got an error in getting the data, need to handle it
print("Error: \(error!)")
}
//print(response)
print("Download file en:\(localPath!)")
self.view.hideToastActivity()
//self.actioncall()
}
}
I need open file from localpath...
You should use UIDocumentInteractionController. You can read about it on this Apple documentation page.
By doing some Googling you should see even some example implementations. For example here you can see some code about this done by "mattneub".
I let you one more code that you can use:
var documentInteractionController: UIDocumentInteractionController!
#IBAction func openDocument(sender: UIButton) {
let URL: NSURL = NSBundle.mainBundle().URLForResource("yourPDF", withExtension: "pdf")!
if (URL != "") {
// Initialize Document Interaction Controller
self.documentInteractionController = UIDocumentInteractionController(URL: URL)
// Configure Document Interaction Controller
self.documentInteractionController.delegate = self
// Present Open In Menu
self.documentInteractionController.presentOptionsMenuFromRect(sender.frame, inView: self.view, animated: true)
//presentOpenInMenuFromRect
}
}
// UIDocumentInteractionControllerDelegate
func documentInteractionControllerViewControllerForPreview(controller: UIDocumentInteractionController) -> UIViewController {
return self
}
I'm trying to open a .pdf file after download which is downloaded with Alamofire. But I've seen only using a "webview". Thus the application consumes lots of memory and is not viable.
What I want is to open it with the native device application. Any suggestions? Thank you.
Edit: This is my code for download file:
var localPath: NSURL?
Alamofire.download(.GET, url, destination: { (temporaryURL, response) in
let directoryURL = NSFileManager.defaultManager().URLsForDirectory(.DocumentDirectory, inDomains: .UserDomainMask)[0]
let pathComponent = response.suggestedFilename
localPath = directoryURL.URLByAppendingPathComponent(pathComponent!)
return localPath!
})
.response { (request, response, _, error) in
if error != nil
{
// got an error in getting the data, need to handle it
print("Error: \(error!)")
}
//print(response)
print("Download file en:\(localPath!)")
self.view.hideToastActivity()
//self.actioncall()
}
}
I need open file from localpath...
You should use UIDocumentInteractionController. You can read about it on this Apple documentation page.
By doing some Googling you should see even some example implementations. For example here you can see some code about this done by "mattneub".
I let you one more code that you can use:
var documentInteractionController: UIDocumentInteractionController!
#IBAction func openDocument(sender: UIButton) {
let URL: NSURL = NSBundle.mainBundle().URLForResource("yourPDF", withExtension: "pdf")!
if (URL != "") {
// Initialize Document Interaction Controller
self.documentInteractionController = UIDocumentInteractionController(URL: URL)
// Configure Document Interaction Controller
self.documentInteractionController.delegate = self
// Present Open In Menu
self.documentInteractionController.presentOptionsMenuFromRect(sender.frame, inView: self.view, animated: true)
//presentOpenInMenuFromRect
}
}
// UIDocumentInteractionControllerDelegate
func documentInteractionControllerViewControllerForPreview(controller: UIDocumentInteractionController) -> UIViewController {
return self
}