Aleksandrs Muravjovs
Aleksandrs Muravjovs

Reputation: 173

Correct way of storing large amount of images locally in the app

I am trying to find out the most convenient and correct way of storing large quantity of images locally on device and showing them on screen when required.

As of now all my images and icons are placed in Assets folder and I also have created a separate swift file where I created arrays to devide them in to appropriate arrays depending on image types. Around 30 Arrays. Like below:

let instrumentImages = [UIImage(named: "accordion")!, UIImage(named: "drums")!, UIImage(named: "electricGuitar")!, UIImage(named: "flute")!, UIImage(named: "harp")!, UIImage(named: "maracas")!, UIImage(named: "piano")!, UIImage(named: "guitar")!, UIImage(named: "saxophone")!, UIImage(named: "trumpet")!, UIImage(named: "violin")!, UIImage(named: "tuba")!]

When image icon is selected I am passing the actual image to the Detail view controller where image is shown on the screen, like below:

let mainStoryboard:UIStoryboard = UIStoryboard(name: "Main", bundle: nil)
        let destVC = mainStoryboard.instantiateViewController(withIdentifier: "DetailViewController") as! DetailViewController
        destVC.image = ImageModel().instrumentImages[indexPath.row]

        self.navigationController?.pushViewController(destVC, animated: true)

Everything is quite simple and working perfectly, but as soon as I have large amount of images as I said 100+ app is starting to work very slow.

So my question is what is the best way in my case to store such large quantity of images and show them on the screen when needed so that my app work smoothly.

Upvotes: 0

Views: 791

Answers (2)

Abizern
Abizern

Reputation: 150605

With your image loader you are unarchiving all the images just to get a single one to pass to another view controller. As other answers have said, this is inefficient.

A solution that I have used in these cases is to separate the definition of the asset from the actual asset with:

/// A wrapper around an image asset in the catalogue that can be return an image using `UIImage(named:in:compatibleWith)` with `self`
public struct ImageAsset: Equatable {
    private let named: String
    private let bundle: Bundle
    private let compatibleWith: UITraitCollection?


    /// Initialise the wrapper with the properties required to call UIImage(named:in:compatibleWith)`
    ///
    /// - Parameters:
    ///   - named: The name of the asset
    ///   - bundle: The bundle for the asset catalogue - defaults to `Bundle.main`
    ///   - compatibleWith: The UITraitCollection - defaults to `nil`
    public init(named: String, bundle: Bundle = Bundle.main, compatibleWith: UITraitCollection? = nil) {
        self.named = named
        self.bundle = bundle
        self.compatibleWith = compatibleWith
    }

    // the result of calling `UIImage(named:in:compatibleWith)` with `self`
    public func image() -> UIImage? {
        return UIImage(named: named, in: bundle, compatibleWith: compatibleWith)
    }
}

extension ImageAsset: ExpressibleByStringLiteral {
    // Convenient extension for creating the most common values from string literals
    public init(stringLiteral value: String) {
        self.init(named: value)
    }
}

This is just a struct, from which you can extract an image when required, which uses a lot less memory than an array of actual images. Now, rather than an array of images you can define an array of ImageAssets which looks like (through the magic of ExpressibleByStringLiteral):

let instrumentImages: [ImageAsset] = ["accordion", "drums", "electricGuitar, "flute"]

And you can pass it to your view controller with:

destVC.image = ImageModel().instrumentImages[indexPath.row].image()!

For convenience you want to namespace your images, you can rewrite ImageModel() as an enum with no cases that returns the image list:

enum ImageModel {
    static var instrumentImages: [ImageAssets] = ["accordion", "drums", "electricGuitar, "flute"]
}

And then passing it to your ViewConroller in a segue looks like:

destVC.image = ImageModel.instrumentImages[indexPath.row].image()!

And even more efficiently, rather your view controller taking a UIImage, you can make it take an ImageAsset:

destVC.imageAsset = ImageModel.instrumentImages[indexpath.row]

And then destVC can call .image() on this imageAsset when it needs to actually get an image.

Upvotes: 1

mag_zbc
mag_zbc

Reputation: 6982

I would say the issue lies here:

destVC.image = ImageModel().instrumentImages[indexPath.row]

Every time you want to load a single image, you create an instance of ImageModel, which (I presume) loads all the images, and returns just one, the one you need. This is very inefficient, there's no point in loading all the images, get just one, and discard all the rest.

If you insist on pre-loading the images, load them just once and store them in a static instance of ImageModel, like so

class ImageModel
{
    static let sharedInstance = ImageModel()

    init()
    {
        // load images here
    }
}

Then retrieve an image like so

destVC.image = ImageModel.sharedInstance.instrumentImages[indexPath.row]

This way you will load all images just once, in a constructor of the shared ImageModel instance.

However, I would argue that there's no point in loading all the images into memory - in your ImageModel class you should rather keep only file names of images, and load them only when necessary

class ImageModel 
{
    let instrumentImages = ["accordion", "drums", ... ]

    static let imageWithIndex(_ index : Int) -> UIImage?
    {
        return UIImage(named: instrumentImages[index])
    }
}

and call it like so

destVC.image = ImageModel.imageWithIndex(indexPath.row)

No point in pre-loading images you might end up not even using, because with large quantity of high-definition images, your app might even run out of memory.

Upvotes: 1

Related Questions