Search code examples
iosswiftcompiler-errorsstoring-dataxcode9.1

How do you close open files using Swift?


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
}

Solution

  • 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.