I am building an App that fetches photos via nasa api and displays them in a collectionView. I am also trying to allow the user to favorite any photo they want by clicking a favorite button. Every time I click the favorites button to favorite a photo, all the ones that I have favorited prior keeps getting reinserted into the tableView over and over and that’s the problem. The tableView keeps inserting the same photo and its metadata more than . This is really bizarre because I made sure the data model for the favorites viewcontroller isn’t duplicated. I checked the array that holds the data model and there aren’t any duplicates. In the FavoritesViewController(), I made sure that the photo object being passed isn’t inserted into the data model array if it’s already in there by comparing the their URLS, and this seems to be working. I read many similar questions on here but I couldn’t find any similar to mine.
the three viewcontrollers involved are as follows:
- HomeViewController(): All this vc does is show the latest picture from the api and its metadata and allow the user to click a favorites button to add it in favoritesViewController() using a tableView.
2: FavoritesViewController(): This one displays the photos that the user favorited in a tableView
3: PicturesOftheDayViewController(): This one displays all the photos in a grid using collectionViews.
For the life of me, I can’t figure out why the tableView would duplicate entries when that viewcontroller has access to one photo at a time. I read many questions here where responders were saying it might the way the tableView is dequeing its cell but none of their solutions worked for me.
Will post the code below. It’s rather long but keep a close eye on the favsButtonTapped() method, and the favoritesViewController() as well as the tableDataSource object. Or maybe I am breaking something when I switch between the viewController using the tabBarController’s selectedIndex?
HomeViewController()
import UIKit
import JGProgressHUD
class HomeViewController: UIViewController {
let userDefaults = UserDefaults.standard
var isDataBeingPassed = false
var photo: Photos!
{
didSet {
updateImageView(for: photo)
}
}
var photos = [Photos]()
var store: PhotoStore!
let scrollView: UIScrollView = {
let scrollView = UIScrollView()
scrollView.translatesAutoresizingMaskIntoConstraints = false
return scrollView
}()
private let contentView: UIView = {
let contentView = UIView()
contentView.translatesAutoresizingMaskIntoConstraints = false
return contentView
}()
var imageView: UIImageView = {
let imageview = UIImageView()
imageview.translatesAutoresizingMaskIntoConstraints = false
imageview.contentMode = .scaleAspectFill
imageview.layer.masksToBounds = true
return imageview
}()
private var titleLabel: UILabel = {
let label = UILabel()
label.translatesAutoresizingMaskIntoConstraints = false
label.numberOfLines = 0
label.font = .systemFont(ofSize: 24, weight: .semibold)
label.backgroundColor = .tertiarySystemFill
return label
}()
private var descriptionTextView: UITextView = {
let textview = UITextView()
textview.translatesAutoresizingMaskIntoConstraints = false
textview.font = .systemFont(ofSize: 21)
textview.textAlignment = .justified
textview.textColor = .white
textview.isEditable = false
textview.backgroundColor = .black
return textview
}()
override func loadView() {
super.loadView()
}
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .systemBackground
navigationController?.navigationBar.prefersLargeTitles = true
addSubviews()
showHUD()
constraints()
//MARK: - navigation buttons
navigationItem.leftBarButtonItem = UIBarButtonItem(image: UIImage(systemName: "calendar"),
style: .plain,
target: self,
action: #selector(dateButtonTapped(_:)))
navigationItem.rightBarButtonItem = UIBarButtonItem(image: UIImage(systemName: "star"),
style: .plain,
target: self,
action: #selector(favsButtonTapped(_:)))
navigationItem.leftBarButtonItem?.tintColor = .systemRed
navigationItem.rightBarButtonItem?.tintColor = .systemRed
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
if !isDataBeingPassed {
fetchPhotoFromAPI()
}
}
func fetchPhotoFromAPI() {
store.fetchPhotosFromNasa {[weak self] photoResult in
switch photoResult {
case let .success(photos):
self?.photos = photos
if let lastPhoto = photos.last {
print(lastPhoto.date)
// self?.updateImageView(for: lastPhoto)
self?.photo = lastPhoto
}
case let .failure(error):
print("error fetching interesting photos (error)")
}
}
}
func updateImageView(for photo: Photos) {
//first convert the string date to Date() object
guard let convertedDate = DateFormatters.shared.inputDateFormatter.date(from: photo.date) else {
return
}
let formattedDate = DateFormatters.shared.outPutDateFormatter.string(from: convertedDate)
store.fetchImage(for: photo){[weak self] (imageResult) in
switch imageResult {
case let .success(image):
self?.imageView.image = image
self?.navigationItem.title = formattedDate
self?.titleLabel.text = photo.title
self?.descriptionTextView.text = photo.explanation
case let .failure(error):
print("Error downloading image: (error)")
}
}
}
func showHUD() {
let hud = JGProgressHUD()
hud.textLabel.text = "Loading"
hud.show(in: view)
hud.dismiss(afterDelay: 3.2)
}
@objc func dateButtonTapped(_ sender: UIBarButtonItem) {
//configure and present data picker
}
@objc func favsButtonTapped(_ sender: UIBarButtonItem) {
if let navVC = self.tabBarController?.viewControllers?[2] as? UINavigationController {
let favoritesVC = navVC.topViewController as! FavoritesViewController
favoritesVC.photo = self.photo
favoritesVC.store = self.store
}
}
}
//MARK: - configure views and constraints
extension HomeViewController {
func constraints() {
let heightConstraint = contentView.heightAnchor.constraint(equalTo: scrollView.heightAnchor)
heightConstraint.priority = UILayoutPriority(250)
NSLayoutConstraint.activate([
scrollView.topAnchor.constraint(equalTo: view.layoutMarginsGuide.topAnchor),
scrollView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
scrollView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
scrollView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
contentView.topAnchor.constraint(equalTo: scrollView.topAnchor),
contentView.bottomAnchor.constraint(equalTo: scrollView.bottomAnchor),
contentView.leadingAnchor.constraint(equalTo: scrollView.leadingAnchor),
contentView.trailingAnchor.constraint(equalTo: scrollView.trailingAnchor),
contentView.widthAnchor.constraint(equalTo: scrollView.widthAnchor),
heightConstraint,
//
imageView.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 10),
imageView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor),
imageView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor),
imageView.heightAnchor.constraint(equalTo: contentView.heightAnchor, multiplier: 0.3),
titleLabel.leadingAnchor.constraint(equalTo: contentView.leadingAnchor),
titleLabel.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -40),
titleLabel.topAnchor.constraint(equalTo: imageView.bottomAnchor, constant: 10),
titleLabel.heightAnchor.constraint(equalToConstant: 70),
descriptionTextView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor),
descriptionTextView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor),
descriptionTextView.topAnchor.constraint(equalTo: titleLabel.bottomAnchor, constant: 10),
descriptionTextView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -10),
])
}
func addSubviews() {
view.addSubview(scrollView)
scrollView.addSubview(contentView)
contentView.addSubview(imageView)
contentView.addSubview(titleLabel)
contentView.addSubview(descriptionTextView)
}
}
FavoritesViewController()
import UIKit
import JGProgressHUD
class FavoritesViewController: UIViewController, UITableViewDelegate {
var store: PhotoStore!
// {
// didSet {
// tableDatSource.store = store
//
// }
// }
var photo: Photos!
// {
// didSet {
// tableDatSource.photos.append(photo)
// tableDatSource.photo = photo
// }
// }
var tableView: UITableView?
let tableDatSource = TableDataSource()
let tableview: UITableView = {
let table = UITableView(frame: .zero, style: .grouped)
// table.translatesAutoresizingMaskIntoConstraints = false
table.register(CustomTableViewCell.self, forCellReuseIdentifier: CustomTableViewCell.identifier)
return table
}()
//MARK: - lifecycle
override func loadView() {
super.loadView()
setupTableView()
}
override func viewDidLoad() {
super.viewDidLoad()
title = "Favorites"
view.backgroundColor = .systemBackground
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
if let store = store, let photo = photo {
if !tableDatSource.photos.contains(where: {$0.url == photo.url}) {
tableDatSource.photos.append(photo)
tableDatSource.store = store
tableview.reloadData()
}
}
}
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
print("the section titles is (tableDatSource.sectionTitles.count)")
}
//MARK: - Table
func setupTableView() {
view.addSubview(tableview)
tableview.dataSource = tableDatSource
tableview.delegate = self
tableview.translatesAutoresizingMaskIntoConstraints = false
self.tableView = tableview
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
self.tableView?.frame = view.bounds
}
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
return 95.0
}
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
tableView.deselectRow(at: indexPath, animated: true)
}
}
PicturesOftheDayViewController()
import UIKit
class PictureOfTheDayViewController: UIViewController, UICollectionViewDelegate {
var completion: ((Photos) -> Void)?
var photodatasource = PhotoDataSource()
var tabledatasource = TableDataSource()
var collectionView: UICollectionView?
var store: PhotoStore!
let dateFormatter: DateFormatter = {
let df = DateFormatter()
df.dateStyle = .medium
df.timeZone = .current
return df
}()
//MARK: - lifecycle
override func loadView() {
super.loadView()
view.backgroundColor = .secondarySystemBackground
navigationItem.title = dateFormatter.string(from: Date())
}
override func viewDidLoad() {
view.backgroundColor = .systemBackground
super.viewDidLoad()
configureCollectionView()
//network stuff
store.fetchPhotosFromNasa {[weak self](photoResult) in
switch photoResult {
case let .success(photo):
print("successfully FOUND AND FOUND: (photo.count)")
self?.photodatasource.photos = photo
// self?.tabledatasource.photos = photo
case let .failure(error):
print("error fetching images from nasa: (error)")
self?.photodatasource.photos.removeAll()
}
self?.collectionView?.reloadSections(IndexSet(integer: 0))
}
}
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
collectionView?.frame = view.bounds
}
//MARK: - collectionview
func configureCollectionView() {
let layout = UICollectionViewFlowLayout()
layout.scrollDirection = .vertical
layout.minimumLineSpacing = 2
layout.minimumInteritemSpacing = 0
layout.itemSize = CGSize(width: view.frame.size.width/4, height: view.frame.size.height/7)
let collectionview = UICollectionView(frame: .zero, collectionViewLayout: layout)
view.addSubview(collectionview)
collectionview.delegate = self
collectionview.dataSource = photodatasource
collectionview.translatesAutoresizingMaskIntoConstraints = false
collectionview.register(PhotoCollectionViewCell.self, forCellWithReuseIdentifier: PhotoCollectionViewCell.identifier)
self.collectionView = collectionview
}
func collectionView(_ collectionView: UICollectionView, willDisplay cell: UICollectionViewCell, forItemAt indexPath: IndexPath) {
let photo = photodatasource.photos[indexPath.row]
//download the image data which could take sometime
store.fetchImage(for: photo) {[weak self] (result) -> Void in
//the index path for the photo might have changed between the time the request started and finishes, so find the most recent indexPath
guard let photoIndex = self?.photodatasource.photos.firstIndex(of: photo), case let .success(image) = result else {
return
}
let photoIndexPath = IndexPath(item: photoIndex, section: 0)
//when the request finishes, find the current cell for this photo
if let cell = self?.collectionView?.cellForItem(at: photoIndexPath) as? PhotoCollectionViewCell {
cell.update(displaying: image)
}
}
}
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
if let selectedIndexPath = collectionView.indexPathsForSelectedItems?.first {
let photo = photodatasource.photos[selectedIndexPath.row]
if let navVC = self.tabBarController?.viewControllers?[0] as? UINavigationController {
let homeVC = navVC.topViewController as! HomeViewController
homeVC.isDataBeingPassed = true
if homeVC.photo.url != photo.url{
homeVC.photo = photo
}
homeVC.store = self.store
tabBarController?.selectedIndex = 0
}
}
}
}
import UIKit
class TableDataSource: NSObject, UITableViewDataSource {
var store: PhotoStore!
var photos: [Photos] = [] {
didSet {
for photo in photos {
if sections[photo.date] == nil {
sections[photo.date] = []
}
sections[photo.date]?.append(photo)
}
}
}
var sections: [String: [Photos]] = [:]
var sectionTitles: [String] {
return sections.keys.sorted()
}
func numberOfSections(in tableView: UITableView) -> Int {
return sectionTitles.count
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
let date = sectionTitles[section]
return sections[date]?.count ?? 0
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: CustomTableViewCell.identifier, for: indexPath) as! CustomTableViewCell
// let photo = photos[indexPath.row]
let date = sectionTitles[indexPath.section]
guard let photo = sections[date]?[indexPath.row] else {
fatalError()
}
cell.configure(with: photo)
// guard let imageURL = photo.url else {
// fatalError("Something is wrong with your URL")
// }
store.fetchImage(for: photo) {(result) in
switch result {
case let .success(image):
if tableView.indexPath(for: cell) == indexPath {
cell.CellimageView.image = image
}
case let .failure(error):
print("error getting image for tabledata source(error)")
}
}
return cell
}
func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
return sectionTitles[section]
}
}
Rather long but I’d appreciate any help.
I tried to make sure that the data model array isn’t duplicated.
I tried sending one photo object at a time.
I set the all the subviews inside the tableView to nil in prepareForReuse() method.
Ahmed Abdi is a new contributor to this site. Take care in asking for clarification, commenting, and answering.
Check out our Code of Conduct.