Photo App demo additions

I have been going through the Code With Chris Database course and just finished the Photo App. I have been attempting some additions on my own and have been hitting some roadblocks.

As with most photo apps, the option to tap on the photo from a feed and see the whole photo is common. I tried to implement design from the News app demo earlier in the course, but haven’t been able to get this to work.

I would love to see a tutorial on how to open a tapped photo into it’s own full screen view with navigation bars present, and to be able to zoom in/out using multitouch, and it would also be nice to be able to swipe left/right to navigate through the other photos in the feed without having to go back to the main feed.

So far I created a new view controller called PhotoDetailViewController and placed an edge-to-edge UIScrollView containing a new UIImageView. I created a segue from the FeedViewController to the new PhotoDetailViewController.

In FeedViewController.swift I imported SDWebImage and added the following override method:

override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
        
        // Get the photo which the user tapped on
        let indexPath = tableView.indexPathForSelectedRow
        
        guard indexPath != nil else {
            print("User didn't select a photo")
            return
        }
        
        let photo = photos[(indexPath!.row)]
        // let photoId = photo.photoId
        
        
        // Get a reference to the PhotoDetailViewController
        let photoDetailVC = segue.destination as! PhotoDetailViewController
        
        // Set the imageData / imageURL property of the PhotoDetailViewController
        if let urlString = photo.url {
            let url = URL(string: urlString)
            
            guard url != nil else {
                print("Couldn't create URL object")
                return
            }

            // Set detailed image view to selected image <-- GETTING ERROR HERE
            photoDetailVC.imageView.sd_setImage(with: url) { (image, error, cacheType, url) in
                if error == nil {
                    photoDetailVC.imageView.image = image
                }
            }
        }            
    }

My PhotoDetailViewController.swift code is as follows:

import UIKit

class PhotoDetailViewController: UIViewController {

    @IBOutlet weak var scrollView: UIScrollView!
    @IBOutlet weak var imageView: UIImageView!
    
    var imageURL: String?
    var imageData: UIImage?
    
    override func viewDidLoad() {
        super.viewDidLoad()
        self.navigationController?.isNavigationBarHidden = false
        
        scrollView.minimumZoomScale = 1.0
        scrollView.maximumZoomScale = 6.0
        
        scrollView.delegate = self
        
    }
    
}

extension PhotoDetailViewController: UIScrollViewDelegate {
    
    func viewForZooming(in scrollView: UIScrollView) -> UIView? {
        
        return imageView
    }
}

Unfortunately I’m not getting desired results. In the code above I noted where I am hitting an error, and when I step through I noted that the url is passing correctly for the selected item, but the sd_setImage(with: url) { (image, error, cacheType, url)… is giving me problems.

If I remove that line of code, the view segues to a blank view controller, with no navigation bar present to go back.

Any help would be appreciated. I am definitely new to swift and trying to work through on my own, but not sure where to go from here. Thanks for any input!

Progress!

A key component that I forgot was to make the new view controller the root:

self.view.window?.rootViewController = photoDetailVC

By placing this before the line I was getting an error on, the error went away, and the selected photo now displays! The pinch to zoom in/out functionality also works, and it’s scrollable when zoomed in. Yay!

I also added the following, but leaving it off doesn’t seem to make a difference:

self.view.window?.makeKeyAndVisible()

Also coded in a Double-Tap gesture to zoom in/out…

Within the PhotoDetailViewController.swift, add the following to override func viewDidLoad:

let doubleTapGesture = UITapGestureRecognizer(target: self, action: #selector(handleDoubleTapScrollView(sender:)))
    doubleTapGesture.numberOfTapsRequired = 2
    scrollView.addGestureRecognizer(doubleTapGesture)

…and add the following two methods outside the viewDidLoad:

@objc func handleDoubleTapScrollView(sender: UITapGestureRecognizer) {
    if sender.state == .ended {
        if scrollView.zoomScale == 1 {
            scrollView.zoom(to: zoomRectForScale(scale: scrollView.maximumZoomScale, center: sender.location(in: sender.view)), animated: true)
        }
        else {
            scrollView.setZoomScale(1, animated: true)
        }
    }
}

func zoomRectForScale(scale: CGFloat, center: CGPoint) -> CGRect {
    var zoomRect = CGRect.zero
    zoomRect.size.height = imageView.frame.size.height / scale
    zoomRect.size.width  = imageView.frame.size.width / scale
    let newCenter = scrollView.convert(center, from: imageView)
    zoomRect.origin.x = newCenter.x - (zoomRect.size.width / 2.0)
    zoomRect.origin.y = newCenter.y - (zoomRect.size.height / 2.0)
    return zoomRect
}

Looking great, however…
A key problem that still exists is the lack of a navigation bar to go back. Currently, soon as I tap an image in the feed, it takes me to a full screen image, and have no way to get back or change the image without stopping the run or force closing the app…

I have tried forcing it on with code, and have also set it to be displayed in the Main.storyboard. I have also made sure my scroll view constraints don’t cover it for any reason. I initially did not have a Navigation Controller scene and thought that may be the issue, but I added one with a “Show segue” pointing to my Main Tab Bar Controller scene, and still no luck.

If anyone has any suggestions I’d love to try them out! Thanks!

Hmm, I would need to play around with that lesson again.

Did you embed in a navigation controller? Editor > Embed In > Navigation Controller
It should automatically handle adding a back button on the top bar, unless you have an object that covers that safe area.

Blessings,
—Mark

Thanks for the reply!

So, I did try the embed in navigation controller option… ended up not working and gave me more errors. Part of the issue seems to be that there is already a Tab View Controller feeding the tab screens I have (which the Feed is one of). Tried placing the Navigation Controller before the Tab Bar Controller, as well as in between the Tab Bar Controller and the Feed VC, and between the Feed VC and the Photo Detail VC. None of these really worked as expected.

A work around was to just place a UIButton on the PhotoDetailViewController in the upper left and assign:

@IBAction func backTapped(_ sender: UIButton) {
    dismiss(animated: true, completion: nil)
}

This worked very simply.

Added some additional functionality by setting up a swipe gesture to call that same dismiss code when the user swipes from the left of the screen. Both work, but the swipe code I wrote did not give that nice visual of the page sliding with your finger… the swipe itself (while swiping) had no animation, and the transition kicks in after the swipe is completed.

I do love the ability in most apps to swipe from the left and see the page your on slide to the right under your finger as the page beneath becomes displayed. If anyone has tips on how to incorporate that, I’d love to give it a try.

Oops, sorry, yea, I would have expected that I have not done that lesson so I did not realize it uses a tab UI.

In terns of swiping left to dismiss, you can try this:

In viewDidLoad add:
let swipeLeft = UISwipeGestureRecognizer(target: self , action: #selector (ViewController.swipeGestures))
swipeLeft.direction = UISwipeGestureRecognizer.Direction.left
self.view.addGestureRecognizer(swipeLeft)

Then add a function:

@objc func swipeGestures(sender: UISwipeGestureRecognizer) {

     if sender.direction == .left {
            dismiss(animated: true, completion: nil)
             } 
} // end swipeGesture func

Good luck.
Blessings,
—Mark