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.
Related
I am very new to learning to code in Swift. I am trying to make an application that keeps a list of people who are coming in. I want it to log the name they input, time of visit, and the nature of their visit. However, I want this to be able to be exported to a program like Numbers or Excel. I have found some info on storing the inputs from the user but those seem to get deleted if the app is closed. I can't seem to find any other info, but perhaps I'm just searching the wrong info. Any help or guidance is appreciated.
Just store your log in a Array and then with this function you safe it to a .csv file.
func saveCSV(_ name : String,_ customUrl : URL) -> Bool {
let fileName = "\(name).csv"
let b = customUrl.appendingPathComponent(fileName)
var csvText = ""
var id = "1"
var name = "test"
csvText = "ID,Name\n"
let newLine = "\(id),\(name))\n"
csvText.append(newLine)
//or create a loop
// Task is my custom Struct
var array : [Task]
for task in customArray {
let newLine = "\(task.ean),\(task.menge),\(task.name)\n"
csvText.append(newLine)
}
do {
try csvText.write(to: b, atomically: true, encoding: String.Encoding.utf8)
return true
} catch {
print("Failed to create file")
print("\(error)")
return false
}
}
func createDic()->URL?{
let documentsPath1 = NSURL(fileURLWithPath: NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true)[0])
// CSV is the folder name
let logsPath = documentsPath1.appendingPathComponent("CSV")
do {
try FileManager.default.createDirectory(atPath: logsPath!.path, withIntermediateDirectories: true, attributes: nil)
return logsPath
} catch let error as NSError {
NSLog("Unable to create directory \(error.debugDescription)")
}
return nil
}
var customUrl = createDic()
and now you can call it :
if saveCSV(userList, customUrl){
print("success")
}
and after this you can do what you want with the .csv file
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
I have an app that stores some information in coredata and reads them.
I'm writing a message extension of this application and I'd like to have this extension reading the same data but I always have empty response.
Here is the code I'm using in the main app:
context = (UIApplication.shared.delegate as! AppDelegate).persistentContainer.viewContext
fetchImages(Date()){ (array, arrayData) in
for image in array{
imagesArray.insert(image, at:0)
}
}
I'm using exactly the same code in the extension but it does not read the data.
What I'm wondering about is that I'm not using the appGroupIdentifier anywhere in the code.
How can I do to achieve that?
Thanks.
Here is the code of fetchImages function:
func fetchImages(_ predicate:Date, completion:(_ array:[Image], _ arrayData:NSArray)->()){
var arrData = [NSManagedObject]()
var existingImages = [Image]()
let request :NSFetchrequest<NSFetchrequestResult> = NSFetchrequest(entityName: "Photo")
do {
let results = try context?.fetch(request)
var myImage = Image()
if ((results?.count) != nil) {
for result in results! {
myImage.imageUrl = (resultat as! NSManagedObject).value(forKey:"url") as! String
myImage.imageFileName = (resultat as! NSManagedObject).value(forKey:"imageFileName") as! String
existingImages.append(myImage)
arrData.append(result as! NSManagedObject)
}
} else{
print ("No photo.")
}
completion(existingImages, arrData as NSArray)
} catch{
print ("Error during CoreData request")
}
}
Turning on app groups is the first step, but now you need to tell Core Data to use the app group.
First you get the location of the shared group container, from FileManager. Use containerURL(forSecurityApplicationGroupIdentifier:) to get a file URL for the directory.
You can use that URL without changes if you want. It's probably a good idea to create a subdirectory in it to hold your Core Data files. If you do that, add a directory name to the URL with the appendingPathComponent() method on URL. Then use FileManager to create the new directory with the createDirectory(at:withIntermediateDirectories:attributes:) method.
Now that you have a shared directory to use, tell NSPersistentContainer to put its files there. You do that by using NSPersistentStoreDescription. The initializer can take a URL that tells it where to store its data.
Your code will be something approximating this:
let directory: URL = // URL for your shared directory as described above
let containerName: String = // Your persistent container name
let persistentContainer = NSPersistentContainer(name: containerName)
let persistentStoreDirectoryUrl = directory.appendingPathComponent(containerName)
guard let _ = try? FileManager.default.createDirectory(at: persistentStoreDirectoryUrl, withIntermediateDirectories: true, attributes: nil) else {
fatalError()
}
let persistentStoreUrl = persistentStoreDirectoryUrl.appendingPathComponent("\(containerName).sqlite")
let persistentStoreDescription = NSPersistentStoreDescription(url: persistentStoreUrl)
persistentContainer.persistentStoreDescriptions = [ persistentStoreDescription ]
persistentContainer.loadPersistentStores {
...
}
I cannot retrieve an object saved as a NSkeyed archive in Swift 3, and am scracthcing my head. The object is successfully saved as a plist, but returned as nil when loading back in.
Here is the code I use:
The class itself to be saved as an object is fairly easy:
import Foundation
class ItemList:NSObject, NSCoding {
var name: String = "" //Name of the Item list
var contents: [Int] = [] //Ints referencing the CoreData PackItems
init (listname:String, ContentItems:[Int]) {
self.name=listname
self.contents=ContentItems
}
//MARK: NSCoding
public convenience required init?(coder aDecoder: NSCoder) {
let thename = aDecoder.decodeObject(forKey: "name") as! String
let thecontents = aDecoder.decodeObject(forKey: "contents") as! [Int]
self.init(listname: thename,ContentItems: thecontents)
}
func encode(with aCoder: NSCoder) {
aCoder.encode(self.name,forKey:"name")
aCoder.encode(self.contents, forKey: "contents")
}
}
The code to load and save the object:
class FileHandler: NSObject {
class func getDocumentsDirectory() -> URL {
let filemgr = FileManager.default
let urls = filemgr.urls(for: .documentDirectory, in: .userDomainMask)
let result:URL = urls.first!
return result
}
///This returns the contents of the handed file inside the Documents directory as the object it was saved as.
class func getFileAsObject(filename:String) -> AnyObject? {
let path = getDocumentsDirectory().appendingPathComponent(filename)
if let result = NSKeyedUnarchiver.unarchiveObject(withFile: path.absoluteString) {
//Success
print("Loaded file '"+filename+"' from storage")
return result as AnyObject?
} else {
print("Error: Couldn't find requested object '"+filename+"' in storage at "+path.absoluteString)
return nil
}
}
///This saves the handed object under the given filename in the App's Documents directory.
class func saveObjectAsFile(filename:String, Object:AnyObject) {
let data = NSKeyedArchiver.archivedData(withRootObject: Object)
let fullPath = getDocumentsDirectory().appendingPathComponent(filename)
do {
try data.write(to: fullPath)
print("Wrote file '"+filename+"' to storage at "+fullPath.absoluteString)
} catch {
print("Error: Couldn't write file '"+filename+"' to storage")
}
}
}
...and finally, this is what I do to call it all up:
let testobject:ItemList = ItemList.init(listname: "testlist", ContentItems: [0,0,1,2])
FileHandler.saveObjectAsFile(filename:"Test.plist",Object:testobject)
let tobi = FileHandler.getFileAsObject(filename:"Test.plist") as! ItemList
Alas, I get this as output:
Wrote file 'Test.plist' to storage at file:///…/data/Containers/Data/Application/6747B038-B0F7-4B77-85A8-9EA02BC574FE/Documents/Test.plist
Error: Couldn't find requested object 'Test.plist' in storage at file:///…/data/Containers/Data/Application/6747B038-B0F7-4B77-85A8-9EA02BC574FE/Documents/Test.plist
Note that this is my own output -- so I do (and have checked) that the file was created correctly. But it just won't load. Can anyone tell me what I am doing wrong?
The problem is with the path you pass to unarchiveObject(withFile:).
Change:
if let result = NSKeyedUnarchiver.unarchiveObject(withFile: path.absoluteString) {
to:
if let result = NSKeyedUnarchiver.unarchiveObject(withFile: path.path) {
On a side note, you should use symmetric APIs for your writing and reading logic. When you write the data you archive a root object to a Data object and then write the Data object to a file. But when reading, you directly unarchive the object tree given a file path.
Either change your writing code to use archiveRootObject(_:toFile:) or change the reading code to load the Data from a file and then unarchive the data. Your current code works (once you fix the path issue) but it's not consistent.
I am trying to download a video from my firebase storage. The way I am doing that is by using the .downloadURLWithCompletion function. When ever the function executes, I receive this error
Error Domain=FIRStorageErrorDomain Code=-13010 "Object videos/video1.m4v
does not exist." UserInfo={object=videos/video1.m4v
, bucket=**********.appspot.com, ResponseBody={
"error": {
"code": 404,
"message": "Not Found"
}
}, data=<7b0a2020 22657272 6f72223a 207b0a20 20202022 636f6465 223a2034 30342c0a 20202020 226d6573 73616765 223a2022 4e6f7420 466f756e 64220a20 207d0a7d>, NSLocalizedDescription=Object videos/video1.m4v
does not exist., ResponseErrorDomain=com.google.HTTPStatus, ResponseErrorCode=404}
I have changed my storage settings on firebase to allow unauthenticated access:
I have also checked to make sure that the storage link is correct:
Here is the code that is accessing the Firebase storage:
import UIKit
import AVKit
import AVFoundation
import FirebaseStorage
class VideoViewController: UIViewController
{
var videoUrl:NSURL!
var storageRef:FIRStorageReference!
override func viewDidLoad()
{
let storage = FIRStorage.storage()
storageRef = storage.referenceForURL("gs://**********.appspot.com")
let videosRef = storageRef.child("videos")
let videoName = NSUserDefaults.standardUserDefaults().objectForKey("videoName") as! String
videosRef.child(videoName).downloadURLWithCompletion { (URL, error) -> Void in
if (error != nil)
{
print(error!)
}
else
{
self.videoUrl = URL
do
{
try self.playVideo()
}
catch
{
print("Error")
}
}
}
super.viewDidLoad()
// Do any additional setup after loading the view.
}
So, I tried using a direct link and it worked!
override func viewDidLoad()
{
let storage = FIRStorage.storage()
storageRef = "gs://*************.appspot.com"
let videosRef = "videos"
let videoName = NSUserDefaults.standardUserDefaults().objectForKey("videoName") as! String
storage.referenceForURL("\(storageRef)/\(videosRef)/\(videoName)").downloadURLWithCompletion { (URL, error) in
if (error != nil)
{
print(error!)
}
else
{
self.videoUrl = URL
do
{
try self.playVideo()
}
catch
{
print("Error")
}
}
}
Of course, using a direct link for something like this isn't exactly the best way to get data. So next I compared the two links generated by printing them out. Here is how I printed the first link:
var videoUrl:NSURL!
var storageRef:FIRStorageReference!
override func viewDidLoad()
{
let storage = FIRStorage.storage()
storageRef = storage.referenceForURL("gs://*********.appspot.com")
let videosRef = storageRef.child("videos")
let videoName = NSUserDefaults.standardUserDefaults().objectForKey("videoName") as! String
print(videosRef.child(videoName))
and it printed
gs://***********.appspot.com/videos/video1.m4v
And the second link:
var videoUrl:NSURL!
var storageRef:String!
override func viewDidLoad()
{
let storage = FIRStorage.storage()
storageRef = "gs://***********.appspot.com"
let videosRef = "videos"
let videoName = NSUserDefaults.standardUserDefaults().objectForKey("videoName") as! String
print("\(storageRef)/\(videosRef)/\(videoName)")
What it printed
gs://***********.appspot.com/videos/video1.m4v
Now, I also tried printing the value of videoName to make sure that it was correct and every time that I printed it out it was video1.m4v
I banked out the link to my firebase storage, but I can assure you that the link is correct all around.
Can someone explain to me why I am getting this error? To me everything looks to be in place.
Thanks!
Try this -- if there is an issue with the underlying representation of a ref this may help:
instead of:
videosRef.child(videoName).downloadURLWithCompletion { (URL, error) -> Void in
do:
storage.referenceForURL(String(videosRef.child(videoName))).downloadURLWithCompletion { (URL, error) -> Void in
that is, does referenceForURL of the stringValue do something different than a direct call. It shouldn't -- if it does, it might have something to do with your videoName. Maybe it ends with a slash? Can you post the value of your videoName?
So, if I understand correctly, you want to download the image without passing the full URL path?
If so, I think downloadURLWithCompletion requires the full URL path.
I can't test this, since I don't have my data set up this way (I just store the full URLs to media files in firebase storage to my firebase database), but try this:
videosRef.child(videoName).dataWithMaxSize(INT64_MAX, completion: { (data, error) in
if let error = error {
print("Error downloading: \(error)")
return
}
cell.imageView?.image = UIImage.init(data: data!)
})
In your firebase storage, you haven't placed your video file inside a folder called videos.
And despite this you try to access to .../videos/filename which doesn't exist. Either try to remove the /videos from: gs://***********.appspot.com /videos /video1.m4v
or
Either create a folder called videos inside your firebase storage and then add the same video inside it with the same name (since you cant drag and drop files into other folders), or remove the:
let videosRef = "videos"
from your path.
Hope it helps.