4

I'm going through Stanford's CS193P online course doing ios dev. Lecture 9 deals with UIScrollView / delegation via simple url UIImage fetch app. Said app works perfectly fine in simulator but launches then crashes on live device (iPhone5) after trying to fetch an img with the following:

Message from debugger: Terminated due to Memory Error

I went back into my code, reread about delegation, searched SO (I found a similar thread, I made sure my project scheme does NOT have zombies enabled). I updated my device, my compiler / os, and am kinda bummed about what might be preventing this from running on the device... The class example can be downloaded from Stanford at https://web.stanford.edu/class/cs193p/cgi-bin/drupal/system/files/sample_code/Cassini.zip but this code behaves the same way! This was originally written for ios 8.1 and we're at 8.4, are there any known issues?

code for the imageview controller:

import UIKit

class ImageViewController: UIViewController, UIScrollViewDelegate
{
    // our Model
    // publicly settable
    // when it changes (but only if we are on screen)
    //   we'll fetch the image from the imageURL
    // if we're off screen when this happens (view.window == nil)
    //   viewWillAppear will get it for us later
    var imageURL: NSURL? {
        didSet {
            image = nil
            if view.window != nil {
                fetchImage()
            }
        }
    }

    // fetches the image at imageURL
    // does so off the main thread
    // then puts a closure back on the main queue
    //   to handle putting the image in the UI
    //   (since we aren't allowed to do UI anywhere but main queue)
    private func fetchImage()
    {
        if let url = imageURL {
            spinner?.startAnimating()
            let qos = Int(QOS_CLASS_USER_INITIATED.value)
            dispatch_async(dispatch_get_global_queue(qos, 0)) { () -> Void in
                let imageData = NSData(contentsOfURL: url) // this blocks the thread it is on
                dispatch_async(dispatch_get_main_queue()) {
                    // only do something with this image
                    // if the url we fetched is the current imageURL we want
                    // (that might have changed while we were off fetching this one)
                    if url == self.imageURL { // the variable "url" is capture from above
                        if imageData != nil {
                            // this might be a waste of time if our MVC is out of action now
                            // which it might be if someone hit the Back button
                            // or otherwise removed us from split view or navigation controller
                            // while we were off fetching the image
                            self.image = UIImage(data: imageData!)
                        } else {
                            self.image = nil
                        }
                    }
                }
            }
        }
    }

    @IBOutlet private weak var spinner: UIActivityIndicatorView!

    @IBOutlet private weak var scrollView: UIScrollView! {
        didSet {
            scrollView.contentSize = imageView.frame.size // critical to set this!
            scrollView.delegate = self                    // required for zooming
            scrollView.minimumZoomScale = 0.03            // required for zooming
            scrollView.maximumZoomScale = 1.0             // required for zooming
        }
    }

    // UIScrollViewDelegate method
    // required for zooming
    func viewForZoomingInScrollView(scrollView: UIScrollView) -> UIView? {
        return imageView
    }

    private var imageView = UIImageView()

    // convenience computed property
    // lets us get involved every time we set an image in imageView
    // we can do things like resize the imageView,
    //   set the scroll view's contentSize,
    //   and stop the spinner
    private var image: UIImage? {
        get { return imageView.image }
        set {
            imageView.image = newValue
            imageView.sizeToFit()
            scrollView?.contentSize = imageView.frame.size
            spinner?.stopAnimating()
        }
    }

    // put our imageView into the view hierarchy
    // as a subview of the scrollView
    // (will install it into the content area of the scroll view)
    override func viewDidLoad() {
        super.viewDidLoad()
        scrollView.addSubview(imageView)
    }

    // for efficiency, we will only actually fetch the image
    // when we know we are going to be on screen
    override func viewWillAppear(animated: Bool) {
        super.viewWillAppear(animated)
        if image == nil {
            fetchImage()
        }
    }
}
Racil Hilan
  • 24,690
  • 13
  • 50
  • 55
John
  • 465
  • 6
  • 14

1 Answers1

2

The source of the issue that decompression image from data (file format representative of image data) to screen can 'eat' a lot of memory. Here is very good article about iOS image decompression -> Avoiding Image Decompression Sickness

Since all images in Cassini application are VERY large (wave_earth_mosaic_3.jpg (9999×9999), pia03883-full.jpg (14400×9600)) image decompression process 'eat' all phone memory. This leads to application crash.

To fix Cassini issue I modified code and added small function to lower images resolution by 2.

Here is code example (code fixed to Swift 2.0):

     ...
       if imageData != nil {
                        // this might be a waste of time if our MVC is out of action now
                        // which it might be if someone hit the Back button
                        // or otherwise removed us from split view or navigation controller
                        // while we were off fetching the image
                        if let imageSource = UIImage(data: imageData!) {
                            self.image = self.imageResize(imageSource)
                        }
                    } else {
                        self.image = nil
                    }
   ...

   func imageResize (imageOriginal:UIImage) -> UIImage {
    let image = imageOriginal.CGImage

    let width = CGImageGetWidth(image) / 2
    let height = CGImageGetHeight(image) / 2
    let bitsPerComponent = CGImageGetBitsPerComponent(image)
    let bytesPerRow = CGImageGetBytesPerRow(image)
    let colorSpace = CGImageGetColorSpace(image)
    let bitmapInfo = CGImageGetBitmapInfo(image)

    let context = CGBitmapContextCreate(nil, width, height, bitsPerComponent, bytesPerRow, colorSpace, bitmapInfo.rawValue)

    CGContextSetInterpolationQuality(context, CGInterpolationQuality.High)

    CGContextDrawImage(context, CGRect(origin: CGPointZero, size: CGSize(width: CGFloat(width), height: CGFloat(height))), image)

    let scaledImage = UIImage(CGImage: CGBitmapContextCreateImage(context)!)

    return scaledImage
}

So now application load all images without crash.

SWIFT 2.0 fix:

add this to Info.plist to allow HTTP loading

<key>NSAppTransportSecurity</key>
<dict>
  <!--Include to allow all connections (DANGER)-->
  <key>NSAllowsArbitraryLoads</key>
      <true/>
</dict>
CTiPKA
  • 2,944
  • 1
  • 24
  • 27
  • this lets the app run on the device, thank you very much for the linking to the page explanation - but this seem to be a temporary fix, what if I wanted the full resolution image? is that not doable? – John Sep 15 '15 at 20:34
  • You are right. You like just do not 'want' to show so large images on device. – CTiPKA Sep 15 '15 at 22:24
  • @John, if you want to learn more about displaying enormous images this [WWDC video](https://developer.apple.com/videos/play/wwdc2011-104/) should help you (starting from ~43:40 where they go about it in detail, but the whole session is awesome) - and in general [WWDC](https://developer.apple.com/videos/) is a great source of knowledge. – Islam Jan 01 '16 at 13:15