cs193p – Project #4 Assignment #4 Task #7

By Franz Bley & H. Berdrow ("Botanisches Bilderbuch für Jung und Alt") [Public domain], via Wikimedia Commons

Please note, this blog entry is from a previous course. You might want to check out the current one.

If the user clicks on an image in your newly created view controller, segue to a new MVC which lets the user scroll around and zoom in on the image. When the image first appears in the MVC, it should display zoomed (in its normal aspect ratio) to show as much of the image as possible but with no “whitespace” around it.

Copy the image-view controller from the storyboard of the Cassini project to the smashtag storyboard and create a segue from the image-table-view cell to the new view controller:

cs193p - Project #4 Assignment #4 Task #7 - image view controller
cs193p – Project #4 Assignment #4 Task #7 – image view controller


… don’t forget to copy the class file, too.

When preparing the new segue, set the image url and the title of the image:

    private struct Storyboard {
        ...
        static let ImageSegueIdentifier = "Show Image"
    }

    override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
        ...
            } else if identifier == Storyboard.ImageSegueIdentifier {
                if let ivc = segue.destinationViewController as? ImageViewController {
                    if let cell = sender as? ImageTableViewCell {
                        ivc.imageURL = cell.imageUrl
                        ivc.title = title
                    }
                }
            }
        }
    }

To know whether the image view has already been zoomed or scrolled, create a boolean property to control if the scale should be adjusted automatically:

    private var scrollViewDidScrollOrZoom = false
    
    private func autoScale() {
        if scrollViewDidScrollOrZoom {
            return
        }
        if let sv = scrollView {
            if image != nil {
                sv.zoomScale = max(sv.bounds.size.height / image!.size.height,
                    sv.bounds.size.width / image!.size.width)
                sv.contentOffset = CGPoint(x: (imageView.frame.size.width - sv.frame.size.width) / 2,
                    y: (imageView.frame.size.height - sv.frame.size.height) / 2)
                scrollViewDidScrollOrZoom = false
            }
        }
    }

When the image has finished loading, try to scale it:

    private var image: UIImage? {
        get { return imageView.image }
        set {
            ...
            scrollViewDidScrollOrZoom = false
            autoScale()
        }
    }

… and when the device has rotated:

    override func viewDidLayoutSubviews() {
        super.viewDidLayoutSubviews()
        autoScale()
    }

To know when the image has been scrolled or zoomed, use the delegate methods of the scroll view:

    func scrollViewDidScroll(scrollView: UIScrollView) {
        scrollViewDidScrollOrZoom = true
    }
    
    func scrollViewDidZoom(scrollView: UIScrollView) {
        scrollViewDidScrollOrZoom = true
    }

Update: It might be better to use scrollViewDidEndZooming and an equivalent for scrolling …

The complete code for task #7 is available on GitHub.

FacebooktwitterredditpinterestlinkedintumblrmailFacebooktwitterredditpinterestlinkedintumblrmail

18 thoughts on “cs193p – Project #4 Assignment #4 Task #7”

  1. Hi,

    I’ve been working on this for a while now. I got it mainly working, but I’ve got a bug that crashes the app with EXC_BAD_ACCESS (code = EXC_I386_GPFLT) when segueing back from the ImageViewController. It seems like I can prevent it by scrolling to the top of the image before segueing. I tried to compare your code to mine, and I couldn’t find any big differences. Any ideas what causes the bug?

    1. Try to set a breakpoint before it crashes and then step through the comments.

      Are your really segueing back using a dedicated segue? You should not do that. Either use the intrinsic navigator view controller functionality, or use an unwind segue.

      1. I might have used a wrong word there. I’m using the “back button” in the navigation bar, which is created automatically. I also noticed that commenting out sv.zoomScale line will prevent the crash.

        1. Having the same experience.
          1) Using the Navigation Controller’s “back” button to get back from the Image View Controller
          2) App stops crashing when I comment-out the following line of code:
          sv.zoomScale = max(sv.bounds.size.height / image!.size.height,
          sv.bounds.size.width / image!.size.width)

          1. Yeah, I’m doing the null checks. I pasted your method in and it still crashes in the same spot. However, I download your project and it works fine so it’s something that I’m doing wrong.

            In the meantime, I’m using this code which seems to work okay and doesn’t crash the app:

            private func autoScale() {

            if self.scrollView != nil {
            let scaleWidth = self.scrollView.frame.size.width / self.scrollView.contentSize.width
            let scaleHeight = self.scrollView.frame.size.height / self.scrollView.contentSize.height

            let minScale = min(scaleWidth, scaleHeight)
            let maxScale = max(scaleWidth, scaleHeight)

            self.scrollView.minimumZoomScale = minScale
            self.scrollView.maximumZoomScale = maxScale
            self.scrollView.zoomScale = maxScale

            self.scrollView.contentOffset = CGPoint(x: (self.imageView.frame.size.width – self.scrollView.frame.size.width) / 2,
            y: (self.imageView.frame.size.height – self.scrollView.frame.size.height) / 2)
            }
            }

    2. Having the same issue:
      EXC_BAD_ACCESS (code = EXC_I386_GPFLT) when segueing back from the ImageViewController. For me, scrolling to the top of the image before segueing has no effect – crashes every time.

  2. Hi,

    thanks for your work with these tasks!

    One problem I’m having with this one…

    If we tap on an image, it’s supposed to be shown completely, i.e. zoomed out so either the height or the width of the scrollview is filled completely…but your solution doesn’t do that. I think, in autoScale() it should be min(...) instead of max(...).
    Then the image would be completely on screen.

    But it still won’t rescale when I rotate the device, it just keeps the size from portrait mode.

    1. Have a look at the image view controller copied from the Cassini project. Because the images from twitter are quite small, the maximum zoom factor might be too small. I increased it to 5.0 but this might still be to small …

  3. Hi Martin,
    congratulation for your inspiring elegant code, it really helps me.
    I’ve tried to implement the zooming part of the assignment like this:

    if let imageToSize = self.image {
    let imageRect = CGRect(origin: CGPointZero, size: CGSize(width: imageToSize.size.width, height: myImage.size.height))
    scrollView.zoomToRect(imageRect, animated: true)
    }

    and it works as I wanted.
    But if I use the same code in viewWillLayoutSubviews method to adjust the autorotation, the image is resized but displayed at the bottom of the screen when in portrait or at the right when in landscape. (I mean that it doesn’t start from the origin of the view)
    I should be curious to understand what’s wrong whit it: do you have any idea?
    Thanks
    Alessandro

      1. Unfortunately I get the same behavior but i’ll continue to investigate 😉
        Thank you for the reply, very kind!

        Alessandro

  4. While I like the idea of re-using the ImageViewController from Cassini, the program is now downloading the image twice: once to display it in the tweet details, and then a second time for the image view controller. Certainly wasteful if you’re on a slow network.

    Since the tweet details view controller already downloads the image, it can just pass the image as the data model (rather than the URL) to a stripped down ImageViewController. This stripped down controller doesn’t need image fetching or multithreading!

    (Can the user click on a row of the table containing an image before the image has been downloaded? In that case, just don’t make the row selectable!)

    Thoughts?

    1. … not even remembering any more … is it the same picture used in all views? If yes, you are right, downloading at multiple times makes no sense … hopefully I took care of that issue when I added the cache … but I don’t think I did …

  5. Hello,

    Hint number 20 suggests to show the image when the device is rotated to landscape. It also says that solution should not require any code, but everything I found on the internet involved a lot of code. Any thoughts on how to implement this?

    Thanks,
    Alex

Leave a Reply

Your email address will not be published.