3

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
}
Vetuka
  • 1,523
  • 1
  • 24
  • 40
  • 2
    Don't store `UIImage`s in `UserDefaults`. Never ever. Especially not 1300 images. – Tamás Sengel Nov 21 '17 at 18:14
  • I appreciate your input. However, would not be better if you also point out where to put those? – Vetuka Nov 21 '17 at 20:43
  • https://stackoverflow.com/questions/6238139/ios-download-and-save-image-inside-app try this – Paul Lafytskyi Nov 21 '17 at 21:08
  • Paul, thank you but those are all in Obj-C which I have no experience with. As the question title indicates I am looking for Swift solution. Although, I see that I can use the Apps local data storage folders ie. "temp" folders as such. Will continue to research. – Vetuka Nov 22 '17 at 13:48
  • @Rob You are right, I provided more code. Thank you. – Vetuka Jan 10 '18 at 01:55
  • @Rob I was looping through my currency array and trying to download each image and store them into user default. Using those functions, instead of directly calling inside cellForRowAt. – Vetuka Jan 10 '18 at 02:27
  • @Rob please excuse my lack of knowledge, I could not figure out how to implement URLSession.shared into existing code I have. I am sure it is easy but could not achieve it. – Vetuka Jan 10 '18 at 17:45
  • I can create array of URLs for images, but I don't know, how I can download them inside single URLSession instance. – Vetuka Jan 10 '18 at 17:47
  • @Rob WOW! This fixed all the retrievals. With this change in the code, now I am able to do get images as fast as assigning them into TableView. That is such a performance boost compare to previous setup. I cannot THANK YOU enough! Amazing! If you like to post your comment as answer, I would mark it might help someone else. – Vetuka Jan 10 '18 at 18:11

1 Answers1

2

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.

Rob
  • 415,655
  • 72
  • 787
  • 1,044