How do you close open files using Swift? - ios

I am downloading ~1300 images. Those are small images total size is around ~500KB. However, after downloading and putting them into userDefault, I get error as below:
libsystem_network.dylib: nw_route_get_ifindex :: socket(PF_ROUTE, SOCK_RAW, PF_ROUTE) failed: [24] Too many open files
Assumingely, downloaded png images are not being closed.
I already extended cache size via below:
// Configuring max network request cache size
let memoryCapacity = 30 * 1024 * 1024 // 30MB
let diskCapacity = 30 * 1024 * 1024 // 30MB
let urlCache = URLCache(memoryCapacity: memoryCapacity, diskCapacity: diskCapacity, diskPath: "myDiscPath")
URLCache.shared = urlCache
And this is the approach I got to store images:
func storeImages (){
for i in stride(from: 0, to: Cur.count, by: 1) {
// Saving into userDefault
saveIconsToDefault(row: i)
}
}
I get the error after all of them being added into userDefault. So, I know they are there.
EDIT:
FUNCTIONS:
func getImageFromWeb(_ urlString: String, closure: #escaping (UIImage?) -> ()) {
guard let url = URL(string: urlString) else {
return closure(nil)
}
let task = URLSession(configuration: .default).dataTask(with: url) { (data, response, error) in
guard error == nil else {
print("error: \(String(describing: error))")
return closure(nil)
}
guard response != nil else {
print("no response")
return closure(nil)
}
guard data != nil else {
print("no data")
return closure(nil)
}
DispatchQueue.main.async {
closure(UIImage(data: data!))
}
}; task.resume()
}
func getIcon (id: String, completion: #escaping (UIImage) -> Void) {
var icon = UIImage()
let imageUrl = "https://files/static/img/\(id).png"
getImageFromWeb(imageUrl) { (image) in
if verifyUrl(urlString: imageUrl) == true {
if let image = image {
icon = image
completion(icon)
}
} else {
if let image = UIImage(named: "no_image_icon") {
icon = image
completion(icon)
}
}
}
}
USAGE:
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
guard let cell = tableView.dequeueReusableCell(withIdentifier: "CurrencyCell", for: indexPath) as? CurrencyCell else { return UITableViewCell() }
if currencies.count > 0 {
let noVal = currencies[indexPath.row].rank ?? "N/A"
let nameVal = currencies[indexPath.row].name ?? "N/A"
let priceVal = currencies[indexPath.row].price_usd ?? "N/A"
getIcon(id: currencies[indexPath.row].id!, completion: { (retImg) in
cell.configureCell(no: noVal, name: nameVal, price: priceVal, img: retImg)
})
}
return cell
}

The URLSession(configuration: .default) syntax is creating a new URLSession for each request. Create a single URLSession (saving it in some property) and then reuse it for all of the requests. Or, if you're really not doing any custom configuration of the URLSession, just use URLSession.shared:
let task = URLSession.shared.dataTask(with: url) { data, response, error in
...
}
task.resume()
You mention that you're saving 1300 images in UserDefaults. That's not the right place to store that type of data nor for that quantity of files. I'd suggest you use the "Caches" folder as outlined in the File System Programming Guide: The Library Directory Stores App-Specific Files.
let cacheURL = try! FileManager.default
.url(for: .cachesDirectory, in: .userDomainMask, appropriateFor: nil, create: true)
.appendingPathComponent("images")
// create your subdirectory before you try to save files into it
try? FileManager.default.createDirectory(at: cacheURL, withIntermediateDirectories: true)
Do not be tempted to store them in the "Documents" folder, either. For more information, see iOS Storage Best Practices.

Related

Nil while caching images

I've been able to solve the issue of caching images to improve scroll performance in my app. However nil is found when it tries to add it to cache. Also how can I add a placeholder image for images that failed to load or aren't available ?
let imageCache = NSCache<NSString, UIImage>()
extension UIImageView {
func downloadImage(from imgURL: String) -> URLSessionDataTask? {
guard let url = URL(string: imgURL) else { return nil }
// set initial image to nil so it doesn't use the image from a reused cell
image = nil
// check if the image is already in the cache
if let imageToCache = imageCache.object(forKey: imgURL as NSString) {
self.image = imageToCache
return nil
}
// download the image asynchronously
let task = URLSession.shared.dataTask(with: url) { (data, response, error) in
if let err = error {
print(err)
return
}
DispatchQueue.main.async {
// create UIImage
let imageToCache = UIImage(data: data!)
// add image to cache
imageCache.setObject(imageToCache!, forKey: imgURL as NSString)
self.image = imageToCache
}
}
task.resume()
return task
}
}
A couple of observations:
Just supply placeholder as parameter to function and use it instead of nil to initialize the image.
Do that after checking the cache (because there’s no point in using the placeholder if you found desired image in the cache).
Avoid use of ! forced unwrapping operator.
Check that UIImage(data:) found an image in the guard statement (and on the session queue, not the main thread).
Thus:
let imageCache = NSCache<NSString, UIImage>()
extension UIImageView {
func downloadImage(from imgURL: String, placeholder: UIImage? = nil) -> URLSessionDataTask? {
guard let url = URL(string: imgURL) else { return nil }
// check if the image is already in the cache
if let imageToCache = imageCache.object(forKey: imgURL as NSString) {
image = imageToCache
return nil
}
// set initial image to placeholder so it doesn't use the image from a reused cell
image = placeholder
// download the image asynchronously
let task = URLSession.shared.dataTask(with: url) { data, _, error in
guard
let data = data,
error == nil,
let imageToCache = UIImage(data: data)
else {
print(error ?? URLError(.badServerResponse))
return
}
imageCache.setObject(imageToCache, forKey: imgURL as NSString)
DispatchQueue.main.async {
self.image = imageToCache
}
}
task.resume()
return task
}
}

ErrorDomain Code=260 When attempting to read back a locally-stored image file

I'm making an application with one Core Data entity, Park, which is decoded from the data returned from an API request and stores the image url and local file/download location (if it has been downloaded) for each image as attributes. I created a computed property that returns a dictionary of ImageInfoObjects (which is a struct that basically just bundles the information together) based on the stored attributes. The first time I run the app, everything works fine but when I close the app and run it again it gives me the error "the file couldn't be opened because there is no such file". So I know there must be an issue with the way I'm storing the file paths in Core Data, or the way I'm reading them back to display the images. Any help would be appreciated. Code snippets below.
The method which catches the error:
func displayPhoto(_ object: ImageInfoObject, imageView: UIImageView) {
guard let location = object.downloadLocation else { return }
do {
let imageData = try Data(contentsOf: location)
let image = UIImage(data: imageData)
DispatchQueue.main.async {
imageView.image = image
}
} catch (let error) {
print(error)
}
}
The URLSessionDownloadDelegate method which is called once each image is finished downloading:
func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL) {
let fileManager = FileManager.default
guard let documentsPath = fileManager.urls(for: .documentDirectory, in: .userDomainMask).first,
let sourceURL = downloadTask.originalRequest?.url,
let download = self.photoDownloads[sourceURL] else {
fatalError()
}
let lastPathComponent = sourceURL.lastPathComponent
let destinationURL = documentsPath.appendingPathComponent(lastPathComponent)
do {
if fileManager.fileExists(atPath: destinationURL.path) {
try fileManager.removeItem(at: destinationURL)
}
try fileManager.copyItem(at: location, to: destinationURL)
let index = download.imageInfoObject.index
let newImageInfoObject = ImageInfoObject(url: sourceURL, index: index, downloadLocation: destinationURL)
self.park?.photoInfoObjects[index] = newImageInfoObject
switch newImageInfoObject.index {
case 1:
displayPhoto(newImageInfoObject, imageView: self.photo1View)
case 2:
displayPhoto(newImageInfoObject, imageView: self.photo2View)
default:
break
}
try context?.save()
} catch {
print(error)
}
}
The computed property that stores and retrieves the paths from the CoreData entity. The NSManaged attributes are self.photo1_url, self.photo1_location, self.photo2_url, and self.photo2_location.
public var photoInfoObjects: Dictionary<Int, ImageInfoObject> {
get {
var dictionary: Dictionary<Int, ImageInfoObject> = [:]
var object: ImageInfoObject
if let photo1_url = self.photo1_url,
let url = URL(string: photo1_url) {
if let photo1_location = self.photo1_location {
let location = URL(fileURLWithPath: photo1_location)
object = ImageInfoObject(url: url, index: 1, downloadLocation: location)
object.isDownloaded = true
} else {
object = ImageInfoObject(url: url, index: 1)
}
dictionary[1] = object
}
var object2: ImageInfoObject
if let photo2_url = self.photo2_url,
let url = URL(string: photo2_url) {
if let photo2_location = self.photo2_location {
let location = URL(fileURLWithPath: photo2_location)
object2 = ImageInfoObject(url: url, index: 2, downloadLocation: location)
object2.isDownloaded = true
} else {
object2 = ImageInfoObject(url: url, index: 2)
}
dictionary[2] = object2
}
return dictionary
}
set {
self.photo1_url = newValue[1]?.url.absoluteString
self.photo1_location = newValue[1]?.downloadLocation?.path
self.photo2_url = newValue[2]?.url.absoluteString
self.photo2_location = newValue[2]?.downloadLocation?.path
}
}

How do I asynchronously download and cache videos for use in my app?

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;
}
}

How to download image from web?

I am new to iOS i want download image to display it is working code but here lot of code duplication
let url = URL(string: iteminfo.imageUrl!)
let urlRequest = URLRequest(url: url!)
let task = URLSession.shared.dataTask(with: urlRequest) { (data, response, error) in
if error != nil {
print(error)
}
if let data = data {
print(data)
self.imageViewItemPic.image = UIImage(data: data)
}
}
task.resume()
let url2 = URL(string: iteminfo.cookerProfilePicUrl!)
let urlRequest2 = URLRequest(url: url2!)
let task2 = URLSession.shared.dataTask(with: urlRequest2) { (data, response, error) in
if error != nil {
print(error)
}
if let data = data {
print(data)
self.imageViewCookerProfilePic.image = UIImage(data: data)
}
}
task2.resume()
So I want to reuse my code but i unfortunately i can not reach my goal. there have no error and url is correct . every time goes else statement . i am missing something but what is that ?
if let image = downlaodImage(urlImage: iteminfo.imageUrl){
print("first \(image)")
imageViewItemPic.image = image
}else{
print("first wrong......")
}
if let image = downlaodImage(urlImage: iteminfo.cookerProfilePicUrl){
print("second \(image)")
imageViewCookerProfilePic.image = image
}
else{
print("second wrong......")
}
Here is my method :
func downlaodImage(urlImage : String?) -> UIImage?{
var image : UIImage?
let url = URL(string: urlImage!)
let urlRequest = URLRequest(url: url!)
let task = URLSession.shared.dataTask(with: urlRequest) { (data, response, error) in
if let data = data {
// print(data)
image = UIImage(data: data)
}
}
task.resume()
return image
}
note: I am not sure is it best way or not . if it is not best practice feel free to guide me .
There's no need of so much hassle. You have the URL of the image so you can simply download the image from the URL. For example:
func downloadImage(imageURL: String) {
DispatchQueue.global().async {
let data = NSData.init(contentsOf: NSURL.init(string: imageURL) as! URL)
DispatchQueue.main.async {
let image = UIImage.init(data: data as! Data)
imageView.image = image
}
}
}
Edit: To reuse this code I would suggest to use extension of UIImageView. Here's an example:
extension UIImageView {
func setImageFromURL(url: String) {
DispatchQueue.global().async {
let data = NSData.init(contentsOf: NSURL.init(string: url) as! URL)
DispatchQueue.main.async {
let image = UIImage.init(data: data as! Data)
self.image = image
}
}
}
}
Use this method whenever you want to set the image of an imageView from a url like this:
self.imageViewCookerProfilePic.setImageFromURL(url: "https://encrypted-tbn1.gstatic.com/images?q=tbn:ANd9GcQNpKmjx1w3DRDZ9IXN81-uhSUA6qL6obkOthoUkb9RZkXf5pJ8")
Dude. You should learn some staff about async and sync code.
Here is the thing. Code in you downloadImage works synchronically, so it pass you URLTask and go straight to return, there you return you image variable, that is nil.
One of the solutions in to use callback block like this:
func downloadImage(urlImage : String?, complete: ((UIImage?)->Void)? = nil){
let url = URL(string: urlImage!)
let urlRequest = URLRequest(url: url!)
let task = URLSession.shared.dataTask(with: urlRequest) { (data, response, error) in
if let data = data {
complete?(UIImage(data: data))
}
}
task.resume()
}
And then use it like this:
{ ...
downloadImage(urlImage: "", complete: { image in
if let image = image{
self.imageViewItemPic.image = image
}else{
print("no image")
}
})
...
}
You should read some tutorials about async code and web in swift. You could start with this site
downlaodImage() downloads an image asynchronously so
if let image = downlaodImage(...) { ... }
is always going to fail because program execution has continued before your response data has come back.
It would be easier just to set your images in the callback function closure of downlaodImage() as below by adding a UIImageView parameter to downlaodImage(). This way you can reduce the repetition of if else blocks by moving them to the downlaodImage function.
func downlaodImage(urlImage : String?, imageView: UIImageView) -> UIImage?{
var image : UIImage?
let url = URL(string: urlImage!)
let urlRequest = URLRequest(url: url!)
let task = URLSession.shared.dataTask(with: urlRequest) { (data, response, error) in
if let data = data {
// print(data)
if let image = UIImage(data: data) {
imageView.image = image
} else {
print("failed to load image")
}
}
}
task.resume()
return image
}
Simplified code without if/else blocks
downlaodImage(urlImage: "https://encrypted-tbn1.gstatic.com/images?q=tbn:ANd9GcQNpKmjx1w3DRDZ9IXN81-uhSUA6qL6obkOthoUkb9RZkXf5pJ8", imageView: imageViewItemPic)
downlaodImage(urlImage: "https://www.dominos.co.nz/ManagedAssets/OLO/eStore/all/Product/NZ/P015/P015_ProductImage_Small_en_Default_20140203_105245.png", imageView: imageViewCookerProfilePic)

Populate collectionView from JSON image url swift

I want to Load image using JSON parsing in collectionView. I'm getting array of image URLs, but images are not shown on UIImageView. I have used this code after getting JSON array and displaying it in UICollectionView's UIImageView.
if let url = NSURL(string: self.Products[indexPath.row].image) {
if let data = NSData(contentsOfURL: url){
cell.product_image.contentMode = UIViewContentMode.ScaleAspectFit
cell.product_image.image = UIImage(data: data)
}
}
But I am not able to load image, but text is displayed. I have used this code in cellForItemAtIndexPath method.. Can anyone suggest me what am I doing wrong?
func jsonParsingFromURL(){
// 1
let reposURL = NSURL(string: "http://api.room2shop.com/api/product/GetProducts?categoryId=24&filter=2&pageNumber=1")
// 2
if let JSONData = NSData(contentsOfURL: reposURL!) {
// 3
do
{
if let json = try NSJSONSerialization.JSONObjectWithData(JSONData, options: .AllowFragments) as? NSDictionary {
// 4
if let reposArray = json["ProductList"] as? [NSDictionary] {
// 5
for item in reposArray {
Products.append(ProductList(json: item))
}
}
}
}
catch {
print("Error with Json: \(error)")
}
}
}
This is my JSON parsing code
to reflect changes, you need to use
self.collectionView?.reloadData()
EDIT
1- please replace this block with this, and tell me if you get the urls normally, for me I get the urls normally
for item in reposArray
{
//ProductList(json: item)
//Products.append(ProductList(json: item))
//print(item)
print("__________")
print(item["Image"]!)
}
2- i was getting
Transport Security has Blocked a cleartext HTTP
solution was here.
Transport security has blocked a cleartext HTTP
use this
// taking the URL , then request image data, then assigne UIImage(data: responseData)
let imgURL: NSURL = NSURL(string: "www.example.com/image.jpg")!
let request: NSURLRequest = NSURLRequest(URL: imgURL)
let session = NSURLSession.sharedSession()
let task = session.dataTaskWithRequest(request) {
(data,response,error) -> Void in
if ( error == nil && data != nil ) {
func display_image() {
// imageView.post.image = your UIImage
self.imageViewPost.image = UIImage(data: data!)
}
dispatch_async(dispatch_get_main_queue(), display_image)
}
}
task.resume()
// end of loading img
The image is a URL which is not saved locally inside the image.xassets, so you need to parse the URL using the extension given below.
// add the extension
extension UIImageView {
func loadImage(urlString: String) {
let url = URL(string: urlString)!
URLSession.shared.dataTask(with: url, completionHandler: { (data, respones, error) in
if error != nil {
print(error ?? "")
return
}
DispatchQueue.main.async {
self.image = UIImage(data: data!)
}
}).resume()
}
}
// call this statement in your vc (where you polpulate)
yourImageView.loadImage(urlString:yourUrl)

Resources