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:
… 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.
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?
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.
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.
Strange, do you have your code somewhere I/we can access it?
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)
Could it be you do not check if image is nil before you try to unwrap it?
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)
}
}
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.
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 ofmax(...)
.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.
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 …
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
Try to use viewDidLayoutSubviews instead. You use WillLayout in combination with animation.
Unfortunately I get the same behavior but i’ll continue to investigate 😉
Thank you for the reply, very kind!
Alessandro
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?
… 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 …
Yup, i also passed the fetched image from the mention VC to the image VC and it works..
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
… it should be handled by calling
autoScale()
fromviewDidLayoutSubviews()