I'm quite new to Swift and iOS development and I have encountered a issue while building an app. In a ViewController I have a ScrollView with nested inside a UIView with two UIImageView children stacked one on top of the other. I use the background imageview to display a jpg image containing a fantasy map, meanwhile the top imageview is there to display a png image (with transparent background) with paths drawn into it that represents the journey of some characters. Inside the ViewController there is then a vertical StackView with inside a Label and a UISlider. With this slider the user can select a date and the top imageview should be updated with the correct image representing the characters' path up to said date.
The following is the ViewController's class
`import UIKit
class MapViewController: UIViewController {
@IBOutlet weak var scrollView: UIScrollView!
@IBOutlet weak var imagesView: UIView!
@IBOutlet weak var backgroundImageView: UIImageView!
@IBOutlet weak var foregroundImageView: UIImageView!
@IBOutlet weak var menuStackView: UIStackView!
@IBOutlet weak var coloredSwitch: UISwitch!
@IBOutlet weak var dateLabel: UILabel!
@IBOutlet weak var dateSlider: UISlider!
let mapPins = MapPins()
override func viewDidLoad() {
super.viewDidLoad()
menuStackView.layer.cornerRadius = 11.0
menuStackView.layer.masksToBounds = true
dateSlider.maximumValue = Float(mapPins.pinsLabelTexts.count)
scrollView.delegate = self
updateBackgroundMap(isColored: true)
}
func updateBackgroundMap(isColored: Bool) {
var backImage: UIImage? = nil
if isColored {
backImage = UIImage(named: "Mappa Antico Mondo - colori.jpg")
} else {
backImage = UIImage(named: "Mappa Antico Mondo - bn.jpg")
}
backgroundImageView.image = backImage
backgroundImageView.contentMode = .scaleAspectFit
backgroundImageView.frame.size = backImage!.size
}
func updateTopImage(imageName: String?) {
if imageName == nil {
foregroundImageView.image = nil
} else {
let image = UIImage(named: imageName!)
foregroundImageView.image = image
}
}
func updateCharactersPins() {
var index = Int(dateSlider.value)
if index > mapPins.pinsLabelTexts.count-1 {
index = mapPins.pinsLabelTexts.count-1
}
dateLabel.text = mapPins.pinsLabelTexts[index-1]
updateTopImage(imageName: mapPins.getPinsImageName(for: index-1))
}
@IBAction func toggleColors(_ sender: UISwitch) {
updateBackgroundMap(isColored: sender.isOn)
}
@IBAction func dateSliderChanged(_ sender: UISlider) {
updateCharactersPins()
}
}
extension MapViewController: UIScrollViewDelegate {
func viewForZooming(in scrollView: UIScrollView) -> UIView? {
return imagesView
}
}`
There are 104 possible "mapPins" (the png images for the top imageview) and when I test the code everything works "fine", but I can see in the debugger that the memory usage increases every time I change the slider and in the end the app crashed because it's using too much memory. The same thing does not happen if I use the app on the Xcode simulator. Moreover, I tried to remove the slider and to call the function updateCharactersPins() 104 times in the viewDidLoad() method in order to recreate the function calls as many times as the slider does (if the user just increases it) and I discovered that the memory usage remains fine. So it seams that there is problem (probably in my code) with of the @IBAction of my slider handles the function calls. Could it be that every UIImage object remains in memory?
Note: bunch of animated gif images in this post. If they're not animating, click on them...
First, File Size has very little to do with memory usage.
A 4096 x 3072 image when loaded into memory will use 48 megabytes:
width * height * 4-bytes-per-pixel
4096.0 * 3072.0 * 4.0 / 1048576.0 == 48.0 MB
To compare,I created three 4096 x 3072 images, saved as PNGs:
57 KB
61 KB
18.9 MB
Here's a tiny version of the mountain scene image:
Each of those images, when loaded into memory, will use 48 MB
Second, when loading images with let img = UIImage(named: "sample")
, iOS caches the images in memory, assuming you'll want to use them again.
If we run this code ("r" == red image, "t" == transparent image, "m" == mountain image):
let imgNames: [String] = [
"r4096x3072",
"t4096x3072",
"m4096x3072",
]
var i: Int = 0
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
let nm: String = imgNames[i % imgNames.count]
if let img = UIImage(named: nm) {
print("img:", nm, "size:", img.size)
self.imgView.image = img
}
i += 1
}
the memory usage looks like this:
The first three taps loads the next image and sets it to imgView.image
-- as we see, memory keeps increasing.
I continue to tap, reloading the images... and the memory stops increasing, because iOS / UIKit has all three images cached in memory.
As I mentioned in my comment, in general the caching is smart enough not to overload memory... but, with 104 very-large-images, and loading them rapidly, well... we get a memory crash.
We can prevent the caching by using UIImage(contentsOfFile: path)
. However, the images will need to be stored as resources in your app bundle - we can't load them that way from the Assets catalog. So the project files might look like this:
We can change the loading like this:
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
let nm: String = imgNames[i % imgNames.count]
if let path = Bundle.main.path(forResource: nm, ofType: "png"),
let img = UIImage(contentsOfFile: path) {
print("img:", nm, "size:", img.size)
self.imgView.image = img
}
i += 1
}
Now, we get this memory usage:
We only get TWO memory jumps instead of THREE (because the image view also holds a copy).
I put together a quick example, using 100 400 x 600
images (so we don't get a memory crash). There is a gray "map image view" and an overlaid image view that will hold the mostly-transparent images.
It auto-runs, updating the image every 1/10th second, to simulate your "slider dragging."
Looks like this (scaled way down):
And here's the memory usage with UIImge(named:...)
:
and with UIImage(contentsOfFile:...)
: