I'm trying to read files from an SD card. The following code works great when my iOS app is run in Xcode's Simulator, but fails to read the directory when i build to the iPad. A similar question was asked 10 years ago and it appears no solution existed back then.
func getFilesfromUSB() {
let filePath = URL(fileURLWithPath: "/Volumes/" + sdCardName + "/FILES")
var fileURLs: [URL] = []
DispatchQueue.global(qos: .userInitiated ).async {
do {
fileURLs = try self.fm.contentsOfDirectory(at: filePath, includingPropertiesForKeys: nil)
print("getting files via USB")
print(fileURLs)
} catch {
print("failed to read directory – bad permissions, perhaps?")
}
DispatchQueue.main.async {
self.parseFiles(from: fileURLs)
}
}
}
How would I go about reading (and writing) to an SD card from iOS 14+?
Related
I'm trying to build out a simple way for my users to export their data outside of the app.. nothing that needs to be imported back in, just some way for them to back up the data for reference purposes. I have a Core Data Entity Project and the users are able to individually share a project in order to save the project data and images using the standard iOS Share Sheet. Works great.
However I'd like there to be a solution to export everything at once, not just individual projects one at a time.
I have part of it working, where I can export the data from Core Data (that isn't an image) into a CSV for users to reference. However I'm stuck on finding the best way to get all the Images exported in a similar singular button. Allowing the user to pick a location where a Folder would be created containing the images would be ideal.
Here's my code for the CSV export which works great:
func exportCSV() {
let fileName = "Metadata_Export_\(Date()).csv"
let path = NSURL(fileURLWithPath: NSTemporaryDirectory()).appendingPathComponent(fileName)
var csvText = "Name,Date,Project_Description\n"
for project in projects {
csvText += "\(project.person?.name ?? "-"),\(project.date ?? Date()),\(project.bodyText ?? "-"),\n"
}
do {
try csvText.write(to: path!, atomically: true, encoding: String.Encoding.utf8)
} catch {
print("Failed to create file")
print("\(error)")
}
print(path ?? "not found")
var filesToShare = [Any]()
filesToShare.append(path!)
let av = UIActivityViewController(activityItems: filesToShare, applicationActivities: nil)
UIApplication.shared.windows.first?.rootViewController?.present(av, animated: true, completion: nil)
isShareSheetShowing.toggle()
}
Now I just need to get the Images exported out. Images are saved in Core Data as Binary objects, and will be written to File if they're larger than 128kb (and therefore written to blob in CD if less than 128kb).
The images are stored in CD as Optionals, project.image1, project.image2, project.image3, and project.image4
I've looked at examples using fileManager and other solutions, but I'm not sure on the correct approach to pursue since many of those are actually alternatives to saving images in Core Data - not necessarily configuring user interaction for picking where to export images.
Can the above exportCSV function be adapted to a similar result for the project's images? My app supports iOS 14 and later, if that makes a difference. Thanks for any suggestions/direction!
=== UPDATE ===
I've discovered fileExporter() which seems like a promising solution. I've been able to implement a simple POC of this method by exporting an Image I have stored in my Assets folder. Has anyone used this method to achieve exporting all images out of Core Data?
I can add the modifier to my view:
.fileExporter(isPresented: $exportFile, documents: [
ImageDocument(image: UIImage(named: "testimage"))
],
contentType: .png, onCompletion: { (result) in
if case .success = result {
print("Success")
} else {
print("Failure")
}
})
}
Using an ImageDocument Struct as follows:
struct ImageDocument: FileDocument {
static var readableContentTypes: [UTType] { [.jpeg, .png, .tiff] }
var image: UIImage
init(image: UIImage?) {
self.image = image ?? UIImage()
}
init(configuration: ReadConfiguration) throws {
guard let data = configuration.file.regularFileContents,
let image = UIImage(data: data)
else {
throw CocoaError(.fileReadCorruptFile)
}
self.image = image
}
func fileWrapper(configuration: WriteConfiguration) throws -> FileWrapper {
// You can replace tiff representation with what you want to export
return FileWrapper(regularFileWithContents: image.jpegData(compressionQuality: 1)!)
}
}
So how can I have it include an Array of all images?
I'm making Audio Player.
Importing file from iCloud Drive using .fileImporter.
I get file URL that looks like this: file:///private/var/mobile/Library/Mobile%20Documents/com~apple~CloudDocs/_Storage/Audio-books/%D0%91%D1%80%D0%B5%D0%BD%D0%B4%D1%8F%D1%82%D0%B8%D0%BD%D0%B0/Audiobook.mp3"
Then I pass it to audio player (tried AVPlayer and AVAudioPlayer). Both works on iOS simulator.
On the device after import I get error: The operation couldn’t be completed. (OSStatus error -54.)
I know it's possible, app called Evermusic does quite the same with on device files.
Is there permissions I need to be granted to play audio that stored on device?
How can I access Container for com~apple~CloudDocs?
Thank you very much for help, any suggestions greatly appreciated, I'm seriously stuck.
For future references repo of the project: https://github.com/yaosamo/AudioPlayer
You need to be using startAccessingSecurityScopedResource in order to get read access to those files. See documentation:
https://developer.apple.com/documentation/foundation/nsurl/1417051-startaccessingsecurityscopedreso
https://developer.apple.com/documentation/corefoundation/1543318-cfurlstartaccessingsecurityscope
In addition to #jnpdx answer want to add some details, and my solution example.
Few core things
✅ for my app if you need to access secured audio you need to use startAccessingSecurityScopedResource()
❌ you can't simple store URL and use it later, in fact you don't store fileURL at all. You need to use what's called bookmarkData() on your URL and store it. So later you can restore URL
✅ Watch Apple pres here
Here's how I import file:
.fileImporter(isPresented: $presentImporter, allowedContentTypes: [.mp3]) { result in
switch result {
case .success(let url):
// Start accessing secured url
let StartAccess = url.startAccessingSecurityScopedResource()
defer {
// Must stop accessing once stop using
if StartAccess {
url.stopAccessingSecurityScopedResource()
}
}
// Creating new book
let newBook = Book(context: viewContext)
let _ = print("---- Access Granted?", url.startAccessingSecurityScopedResource())
// Getting bookmarkData of the URL
let bookmarkData = try? url.bookmarkData()
newBook.name = "\(url.lastPathComponent)"
// Save bookmarkURL into CoreData
newBook.urldata = bookmarkData
// Specifiying parent item in CoreData
newBook.origin = playlist.self
try? viewContext.save()
case .failure(let error):
print(error)
}
}
Player retrieving URL:
func Audioplayer(bookmarkData: Data) {
// Restore security scoped bookmark
var bookmarkDataIsStale = false
let playNow = try? URL(resolvingBookmarkData: bookmarkData, bookmarkDataIsStale: &bookmarkDataIsStale)
do {
player = try AVAudioPlayer(contentsOf: playNow!)
// Delegate listen when audio is finished
player?.delegate = del
NotificationCenter.default.addObserver(forName: NSNotification.Name("ended"), object: nil, queue: .main) { (_) in
player?.stop()
ended = true
let _ = print("---- Book has ended ----")
}
} catch let error {
print("Player Error", error.localizedDescription)
}
player?.prepareToPlay()
player?.play()
}
Thank you and again here's repo on git.
Before I updated to iOS 14 on my iPhone, this code was working perfectly. After, iOS 14 this is weirdly not running... it is very odd and I have not seen any solution online, additionally from my investigation, I have not been able to see any change.
This code is used in order to retrieve a videoURL for this video from the imported Camera Roll (I use import Photos...).
phResourceManager.writeData(for: resource.last!, toFile: newURL!, options: resourceRequestOptions) { (error) in
if error != nil {
print(error, "not c67omplted error?")
} else {
print("woah completedd 345?")
newUserTakenVideo.videoURL = newURL
print(newUserTakenVideo.videoURL, "<--?")
}
}
EDIT:
To be clear, it "does not run" means the compleition block never runs... as in it never even runs and gives an error, the compleition block simply never is called (nothing prints at least..)
And here is a print statement printing out all the values I pass in to the parameters:
phResourceManager:
<PHAssetResourceManager: 0x282d352c0>
resource.last:
Optional(<PHAssetResource: 0x28128bc00> {
type: video
uti: public.mpeg-4
filename: v07044090000bu6n1nhlp4leque7r720.mp4
asset: C97B45D3-7039-4626-BA3E-BCA67912A2A9/L0/001
locallyAvailable: YES
fileURL: file:///var/mobile/Media/DCIM/113APPLE/IMG_3404.MP4
width: 576
height: 1024
fileSize: 4664955
analysisType: unavailable
cplResourceType: Original
isCurrent: YES
})
newURL:
Optional(file:///var/mobile/Containers/Data/Application/E2792F47-142E-4601-8D5B-F549D03C9AFE/Documents/Untitled%2027228354.MP4)
resourceRequestOptions:
<PHAssetResourceRequestOptions: 0x28230d480>
Note: this is the decleration for the resource variable:
let resource = PHAssetResource.assetResources(for: (cell?.assetPH)!)
I have a solution to this! Swift 4+, tested on iOS 14!
I looked through using a PHAssetResourceRequest, but the file names were messed with in the process, and it generally didn't work with my sandbox. Then I also tried requesting a AVPlayerItem from the PHAsset but this too, did not work with sandboxing...
But then, I tried simply using PHAssetResourceManager.default().writeData(... and seemingly started working!
I tested a bit more and seemed to work, here is the full code:
let resource = PHAssetResource.assetResources(for: (cell?.assetPH)!)
let resourceRequestOptions = PHAssetResourceRequestOptions()
let newURL = ExistingMediaVC.newFileUrl
PHAssetResourceManager.default().writeData(for: resource.last!, toFile: newURL!, options: resourceRequestOptions) { (error) in
if error != nil {
print(error, "error")
} else {
print("good")
newUserTakenVideo.videoURL = newURL
}
}
It is quite simple!! Tell me if anything is not working, and note I still use the ExisitingMedia.fileURL variable you used in your original code as well :)
I am currently working on a code that downloads .m4a audio format files from Firebase and allows the user to hear the music play within the same View Controller. Through lots of research on SO and other websites, I managed to get the audio downloaded, but when the audio is supposed to be being played, I only hear static. In fact, it actually plays the entire duration of the audio clip (e.g. the audio is 6s long, so a user would hear static for exactly 6s), so I know for a fact it is actually downloading something, just nothing is playing.
Below is my source code for the download function and play music function. I tried searching this topic on SO, but there are very few articles pertaining to audio download to iOS from Firebase (mostly it seems to be "how to download images", etc.)
Thank you very much in advance!
Sam
//Function to start playing music
func playMusic(){
do {
self.audioPlay = try AVAudioPlayer(contentsOf: self.localSongURL)
self.audioPlay.delegate = self
self.audioPlay.prepareToPlay()
self.audioPlay.play()
self.audioPlay.volume = 1.0 //I tried adjusting volumes to see if it would make a difference
} catch {
createAlert(title: "File Not Found", message: "Audio downloaded cannot be interpereted.")
}
}
//Function to download music
//In the Firebase storage, songs are listed underneath an ID, so the "tillAt"s help to parse through
//the appropriate strings to get the right path for collection
func downloadMusic(){
let tillAt1 = self.songPath.components(separatedBy: ID + "/")
let tillAt2 = tillAt1[1].components(separatedBy: ".")
store = Storage.storage().reference().child(patientID).child(tillAt1[1])
//From here is where I think the issue is within
self.localSongURL = try! FileManager.default.url(for: .documentDirectory, in: .userDomainMask, appropriateFor: nil, create: false).appendingPathComponent(tillAt2[0] + ".m4a")
store.getData(maxSize: 128 * 1024 * 1024, completion: {(data, error) in
if let error = error {
print("Error Here _______________ Level 1")
print(error)
} else {
if let d = data {
do {
try d.write(to: self.localSongURL)
} catch {
print("Error Here _______________ Level 2")
print(error)
}
}
}
})
}
I'm storing files in iCloud as a means of backup. Uploading and downloading files seems to work fine, but if I delete the app on a device and re-install the app on that same device, I no longer see the files that were uploaded to iCloud (even from that device just before deleting the app). The ubiquityIdentityToken is the same from all installs. I'm not trying to sync among devices, just store and retrieve. I can see the files in \settings\icloud\manage storage\ but not by running this code:
func createListOfSQLBaseFilesIniCloudDocumentsDirectory() -> [String]{
let iCloudDocumentsURL = FileManager.default.url(forUbiquityContainerIdentifier: nil)?.appendingPathComponent("Documents")
var iCloudBackupSQLFiles = [String]()
do{
let directoryContents = try FileManager.default.contentsOfDirectory(at: iCloudDocumentsURL!, includingPropertiesForKeys: nil, options: [])
for myFile in directoryContents {
if myFile.pathExtension == "sqlite" {
let fileWithExtension = myFile.lastPathComponent
//backupSQLFiles is an array of full file names - including the extension
iCloudBackupSQLFiles.append(fileWithExtension)
}//if myFile
}//for in
} catch let error as NSError {
print("Unresolved error \(error), \(error.userInfo)")
}//do catch
return iCloudBackupSQLFiles
}//createListOfSQLBaseFilesIniCloudDocumentsDirectory
Any guidance would be appreciated. Swift 3, iOS 10, Xcode 8
Hard to believe no one else has had this issue. Again, this is for simple file storage and retrieval, not syncing dynamically among devices.
The gist of the issue is that iCloud does not automatically sync cloud files down to a new device. Your code must do that. So if you remove an app from a device (but not from iCloud) and reinstall that same app, the app will not see prior iCloud files. You can add new ones and see them with the code above, but you are really just seeing the local ubiquitous container copy. To see previous items you need to perform a metadataQuery on iCloud, parse the filenames of the files of interest from the metadataQuery results and then run
startDownloadingUbiquitousItem(at:) on each file. For example, make an array of files in iCloud from the metadataQuery results and put this do-catch in a for-in loop.
let fileManager = FileManager.default
let iCloudDocumentsURL = FileManager.default.url(forUbiquityContainerIdentifier: nil)?.appendingPathComponent("Documents", isDirectory: true)
let iCloudDocumentToCheckURL = iCloudDocumentsURL?.appendingPathComponent(whateverFileName, isDirectory: false)
do {
try fileManager.startDownloadingUbiquitousItem(at: iCloudDocumentToCheckURL!)
print("tested file: \(whateverFileName)")
} catch let error as NSError {
print("Unresolved error \(error), \(error.userInfo)")
}//do catch