I am building an app that fetches pictures from a nasa API and then shows it a CollectionView as well as in a UITableView. My app is driven by a UITabBarController which has an array of three view controllers.
HomeViewController((): This is where the app defaults to when it initially launches, this viewcontroller shows the latest picture retrieved from the API, i.e today’s date. I have a button here that when clicked, should send the latest photo being displayed to the FavoriteViewController(), but without presenting it that viewcontroller. And so far it only works if I also present this VC, and if I don’t, the data isn’t being sent.
BrowseViewController(): this is the second view controller in the UITabBar’s array of view controllers and this is where I use collectionViews to show all of the images in a grid layout. I have no issues here.
FavoritesViewController(): This is the last view controller in the array and it’s of type UITableViewController. What I want to do here is display the photo’s title, the date it was taken and the photo itself. The only problem is I am unable to pass the photo from the HomeViewController()
I tried using a closure aka a completion handler, I tried posting a NotificationsCenter, and to no avail. I have been told to use a singleton approach but I am trying to avoid that as I am sure there’s a cleaner way to do this. Thanks
By the way, I am not using storyboards at all.
This is the Photos object
import UIKit
struct Photos: Codable {
let date: String
let explanation: String
let title: String
let url: URL?
let media_type: String
}
extension Photos: Equatable {
static func == (lhs: Photos, rhs: Photos) -> Bool {
return lhs.url == rhs.url
}
}
enum PhotoError: Error {
case imageCreationError
case missingImageURL
}
This is the HomeViewController()
import UIKit
import JGProgressHUD
class HomeViewController: UIViewController {
var photo: Photos!
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
}()
var favsButton: UIButton = {
let button = UIButton()
button.translatesAutoresizingMaskIntoConstraints = false
button.setImage(UIImage(systemName: "heart"), for: .normal)
return button
}()
var dateButton: UIButton = {
let button = UIButton()
button.translatesAutoresizingMaskIntoConstraints = false
button.setImage(UIImage(systemName: "star"), for: .normal)
button.backgroundColor = .systemGreen
return button
}()
override func loadView() {
super.loadView()
}
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .systemBackground
navigationController?.navigationBar.prefersLargeTitles = true
addSubviews()
showHUD()
fetchPhotoFromAPI()
constraints()
//MARK: - navigation buttons
navigationItem.leftBarButtonItem = UIBarButtonItem(image: UIImage(systemName: "calendar"),
style: .plain,
target: self,
action: #selector(dateButtonTapped(_:)))
navigationItem.leftBarButtonItem?.tintColor = .systemRed
navigationItem.rightBarButtonItem?.tintColor = .systemRed
}
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: UIButton) {
//The notifications approach
// NotificationCenter.default.post(name: Notification.Name("photo"), object: photo)
//also tried this approach
let vc = FavoritesViewController()
vc.store = self.store
vc.photo = self.photo
//but without presenting it like you normally would
}
}
//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),
dateButton.leadingAnchor.constraint(equalTo: titleLabel.trailingAnchor, constant: 5),
dateButton.topAnchor.constraint(equalTo: titleLabel.topAnchor, constant: 10),
dateButton.widthAnchor.constraint(equalToConstant: 35),
dateButton.heightAnchor.constraint(equalToConstant: 45),
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)
contentView.addSubview(dateButton)
dateButton.addTarget(self, action: #selector(favsButtonTapped(_:)), for: .touchUpInside)
}
}
and this is the FavoriteVC
import UIKit
import JGProgressHUD
class FavoritesViewController: UIViewController, UITableViewDelegate {
var observer: NSObjectProtocol?
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()
tableview.reloadData()
}
override func viewDidLoad() {
super.viewDidLoad()
title = "Favorites"
view.backgroundColor = .systemBackground
observer = NotificationCenter.default.addObserver(forName: Notification.Name("photo"),
object: nil, queue: .main,
using: {[weak self] notification in
guard let photo = notification.object as? Photos else{return}
self?.photo = photo
})
print("Received photo and it's (photo)")
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
}
//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)
}
I tried using three different approaches to pass along the photo.
1: I tried posting a notifications in the sender viewcontroller() and an observer in the receiver.
2: a Completion handler.
3: I also tried to instantiate the receiver viewconteroller() inside the function in which the tap event takes place.
All didn’t work.
2
Answers
Below is what you will follow.
In HomeViewController, when click on Favorite button, you will save this photo data to local database (You can use CoreData)
In FavoriteViewController, in viewWillAppear you will read data from this local database and show in the UITableView or UICollectionView
And you are done…
Don’t store data to
NSUserDefaults
as if data is increased, it will lower the performance and hence local database like CoreData OR SQL Lite should be used.NotificationCenter is not useful because still you didn’t open Favorite, so listener that you have added will not work. If you go to FavoriteViewController once, your code will work.
Let me know if you have any query.
Your mistake is here:
When you say
FavoritesViewController()
you are telling the class to create a new instance of the FavoritesViewController. This is wrong. There is already an instance of the FavoritesViewController; it is the third child of the tab bar controller. And that is the instance you want to talk to right now.So what you need to do is:
There is absolutely no need for the FavoritesViewController to be displayed in order to update its view’s interface with the new favorite. The user will see it if and when the user switches to that third tab.