There are two things to manage - views and images. You only need enough scroll view subviews to fill the visible portion of the scroll view. The nice pattern here is to have a reuse pool. When you need a subview for the scroll view, check for one in the reuse pool. If there's none there, allocate one. When scrolling happens, put views that are no longer visible into the reuse pool and add views (first checking the reuse pool) in newly visible spaces.
Images can be placed in a cache indexed by their url and by time. A mutable dictionary is well suited for the url index. A mutable array containing the urls can be a nice FIFO queue for the time index. The url can be either a file url for images packaged in the app or a remote url if the images are downloaded. To add to the cache, add a url-image pair to the dictionary, and add the url to the front (index 0) of the time array.
Each time you add to the cache, check to see if it's size exceeds your goal. If it does remove the oldest image. To do that, get the lastObject from the array, remove that url key-value pair from the dictionary and removeLastObject from the array.
This image cache can/should be larger than the number of visible views in the scroll view. You can tune this size to match the desired memory goal, taking into account the time it takes to get the image (if the images are remote, you'll probably want a bigger cache).
When adding an image subview to the scroll view, assign it a default image. Check the cache for a cached image using a url lookup. If there's one there, replace the default. If not, start an asynchronous get for that image. When that image arrives add it to the cache and check the scroll view to see if the subview containing the image is still visible (it may have been scrolled away). If it is, set the image.