Silver Challenge - crashes when implementing UISegmentedControl

I decided to implement a UISegmentedControl to switch from “Interesting” to “Recent” Flickr photos.

It builds successfully, but when I ran the project just to see the segmented control, it crashed.

The log doesn’t display much but I uncovered this error:
Thread 1: EXC_BAD_ACCESS (code=2, address=0x7ffeeb8afc98)

I simply grafted the code from WorldTrotter into PhotosViewController.swift:

override func loadView() {
    
    
    let interestingString = NSLocalizedString("Interesting", comment: "Interesting photos")
    let recentString = NSLocalizedString("Recent", comment: "Most recent photos")
    
    let segmentedControl = UISegmentedControl(items: [interestingString, recentString])
    // runtime error points me here, but without showing anything explicit in the console log
    
    segmentedControl.backgroundColor = UIColor.systemBackground
    segmentedControl.selectedSegmentIndex = 0
    
   // segmentedControl.addTarget(self, action: #selector(mapTypeChanged(_:)), for: .valueChanged) not ready to implement this yet...unless this is what's crashing it. But I suppose here is where I toggle between my "interesting" and "recent" functions
    
    
    segmentedControl.translatesAutoresizingMaskIntoConstraints = false
    view.addSubview(segmentedControl)
    
    let topConstraint = segmentedControl.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 8)
    // using the layout margins of the photo view
    let margins = view.layoutMarginsGuide
    let leadingConstraint = segmentedControl.leadingAnchor.constraint(equalTo: margins.leadingAnchor)
    let trailingConstraint = segmentedControl.trailingAnchor.constraint(equalTo: margins.trailingAnchor)
    
    // activating the programmatic layout constraints
    topConstraint.isActive = true
    leadingConstraint.isActive = true
    trailingConstraint.isActive = true
}

What could cause the crash? And have others considered other ways to implement
switching between the different types of photo sets?

It looks to me like when you try to add the segmented control to the view, it’s causing loadView to get called again, and you end up in an infinite recursion of loadView calls. I’m not sure why. It might be that the navigation control is conflicting with the segmented control in some way. In WorldTrotter, the segmented control was added to the map view; here it’s being added to the default top view, which in this case might be the navigation controller.

You might need to add the segmented control to the Photorama scene using the storyboard rather than doing it programmatically.

I chose to do this by adding a button to the navigation controller.

Alright, I went with a Segmented Control:

However, while I could pull up Recent photos after replacing the function from the book with a custom function I adapted for Flickr’s recent photos, the segmented control doesn’t load a recent image when I switch to it.

FlickrAPI.swift:

enum EndPoint: String {
      case interestingPhotos = "flickr.interestingness.getList"
      case recentPhotos = "flickr.photos.getRecent" 
}

struct FlickrAPI {
          // Silver challenge - expose a URL for recent photos
      static var recentPhotosURL: URL {
           return flickrURL(endPoint: .recentPhotos, parameters: ["extras": "url_z,date_taken"])
      }

}

PhotosViewController.swift:

@IBOutlet weak var segmentedControl: UISegmentedControl! // Silver challenge

override func viewDidLoad() {
    super.viewDidLoad()
    ...
    // Silver challenge
    switch segmentedControl.selectedSegmentIndex {
    case 0:
        // fetch interesting photos
        store.fetchInterestingPhotos {
            (photosResult) in
            
            switch photosResult {
            case let .success(photos):
                print("Successfully found \(photos.count) photos.")
                // listing 20.34 p 414 - show the first photo
                if let firstInterestingPhoto = photos.first {
                    self.updateImageView(for: firstInterestingPhoto)
                }

            case let .failure(error):
                print("Error fetching interesting photos: \(error)")
            }
        }
    default:
        // fetch recent photos
        store.fetchRecentPhotos {
            (photosResult) in
            
            switch photosResult {
            case let .success(photos):
                print("Successfully found \(photos.count) photos.")
                // listing 20.34 p 414 - show the first photo
                if let firstRecentPhoto = photos.first {
                    self.updateImageView(for: firstRecentPhoto)
                }

            case let .failure(error):
                print("Error fetching interesting photos: \(error)")
            }
        }
    }
    
}

Do I have to do something programmatically to refresh the UIImageView? Or did I forget to do something else?

Yes. If you go back & look at WorldTrotter, there was a mapTypeChanged function, and in loadView that function was connected to the segmented control. Every time the segmented control selection changed, the function would get called to update the map to the selected type.

You’ll need to do something similar here & provide a function that the segmented control can call which will display the selected photograph type.

Okay, I got it to work the way I wanted. Thanks again, Jon.

However, I’ll need to return to this and refactor it because I’m repeating store.fetchInterestingPhotos so that an interesting photo appears when I first load the app:

PhotosViewController.swift

override func viewDidLoad() {
    super.viewDidLoad()
    
    // listing 20.15 p401 - initiating the web service request
    store.fetchInterestingPhotos {
        (photosResult) in
        
        switch photosResult {
        case let .success(photos):
            print("Successfully found \(photos.count) photos.")
            // listing 20.34 p 414 - show the first photo
            if let firstInterestingPhoto = photos.first {
                self.updateImageView(for: firstInterestingPhoto)
            }

        case let .failure(error):
            print("Error fetching interesting photos: \(error)")
        }
    }
    
    // Silver challenge
    segmentedControl.addTarget(self, action: #selector(photoTypeChanged(_:)), for: .valueChanged)
    
}

...
// for Silver Challenge:
@objc func photoTypeChanged(_ segControl: UISegmentedControl) {
    switch segControl.selectedSegmentIndex {
    case 0:
        // fetch interesting photos
        store.fetchInterestingPhotos {
            (photosResult) in
            
            switch photosResult {
            case let .success(photos):
                print("Successfully found \(photos.count) photos.")
                // listing 20.34 p 414 - show the first photo
                if let firstInterestingPhoto = photos.first {
                    self.updateImageView(for: firstInterestingPhoto)
                }

            case let .failure(error):
                print("Error fetching interesting photos: \(error)")
            }
        }
        
    default:
        // fetch recent photos
        store.fetchRecentPhotos {
            (photosResult) in
            
            switch photosResult {
            case let .success(photos):
                print("Successfully found \(photos.count) photos.")
                // listing 20.34 p 414 - show the first photo
                if let firstRecentPhoto = photos.first {
                    self.updateImageView(for: firstRecentPhoto)
                }

            case let .failure(error):
                print("Error fetching interesting photos: \(error)")
            }
        }
    }
}

Yep, that’s what I did.