How can I change the filename in a SwiftUI Document-based app? - swiftui

I've been using the new template for a document-based SwiftUI app. While you get a lot of file-management "for free" in the new template, as it stands in the iOS version users have to back out of the file to the file browser to change the filename. I want to create an opportunity for the user to rename the file while it is open.
Here's a sample project focused on the issue: https://github.com/stevepvc/DocumentRenamer
In the code, I've added to the template code a simple UI with a textfield for the user to enter a new name. When the user hits the "rename" button, the app checks to see if the URL with that name component is available, appending a suffix if necessary to create a target url.
func getTargetURL() -> URL {
let baseURL = self.fileurl.deletingLastPathComponent()
print("filename: \(self.filename)")
print("fileURL: \(self.fileurl)")
print("BaseURL: \(baseURL)")
var target = URL(fileURLWithPath: baseURL.path + "/\(filename).exampletext")
var nameSuffix = 1
while (target as NSURL).checkPromisedItemIsReachableAndReturnError(nil) {
target = URL(fileURLWithPath: baseURL.path + "/\(filename)-\(nameSuffix).sermon")
print("Checking: \(target)")
nameSuffix += 1
}
print("Available Target: \(target)")
return target
}
It then attempts to rename the file, and this is when I am stuck. I have tried several methods, most recently the following:
func changeFilename(){
let target = getTargetURL()
var rv = URLResourceValues()
let newFileName = target.deletingPathExtension().lastPathComponent
rv.name = newFileName
do {
if fileurl.startAccessingSecurityScopedResource(){
try fileurl.setResourceValues(rv)
fileurl.stopAccessingSecurityScopedResource()
}
} catch {
print("Error:\(error)")
}
}
But I keep getting the following error:
Domain=NSCocoaErrorDomain Code=513 "You don’t have permission to save the file “Untitled” in the folder “DocumentRenamer”."
I have also tried this without the startAccessingSecurityScopedResource() check, and alternatively have tried creating a helper class as follows:
class FileMover: NSObject {
func moveFile(originalURL: URL, updatedURL:URL) -> Bool {
let coordinator = NSFileCoordinator(filePresenter: nil)
var writingError: NSError? = nil
var success : Bool = true
print("moving file")
coordinator.coordinate(writingItemAt: originalURL, options: NSFileCoordinator.WritingOptions.forMoving, error: &writingError, byAccessor: { (coordinatedURL) in
do {
try FileManager.default.moveItem(at: coordinatedURL, to: updatedURL)
success = true
print("file moved")
} catch {
print(error)
success = false
}
})
return success
}
}
But using this method locks up the app entirely. It's possible that there is something about iCloud permissions going on there, but I think I've have those set up appropriately.
It appears to work fine in the simulator, but not when run on a device.
What is the correct method for renaming a file in the app's container?

Related

Swiftui FileManager/URLSession not writing to documentDirectory when running as background task

Hope you're doing well!
I've built an app that generates a view from a .csv file that I have hosted on my website. I've previously managed to get everything working as expected where I called the csv from the website and wrote the contents directly to a variable and then processed it from there. Obviously this wasn't good practice as the app started mis-behaving when the internet couldn't be accessed (despite writing in connectivity checks).
I've now built out the app to call the URL, save the csv with Filemanager, then when the app refreshes, it will use FileManager.default.replaceItemAt to replace the previous version if there is internet connectivity, if not the app builds from the previously stored .csv
This all works fine when the app is running, however I'm running into issues with the background processing task. It seems the app doesn't have permissions to write with FileManager when it is executed from the background task. Is there an additional step I'm missing when using this in background tasks? I've attempted to use FileManager.default.removeItem followed by FileManager.default.copyItem instead of replaceItemAt but it doesn't seem to make a difference as expected.
UPDATE 22/06 - Still scouring the internet for similar issues or examples I think I might be going down the wrong rabbit hole here. This could be issues with the way the new background task has been configured for retrieving data from my website, although the background tasks worked fine before there seems to be a bit more legwork needed for this method to work as a background task.
func handleAppRefresh(task: BGProcessingTask) {
//Schedules another refresh
scheduleAppRefresh()
DispatchQueue.global(qos: .background).async {
pullData()
print("BG Background Task fired")
}
pullData() will call loadCSV() and then do some data processing. At the moment I'm just using a print straight after loadCSV() is called to validate if the downloads etc are successful.
// Function to pass the string above into variables set in the csvevent struct
func loadCSV(from csvName: String) -> [CSVEvent] {
var csvToStruct = [CSVEvent]()
// Creates destination filepath & filename
let documentsUrl:URL = (FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first as URL?)!
let destinationFileUrl = documentsUrl.appendingPathComponent("testcsv.csv")
//Create URL to the source file to be downloaded
let fileURL = URL(string: "https://example.com/testcsv.csv")!
let sessionConfig = URLSessionConfiguration.default
let session = URLSession(configuration: sessionConfig)
let request = URLRequest(url:fileURL)
let task = session.downloadTask(with: request) { (tempLocalUrl, response, error) in
if let tempLocalUrl = tempLocalUrl, error == nil {
if let statusCode = (response as? HTTPURLResponse)?.statusCode {
print("File downloaded Successfully. Response: \(statusCode)")
}
do {
let _ = try FileManager.default.replaceItemAt(destinationFileUrl, withItemAt: tempLocalUrl)
} catch (let writeError) {
print("Error creating a file \(destinationFileUrl) : \(writeError)")
}
} else {
print("Error" )
}
}
task.resume()
let data = readCSV(inputFile: "testcsv.csv")
var rows = data.components(separatedBy: "\n")
rows.removeFirst()
// Iterates through each row and sets values
for row in rows {
let csvColumns = row.components(separatedBy: ",")
let csveventStruct = CSVEvent.init(raw: csvColumns)
csvToStruct.append(csveventStruct)
}
print("LoadCSV has run and created testcsv.csv")
return csvToStruct
}
Any help or pointers to why these files aren't being updated in background tasks but are working fine in app would be massively appreciated!
Thanks in advance.
EDIT: adding new BGProcessingTask
func handleAppRefresh(task: BGProcessingTask) {
//Schedules another refresh
print("BG Background Task fired")
scheduleAppRefresh()
Task.detached {
do {
let events = try await loadCSV(from: "Eventtest").filter { !dateInPast(value: $0.date) }
print(events)
pullData(events: events)
} catch {
print(error)
}
}
}
The problem is not the background task per se, the problem is the asynchronous behavior of downloadTask. readCSV is executed before the data is downloaded.
In Swift 5.5 and later async/await provides asynchronous behavior but the code can be written continuously.
func loadCSV(from csvName: String) async throws -> [CSVEvent] {
var csvToStruct = [CSVEvent]()
// Creates destination filepath & filename
let documentsUrl:URL = (FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first as URL?)!
let destinationFileUrl = documentsUrl.appendingPathComponent("testcsv.csv")
//Create URL to the source file to be downloaded
let fileURL = URL(string: "https://example.com/testcsv.csv")!
let sessionConfig = URLSessionConfiguration.default
let session = URLSession(configuration: sessionConfig)
let request = URLRequest(url:fileURL)
let (url, response) = try await session.download(for: request)
if let statusCode = (response as? HTTPURLResponse)?.statusCode {
print("File downloaded Successfully. Response: \(statusCode)")
}
let _ = try FileManager.default.replaceItemAt(destinationFileUrl, withItemAt: url)
let data = readCSV(inputFile: "testcsv.csv")
var rows = data.components(separatedBy: "\n")
rows.removeFirst()
// Iterates through each row and sets values
for row in rows {
let csvColumns = row.components(separatedBy: ",")
let csveventStruct = CSVEvent.init(raw: csvColumns)
csvToStruct.append(csveventStruct)
}
print("LoadCSV has run and created testcsv.csv")
return csvToStruct
}
To call the function you have to wrap it in a detached Task which replaces the GCD queue
Task.detached {
do {
let events = try await loadCSV(csvName: "Foo")
print("BG Background Task fired")
} catch {
print(error)
}
}

How to pass foldername to save file into the folder?

I was doing download system with the code from https://stackoverflow.com/a/32322851/7789222. It was a great and complete code but I can find a way to pass foldername from view controller to download file to specific folder. Can anyone help me with it please. I am using swift 3 xcode 8.
If I hard code the custom directory in func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL) , every file will be downloaded to same folder. I want to pass the foldername from view controller so I can download files to different folder. I cant hardcode because I retrieve file name and foldername from server
The destination URL in the example is given by
let destinationURL = try manager.url(
for: .documentDirectory,
in: .userDomainMask,
appropriateFor: nil,
create: false
).appendingPathComponent(url.lastPathComponent)
(Line 17)
You can just pass a destination folder URL to the initializer of DownloadOperation which replaces the destination URL in the example:
let destinationURL = yourDestinationFolder.appendingPathComponent(url.lastPathComponent)
Your modified DownloadOperation would look something like this:
class DownloadOperation : AsynchronousOperation {
var task: URLSessionTask!
let destinationFolder: URL
init(session: URLSession, url: URL, destinationFolder: URL) {
super.init()
self.destinationFolder = destinationFolder
task = session.downloadTask(with: url) { temporaryURL, response, error in
defer { self.completeOperation() }
guard error == nil && temporaryURL != nil else {
print("\(error)")
return
}
do {
let manager = FileManager.default
let destinationURL = destinationFolder.appendingPathComponent(url.lastPathComponent)
_ = try? manager.removeItem(at: destinationURL) // remove the old one, if any
try manager.moveItem(at: temporaryURL!, to: destinationURL) // move new one there
} catch let moveError {
print("\(moveError)")
}
}
}
...
}
The code for adding operations is then
queue.addOperation(DownloadOperation(session: session, url: url, destinationFolder: destinationFolder))
If you want to use the DownloadManager:
class DownloadManager {
#discardableResult
func addDownload(_ url: URL, to destinationFolder: URL) -> DownloadOperation {
let operation = DownloadOperation(session: session, url: url, destinationFolder: destinationFolder)
operations[operation.task.taskIdentifier] = operation
queue.addOperation(operation)
return operation
}
...
}
The extension:
extension DownloadOperation: URLSessionDownloadDelegate {
func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL) {
do {
let manager = FileManager.default
let destinationURL = destinationFolder.appendingPathComponent(downloadTask.originalRequest!.url!.lastPathComponent)
if manager.fileExists(atPath: destinationURL.path) {
try manager.removeItem(at: destinationURL)
}
try manager.moveItem(at: location, to: destinationURL)
} catch {
print("\(error)")
}
}
...
}
Then you can add downloads with
downloadManager.addDownload(url, to: destinationFolder)

UIWebView: ics and vcard-Links not handled

I do have a UIWebView included where a public URL is loaded; unfortunately, vcard and ical-Links are not handled, i.e. nothing happens when I click on them.
I tried to set all data detectors, no luck unfortunately.
In the Xcode-log, I get this here when clicking on such a link:
2017-07-14 13:43:00.982413+0200 xxx[2208:967973] WF: _userSettingsForUser mobile: {
filterBlacklist = (
);
filterWhitelist = (
);
restrictWeb = 1;
useContentFilter = 0;
useContentFilterOverrides = 0;
whitelistEnabled = 0;
}
In Safari, the same stuff works as expected.
If I use UIApplication.shared.openURL(icsOrVcardUrl) Safari gets opened and from there everything works as expected again, but I don't want the user to leave the app...
EDIT
This doesn't work either:
func webView(_ webView: UIWebView, shouldStartLoadWith request: URLRequest, navigationType: UIWebViewNavigationType) -> Bool {
if let url = request.url {
if url.absoluteString.contains("=vcard&") || url.absoluteString.contains("/ical/") {
let sessionConfig = URLSessionConfiguration.default
let session = URLSession(configuration: sessionConfig)
let request = URLRequest(url:url)
let task = session.downloadTask(with: request) { (tempLocalUrl, response, error) in
if let tempLocalUrl = tempLocalUrl, error == nil {
DispatchQueue.main.async {
self.documentController.url = tempLocalUrl
self.documentController.presentPreview(animated: true)
}
}
}
task.resume()
return false
}
}
return true
}
Use a UIDocumentInteractionController to preview without leaving your app.
I tested it quickly with an .ics file and it works fine.
Implement the UIDocumentInteractionControllerDelegate protocol
extension MainViewController: UIDocumentInteractionControllerDelegate {
func documentInteractionControllerViewControllerForPreview(_ controller: UIDocumentInteractionController) -> UIViewController {
return self;
}
}
Create an instance of the interaction controller:
let documentController = UIDocumentInteractionController()
Intercept the clicks in your UIWebView in shouldStartLoadWithRequest, return false for links you want to handle with the in-app preview and true for all the rest. And finally:
func previewDocument(_ url: URL) {
documentController.url = url
documentController.presentPreview(animated: true)
}
Here it is in the simulator
EDIT:
In response to the comment to this answer:
The reason it doesn't work for you is because the UIDocumentInteractionController depends on the file extension. The extension of the temp file is .tmp
Renaming the file after the download solves the problem. Quick and dirty example:
let task = session.downloadTask(with: url!) { (tempLocalUrl, response, error) in
if let tempLocalUrl = tempLocalUrl, error == nil {
do {
let filemgr = FileManager.default
let newUrl = tempLocalUrl.appendingPathExtension("ics")
try filemgr.moveItem(at: tempLocalUrl, to: newUrl)
DispatchQueue.main.async {
self.documentController.url = newUrl
self.documentController.presentPreview(animated: true)
}
} catch let error {
print("Error!!!: \(error.localizedDescription)")
}
}
}
task.resume()
In this case it is advisable to clean after yourself, because the file won't be deleted after the task completes although the OS will delete it eventually, when space is needed. If you often access the same urls, Library/Caches/ may be a better place for this files, just come up with good naming schema, and check if the file doesn't exist already.

How to Use Core Data Models with In-Memory Store Type Using Prepopulated SQLite File?

I am following an outdated tutorial from the Ray Wenderlich team that walks through the repopulation of a Core Data-backed application by using a Command Line Tool application.
I have successfully prepopulated the intended entities, verified by performing an NSFetchRequest.
Now, I want to use the same prepopulated data in my unit tests to verify that my interactions with CoreData are happening correctly. I tried setting up my mocked CoreDataStack subclass to use an in-memory store, but when I attempt to verify that I have the prepopulated data for use in my unit tests, I am getting a count of 0.
The class responsible for interacting with CoreData in my application's target, named CoreDataStack, follows:
/// The object that is responsible for managing interactions with Core Data.
internal class CoreDataStack {
// MARK: - Properties
/// The name of the `NSManagedObjectModel` object used for storing information with Core Data.
private let modelName: String
/// The `NSManagedObjectContext` object that is associated with the main queue.
internal lazy var mainContext: NSManagedObjectContext = {
return self.storeContainer.viewContext
}()
/// The `NSPersistentContainer` object that encapsulates the application's Core Data stack.
internal lazy var storeContainer: NSPersistentContainer = {
let container = NSPersistentContainer(name: self.modelName)
let directory = NSPersistentContainer.defaultDirectoryURL()
let storeURL = directory.appendingPathComponent("\(self.modelName).sqlite")
if !FileManager.default.fileExists(atPath: (storeURL.path)) {
guard let populatedURL = Bundle.main.url(forResource: self.modelName, withExtension: "sqlite") else {
fatalError("Invalid populated .sqlite file URL")
}
do {
try FileManager.default.copyItem(at: populatedURL, to: storeURL)
} catch {
fatalError("Error: \(error)")
}
}
let description = NSPersistentStoreDescription()
description.url = storeURL
container.persistentStoreDescriptions = [description]
container.loadPersistentStores(completionHandler: { (storeDescription, error) in
if let error = error as NSError? {
fatalError("Error: \(error)")
}
})
return container
}()
// MARK: - Initialization
/// Returns an instance of `CoreDataStack`.
/// - parameter modelName: The name of the `NSManagedObjectModel` object used for storing information with Core Data.
internal init(modelName: String) {
self.modelName = modelName
}
/// Attempts to save items to Core Data by committing changes to `NSManagedObject`s in a `NSManagedObjectContext`.
/// - parameter context: The `NSManagedObjectContext` of which changes should be committed.
internal func saveContext(_ context: NSManagedObjectContext) {
context.perform {
do {
try context.save()
} catch let error as NSError {
fatalError("Unresolved error \(error), \(error.userInfo)")
}
}
}
}
The subclass of CoreDataStack, MockCoreDataStack, used for testing follows:
internal class MockCoreDataStack: CoreDataStack {
// MARK: - Initialization
convenience init() {
self.init(modelName: "Currency")
}
override init(modelName: String) {
super.init(modelName: modelName)
let container = NSPersistentContainer(name: modelName)
let directory = NSPersistentContainer.defaultDirectoryURL()
let storeURL = directory.appendingPathComponent("\(modelName).sqlite")
if !FileManager.default.fileExists(atPath: (storeURL.path)) {
guard let populatedURL = Bundle(for: type(of: self)).url(forResource: modelName, withExtension: "sqlite") else {
fatalError("Invalid populated .sqlite file URL")
}
do {
try FileManager.default.copyItem(at: populatedURL, to: storeURL)
} catch {
fatalError("Error: \(error)")
}
}
let description = NSPersistentStoreDescription()
description.url = storeURL
description.type = NSInMemoryStoreType
container.persistentStoreDescriptions = [description]
container.loadPersistentStores { (storeDescription, error) in
if let error = error as NSError? {
fatalError("Unresolved error \(error), \(error.userInfo)")
}
}
self.storeContainer = container
}
}
The resulting count of my fetch request is 0 in my unit tests target. I expect to return a count consisting of the number of prepopulated objects, just as I get when I return the count in my application's target.
What am I doing incorrectly that is causing me to not return the expected result?
The in memory store doesn't use the store URL. It just creates an empty store in memory.
As an alternative to the in memory store, you could possibly create a parent NSManagedObjectContext between the persistent store and the contexts you actually use. (I'm not sure how that would play with NSPersistentContainer, however.)
Then you can just rollback() the parent context when you want to reset back to your initial state.

FileManager replaceItemAt() results in EXC_BAD_ACCESS

I've written an application that downloads images from a website.
If this image already exists on the device I'm trying to replace it.
func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL) {
let userId = Int.init(downloadTask.taskDescription!)! // task description is definetly set in downloadImage() and is an Int
guard let target = imageFolder?.appendingPathComponent("\(userId).jpg") else {
delegate?.imageDownloadFailed(forUser: userId, error: "Could not create target URL.")
return
}
do {
if fileManager.fileExists(atPath: target.path) {
_ = try fileManager.replaceItemAt(target, withItemAt: location)
} else {
try fileManager.moveItem(at: location, to: target)
}
delegate?.savedImage(forUser: userId, at: target)
} catch let error {
delegate?.imageDownloadFailed(forUser: userId, error: error.localizedDescription)
}
}
The problem occurs in the if-statement:
_ = try fileManager.replaceItemAt(target, withItemAt: location)
I always got EXC_BAD_ACCESS and I can't find the error.
fileManager, target, and location are non-nil.
I've already tried to dispatch the code synchronous to the main thread, but the error still persists.
Any advices?
Edit:
Since I'm not the only one who got this error I decided to create a bug report at Apple.
The report is available at Open Radar; click
I've also uploaded a playground file at pastebin.com which demonstrates the error and provides a quick solution similar to the one of naudec.
Had the same issue. Ended up writing my own version:
let fileManager = FileManager.default
func copyItem(at srcURL: URL, to dstURL: URL) {
do {
try fileManager.copyItem(at: srcURL, to: dstURL)
} catch let error as NSError {
if error.code == NSFileWriteFileExistsError {
print("File exists. Trying to replace")
replaceItem(at: dstURL, with: srcURL)
}
}
}
func replaceItem(at dstURL: URL, with srcURL: URL) {
do {
try fileManager.removeItem(at: dstURL)
copyItem(at: srcURL, to: dstURL)
} catch let error as NSError {
print(error.localizedDescription)
}
}
I call copyItem first.
The class holding this method does not exist any more at the time your download finishes and did release your filemanager. Create the FileManager within your completion closure:
...
let localFilemanager = FileManager.default
do {
...