iOS will purge assets after being downloaded as soon as it needs to free up some space.
Changing the preservation priorities of the assets will not prevent the system from purging them as stated in the "Setting Preservation Priority" section here.
My relevant code to download the On-demand resources is the following:
func requestResourceWith(tag: [String],
onSuccess: #escaping () -> Void,
onFailure: #escaping (NSError) -> Void) {
currentRequest = NSBundleResourceRequest(tags: Set(tag))
guard let request = currentRequest else { return }
request.endAccessingResources()
request.loadingPriority =
NSBundleResourceRequestLoadingPriorityUrgent
request.beginAccessingResources { (error: Error?) in
if let error = error {
onFailure(error as NSError)
return
}
onSuccess()
}
}
After downloading the On-Demand resources, they can be accessed from the main bundle.
Is there anyway to make audios persist, and hence prevent the system from purging them?
In response to #RJB comment above, I will answer my question :)
As soon as the On-demand resources are downloaded, you need to save them in hard disk (The documents directory for example) in order to persist them. Otherwise, iOS will retain the right to purge them as soon as it needs more free space.
Something like the following:
request.beginAccessingResources { (error: Error?) in
if let error = error {
DispatchQueue.main.async {
onFailure(error as NSError)
}
return
}
// Move ODR downloaded assets to Documents folder for persistence
DispatchQueue.main.async {
let path: String! = Bundle.main.path(forResource: "filename", ofType: "mp3")
let sourceURL = URL(fileURLWithPath: path)
let destinationURL = // Build a destination url in the Documents directory or any other persistent Directory of your choice
do {
try FileManager.default.copyItem(at: sourceURL, to: destinationURL)
}catch {
// Handle error accordingly
}
onSuccess()
}
}
Related
I have an extension on the FileManager that uses the new concurrency:
try await Task.detached(priority: .background) closure and then uses self to create a directory enumerator with FileManager.enumerator(at:includingPropertiesForKeys:options:errorHandler:).
On Xcode 14 I get the warning:
I am trying to figure out if it's safe to mark FileManager as Sendable:
extension FileManager: #unchecked Sendable {}.
It seems that FileManager is thread safe with some caveats: The methods of the shared FileManager object can be called from multiple threads safely.
Any reason why Apple did not mark the FileManager as Sendable? I would think they have a reason not to do that in which case I should definitely avoid marking it sendable, or maybe they just did not get to it?
Full code (adapted from src):
func allocatedSizeOfDirectory(at directoryURL: URL) async throws -> UInt64 {
try await Task.detached(priority: .background) {
// The error handler simply stores the error and stops traversal
var enumeratorError: Error?
func errorHandler(_: URL, error: Error) -> Bool {
guard (error as NSError).code != NSFileReadNoPermissionError else { return true }
enumeratorError = error
return false
}
// We have to enumerate all directory contents, including subdirectories.
let enumerator = self.enumerator(at: directoryURL,
includingPropertiesForKeys: Array(allocatedSizeResourceKeys),
options: [],
errorHandler: errorHandler)!
// We'll sum up content size here:
var accumulatedSize: UInt64 = 0
// Perform the traversal.
for item in enumerator {
// Bail out on errors from the errorHandler.
if enumeratorError != nil { break }
// Add up individual file sizes.
let contentItemURL = item as! URL
accumulatedSize += try contentItemURL.regularFileAllocatedSize()
}
// Rethrow errors from errorHandler.
if let error = enumeratorError { throw error }
return accumulatedSize
}.value
}
Im using this below method to download and save my video to gallery, with .mp4 it's work normally, but when change to .m3u8 it's always fail.
func downloadVideoLinkAndCreateAsset(_ videoLink: String,_ fileName : String) {
// use guard to make sure you have a valid url
guard let videoURL = URL(string: videoLink) else { return }
guard let documentsDirectoryURL = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first else { return }
let fileNameToSave = "CiviX_HistoryVideo_\(fileName)"
// check if the file already exist at the destination folder if you don't want to download it twice
if !FileManager.default.fileExists(atPath: documentsDirectoryURL.appendingPathComponent(fileNameToSave).path) {
// set up your download task
URLSession.shared.downloadTask(with: videoURL) { (location, response, error) -> Void in
// use guard to unwrap your optional url
guard let location = location else { return }
// create a deatination url with the server response suggested file name
let destinationURL = documentsDirectoryURL.appendingPathComponent(fileNameToSave)
print("destinationURL: \(destinationURL)")
do {
try FileManager.default.moveItem(at: location, to: destinationURL)
PHPhotoLibrary.requestAuthorization({ (authorizationStatus: PHAuthorizationStatus) -> Void in
// check if user authorized access photos for your app
if authorizationStatus == .authorized {
PHPhotoLibrary.shared().performChanges({
PHAssetChangeRequest.creationRequestForAssetFromVideo(atFileURL: destinationURL)}) { completed, error in
if completed {
print("Video asset created")
} else {
print("Video asset create failed: \(error?.localizedDescription)")
}
}
}
})
} catch { print("file manager error: \(error.localizedDescription)") }
}.resume()
} else {
print("File already exists at destination url")
}
}
then here is method to call
let urlString = response.replacingOccurrences(of: "\"", with: "") -> my m3u8 URL
let videoImageUrl = "https://www.sample-videos.com/video123/mp4/720/big_buck_bunny_720p_1mb.mp4" -> always success
//TEST MP4 file -> ALWAYS SUCCESS
self.downloadVideoLinkAndCreateAsset(videoImageUrl, "big_buck_bunny_720p_1mb.mp4")
//TEST M3U8 FIlE -> FAIL
self.downloadVideoLinkAndCreateAsset(urlString, history.fileName!) -> fileName format is 'abc.mp4'
The log result for MP4
destinationURL: file:///Users/thehe/Library/Developer/CoreSimulator/Devices/05C6DE76-6609-4E4A-B00D-2CE3622D2EF8/data/Containers/Data/Application/90994674-6C07-47F9-A880-D1A80CDA0C27/Documents/CiviX_HistoryVideo_big_buck_bunny_720p_1mb.mp4
-> Video asset created
THe log result for M3U8
self.downloadVideoLinkAndCreateAsset(urlString, history.fileName!)
destinationURL: file:///Users/thehe/Library/Developer/CoreSimulator/Devices/05C6DE76-6609-4E4A-B00D-2CE3622D2EF8/data/Containers/Data/Application/DA6ABC38-4E0A-44C7-9C56-8B65F1DC0D4D/Documents/CiviX_HistoryVideo_20-1-2019_3h18m32s.mp4
-> Video asset create failed: Optional("The operation couldn’t be completed. (Cocoa error -1.)")
I also tried to save with .m3u8 extension but still not working
self.downloadVideoLinkAndCreateAsset(urlString, "TEST_M3U8_FILE.m3u8")
destinationURL: file:///Users/thehe/Library/Developer/CoreSimulator/Devices/05C6DE76-6609-4E4A-B00D-2CE3622D2EF8/data/Containers/Data/Application/9B42A55B-4E3E-4A20-A0DC-6E1ED22471A2/Documents/CiviX_HistoryVideo_TEST_M3U8_FILE.m3u8
-> Video asset create failed: Optional("The operation couldn’t be completed. (Cocoa error -1.)")
M3U8 is an audio/video playlist file and it cannot be saved to the gallery.
i am currently trying to upload multiple file from my device to google firebase, here is my setup:
func upload(completionHandler: #escaping (Bool) -> ()) {
let fileNames = try fileManager.contentsOfDirectory(atPath: documentsUrl.path)
for fileName in fileNames {
let url = URL(fileURLWithPath: "\(FileService.shared.documentsUrl.path)/\(fileName)")
let ref = storage.reference().child("path/\(fileName)")
let upload = ref.putFile(from: url, metadata: nil, completion: { (metadata, error) in
if error != nil {
completionHandler(true)
}
completionHandler(true)
})
}
}
How i call it:
self.upload { (success) in
print("operation \(success)")
}
What i see:
When i just have one file in the document directory, the loop runs just once and the file is being uploaded!
BUT: When there are multiple files, the loops also runs multiple times and the files are NOT being uploaded!
FIRStorageErrorDomain Code=-13000
Anybody could help me with this?
Thanks and Greetings!
I'm downloading and writing ~200mb podcasts into the Documents directory with the following code:
var podcastRequest = NSURLRequest(URL: audioUrl)
NSURLConnection.sendAsynchronousRequest(podcastRequest, queue: NSOperationQueue.mainQueue(), completionHandler: {(response: NSURLResponse!, data: NSData!, error: NSError!) -> Void in
if let myPodcastDataFromUrl = NSData(contentsOfURL: audioUrl) {
if myPodcastDataFromUrl.writeToURL(destinationUrl, atomically: true) {
// add to the array to track the download
var tempDic = self.posts[theRow] as! NSMutableDictionary as NSMutableDictionary
tempDic["downloaded"] = "true"
self.posts[theRow] = tempDic
} else {
println("Error saving file")
}
}
})
The sendAsynchronousRequest call prevents the lockup from happening during the download, but the app still freezes when it starts actually writing it to the directory.
Is there a way to prevent the lockup from happening at all, or am I going to have to write smaller chunks at a time?
You won't be able to store 200MB in memory before trying to write it to disk, but you can use downloadTaskWithURL method, it writes the file to a temporary folder and you can move it when it finishes to the documents folder as follow.
let documentsDirectoryURL = NSFileManager().URLsForDirectory(.DocumentDirectory, inDomains: .UserDomainMask).first as! NSURL
NSURLSession.sharedSession().downloadTaskWithURL(audioUrl, completionHandler: {
(location, response, error) -> Void in
if let error = error {
println(error.description)
}
else {
if NSFileManager().moveItemAtURL(location, toURL: documentsDirectoryURL.URLByAppendingPathComponent(response.suggestedFilename!), error: nil) {
println("saved")
} else {
println("not saved.")
}
}
}).resume()
I am new at Alamofire framework. I try to download data file. The code is :
Alamofire.download(.GET, urlStr, { (temporaryURL, response) in
if let directoryURL = NSFileManager.defaultManager()
.URLsForDirectory(.DocumentDirectory,
inDomains: .UserDomainMask)[0]
as? NSURL {
let pathComponent = response.suggestedFilename
return directoryURL.URLByAppendingPathComponent(pathComponent!)
}
return temporaryURL
})
File downloading successfully. However all process doing with memory. As you see the problem is, if i try to download big file(I mean over 50mb), i got didReceiveMemoryWarning and app closed itself. How i can prevent that?
In test I try to download a movie (size is 220mb) and in simulator, memory usage goes up to 500mb. and when i try my phone. it closed itself after showin memory warning.
If you want to download large files, you may consider another lib called TCBlobDownloadSwift by thibaultCha. It is a Swift version of TCBlobDownload, which was tested with files from ~150MB to ~1.2GB, mostly videos.
Its usage is similar to Alamofire:
import TCBlobDownloadSwift
// Here is a simple delegate implementing TCBlobDownloadDelegate.
class DownloadHandler: NSObject, TCBlobDownloadDelegate {
init() {}
func download(download: TCBlobDownload, didProgress progress: Float, totalBytesWritten: Int64, totalBytesExpectedToWrite: Int64) {
println("\(progress*100)% downloaded")
}
func download(download: TCBlobDownload, didFinishWithError error: NSError?, atLocation location: NSURL?) {
println("file downloaded at \(location)")
}
}
let fileURL = NSURL(string: "http://some.huge/file.mp4")
let download = TCBlobDownloadManager.sharedInstance
.downloadFileAtURL(fileURL!, toDirectory: nil, withName: nil, andDelegate: DownloadHandler())