I attempted the following caching mechanism to cache videos which are added in cells in my app:
import Foundation
public enum Result<T> {
case success(T)
case failure(NSError)
}
class CacheManager {
static let shared = CacheManager()
private let fileManager = FileManager.default
private lazy var mainDirectoryUrl: URL = {
let documentsUrl = self.fileManager.urls(for: .cachesDirectory, in: .userDomainMask).first!
return documentsUrl
}()
func getFileWith(stringUrl: String, completionHandler: #escaping (Result<URL>) -> Void ) {
let file = directoryFor(stringUrl: stringUrl)
//return file path if already exists in cache directory
guard !fileManager.fileExists(atPath: file.path) else {
completionHandler(Result.success(file))
return
}
DispatchQueue.global().async {
if let videoData = NSData(contentsOf: URL(string: stringUrl)!) {
videoData.write(to: file, atomically: true)
DispatchQueue.main.async {
completionHandler(Result.success(file))
}
} else {
DispatchQueue.main.async {
let error = NSError(domain: "SomeErrorDomain", code: -2001 /* some error code */, userInfo: ["description": "Can't download video"])
completionHandler(Result.failure(error))
}
}
}
}
private func directoryFor(stringUrl: String) -> URL {
let fileURL = URL(string: stringUrl)!.lastPathComponent
let file = self.mainDirectoryUrl.appendingPathComponent(fileURL)
return file
}
}
Used like this for each cell if at that index it is a video:
CacheManager.shared.getFileWith(stringUrl: videoURL) { result in
switch result {
case .success(let url):
let player = AVPlayer(url: url)
let playerLayer = AVPlayerLayer(player: player)
playerLayer.frame = CGRect(x: -8, y: 0, width: 138, height: 217)
cell.imageOrVideoView.layer.addSublayer(playerLayer)//seems to use the the first video for all videos...
cell.profImage.sd_setImage(with: URL(string: "\(self.postArray[indexPath.item].user.profileImageUrlString!)"), placeholderImage: UIImage(named: "media"))
break;
// do some magic with path to saved video
case .failure(let error):
print(error, " failure in the Cache of video")
break;
// handle errror
}
}
The problem with this is that now the second video URL is used as the video (which I don't play) for every single video cell.
Do I need to reload cache? This seems wrong...
I have found that this seems to be the problematic code:
//return file path if already exists in cache directory
guard !fileManager.fileExists(atPath: file.path) else {
completionHandler(Result.success(file))
return
}
When commenting out the guard each cell does get the correct video. The problem is that it does not cache it. How can I fix this?
Edit:
I found something weird which may be a symptom of the problem. if at the index path the cell should be a video, then I print video else it must be a image print image. When looking in the console however, I see that image gets printed 5 times and video gets printed 2, even though there are 11 cells in the collection view. Also I can confirm that regardless of which it is I print "in here" which gets printed 7 times. Again, this all when (when you scroll), there are 11 cells all of which should be different but of course all the video ones have the second videos first frame.
What happens is when the first cell is dequeued you get it with it's video layer added successfully then when you scroll this callback
CacheManager.shared.getFileWith(stringUrl: videoURL) { result in
is very slow so the previous cell content appears at that time making the fact that it's the second cell in all cells and that because of cell dequeuing
You have to clear the previous layers before the above line which is caused by
cell.imageOrVideoView.layer.addSublayer(playerLayer)
Edit: after the dequeue line do
cell.imageOrVideoView.layer.sublayers?.forEach {
if $0 is AVPlayerLayer {
$0.removeFromSuperlayer()
}
}
The problem seemed to be tied to using the cache manager for each cell. and the fact that I download a video for each cell. Instead used the thumbnail for each image which I then had to save.
Related
I am having a weird situation and have no clue how to handle this , I am downloading the videos from firestorage and caching into device for future use , meanwhile the background thread is already doing its job , I am passing a video url to the function to play the video. The issue is that sometimes avplayer is playing the right video and sometimes taking some other video url from the cache.
you can find the code in below :
func cacheVideo(for exercise: Exercise) {
print(exercise.imageFileName)
guard let filePath = filePathURL(for: exercise.imageFileName) else { return }
if fileManager.fileExists(atPath: filePath.path) {
// print("already exists")
} else {
exercise.loadRealURL { (url) in
print(url)
self.getFileWith(with: url, saveTo: filePath)
}
}
}
writing file here
func getFileWith(with url: URL, saveTo saveFilePathURL: URL) {
DispatchQueue.global(qos: .background).async {
print(saveFilePathURL.path)
if let videoData = NSData(contentsOf: url) {
videoData.write(to: saveFilePathURL, atomically: true)
DispatchQueue.main.async {
// print("downloaded")
}
} else {
DispatchQueue.main.async {
let error = NSError(domain: "SomeErrorDomain", code: -2001 /* some error code */, userInfo: ["description": "Can't download video"])
print(error.debugDescription)
}
}
}
}
now playing the video using this
func startPlayingVideoOnDemand(url : URL) {
activityIndicatorView.startAnimating()
activityIndicatorView.isHidden = false
print(url)
let cachingPlayerItem = CachingPlayerItem(url: url)
cachingPlayerItem.delegate = self
cachingPlayerItem.download()
// cachingPlayerItem.preferredPeakBitRate = 0
let avasset = AVAsset(url: url)
let playerItem = AVPlayerItem(asset: avasset)
let player = AVPlayer(playerItem: playerItem)
player.automaticallyWaitsToMinimizeStalling = false
initializeVideoLayer(for: player)
}
any suggestions would be highly appreciated.
this was solved because the data model which i was using to download bunch of videos files was accessed in background thread and meanwhile i was trying to assign the url to the same data model class in order to fetch the video and play in avplayer. Hence this was the issue and resolved by simply adding a new attribute into data model for assigning the url to play right away.
I know that SDWebImage loads the image in a background thread so you're not blocking the UI/main thread when this downloading is going on. Furthermore, it will also disk-cache all the images you've downloaded and will NEVER re-download an image from the same URL.
So I wonder if there is something similar or the same for videos?
Something to note: I add Videos as Sublayer.
let videoURL = URL(string: postArray[indexPath.item].media[0].videoURLString!)//need to do error handlin here
print(videoURL as Any, "<-- video url in dispkay")
let player = AVPlayer(url: videoURL! as URL)
let playerLayer = AVPlayerLayer(player: player)
playerLayer.frame = CGRect(x: -8, y: 0, width: 138, height: 217)//cell.frame
cell.imageOrVideoView.layer.addSublayer(playerLayer)
//Other code and play()
This was recommended in the past but it seems like it does something different or at the very leased has too much extra functionality I dont need.
Update:
What I am testing:
DispatchQueue.global(qos: .default).async(execute: {
var downloadedData: Data? = nil
if let url = URL(string: videoURL) {
do {
downloadedData = try Data(contentsOf: url)
} catch {
print(error, "downloaded Data failed")
}
}
if downloadedData != nil {
// STORE IN FILESYSTEM
var cachesDirectory = NSSearchPathForDirectoriesInDomains(.cachesDirectory, .userDomainMask, true)[0]
var file = URL(fileURLWithPath: cachesDirectory).appendingPathComponent(videoURL).absoluteString
do {
try downloadedData?.write(to: URL(string: file)!)
} catch {
print(error, "error dowloading data and writing it")
}
// STORE IN MEMORY
if let downloadedData = downloadedData {
memoryCache?.setObject(downloadedData as AnyObject, forKey: videoURL as AnyObject)
}
}
// NOW YOU CAN CREATE AN AVASSET OR UIIMAGE FROM THE FILE OR DATA
})
I do not understand however if I should do something right after the last line or if I should do it after the }) or if I need to add a Update UI there.
So I was able to solve the problem with the following:
Swift 4:
import Foundation
public enum Result<T> {
case success(T)
case failure(NSError)
}
class CacheManager {
static let shared = CacheManager()
private let fileManager = FileManager.default
private lazy var mainDirectoryUrl: URL = {
let documentsUrl = self.fileManager.urls(for: .cachesDirectory, in: .userDomainMask).first!
return documentsUrl
}()
func getFileWith(stringUrl: String, completionHandler: #escaping (Result<URL>) -> Void ) {
let file = directoryFor(stringUrl: stringUrl)
//return file path if already exists in cache directory
guard !fileManager.fileExists(atPath: file.path) else {
completionHandler(Result.success(file))
return
}
DispatchQueue.global().async {
if let videoData = NSData(contentsOf: URL(string: stringUrl)!) {
videoData.write(to: file, atomically: true)
DispatchQueue.main.async {
completionHandler(Result.success(file))
}
} else {
DispatchQueue.main.async {
let error = NSError(domain: "SomeErrorDomain", code: -2001 /* some error code */, userInfo: ["description": "Can't download video"])
completionHandler(Result.failure(error))
}
}
}
}
private func directoryFor(stringUrl: String) -> URL {
let fileURL = URL(string: stringUrl)!.lastPathComponent
let file = self.mainDirectoryUrl.appendingPathComponent(fileURL)
return file
}
}
Usage:
CacheManager.shared.getFileWith(stringUrl: videoURL) { result in
switch result {
case .success(let url):
// do some magic with path to saved video
break;
case .failure(let error):
// handle errror
print(error, " failure in the Cache of video")
break;
}
}
I know this type of question has been asked 1e7 times but I have come across a specific issue that I don't think has been covered/is blatantly obvious but I am too novice to fix it on my own.
I have the following code snippet within my cellForRowAt method in a TableViewController:
let currentDictionary = parser.parsedData[indexPath.row] as Dictionary<String,String>
let urlString = currentDictionary["media:content"]
if urlString != nil {
let url = NSURL(string: urlString!)
DispatchQueue.global().async {
let data = try? Data(contentsOf: url! as URL) //make sure your image in this url does exist, otherwise unwrap in a if let check / try-catch
DispatchQueue.main.async {
cell.thumbnailImageView.image = UIImage(data: data!)
}
}
}
Which executes fine, downloads the images and assigns them to the UIImageView of each tableViewCell.
There is a finite delay when scrolling the table as the images are downloaded 'on the fly' so to speak.
What I want to do is pre-download all these images and save them in a data structure so they are fetched from URL's less frequently.
I have tried the following implementation:
var thumbnail = UIImage()
for item in parser.parsedData {
let currentDictionary = item as Dictionary<String,String>
let title = currentDictionary["title"]
let link = currentDictionary["link"]
let urlString = currentDictionary["media:content"]
let url = NSURL(string: urlString!)
if urlString != nil {
let url = NSURL(string: urlString!)
DispatchQueue.global().async {
let data = try? Data(contentsOf: url! as URL)
DispatchQueue.main.sync {
thumbnail = UIImage(data: data!)!
}
}
}
var newsArticle: News!
newsArticle = News(title: title!, link: link!, thumbnail: thumbnail)
news.append(newsArticle)
Where news is my data structure. This code also executes fine, however each thumbnail is a 0x0 sized image, size {0, 0} orientation 0 scale 1.000000, according to the console output.
Does anyone have any ideas how to download these images but not immediately assign them to a UIImageView, rather store them for later use?
The problem is that you create your newsArticle before the global dispatch queue even started to process your url. Therefore, thumbnail is still the empty UIImage() created in the very first line.
You'll have to create the thumbnail inside the inner dispatch closure, like:
for item in parser.parsedData {
guard let currentDictionary = item as? Dictionary<String,String> else { continue /* or some error handling */ }
guard let title = currentDictionary["title"] else { continue /* or some error handling */ }
guard let link = currentDictionary["link"] else { continue /* or some error handling */ }
guard let urlString = currentDictionary["media:content"] else { continue /* or some error handling */ }
guard let url = URL(string: urlString) else { continue /* or some error handling */ }
DispatchQueue.global().async {
if let data = try? Data(contentsOf: url) {
DispatchQueue.main.sync {
if let thumbnail = UIImage(data: data) {
let newsArticle = News(title: title, link: link, thumbnail: thumbnail)
news.append(newsArticle)
}
}
}
}
}
By the way, your very first code (cellForRow...) is also broken: You must not reference the cell inside the dispatch closure:
DispatchQueue.main.async {
// Never do this
cell.thumbnailImageView.image = UIImage(data: data!)
}
Instead, reference the IndexPath, retrieve the cell inside the clousure, and go on with that cell. But as you already mentioned, there are many many entries on stackoverflow regarding this issue.
There is a tableViewController with 5 cells. When I click on a cell, the download starts, if the file is not found.
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
if indexPath.row > 0 {
let path = NSSearchPathForDirectoriesInDomains(FileManager.SearchPathDirectory.documentDirectory, FileManager.SearchPathDomainMask.userDomainMask, true)
let documentDirectoryPath:String = path[0]
let fileManager = FileManager()
let destinationURLForFile = URL(fileURLWithPath: documentDirectoryPath.appendingFormat("/file%d.pdf",indexPath.row))
if fileManager.fileExists(atPath: destinationURLForFile.path){ self)
}else{
var downloadTask: URLSessionDownloadTask!
index = indexPath.row
let url = URL(string: "http://publications.gbdirect.co.uk/c_book/thecbook.pdf")!
downloadTask = backgroundSession.downloadTask(with: url)
downloadTask.resume()
}}
func urlSession(_ session: URLSession,
downloadTask: URLSessionDownloadTask,
didFinishDownloadingTo location: URL){
let path = NSSearchPathForDirectoriesInDomains(FileManager.SearchPathDirectory.documentDirectory, FileManager.SearchPathDomainMask.userDomainMask, true)
let documentDirectoryPath:String = path[0]
let fileManager = FileManager()
let destinationURLForFile = URL(fileURLWithPath: documentDirectoryPath.appendingFormat("/file%d.pdf",index))
do {
try fileManager.moveItem(at: location, to: destinationURLForFile)
}catch{
print("An error occurred while moving file to destination url")
}
}
The problem is that downloading one of the files stops if I download 2 files at a time. How to fix it?
A couple of thoughts:
A single, numeric index property is obviously insufficient to keep track of the multiple downloads that might be in progress. You need some structure to keep track of the correlation between downloads and their eventual file names in the Documents folder. It might be:
struct Download {
enum Status {
case notStarted
case started
case finished
case failed(Error?)
}
/// URL of resource on web server
let remoteURL: URL
/// URL of file in Documents folder
let localURL: URL
/// The status of the download
var status: Status
}
Now that you have a type to keep track of the state of an individual download, create an array of those Download objects:
var downloads = [Download]()
You might populate that in viewDidLoad, or something like that:
override func viewDidLoad() {
super.viewDidLoad()
// create the `Download` objects, e.g. I'll create one here
let remoteURL = URL(string: "http://publications.gbdirect.co.uk/c_book/thecbook.pdf")!
let fileURL = try! FileManager.default.url(for: .documentDirectory, in: .userDomainMask, appropriateFor: nil, create: false)
.appendingPathComponent("file0.pdf")
let status: Download.Status
if try! fileURL.checkResourceIsReachable() {
status = .finished
} else {
status = .notStarted
}
downloads.append(Download(remoteURL: remoteURL, localURL: fileURL, status: status))
// since you're dealing with background session (e.g. tasks may have been previously
// scheduled), let's iterate through any pending tasks, updating status accordingly
backgroundSession.getAllTasks { tasks in
for task in tasks {
guard let index = self.downloads.index(where: { $0.remoteURL == task.originalRequest?.url }) else {
print("cannot find download for \(task.originalRequest?.url)")
return
}
self.downloads[index].status = .started
}
}
}
When the download is done, you can now just look up that download in our array of downloads in order to determine the file URL:
func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL){
guard let index = downloads.index(where: { $0.remoteURL == downloadTask.originalRequest?.url }) else {
print("cannot find download for \(downloadTask.originalRequest?.url)")
return
}
do {
try FileManager.default.moveItem(at: location, to: downloads[index].localURL)
downloads[index].status = .finished
} catch {
print("An error occurred while moving file to destination url: \(error.localizedDescription)")
downloads[index].status = .failed(error)
}
}
It's worth noting that the logic that says "if the file doesn't exist, then start download" is, most likely, insufficient. Sure, if the file exists, then the download is done. But what if a download has been started already, but hasn't yet finished? You probably do not want to start a new download if a previously initiated download simply hasn't yet finished.
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
if indexPath.row > 0 {
let index = indexPath.row - 1 // for some reason you're looking at indexes greater than zero, so let's adjust our index for a zero-based index within our array
switch downloads[index].status {
case .notStarted:
let downloadTask = backgroundSession.downloadTask(with: downloads[index].remoteURL)
downloads[index].status = .started
downloadTask.resume()
default:
break
}
}
}
Now, I don't want to get too lost in the details of these code snippets above, but rather I want to make sure you grok the basic concept, namely that you can't have a single numeric index, but rather you need some collection (an array or dictionary) to keep track of all of the various downloads that may be in progress at any given time.
You cannot download two files at a time if you are using single variables (index and downloadTask). Whenever the user selects the second cell, a new value for index is set, so using that value in urlSession:downloadTask:didFinishDownloadingTo: is a mistake when it is being called by the first task.
You need to keep that values in a collection, for example an array of tuples, keeping the index, the task and any other info about the file, for example the file path.
if let toID = message.chatPartnerId() {
firebaseReference.child(toID).observeSingleEvent(of: .value, with: { (snapshot) in
if let dictionary = snapshot.value as? [String: Any] {
cell.nameLabel.text = dictionary["displayname"] as? String
let pic = dictionary["pictureURL"] as! String
print("THIS IS THE URL FOR EACH DISPLAYNAME")
print(dictionary["displayname"] as? String)
print(pic)
if let imageFromCache = MainPageVC.imageCache.object(forKey: pic as NSString) {
cell.pictureLabel.image = imageFromCache
} else {
let requested = URLRequest(url: URL(string: pic )!)
URLSession.shared.dataTask(with: requested) {data, response, err in
if err != nil {
print(err)
} else {
DispatchQueue.main.async {
let imageToCache = UIImage(data: data!)
MainPageVC.imageCache.setObject(imageToCache!, forKey: pic as NSString)
//cell.pictureLabel.image = nil
cell.pictureLabel.image = imageToCache
}
}
}.resume()
}
}
})
}
return cell
}
I'm running this code in my cellForRowAtIndexPath and I'm getting a ton of really bad behavior. I'm also getting similar behavior on other pages but for some reason this block of code with about a 90% consistency returns incorrect information for cells.
I get a lot of duplicate pictures being used, displaynames in the wrong places, but when I'm actually clicking into a person, my detail page shows the correct information every single time. That code is the typical didSelectRowAtIndexPath and passing the person.
What I don't understand is why on the initial load of this page all of the information is screwed up, but if I click into someone and come back the entire tableview has correct names and pictures. The names/pics also fix if I scroll a cell off the screen then come back to it.
I'm getting this behavior all over my app, meanwhile I see caching/loading done like this everywhere. Is it because I'm running the code in my cellForRowAtIndexPath? The only difference I see is that I'm running it there instead of creating a function inside of my Person class that configures cells and running it like that. What I don't understand is why that would make a difference because as far as I'm aware running a function within cellforRowAtIndexpath would be the same as copy-pasting that same code into there?
Any ideas/suggestions?
Edit: I'm getting a very similar situation when I'm running the following code:
self.PersonalSearchesList = self.PersonalSearchesList.sorted{ $0.users > $1.users }
self.tableView.reloadData()
Where I'm sorting my array before reloading my data. The information sometimes loads in incorrectly at first, but once I scroll the cell off the screen then come back to it it always corrects itself.
if you are using swift 3 here are some handy functions that allow you to save an image to your apps directory from an URL and then access it from anywhere in the app:
func saveCurrentUserImage(toDirectory urlString:String?) {
if urlString != nil {
let imgURL: URL = URL(string: urlString!)!
let request: URLRequest = URLRequest(url: imgURL)
let session = URLSession.shared
let task = session.dataTask(with: request, completionHandler: {
(data, response, error) -> Void in
if (error == nil && data != nil) {
func display_image() {
let userImage = UIImage(data: data!)
if let userImageData = UIImagePNGRepresentation(userImage!) {
let filename = self.getDocumentsDirectory().appendingPathComponent("userImage")
try? userImageData.write(to: URL(fileURLWithPath: filename), options: [.atomic])
}
}
DispatchQueue.main.async(execute: display_image)
}
})
task.resume()
}
}
and then access it with any view controller using this:
extension UIViewController {
func getImage(withName name: String) -> UIImage {
let readPath = getDocumentsDirectory().appendingPathComponent(name)
let image = UIImage(contentsOfFile: readPath)
return image!
}
}
and finally calling it like this:
cell.pictureLabel.image = getImage(withName: "userImage")
If you can run the saveCurrentUserImage function prior to running cellForRowAtIndexPath then you can just check if the photo is nil in the directory before attempting to download it. You might be getting funny behavior when the page initially loads because you have multiple network calls going on at once. I wouldn't recommend making any network calls in cellForRowAtIndexPath because every time the cells are re-initialized it's going to make that network call for each cell.
Hope it helps!
EDIT: This method of image saving and retrieval is for images that you want to persist. If you want to erase them from memory you'll have to delete them from your directory.