How to display an icon/image on uicollectionviewcell after it has been selected - ios

I have a uiCollectionViewCell which loads image from an api. I want to display another image/icon on the cell when a user clicks on it. In my custom cell I have two images one which display the image from the URL and the second one is the one I would like to show if the user has clicked on it. I'm doing this to alert the user that they have selected that cell. Below is my sample code
protocol ModalDelegate {
func changeValue(userChoice: String, rateMovieID: String, rateImageUrl: String, title: String)
}
class GuestRateMovieView: UIViewController, ModalDelegate {
func changeValue(userChoice: String, rateMovieID: String, rateImageUrl: String, title: String) {
self.userChoice = userChoice
totalRated = totalRated + 1
lblRated.text = "\(totalRated) rated"
if totalRated > 0 {
ratedView.backgroundColor = .ratedGoldColour
}else{
ratedView.backgroundColor = .white
}
if totalRated >= 5 {
btnFloatNext.alpha = 1
}
if totalRated > 5 {
userChoiceMovieImage.sd_setImage(with: URL(string: rateImageUrl), placeholderImage: UIImage(named: "ImagePlaceholder"))
lblUserChoice.text = "Great taste. We love the \(title) too."
}
var rating = 1
if userChoice == "Hate it"{
rating = 1
}else if userChoice == "Good" {
rating = 3
}else{
rating = 5
}
let guestRatingValues = GuestUserRate(id: rateMovieID, imageUrl: rateImageUrl, userRate: rating)
GuestRateMovieView.createUserRating(guestRatingValues) =>", rateImageUrl)
print("Received on movie", totalRated)
}
func getMovieDetails(){
activityLoader.displayActivityLoader(image: activityLoader, view: activityLoaderView)
let urlString = "https://t2fmmm2hfg.execute-api.eu-west-2.amazonaws.com/mobile/media/onboarding-items"
let url = URL(string: urlString)!
var request = URLRequest(url: url)
request.setValue("application/json", forHTTPHeaderField: "Accept")
request.addValue("application/json", forHTTPHeaderField: "Content-Type")
request.httpMethod = "POST"
let postParameters: Dictionary<String, Any> = [
"category": "tv"
]
if let postData = (try? JSONSerialization.data(withJSONObject: postParameters, options: JSONSerialization.WritingOptions.prettyPrinted)){
request.httpBody = postData
let task = URLSession.shared.dataTask(with: request) { [weak self] data, response, error in
guard let data = data, error == nil else {
return
}
do {
print("got data")
let jsonResult = try JSONDecoder().decode([Responder].self, from: data)
DispatchQueue.main.async {
self?.movieObj = jsonResult
self?.moviesCollectionView.reloadData()
self?.activityLoader.removeActivityLoader(image: self!.activityLoader, view: self!.activityLoaderView)
}
// jsonResult.forEach { course in print(course.type) }
}catch {
print(error)
}
}
task.resume()
}
}
var movieObj: [Responder] = []
override func viewDidLoad() {
super.viewDidLoad()
getMovieDetails()
}
}
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return movieObj.count
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: reUseMoviesCellID, for: indexPath) as! MoviesCollectionCell
cell.movieImage.image = nil
cell.configure(with: movieObj[indexPath.row].packShot?.thumbnail ?? ""
return cell
}
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
let modalVC = RateSingleMovieView()
modalVC.movieID = movieObj[indexPath.row].id
modalVC.movieTitle = movieObj[indexPath.row].title
modalVC.movieImageURL = movieObj[indexPath.row].packShot?.thumbnail ?? ""
modalVC.delegate = self
modalVC.modalPresentationStyle = .overCurrentContext
modalVC.modalTransitionStyle = .crossDissolve
present(modalVC, animated: true, completion: nil)
}
class MoviesCollectionCell: UICollectionViewCell {
private var movieImages = NSCache<NSString, NSData>()
weak var textLabel: UILabel!
let movieImage: UIImageView = {
let image = UIImageView()
image.translatesAutoresizingMaskIntoConstraints = false
image.clipsToBounds = true
image.contentMode = .scaleAspectFill
image.layer.cornerRadius = 10
return image
}()
let btnRate: UIImageView = {
let image = UIImageView()
image.translatesAutoresizingMaskIntoConstraints = false
image.clipsToBounds = true
image.contentMode = .scaleAspectFit
image.alpha = 0
return image
}()
override init(frame: CGRect) {
super.init(frame: frame)
contentView.addSubview(movieImage)
movieImage.addSubview(btnRate)
NSLayoutConstraint.activate([
movieImage.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 10),
movieImage.topAnchor.constraint(equalTo: contentView.topAnchor),
movieImage.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -10),
movieImage.bottomAnchor.constraint(equalTo: contentView.bottomAnchor),
btnRate.centerXAnchor.constraint(equalTo: movieImage.centerXAnchor),
btnRate.centerYAnchor.constraint(equalTo: movieImage.centerYAnchor),
btnRate.widthAnchor.constraint(equalToConstant: 30),
btnRate.heightAnchor.constraint(equalToConstant: 30)
])
btnRate.tintColor = .white
btnRate.layer.shadowColor = UIColor.black.cgColor
btnRate.layer.shadowOffset = CGSize(width: 1.0, height: 2.0)
btnRate.layer.shadowRadius = 2
btnRate.layer.shadowOpacity = 0.8
btnRate.layer.masksToBounds = false
}
override func prepareForReuse() {
super.prepareForReuse()
movieImage.image = nil
btnRate.image = nil
}
func configure(with urlString: String, ratingObj: MovieRating){
movieImage.sd_setImage(with: URL(string: urlString), placeholderImage: UIImage(named: "ImagePlaceholder"))
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
Now in my modal where the user will rate. In my case I'm using swipe gesture for the rating
class RateSingleMovieView: UIViewController, ModalDelegate, ModalSearchDelegate {
func changeValue(userChoice: String, rateMovieID: String, rateImageUrl: String, title: String) {
self.userChoice = userChoice
totalRated = totalRated + 1
}
var delegate: ModalDelegate?
override func viewDidLoad() {
super.viewDidLoad()
redBottomView.addGestureRecognizer(createSwipeGestureRecognizer(for: .up))redBottomView.addGestureRecognizer(createSwipeGestureRecognizer(for: .left))redBottomView.addGestureRecognizer(createSwipeGestureRecognizer(for: .right))
}
#objc private func didSwipe(_ sender: UISwipeGestureRecognizer) {
switch sender.direction {
case .up:
showUserRatingSelection(userChoice: "Good")
case .left:
showUserRatingSelection(userChoice: "Hate it")
case .right:
showUserRatingSelection(userChoice: "Love it")
default:
break
}
}
#objc private func removeModal(){
dismiss(animated: true, completion: nil)
}
private func showUserRatingSelection(userChoice: String){
self.hateItView.alpha = 1
if userChoice == "Hate it"{
userChoiceEmoji.image = UIImage(named: "HateIt")
lblRate.text = "Hate it"
}else if userChoice == "Good" {
userChoiceEmoji.image = UIImage(named: "goodRate")
lblRate.text = "Good"
}else{
userChoiceEmoji.image = UIImage(named: "LoveIt")
lblRate.text = "Love it"
}
userChoiceEmoji.alpha = 1
DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
print("hello ", userChoice)
self.delegate?.changeValue(userChoice: userChoice, rateMovieID: self.movieID!, rateImageUrl: self.movieImageURL!, title: self.movieTitle!)
self.removeModal()
}
}
}
I am able to use the delegate here to send info back to GuestRateMovieView Controller and update a label there. Now my only problem is displaying the icon on the selected cell with the user choice.

First note... setup your constraints in init -- Absolutely NOT in layoutSubviews().
Edit -- forget everything else previously here, because it had nothing to do with what you're actually trying to accomplish.
New Answer
To clarify your goal:
display a collection view of objects - in this case, movies
when the user selects a cell, show a "Rate This Movie" view
when the user selects a Rating (hate, good, love), save that rating and update the cell with a "Rating Image"
So, the first thing you need is a data structure that includes a "rating" value. Let's use an enum for the rating itself:
enum MovieRating: Int {
case none, hate, good, love
}
Then we might have a "Movie Object" like this:
struct MovieObject {
var title: String = ""
var urlString: String = ""
var rating: MovieRating = .none
// maybe some other properties
}
For our data, we'll have an Array of MovieObject. When we configure each cell (in cellForItemAt), we need to set the Movie Image and the Rating Image.
So, your cell class may have this:
func configure(with movieObj: MovieObject) {
movieImage.sd_setImage(with: URL(string: movieObj.urlString), placeholderImage: UIImage(named: "ImagePlaceholder"))
switch movieObj.rating {
case .hate:
if let img = UIImage(systemName: "hand.thumbsdown") {
btnRate.image = img
}
case .good:
if let img = UIImage(systemName: "face.smiling") {
btnRate.image = img
}
case .love:
if let img = UIImage(systemName: "hand.thumbsup") {
btnRate.image = img
}
default:
btnRate.image = nil
}
}
and your cellForItemAt would look like this:
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let c = collectionView.dequeueReusableCell(withReuseIdentifier: "c", for: indexPath) as! MoviesCollectionCell
c.configure(with: moviesArray[indexPath.row])
return c
}
When the user selects a cell, we can present a "Rate This Movie" view controller - which will have buttons for Hate / Good / Love.
If the user taps one of those buttons, we can use a closure to update the data and reload that cell:
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
let vc = RateTheMovieVC()
vc.movieObj = moviesArray[indexPath.item]
vc.callback = { [weak self] rating in
guard let self = self else { return }
// update the data
self.moviesArray[indexPath.item].rating = rating
// reload the cell
self.collectionView.reloadItems(at: [indexPath])
// dismiss the RateTheMovie view controller
self.dismiss(animated: true)
}
// present the RateTheMovie view controller
present(vc, animated: true)
}
Here's a complete example... I don't have your data (movie names, images, etc), so we'll use an array of "Movie Titles" from A to Z, and the cells will look like this:
and so on.
enum and struct
enum MovieRating: Int {
case none, hate, good, love
}
struct MovieObject {
var title: String = ""
var urlString: String = ""
var rating: MovieRating = .none
}
collection view cell
class MoviesCollectionCell: UICollectionViewCell {
let movieImage: UIImageView = {
let image = UIImageView()
image.translatesAutoresizingMaskIntoConstraints = false
image.clipsToBounds = true
image.contentMode = .scaleAspectFill
image.layer.cornerRadius = 10
image.backgroundColor = .blue
return image
}()
let btnRate: UIImageView = {
let image = UIImageView()
image.translatesAutoresizingMaskIntoConstraints = false
image.clipsToBounds = true
image.contentMode = .scaleAspectFit
return image
}()
// we don't have Movie Images for this example, so
// we'll use some labels for the Movie Title
var labels: [UILabel] = []
override init(frame: CGRect) {
super.init(frame: frame)
contentView.addSubview(movieImage)
for _ in 0..<4 {
let v = UILabel()
v.translatesAutoresizingMaskIntoConstraints = false
v.textAlignment = .center
v.textColor = .cyan
if let f = UIFont(name: "TimesNewRomanPS-BoldMT", size: 60) {
v.font = f
}
contentView.addSubview(v)
labels.append(v)
}
// stack views for the labels
let stTop = UIStackView()
stTop.axis = .horizontal
stTop.distribution = .fillEqually
stTop.addArrangedSubview(labels[0])
stTop.addArrangedSubview(labels[1])
let stBot = UIStackView()
stBot.axis = .horizontal
stBot.distribution = .fillEqually
stBot.addArrangedSubview(labels[2])
stBot.addArrangedSubview(labels[3])
let st = UIStackView()
st.axis = .vertical
st.distribution = .fillEqually
st.addArrangedSubview(stTop)
st.addArrangedSubview(stBot)
st.translatesAutoresizingMaskIntoConstraints = false
contentView.addSubview(st)
contentView.addSubview(btnRate)
// setup constriaints here
NSLayoutConstraint.activate([
movieImage.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 10),
movieImage.topAnchor.constraint(equalTo: contentView.topAnchor),
movieImage.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -10),
movieImage.bottomAnchor.constraint(equalTo: contentView.bottomAnchor),
st.topAnchor.constraint(equalTo: movieImage.topAnchor),
st.leadingAnchor.constraint(equalTo: movieImage.leadingAnchor),
st.trailingAnchor.constraint(equalTo: movieImage.trailingAnchor),
st.bottomAnchor.constraint(equalTo: movieImage.bottomAnchor),
btnRate.centerXAnchor.constraint(equalTo: movieImage.centerXAnchor),
btnRate.centerYAnchor.constraint(equalTo: movieImage.centerYAnchor),
btnRate.widthAnchor.constraint(equalToConstant: 40),
btnRate.heightAnchor.constraint(equalToConstant: 40)
])
btnRate.tintColor = .white
btnRate.layer.shadowColor = UIColor.black.cgColor
btnRate.layer.shadowOffset = CGSize(width: 1.0, height: 2.0)
btnRate.layer.shadowRadius = 2
btnRate.layer.shadowOpacity = 0.8
btnRate.layer.masksToBounds = false
}
override func prepareForReuse() {
super.prepareForReuse()
movieImage.image = nil
}
func configure(with movieObj: MovieObject) {
// I don't have your cell images, or the "sd_setImage" function
// un-comment the next line to set your images
// movieImage.sd_setImage(with: URL(string: movieObj.urlString), placeholderImage: UIImage(named: "ImagePlaceholder"))
labels.forEach { v in
v.text = movieObj.title
}
switch movieObj.rating {
case .hate:
if let img = UIImage(systemName: "hand.thumbsdown") {
btnRate.image = img
}
case .good:
if let img = UIImage(systemName: "face.smiling") {
btnRate.image = img
}
case .love:
if let img = UIImage(systemName: "hand.thumbsup") {
btnRate.image = img
}
default:
btnRate.image = nil
}
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
example view controller
class ViewController: UIViewController, UICollectionViewDataSource, UICollectionViewDelegate {
var collectionView: UICollectionView!
var moviesArray: [MovieObject] = []
override func viewDidLoad() {
super.viewDidLoad()
let fl = UICollectionViewFlowLayout()
fl.itemSize = CGSize(width: 100.0, height: 200.0)
fl.scrollDirection = .vertical
fl.minimumLineSpacing = 8
fl.minimumInteritemSpacing = 8
collectionView = UICollectionView(frame: .zero, collectionViewLayout: fl)
collectionView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(collectionView)
let g = view.safeAreaLayoutGuide
NSLayoutConstraint.activate([
collectionView.topAnchor.constraint(equalTo: g.topAnchor, constant: 20.0),
collectionView.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 20.0),
collectionView.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -20.0),
collectionView.bottomAnchor.constraint(equalTo: g.bottomAnchor, constant: -20.0),
])
collectionView.register(MoviesCollectionCell.self, forCellWithReuseIdentifier: "c")
collectionView.dataSource = self
collectionView.delegate = self
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
// let's change the collection view cell size to fit
// two "columns"
if let fl = collectionView.collectionViewLayout as? UICollectionViewFlowLayout {
fl.itemSize = CGSize(width: (collectionView.frame.width - fl.minimumInteritemSpacing) * 0.5, height: 200.0)
}
simulateGettingData()
}
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return moviesArray.count
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let c = collectionView.dequeueReusableCell(withReuseIdentifier: "c", for: indexPath) as! MoviesCollectionCell
c.configure(with: moviesArray[indexPath.row])
return c
}
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
let vc = RateTheMovieVC()
vc.movieObj = moviesArray[indexPath.item]
vc.callback = { [weak self] rating in
guard let self = self else { return }
// update the data
self.moviesArray[indexPath.item].rating = rating
// reload the cell
self.collectionView.reloadItems(at: [indexPath])
// dismiss the RateTheMovie view controller
self.dismiss(animated: true)
}
// present the RateTheMovie view controller
present(vc, animated: true)
}
func simulateGettingData() {
// let's just create an array of MovieObject
// where each Title will be a letter from A to Z
"ABCDEFGHIJKLMNOPQRSTUVWXYZ".forEach { c in
let m = MovieObject(title: String(c), urlString: "", rating: .none)
moviesArray.append(m)
}
collectionView.reloadData()
}
}
example "Rate The Movie" view controller
class RateTheMovieVC: UIViewController {
// this will be used to tell the presenting controller
// that a rating button was selected
var callback: ((MovieRating) -> ())?
var movieObj: MovieObject!
let movieImage: UIImageView = {
let image = UIImageView()
image.translatesAutoresizingMaskIntoConstraints = false
image.clipsToBounds = true
image.contentMode = .scaleAspectFill
image.layer.cornerRadius = 10
image.backgroundColor = .systemBlue
return image
}()
// we don't have Movie Images for this example, so
// we'll use a label for the "Movie Title"
let titleLabel: UILabel = {
let v = UILabel()
v.translatesAutoresizingMaskIntoConstraints = false
v.numberOfLines = 0
v.textAlignment = .center
v.textColor = .yellow
v.font = .systemFont(ofSize: 240, weight: .bold)
return v
}()
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .lightGray
// let's add 3 "rate" buttons near the bottom
let btnHate = UIButton()
let btnGood = UIButton()
let btnLove = UIButton()
let btns: [UIButton] = [btnHate, btnGood, btnLove]
let names: [String] = ["hand.thumbsdown", "face.smiling", "hand.thumbsup"]
for (b, s) in zip(btns, names) {
b.backgroundColor = .systemRed
b.layer.cornerRadius = 8
b.layer.masksToBounds = true
if let img = UIImage(systemName: s, withConfiguration: UIImage.SymbolConfiguration(pointSize: 32)) {
b.setImage(img, for: [])
}
b.tintColor = .white
b.heightAnchor.constraint(equalToConstant: 60.0).isActive = true
}
let btnStack = UIStackView()
btnStack.spacing = 20
btnStack.distribution = .fillEqually
btns.forEach { b in
btnStack.addArrangedSubview(b)
}
view.addSubview(movieImage)
view.addSubview(titleLabel)
btnStack.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(btnStack)
// setup constriaints here
let g = view.safeAreaLayoutGuide
NSLayoutConstraint.activate([
btnStack.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 20.0),
btnStack.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -20.0),
btnStack.bottomAnchor.constraint(equalTo: g.bottomAnchor, constant: -20.0),
titleLabel.topAnchor.constraint(equalTo: movieImage.topAnchor, constant: 8.0),
titleLabel.leadingAnchor.constraint(equalTo: movieImage.leadingAnchor, constant: 12.0),
titleLabel.trailingAnchor.constraint(equalTo: movieImage.trailingAnchor, constant: -12.0),
titleLabel.bottomAnchor.constraint(equalTo: movieImage.bottomAnchor, constant: -8.0),
movieImage.topAnchor.constraint(equalTo: g.topAnchor, constant: 40.0),
movieImage.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 40.0),
movieImage.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -40.0),
movieImage.bottomAnchor.constraint(equalTo: btnStack.topAnchor, constant: -20.0),
])
// here we would set the movie image
// set the title label text, since we don't have images right now
titleLabel.text = movieObj.title
btnHate.addTarget(self, action: #selector(hateTap(_:)), for: .touchUpInside)
btnGood.addTarget(self, action: #selector(goodTap(_:)), for: .touchUpInside)
btnLove.addTarget(self, action: #selector(loveTap(_:)), for: .touchUpInside)
}
#objc func hateTap(_ sender: UIButton) {
callback?(.hate)
}
#objc func goodTap(_ sender: UIButton) {
callback?(.good)
}
#objc func loveTap(_ sender: UIButton) {
callback?(.love)
}
}
It will look like this on launch:
Then we select the first cell and we see this:
We select the "Thumbs Up" button, and we see this:
Then scroll down and select-and-rate a few other cells:
You mention in a comment a "cache DB" ... assuming that will be persistent data, it's up to you to store the user-selected Rating.

in your MoviesCollectionCell file put function like this
func loadImageAfterClickingCell() {
// TODO: check this cell is already clicked and already download the image like if yourImageView == nil or not nil so you can add guard like guard yourImageView.image == nil else { return } similar to this
guard let preparedUrl = URL(string: urlString) else { return }
yourImageView.alpha = 1
yourImageView.sd_setImage(with: preparedUrl, placeholderImage: UIImage(named: "ImagePlaceholder"))
}
And after that in didSelectItemAt
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: reUseMoviesCellID, for: indexPath) as! MoviesCollectionCell
cell.loadImageAfterClickingCell()
}
A simple method injection like this must save you as you want.

Related

How to change audioCoverImage in the AudioPlayerViewController according to current playing audio?

This is the tableViewController with data from firebase, please pay attention that I put number to the images so that to be able to understand which image is displayed in which position.
This is the AudioPlayerViewController, and user comes here when he taps on the first cell in the tableview. The image displays correctly in all cases at first launch.
But when user changes the track right inside of the AudioPlayerViewController pressing next or previous button, the labels are changing accordingly and correctly but the image remains the one which passed on the first launch.
This is the Audio Structure
import Foundation
import UIKit
struct Audio {
let image: UIImage?
let name: String
let albumName: String
let audioImageName: String
let trackURL: String
let time: String
}
This is the AudiosViewModel which fetches data from firebase-firestorm and populates the cells in ViewController.
import Foundation
import FirebaseFirestore
import SDWebImage
class AudiosViewModel: ObservableObject {
#Published var audios = [Audio]()
private var db = Firestore.firestore()
var image: UIImage?
func fetchData() {
db.collection("audios").addSnapshotListener { [self] (querySnapshot, error) in
guard let documents = querySnapshot?.documents else {
print("No Documents")
return
}
self.audios = documents.map { (queryDocumentSnapshot) -> Audio in
let data = queryDocumentSnapshot.data()
let image = image
let name = data["name"] as? String ?? ""
let albumName = data["albumName"] as? String ?? ""
let audioImageName = data["audioImageName"] as? String ?? ""
let trackURL = data["trackURL"] as? String ?? ""
let time = data["time"] as? String ?? ""
return Audio(image: image, name: name, albumName: albumName, audioImageName: audioImageName, trackURL: trackURL, time: time)
}
}
}
}
This is the ViewController with the list of audios fetched from firebase-firestorm
import UIKit
import AVKit
import AVFoundation
import FirebaseFirestore
import Combine
import SDWebImage
class ViewController: UIViewController, UITableViewDelegate, UITableViewDataSource {
#IBOutlet var table: UITableView!
let placeHolderImage = UIImage(named: "placeHolderImage")
private var viewModel = AudiosViewModel()
private var cancellable: AnyCancellable?
override func viewDidLoad() {
super.viewDidLoad()
self.viewModel.fetchData()
self.title = "Audio Lessons"
let nib = UINib(nibName: "AudioCustomTableViewCell", bundle: nil)
table.register(nib, forCellReuseIdentifier: "audioCustomCell")
table.delegate = self
table.dataSource = self
cancellable = viewModel.$audios.sink { _ in
DispatchQueue.main.async{
self.table.reloadData()
}
}
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
print("audios count = ", viewModel.audios.count)
return viewModel.audios.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "audioCustomCell", for: indexPath) as? AudioCustomTableViewCell
let song = viewModel.audios[indexPath.row]
let imageURL = song.audioImageName
cell?.audioImage.sd_imageIndicator = SDWebImageActivityIndicator.gray
cell?.audioImage.sd_setImage(with: URL(string: imageURL),
placeholderImage: placeHolderImage,
options: SDWebImageOptions.highPriority,
context: nil,
progress: nil,
completed: { downloadedImage, downloadException, cacheType, downloadURL in
if let downloadException = downloadException {
print("error downloading the image: \(downloadException.localizedDescription)")
} else {
print("successfuly downloaded the image: \(String(describing: downloadURL?.absoluteString))")
}
self.viewModel.image = cell?.audioImage.image
})
tableView.tableFooterView = UIView()
cell?.textLabel?.font = UIFont(name: "Helvetica-Bold", size: 14)
cell?.detailTextLabel?.font = UIFont(name: "Helvetica", size: 12)
cell?.chevronImage?.image = UIImage(systemName: "chevron.compact.right")
cell?.chevronImage?.tintColor = .systemMint
cell?.commonInit(song.time ,song.albumName, song.name, viewModel.image, UIImage(systemName: "chevron.compact.right"))
return cell!
}
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
return 120
}
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
tableView.deselectRow(at: indexPath, animated: true)
let position = indexPath.row
let song = viewModel.audios[indexPath.row]
let imageURL = song.audioImageName
SDWebImageManager.shared.loadImage(
with: URL(string: imageURL),
options: .continueInBackground, // or .highPriority
progress: nil,
completed: { [weak self] (image, data, error, cacheType, finished, url) in
guard let self = self else { return }
if error != nil {
// Do something with the error
print("error")
return
}
guard image != nil else {
// No image handle this error
print("error")
return
}
// Do something with image
self.viewModel.image = image
})
guard let vc = storyboard?.instantiateViewController(identifier: "AudioPlayer") as? AudioPlayerViewController else {
return
}
vc.mainImage = viewModel.image
vc.paragraphs = viewModel.audios
vc.position = position
present(vc, animated: true)
}
func tableView(_ tableView: UITableView, didHighlightRowAt indexPath: IndexPath) {
if let cell = tableView.cellForRow(at: indexPath) as? AudioCustomTableViewCell {
cell.cellView.backgroundColor = UIColor(named: "objectHighlightedColor")
cell.mainTitle?.textColor = UIColor(named: "labelHighlighted")
cell.detailTitle?.textColor = UIColor(named: "labelHighlighted")
cell.chevronImage?.tintColor = UIColor(named: "labelHighlighted")
}
}
func tableView(_ tableView: UITableView, didUnhighlightRowAt indexPath: IndexPath) {
if let cell = tableView.cellForRow(at: indexPath) as? AudioCustomTableViewCell {
cell.cellView.backgroundColor = UIColor(named: "objectUnHighlightedColor")
cell.mainTitle?.textColor = UIColor(named: "labelUnHighlighted")
cell.detailTitle?.textColor = UIColor(named: "labelUnHighlighted")
cell.chevronImage?.tintColor = .systemMint
}
}
}
This is the AudioPlayerViewController
import UIKit
import AVFoundation
import MediaPlayer
import AVKit
class MyVolumeView: MPVolumeView {
override func volumeSliderRect(forBounds bounds: CGRect) -> CGRect {
// this will avoid the thumb x-offset issue
// while keeping the route button vertically aligned
return bounds.insetBy(dx: 12.0, dy: 0.0).offsetBy(dx: -12.0, dy: -5.0)
}
}
class AudioPlayerViewController: UIViewController {
public var position: Int = 0
public var paragraphs: [Audio] = []
public var mainImage = UIImage(named: "placeHolderImage")
#IBOutlet var holder: UIView!
var player: AVPlayer?
var playerItem: AVPlayerItem?
var isSeekInProgress = false
var chaseTime = CMTime.zero
fileprivate let seekDuration: Float64 = 15
var playerCurrentItemStatus: AVPlayerItem.Status = .unknown
private lazy var audioImageView: UIImageView = {
let v = UIImageView()
v.translatesAutoresizingMaskIntoConstraints = false
v.contentMode = .scaleAspectFill
v.clipsToBounds = true
v.layer.cornerRadius = 8
return v
}()
private lazy var subjectNameLabel: UILabel = {
let v = UILabel()
v.translatesAutoresizingMaskIntoConstraints = false
v.textAlignment = .center
v.font = .systemFont(ofSize: 14, weight: .bold)
return v
}()
private lazy var lessonNameLabel: UILabel = {
let v = UILabel()
v.translatesAutoresizingMaskIntoConstraints = false
v.textAlignment = .center
v.font = .systemFont(ofSize: 12, weight: .light)
return v
}()
private lazy var progressBar: UISlider = {
let v = UISlider()
v.translatesAutoresizingMaskIntoConstraints = false
v.minimumTrackTintColor = UIColor(named: "subtitleColor")
v.isContinuous = false
return v
}()
private lazy var elapsedTimeLabel: UILabel = {
let v = UILabel()
v.translatesAutoresizingMaskIntoConstraints = false
v.font = .systemFont(ofSize: 14, weight: .light)
v.text = "00:00"
return v
}()
private lazy var remainingTimeLabel: UILabel = {
let v = UILabel()
v.translatesAutoresizingMaskIntoConstraints = false
v.font = .systemFont(ofSize: 14, weight: .light)
v.text = "00:00"
return v
}()
let previousButton: UIButton = {
let v = UIButton()
v.translatesAutoresizingMaskIntoConstraints = false
let config = UIImage.SymbolConfiguration(pointSize: 20)
v.setImage(UIImage(systemName: "backward.fill", withConfiguration: config), for: .normal)
v.tintColor = .white
return v
}()
let seekBackwardButton: UIButton = {
let v = UIButton()
v.translatesAutoresizingMaskIntoConstraints = false
let config = UIImage.SymbolConfiguration(pointSize: 30)
v.setImage(UIImage(systemName: "gobackward.15", withConfiguration: config), for: .normal)
v.tintColor = .white
return v
}()
let playPauseButton: UIButton = {
let v = UIButton()
v.translatesAutoresizingMaskIntoConstraints = false
let config = UIImage.SymbolConfiguration(pointSize: 50)
v.setImage(UIImage(systemName: "pause.fill", withConfiguration: config), for: .normal)
v.tintColor = .white
return v
}()
let seekForwardButton: UIButton = {
let v = UIButton()
v.translatesAutoresizingMaskIntoConstraints = false
let config = UIImage.SymbolConfiguration(pointSize: 30)
v.setImage(UIImage(systemName: "goforward.15", withConfiguration: config), for: .normal)
v.tintColor = .white
return v
}()
let nextButton: UIButton = {
let v = UIButton()
v.translatesAutoresizingMaskIntoConstraints = false
let config = UIImage.SymbolConfiguration(pointSize: 20)
v.setImage(UIImage(systemName: "forward.fill", withConfiguration: config), for: .normal)
v.tintColor = .white
return v
}()
private lazy var controlStack: UIStackView = {
let v = UIStackView(arrangedSubviews: [previousButton, seekBackwardButton, playPauseButton, seekForwardButton, nextButton])
v.translatesAutoresizingMaskIntoConstraints = false
v.axis = .horizontal
v.distribution = .equalSpacing
v.spacing = 20
return v
}()
let volumeViewContainer: UIView = {
let v = UIView()
v.translatesAutoresizingMaskIntoConstraints = false
return v
}()
let volumeView = MyVolumeView()
override func viewDidLoad() {
super.viewDidLoad()
let panGesture = UIPanGestureRecognizer(target: self, action: #selector(panGesture(gesture:)))
self.progressBar.addGestureRecognizer(panGesture)
let audioSession = AVAudioSession.sharedInstance()
do {
try audioSession.setCategory(AVAudioSession.Category.playback)
}
catch{
print(error)
}
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
if holder.subviews.count == 0 {
configure()
}
}
func configure() {
// set up player
let song = paragraphs[position]
let url = URL(string: song.trackURL)
let playerItem: AVPlayerItem = AVPlayerItem(url: url!)
do {
try AVAudioSession.sharedInstance().setMode(.default)
try AVAudioSession.sharedInstance().setActive(true, options: .notifyOthersOnDeactivation)
guard url != nil else {
print("urls string is nil")
return
}
player = AVPlayer(playerItem: playerItem)
let duration : CMTime = playerItem.asset.duration
let seconds : Float64 = CMTimeGetSeconds(duration)
remainingTimeLabel.text = self.stringFromTimeInterval(interval: seconds)
let currentDuration : CMTime = playerItem.currentTime()
let currentSeconds : Float64 = CMTimeGetSeconds(currentDuration)
elapsedTimeLabel.text = self.stringFromTimeInterval(interval: currentSeconds)
progressBar.maximumValue = Float(seconds)
player!.addPeriodicTimeObserver(forInterval: CMTimeMakeWithSeconds(1, preferredTimescale: 1), queue: DispatchQueue.main) { (CMTime) -> Void in
if self.player!.currentItem?.status == .readyToPlay {
let time : Float64 = CMTimeGetSeconds(self.player!.currentTime());
self.progressBar.value = Float(time)
self.elapsedTimeLabel.text = self.stringFromTimeInterval(interval: time)
}
let playbackLikelyToKeepUp = self.player?.currentItem?.isPlaybackLikelyToKeepUp
if playbackLikelyToKeepUp == false{
print("IsBuffering")
self.playPauseButton.isHidden = true
} else {
// stop the activity indicator
print("Buffering completed")
self.playPauseButton.isHidden = false
}
}
//subroutine used to keep track of current location of time in audio file
guard let player = player else {
print("player is nil")
return
}
player.play()
}
catch {
print("error accured")
}
holder.applyBlurEffect()
audioImageView.image = mainImage
holder.addSubview(audioImageView)
holder.addSubview(subjectNameLabel)
holder.addSubview(lessonNameLabel)
holder.addSubview(elapsedTimeLabel)
holder.addSubview(remainingTimeLabel)
holder.addSubview(controlStack)
holder.addSubview(progressBar)
holder.addSubview(volumeViewContainer)
volumeViewContainer.addSubview(volumeView)
NSLayoutConstraint.activate([
audioImageView.leadingAnchor.constraint(equalTo: holder.leadingAnchor, constant: 20),
audioImageView.trailingAnchor.constraint(equalTo: holder.trailingAnchor, constant: -20),
audioImageView.topAnchor.constraint(equalTo: holder.topAnchor, constant: 20),
audioImageView.heightAnchor.constraint(equalToConstant: holder.bounds.width - 40)
])
NSLayoutConstraint.activate([
subjectNameLabel.leadingAnchor.constraint(equalTo: holder.leadingAnchor, constant: 20),
subjectNameLabel.bottomAnchor.constraint(equalTo: lessonNameLabel.topAnchor, constant: -8),
])
NSLayoutConstraint.activate([
lessonNameLabel.leadingAnchor.constraint(equalTo: holder.leadingAnchor, constant: 20),
lessonNameLabel.bottomAnchor.constraint(equalTo: progressBar.topAnchor, constant: -8),
])
NSLayoutConstraint.activate([
progressBar.leadingAnchor.constraint(equalTo: holder.leadingAnchor, constant: 20),
progressBar.trailingAnchor.constraint(equalTo: holder.trailingAnchor, constant: -20),
progressBar.bottomAnchor.constraint(equalTo: remainingTimeLabel.topAnchor, constant: -3),
])
NSLayoutConstraint.activate([
elapsedTimeLabel.leadingAnchor.constraint(equalTo: holder.leadingAnchor, constant: 20),
elapsedTimeLabel.bottomAnchor.constraint(equalTo: controlStack.topAnchor, constant: -8),
])
NSLayoutConstraint.activate([
remainingTimeLabel.trailingAnchor.constraint(equalTo: holder.trailingAnchor, constant: -20),
remainingTimeLabel.bottomAnchor.constraint(equalTo: controlStack.topAnchor, constant: -8),
])
NSLayoutConstraint.activate([
controlStack.leadingAnchor.constraint(equalTo: holder.leadingAnchor, constant: 32),
controlStack.trailingAnchor.constraint(equalTo: holder.trailingAnchor, constant: -32),
controlStack.bottomAnchor.constraint(equalTo: volumeViewContainer.topAnchor, constant: -16),
controlStack.heightAnchor.constraint(equalToConstant: 60)
])
NSLayoutConstraint.activate([
volumeViewContainer.leadingAnchor.constraint(equalTo: holder.leadingAnchor, constant: 20),
volumeViewContainer.trailingAnchor.constraint(equalTo: holder.trailingAnchor, constant: -20),
volumeViewContainer.bottomAnchor.constraint(equalTo: holder.bottomAnchor, constant: -75),
volumeViewContainer.heightAnchor.constraint(equalToConstant: 30.0),
])
lessonNameLabel.text = song.name
subjectNameLabel.text = song.albumName
progressBar.addTarget(self, action: #selector(progressScrubbed(_:)), for: .valueChanged)
volumeViewContainer.addObserver(self, forKeyPath: "bounds", context: nil)
playPauseButton.addTarget(self, action: #selector(didTapPlayPauseButton), for: .touchUpInside)
previousButton.addTarget(self, action: #selector(didTapBackButton), for: .touchUpInside)
nextButton.addTarget(self, action: #selector(didTapNextButton), for: .touchUpInside)
seekForwardButton.addTarget(self, action: #selector(seekForwardButtonTapped), for: .touchUpInside)
seekBackwardButton.addTarget(self, action: #selector(seekBackwardButtonTapped), for: .touchUpInside)
}
#objc func panGesture(gesture:UIPanGestureRecognizer) {
let currentPoint = gesture.location(in: progressBar)
let percentage = currentPoint.x/progressBar.bounds.size.width;
let delta = Float(percentage) * (progressBar.maximumValue - progressBar.minimumValue)
let value = progressBar.minimumValue + delta
progressBar.setValue(value, animated: true)
}
#objc func progressScrubbed(_ :UISlider) {
let seconds : Int64 = Int64(progressBar.value)
let targetTime:CMTime = CMTimeMake(value: seconds, timescale: 1)
player!.seek(to: targetTime)
if player!.rate == 0
{
player?.play()
}
}
func setupNowPlaying() {
// Define Now Playing Info
var nowPlayingInfo = [String : Any]()
nowPlayingInfo[MPMediaItemPropertyTitle] = "Unstoppable"
if let image = mainImage {
nowPlayingInfo[MPMediaItemPropertyArtwork] = MPMediaItemArtwork(boundsSize: image.size) { size in
return image
}
}
nowPlayingInfo[MPNowPlayingInfoPropertyElapsedPlaybackTime] = player?.currentTime
nowPlayingInfo[MPMediaItemPropertyPlaybackDuration] = playerItem?.duration
nowPlayingInfo[MPNowPlayingInfoPropertyPlaybackRate] = player?.rate
MPNowPlayingInfoCenter.default().nowPlayingInfo = nowPlayingInfo
}
func updateNowPlaying(isPause: Bool) {
var nowPlayingInfo = MPNowPlayingInfoCenter.default().nowPlayingInfo!
nowPlayingInfo[MPNowPlayingInfoPropertyElapsedPlaybackTime] = player?.currentTime
nowPlayingInfo[MPNowPlayingInfoPropertyPlaybackRate] = isPause ? 0 : 1
MPNowPlayingInfoCenter.default().nowPlayingInfo = nowPlayingInfo
}
func setupNotifications() {
let notificationCenter = NotificationCenter.default
notificationCenter.addObserver(self,
selector: #selector(handleInterruption),
name: AVAudioSession.interruptionNotification,
object: nil)
notificationCenter.addObserver(self,
selector: #selector(handleRouteChange),
name: AVAudioSession.routeChangeNotification,
object: nil)
}
#objc func handleRouteChange(notification: Notification) {
guard let userInfo = notification.userInfo,
let reasonValue = userInfo[AVAudioSessionRouteChangeReasonKey] as? UInt,
let reason = AVAudioSession.RouteChangeReason(rawValue:reasonValue) else {
return
}
switch reason {
case .newDeviceAvailable:
let session = AVAudioSession.sharedInstance()
for output in session.currentRoute.outputs where output.portType == AVAudioSession.Port.headphones {
print("headphones connected")
DispatchQueue.main.sync {
player?.play()
}
break
}
case .oldDeviceUnavailable:
if let previousRoute =
userInfo[AVAudioSessionRouteChangePreviousRouteKey] as? AVAudioSessionRouteDescription {
for output in previousRoute.outputs where output.portType == AVAudioSession.Port.headphones {
print("headphones disconnected")
DispatchQueue.main.sync {
player?.pause()
}
break
}
}
default: ()
}
}
#objc func handleInterruption(notification: Notification) {
guard let userInfo = notification.userInfo,
let typeValue = userInfo[AVAudioSessionInterruptionTypeKey] as? UInt,
let type = AVAudioSession.InterruptionType(rawValue: typeValue) else {
return
}
if type == .began {
print("Interruption began")
// Interruption began, take appropriate actions
}
else if type == .ended {
if let optionsValue = userInfo[AVAudioSessionInterruptionOptionKey] as? UInt {
let options = AVAudioSession.InterruptionOptions(rawValue: optionsValue)
if options.contains(.shouldResume) {
// Interruption Ended - playback should resume
print("Interruption Ended - playback should resume")
player?.play()
} else {
// Interruption Ended - playback should NOT resume
print("Interruption Ended - playback should NOT resume")
}
}
}
}
func stringFromTimeInterval(interval: TimeInterval) -> String {
let interval = Int(interval)
let seconds = interval % 60
let minutes = (interval / 60) % 60
let timeFormatter = NumberFormatter()
timeFormatter.minimumIntegerDigits = 2
timeFormatter.minimumFractionDigits = 0
timeFormatter.roundingMode = .down
guard let minsString = timeFormatter.string(from: NSNumber(value: minutes)),
let secStr = timeFormatter.string(from: NSNumber(value: seconds)) else {
return "00:00"
}
return "\(minsString):\(secStr)"
}
#objc func didTapPlayPauseButton() {
if player?.timeControlStatus == .playing {
//pause
player?.pause()
//show play button
let config = UIImage.SymbolConfiguration(pointSize: 50)
playPauseButton.setImage(UIImage(systemName: "play.fill", withConfiguration: config), for: .normal)
//shrink image
// UIView.animate(withDuration: 0.2, animations: {
// self.audioImageView.frame = CGRect(x: 50,
// y: 50,
// width: self.holder.frame.size.width - 100,
// height: self.holder.frame.size.width - 100)
//
// })
} else {
//play
player?.play()
//show pause button
let config = UIImage.SymbolConfiguration(pointSize: 50)
playPauseButton.setImage(UIImage(systemName: "pause.fill", withConfiguration: config), for: .normal)
//increase image size
// UIView.animate(withDuration: 0.4, animations: {
// self.audioImageView.frame = CGRect(x: 20,
// y: 20,
// width: self.holder.frame.size.width - 40,
// height: self.holder.frame.size.width - 40)
// })
}
}
#objc func didTapBackButton() {
if position > 0 {
position = position - 1
player?.pause()
for subview in holder.subviews {
subview.removeFromSuperview()
}
configure()
}
}
#objc func didTapNextButton() {
if position < (paragraphs.count - 1) {
position = position + 1
player?.pause()
for subview in holder.subviews {
subview.removeFromSuperview()
}
configure()
}
}
#objc func seekBackwardButtonTapped(){
if player == nil { return }
let playerCurrentTime = CMTimeGetSeconds(player!.currentTime())
var newTime = playerCurrentTime - seekDuration
if newTime < 0 { newTime = 0 }
player?.pause()
let selectedTime: CMTime = CMTimeMake(value: Int64(newTime * 1000 as Float64), timescale: 1000)
player?.seek(to: selectedTime)
player?.play()
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
player?.play()
UIApplication.shared.isIdleTimerDisabled = true
}
override func viewDidDisappear(_ animated: Bool) {
super.viewDidDisappear(animated)
player?.pause()
UIApplication.shared.isIdleTimerDisabled = false
}
override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
if keyPath == "bounds" {
// make sure we're getting notified of the MyVolumeView container view
if let cv = object as? UIView,
let mpv = cv.subviews.first as? MyVolumeView {
// set MyVolumeView frame to container view's bounds
// and offset its y-position by 4-points (because of its buggy layout)
mpv.frame = cv.bounds.offsetBy(dx: 0.0, dy: 4.0)
}
}
}
}

tableView never shows up

I have been at this for some time now. I can not get my tableView to appear. I think it has something to do with the fact that it is being presented as didMove(toParent)
I am trying to create a page that allows you to add a new Card to the profile. Every time I write it programmatically or use storyboard it crashes as it Unexpectedly found nil while implicitly unwrapping an Optional value.
Here is the view Controller that is presenting the Side Menu
import Foundation
import SideMenu
import FirebaseAuth
import UIKit
import CoreLocation
import SwiftUI
class BeginViewController: UIViewController, MenuControllerDelegate, CLLocationManagerDelegate {
private var sideMenu: SideMenuNavigationController?
struct customData {
var title: String
var image: UIImage
}
let data = [
customData(title: "NottingHill", image: #imageLiteral(resourceName: "norali-nayla-SAhImiWmFaw-unsplash")),
customData(title: "Southall", image: #imageLiteral(resourceName: "alistair-macrobert-8wMflrTLm2g-unsplash")),
customData(title: "Tower Hill", image: #imageLiteral(resourceName: "peregrine-communications-0OLnnZWg860-unsplash")),
customData(title: "Mansion House", image: #imageLiteral(resourceName: "adam-birkett-cndNklOnHO4-unsplash")),
customData(title: "Westminster", image: #imageLiteral(resourceName: "simon-mumenthaler-NykjYbCW6Z0-unsplash")),
customData(title: "London Bridge", image: #imageLiteral(resourceName: "hert-niks-CjouXgWrTRk-unsplash"))
]
struct Constants {
static let cornerRadius: CGFloat = 15.0 }
let manager = CLLocationManager()
private let ProfileController = ProfileViewController()
private let MyBookingsController = MyBookingsViewController()
private let WalletController = WalletViewController()
private let FAQController = FAQViewController()
private let SettingsController = SettingsViewController()
#IBOutlet weak var StoreButton: UIButton!
#IBOutlet weak var DeliverButton: UIButton!
#IBOutlet weak var AirportButton: UIButton!
#IBOutlet weak var HotelsButton: UIButton!
fileprivate let collectionView: UICollectionView = {
let layout = UICollectionViewFlowLayout()
layout.scrollDirection = .horizontal
let cv = UICollectionView(frame: .zero, collectionViewLayout: layout)
cv.translatesAutoresizingMaskIntoConstraints = false
cv.register(customCell.self, forCellWithReuseIdentifier: "cell")
return cv
}()
override func viewDidLoad() {
super.viewDidLoad()
// buttons
StoreButton.layer.cornerRadius = Constants.cornerRadius
StoreButton.layer.shadowOffset = .zero
StoreButton.layer.shadowOpacity = 0.3
StoreButton.layer.shadowColor = UIColor.black.cgColor
StoreButton.layer.shadowRadius = 5
DeliverButton.layer.cornerRadius = Constants.cornerRadius
DeliverButton.layer.shadowOffset = .zero
DeliverButton.layer.shadowOpacity = 0.3
DeliverButton.layer.shadowColor = UIColor.black.cgColor
DeliverButton.layer.shadowRadius = 5
AirportButton.layer.cornerRadius = Constants.cornerRadius
AirportButton.layer.shadowOffset = .zero
AirportButton.layer.shadowOpacity = 0.3
AirportButton.layer.shadowColor = UIColor.black.cgColor
AirportButton.layer.shadowRadius = 5
HotelsButton.layer.cornerRadius = Constants.cornerRadius
HotelsButton.layer.shadowOffset = .zero
HotelsButton.layer.shadowOpacity = 0.3
HotelsButton.layer.shadowColor = UIColor.black.cgColor
HotelsButton.layer.shadowRadius = 5
//CollectionViewNearbyPlaces
view.addSubview(collectionView)
collectionView.backgroundColor = .clear
collectionView.topAnchor.constraint(equalTo: view.topAnchor, constant: 450).isActive = true
collectionView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 25).isActive = true
collectionView.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: 0).isActive = true
collectionView.heightAnchor.constraint(equalTo: collectionView.widthAnchor, multiplier: 0.5).isActive = true
collectionView.delegate = self
collectionView.dataSource = self
// title
title = "handl"
//background
let menu = MenuController(with: [ "Home", "Profile", "My Bookings",
"Wallet",
"FAQ","Settings"])
menu.delegate = self
sideMenu = SideMenuNavigationController(rootViewController: menu)
sideMenu?.leftSide = true
sideMenu?.setNavigationBarHidden(true, animated: false)
SideMenuManager.default.leftMenuNavigationController = sideMenu
SideMenuManager.default.addPanGestureToPresent(toView: view)
addChildControllers()
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
manager.desiredAccuracy = kCLLocationAccuracyBest
manager.delegate = self
manager.requestWhenInUseAuthorization()
manager.startUpdatingLocation()
handleNotAuthenticated()
}
private func addChildControllers() {
addChild(ProfileController)
addChild(MyBookingsController)
addChild(WalletController)
addChild(FAQController)
addChild(SettingsController)
view.addSubview(ProfileController.view)
view.addSubview(MyBookingsController.view)
view.addSubview(WalletController.view)
view.addSubview(FAQController.view)
view.addSubview(SettingsController.view)
ProfileController.view.frame = view.bounds
MyBookingsController.view.frame = view.bounds
WalletController.view.frame = view.bounds
FAQController.view.frame = view.bounds
SettingsController.view.frame = view.bounds
ProfileController.didMove(toParent: self)
MyBookingsController.didMove(toParent: self)
WalletController.didMove(toParent: self)
FAQController.didMove(toParent: self)
SettingsController.didMove(toParent: self)
ProfileController.view.isHidden = true
MyBookingsController.view.isHidden = true
WalletController.view.isHidden = true
FAQController.view.isHidden = true
SettingsController.view.isHidden = true
}
#IBAction func SideMenuButton(_ sender: Any) {
present(sideMenu!, animated: true)
}
func didSelectMenuItem(named: String) {
sideMenu?.dismiss(animated: true, completion: { [weak self] in
self?.title = named
if named == "Home" {
self?.ProfileController.view.isHidden = true
self?.MyBookingsController.view.isHidden = true
self?.WalletController.view.isHidden = true
self?.FAQController.view.isHidden = true
self?.SettingsController.view.isHidden = true
}
if named == "Profile" {
self?.ProfileController.view.isHidden = false
self?.MyBookingsController.view.isHidden = true
self?.WalletController.view.isHidden = true
self?.FAQController.view.isHidden = true
self?.SettingsController.view.isHidden = true
}
if named == "My Bookings" {
self?.ProfileController.view.isHidden = true
self?.MyBookingsController.view.isHidden = false
self?.WalletController.view.isHidden = true
self?.FAQController.view.isHidden = true
self?.SettingsController.view.isHidden = true
}
else if named == "Wallet" {
self?.ProfileController.view.isHidden = true
self?.MyBookingsController.view.isHidden = true
self?.WalletController.view.isHidden = false
self?.FAQController.view.isHidden = true
self?.SettingsController.view.isHidden = true
}
else if named == "FAQ" {
self?.ProfileController.view.isHidden = true
self?.MyBookingsController.view.isHidden = true
self?.WalletController.view.isHidden = true
self?.FAQController.view.isHidden = false
self?.SettingsController.view.isHidden = true
}
else if named == "Settings" {
self?.ProfileController.view.isHidden = true
self?.MyBookingsController.view.isHidden = true
self?.WalletController.view.isHidden = true
self?.FAQController.view.isHidden = true
self?.SettingsController.view.isHidden = false
}
})
}
}
extension BeginViewController: UICollectionViewDelegateFlowLayout, UICollectionViewDataSource {
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
return CGSize(width: collectionView.frame.width/2.5, height: collectionView.frame.width/2)
}
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return data.count
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "cell", for: indexPath) as! customCell
cell.data = self.data[indexPath.row]
return cell
}
class customCell: UICollectionViewCell {
var data: customData? {
didSet {
guard let data = data else { return }
bg.image = data.image
}
}
fileprivate let bg: UIImageView = {
let iv = UIImageView()
iv.image = #imageLiteral(resourceName: "adam-birkett-cndNklOnHO4-unsplash")
iv.translatesAutoresizingMaskIntoConstraints = false
iv.contentMode = .scaleAspectFill
iv.layer.shadowColor = UIColor.black.cgColor
iv.layer.shadowOpacity = 1
iv.layer.shadowOffset = CGSize.zero
iv.layer.shadowRadius = 10
iv.layer.shadowPath = UIBezierPath(rect: iv.bounds).cgPath
iv.layer.shouldRasterize = false
iv.layer.cornerRadius = 10
iv.clipsToBounds = true
return iv
}()
override init(frame: CGRect) {
super.init(frame: frame)
contentView.addSubview(bg)
bg.topAnchor.constraint(equalTo: contentView.topAnchor).isActive = true
bg.leadingAnchor.constraint(equalTo: contentView.leadingAnchor).isActive = true
bg.trailingAnchor.constraint(equalTo: contentView.trailingAnchor).isActive = true
bg.bottomAnchor.constraint(equalTo: contentView.bottomAnchor).isActive = true
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
//if user is not logged in show login
private func handleNotAuthenticated() {
//check auth status
if Auth.auth().currentUser == nil {
//show log in screen
let loginVC = LoginViewController()
loginVC.modalPresentationStyle = .fullScreen
present(loginVC, animated: false)
}
}
}
and Here is the viewController I am trying to present a tableView on. It comes up with a white screen, but no tableView. Nor are my navigation items showing. Even when written programmatically.
import UIKit
class WalletViewController: UIViewController {
var addNewCard = [String]()
let button = UIButton()
let tableView = UITableView()
// MARK: - Properties
override func viewDidLoad() {
super.viewDidLoad()
addTable()
view.backgroundColor = UIColor(named: "RED")
button.setTitle("Add New Card", for: .normal)
view.addSubview(button)
button.backgroundColor = UIColor(named: "yellow-2")
button.setTitleColor(UIColor(named: "RED"), for: .normal)
button.frame = CGRect(x: 25, y: 700, width: 350, height: 50)
button.layer.cornerRadius = 15
button.addTarget(self, action: #selector(didTapAddButton), for: .touchUpInside)
if !UserDefaults().bool(forKey: "setup") {
UserDefaults().set(true, forKey: "setup")
UserDefaults().set(0, forKey: "count")
navigationItem.rightBarButtonItem = UIBarButtonItem(title: "Add New Card", style: .plain, target: self,
action: #selector(didTapAdd))
}
}
func updateCard() {
guard let count = UserDefaults().value(forKey: "count") as? Int else {
return
}
for x in 0..<count {
if let addCard = UserDefaults().value(forKey: "addCard\(x+1)") as? String {
addNewCard.append(addCard)
}
}
}
func addTable() {
tableView.frame = view.bounds
tableView.delegate = self
tableView.dataSource = self
tableView.separatorStyle = .singleLine
tableView.register(UITableViewCell.self, forCellReuseIdentifier: "addCard")
self.view.addSubview(tableView)
}
#IBAction func didTapAdd() {
let vc = storyboard?.instantiateViewController(withIdentifier: "addCard") as! addCardViewController
vc.update = {
DispatchQueue.main.async {
self.updateCard()
}
}
navigationController?.pushViewController(vc, animated: true)
}
#objc private func didTapAddButton() {
let rootVC = addCardViewController()
let navVC = UINavigationController(rootViewController: rootVC)
navVC.modalPresentationStyle = .fullScreen
present(navVC, animated: true)
}
}
extension WalletViewController: UITableViewDelegate {
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
tableView.deselectRow(at: indexPath, animated: true)
}
}
extension WalletViewController: UITableViewDataSource {
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let add = tableView.dequeueReusableCell(withIdentifier: "addCard", for: indexPath)
add.textLabel?.text = addNewCard[indexPath.row]
return add
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return addNewCard.count
}
}

Swift: Removing playerLayer in collectionViewCell's prepareForReuse crashes

I am attempting to remove an avPlayerLayer to avoid potential memory issues but it crashes.
I have a main collection view controller that displays TweetCells, and in each TweetCell, I have a mediaCollectionView displaying a horizontally scrolling MediaCells. Each of these mediaCell will contain an avPlayer if the content to display is a video.
As I understand from multiple SO posts, it is a correct practice to remove avPlayerLayer, and thus I am implementing it in prepareForReuse.
Here's my implementation:
//At TweetCell
class TweetCell: UICollectionViewCell {
lazy var mediaCollectionView: UICollectionView = {
let size = NSCollectionLayoutSize(
widthDimension: NSCollectionLayoutDimension.fractionalWidth(1),
heightDimension: NSCollectionLayoutDimension.fractionalHeight(1)
)
let item = NSCollectionLayoutItem(layoutSize: size)
let group = NSCollectionLayoutGroup.horizontal(layoutSize: size, subitem: item, count: 1)
let section = NSCollectionLayoutSection(group: group)
section.orthogonalScrollingBehavior = .paging
let layout = UICollectionViewCompositionalLayout(section: section)
let cv = UICollectionView(frame: .zero, collectionViewLayout: layout)
cv.backgroundColor = .systemGray
cv.translatesAutoresizingMaskIntoConstraints = false
cv.dataSource = self
cv.delegate = self
cv.register(MediaCell.self, forCellWithReuseIdentifier: "cell")
return cv
}()
var tweet: Tweet? {
didSet {
if let tweet = tweet {
//Setup other UI elements...
//Refresh mediaCollectionView
mediaCollectionView.reloadData()
mediaCollectionView.scrollToItem(at: IndexPath(item: 0, section: 0), at: .top, animated: false)
}
}
}
override func prepareForReuse() {
tweet = nil
//Other UI elements reset, eg, profileImageView.image = nil, or tweetLabel.text = ""
super.prepareForReuse()
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "cell", for: indexPath) as! MediaCell
if tweet?.photosArray != nil {
cell.media = tweet?.photosArray?[indexPath.item]
} else if let videos = tweet?.videosArray {
cell.media = videos[indexPath.item]
}
return cell
}
//At MediaCell
class MediaCell: UICollectionViewCell {
let imageView: UIImageView = {
let iv = UIImageView()
iv.translatesAutoresizingMaskIntoConstraints = false
iv.contentMode = .scaleAspectFill
iv.clipsToBounds = true
iv.backgroundColor = .systemGreen
return iv
}()
var videoView: AVPlayerView = {
let v = AVPlayerView()
v.translatesAutoresizingMaskIntoConstraints = false
v.isHidden = false
v.backgroundColor = .systemBlue
return v
}()
var player : AVPlayer?
var playerLayer: AVPlayerLayer? //Not in use
var media: [String: AnyObject]? {
didSet {
guard let media = media else {return}
if mediaType == "photo" {
//PHOTOS
///Toggle image/video view
removeAndDeactivateSubview(view: videoView)
addAndActivateSubview(view: imageView)
let url = URL(string: media["media_url_https"] as? String ?? "")
imageView.sd_setImage(with: url)
} else if mediaType == "video" {
//VIDEOS
///Toggle image/video view
removeAndDeactivateSubview(view: imageView)
addAndActivateSubview(view: videoView)
guard let urlString = media["url"] as? String else {return}
guard let url = URL(string: urlString) else {return}
player = AVPlayer(url: url)
player?.isMuted = true
let castedLayer = videoView.layer as? AVPlayerLayer
castedLayer?.player = player
} else {
print("Some other media content type")
}
}
}
override init(frame: CGRect) {
super.init(frame: frame)
}
func addAndActivateSubview(view: UIView) {
addSubview(view)
activateSubview(view: view)
}
func removeAndDeactivateSubview(view: UIView) {
deactivateSubview(view: view)
view.removeFromSuperview()
}
func activateSubview(view: UIView) {
view.topAnchor.constraint(equalTo: topAnchor).isActive = true
view.leadingAnchor.constraint(equalTo: leadingAnchor).isActive = true
view.trailingAnchor.constraint(equalTo: trailingAnchor).isActive = true
view.bottomAnchor.constraint(equalTo: bottomAnchor).isActive = true
}
func deactivateSubview(view: UIView) {
view.topAnchor.constraint(equalTo: topAnchor).isActive = false
view.leadingAnchor.constraint(equalTo: leadingAnchor).isActive = false
view.trailingAnchor.constraint(equalTo: trailingAnchor).isActive = false
view.bottomAnchor.constraint(equalTo: bottomAnchor).isActive = false
}
override func prepareForReuse() {
imageView.image = nil
player = nil
//CRASH HERE
if let layer = videoView.layer as? AVPlayerLayer {
layer.player = nil
layer.removeFromSuperlayer()
}
super.prepareForReuse()
}
//Custome AVPlayerView
class AVPlayerView: UIView {
override class var layerClass: AnyClass {
return AVPlayerLayer.self
}
}
The crash message points to the .layer object in the if let layer = videoView.layer as? AVPlayerLayer with the following message:
Thread 1: EXC_BAD_ACCESS (code=1, address=0x47cd649dd8e0)
I have attempted removing the avPlayerLayer after super.prepareForReuse but it also crashes.

Struggling to adjust cell height with increase in content

I have been struggling greatly almost regularly to be able to create a cell whose height adjusts with content while trying to do it programatically, some things i am trying are , image shows the problem
use below function
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
return UITableView.automaticDimension
}
2)Use this in viewDidLoad
tableView.rowHeight = UITableView.automaticDimension
tableView.estimatedRowHeight = 100
Setting bottom and top constraints to equal rather then not equal
Below i paste some code to show my struggle or trying every thing to get the cell to expand with content, which it does. not, can any one please suggest some ways to achieve it
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
switch indexPath.row {
case 0:
let cell = restaurantMainTable.dequeueReusableCell(withIdentifier: String(describing: RestaurantMainViewCells.self), for: indexPath) as! RestaurantMainViewCells
cell.heightAnchor.constraint(greaterThanOrEqualToConstant: 60).isActive = true
view.addSubview(cell)
view.addSubview(cell.contentView)
view.addSubview(cell.restaurantName)
view.addSubview(cell.restaurantType)
view.addSubview(cell.restaurantLocation)
view.addSubview(cell.restaurantMiniImage)
view.addSubview(cell.restaurantHeartImage)
cell.restaurantHeartImage.translatesAutoresizingMaskIntoConstraints = false
cell.restaurantMiniImage.translatesAutoresizingMaskIntoConstraints = false
cell.restaurantName.translatesAutoresizingMaskIntoConstraints = false
cell.restaurantLocation.translatesAutoresizingMaskIntoConstraints = false
cell.restaurantType.translatesAutoresizingMaskIntoConstraints = false
//Fonts
let font = UIFont(name: "Rubik-Medium", size: 18)
let fontMetrics = UIFontMetrics(forTextStyle: .body)
let labels = [cell.restaurantName, cell.restaurantLocation, cell.restaurantType]
labels.forEach { label in
label.font = fontMetrics.scaledFont(for: font!)
}
let stackLabels = UIStackView()
view.addSubview(stackLabels)
stackLabels.alignment = .top
stackLabels.distribution = .fill
stackLabels.spacing = 5
stackLabels.axis = .vertical
stackLabels.addArrangedSubview(cell.restaurantName)
stackLabels.addArrangedSubview(cell.restaurantType)
stackLabels.addArrangedSubview(cell.restaurantLocation)
let stackImage = UIStackView()
view.addSubview(stackImage)
stackLabels.translatesAutoresizingMaskIntoConstraints = false
stackImage.alignment = .top
stackImage.distribution = .fill
stackImage.axis = .horizontal
stackImage.spacing = 5
cell.restaurantMiniImage.heightAnchor.constraint(equalToConstant: 60).isActive = true
cell.restaurantMiniImage.widthAnchor.constraint(equalToConstant: 60).isActive = true
cell.restaurantMiniImage.layer.cornerRadius = 30
cell.restaurantMiniImage.clipsToBounds = true
cell.restaurantHeartImage.heightAnchor.constraint(equalToConstant: 20).isActive = true
cell.restaurantHeartImage.widthAnchor.constraint(equalToConstant: 20).isActive = true
cell.restaurantHeartImage.trailingAnchor.constraint(equalTo: cell.trailingAnchor, constant: -10).isActive = true
cell.restaurantHeartImage.topAnchor.constraint(equalTo: cell.topAnchor, constant: 20).isActive = true
stackImage.addArrangedSubview(cell.restaurantMiniImage)
stackImage.addArrangedSubview(stackLabels)
view.addSubview(stackImage)
stackImage.translatesAutoresizingMaskIntoConstraints = false
stackImage.leadingAnchor.constraint(equalTo: cell.leadingAnchor, constant: 10).isActive = true
stackImage.topAnchor.constraint(greaterThanOrEqualTo: cell.topAnchor, constant: 10).isActive = true
// stackImage.topAnchor.constraint(equalTo: cell.topAnchor, constant: 10).isActive = true
stackImage.trailingAnchor.constraint(equalTo: cell.restaurantHeartImage.leadingAnchor, constant: -10).isActive = true
// stackImage.bottomAnchor.constraint(equalTo: cell.bottomAnchor, constant: -10).isActive = true
stackImage.bottomAnchor.constraint(greaterThanOrEqualTo: cell.bottomAnchor, constant: -10).isActive = true
cell.restaurantName.text = restaurants[indexPath.row].name
cell.restaurantType.text = restaurants[indexPath.row].type
cell.restaurantLocation.text = restaurants[indexPath.row].location
cell.restaurantHeartImage.image = UIImage(named: "heart-tick")
if let restaurantImage = restaurants[indexPath.row].image {
cell.restaurantMiniImage.image = UIImage(data: restaurantImage as Data)
}
return cell
default:
fatalError("no data found")
}
}
UPDATE - The whole class
//
// RestaurantMainController.swift
// LaVivaRepeat
//
//
import UIKit
import CoreData
class RestaurantMainController: UIViewController, UITableViewDelegate, UITableViewDataSource, NSFetchedResultsControllerDelegate {
var restaurants: [Restaurant] = []
var fetchResultController: NSFetchedResultsController<Restaurant>!
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return restaurants.count
}
func numberOfSections(in tableView: UITableView) -> Int {
return 1
}
let restaurantMainTable = UITableView()
override func viewDidLoad() {
super.viewDidLoad()
view.addSubview(restaurantMainTable)
//MARK:- add delegates as self, always, else no contact with model will take place
restaurantMainTable.rowHeight = UITableView.automaticDimension
restaurantMainTable.estimatedRowHeight = 60
self.restaurantMainTable.delegate = self
self.restaurantMainTable.dataSource = self
self.restaurantMainTable.separatorStyle = .singleLine
//MARK:- create a view to show when no records are there
let backGroundView = UIView()
view.addSubview(backGroundView)
backGroundView.heightAnchor.constraint(equalToConstant: 500).isActive = true
let backGroundImage = UIImageView()
backGroundImage.translatesAutoresizingMaskIntoConstraints = false
backGroundView.translatesAutoresizingMaskIntoConstraints = false
backGroundImage.heightAnchor.constraint(equalToConstant: 300).isActive = true
backGroundImage.widthAnchor.constraint(equalToConstant: 320).isActive = true
backGroundImage.image = UIImage(named: "empty")
backGroundView.addSubview(backGroundImage)
backGroundView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 30).isActive = true
backGroundView.topAnchor.constraint(equalTo: view.topAnchor, constant: 90).isActive = true
backGroundView.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: 20).isActive = true
restaurantMainTable.backgroundView = backGroundView
restaurantMainTable.backgroundView?.isHidden = true
//MARK:- Add constraints to table
self.restaurantMainTable.translatesAutoresizingMaskIntoConstraints = false
self.restaurantMainTable.topAnchor.constraint(equalTo: view.topAnchor).isActive = true
self.restaurantMainTable.bottomAnchor.constraint(equalTo: view.bottomAnchor).isActive = true
self.restaurantMainTable.leadingAnchor.constraint(equalTo: view.leadingAnchor).isActive = true
self.restaurantMainTable.trailingAnchor.constraint(equalTo: view.trailingAnchor).isActive = true
//MARK:- register RestaurantMainViewCells
self.restaurantMainTable.register(RestaurantMainViewCells.self, forCellReuseIdentifier: String(describing: RestaurantMainViewCells.self))
//MARK:- Get fetch request
let fetchRequest: NSFetchRequest<Restaurant> = Restaurant.fetchRequest()
let sortDescriptor = NSSortDescriptor(key: "name", ascending: true)
fetchRequest.sortDescriptors = [sortDescriptor]
if let appDelegate = (UIApplication.shared.delegate as? AppDelegate) {
let context = appDelegate.persistentContainer.viewContext
fetchResultController = NSFetchedResultsController(fetchRequest: fetchRequest, managedObjectContext: context, sectionNameKeyPath: nil, cacheName: nil)
fetchResultController.delegate = self
do {
try fetchResultController.performFetch()
if let fetchObject = fetchResultController.fetchedObjects {
restaurants = fetchObject
}
}
catch {
print(error)
}
}
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
restaurantMainTable.rowHeight = UITableView.automaticDimension
switch indexPath.row {
case 0:
let cell = restaurantMainTable.dequeueReusableCell(withIdentifier: String(describing: RestaurantMainViewCells.self), for: indexPath) as! RestaurantMainViewCells
// cell.heightAnchor.constraint(greaterThanOrEqualToConstant: 60).isActive = true
view.addSubview(cell)
view.addSubview(cell.contentView)
view.addSubview(cell.restaurantName)
view.addSubview(cell.restaurantType)
view.addSubview(cell.restaurantLocation)
view.addSubview(cell.restaurantMiniImage)
view.addSubview(cell.restaurantHeartImage)
cell.restaurantHeartImage.translatesAutoresizingMaskIntoConstraints = false
cell.restaurantMiniImage.translatesAutoresizingMaskIntoConstraints = false
cell.restaurantName.translatesAutoresizingMaskIntoConstraints = false
cell.restaurantLocation.translatesAutoresizingMaskIntoConstraints = false
cell.restaurantType.translatesAutoresizingMaskIntoConstraints = false
//Fonts
let font = UIFont(name: "Rubik-Medium", size: 18)
let fontMetrics = UIFontMetrics(forTextStyle: .body)
let labels = [cell.restaurantName, cell.restaurantLocation, cell.restaurantType]
labels.forEach { label in
label.font = fontMetrics.scaledFont(for: font!)
}
let stackLabels = UIStackView()
view.addSubview(stackLabels)
stackLabels.alignment = .top
stackLabels.distribution = .fill
stackLabels.spacing = 5
stackLabels.axis = .vertical
stackLabels.addArrangedSubview(cell.restaurantName)
stackLabels.addArrangedSubview(cell.restaurantType)
stackLabels.addArrangedSubview(cell.restaurantLocation)
let stackImage = UIStackView()
view.addSubview(stackImage)
stackLabels.translatesAutoresizingMaskIntoConstraints = false
stackImage.alignment = .top
stackImage.distribution = .fill
stackImage.axis = .horizontal
stackImage.spacing = 5
cell.restaurantMiniImage.heightAnchor.constraint(equalToConstant: 60).isActive = true
cell.restaurantMiniImage.widthAnchor.constraint(equalToConstant: 60).isActive = true
cell.restaurantMiniImage.layer.cornerRadius = 30
cell.restaurantMiniImage.clipsToBounds = true
cell.restaurantHeartImage.heightAnchor.constraint(equalToConstant: 20).isActive = true
cell.restaurantHeartImage.widthAnchor.constraint(equalToConstant: 20).isActive = true
cell.restaurantHeartImage.trailingAnchor.constraint(equalTo: cell.trailingAnchor, constant: -10).isActive = true
cell.restaurantHeartImage.topAnchor.constraint(equalTo: cell.topAnchor, constant: 20).isActive = true
stackImage.addArrangedSubview(cell.restaurantMiniImage)
stackImage.addArrangedSubview(stackLabels)
view.addSubview(stackImage)
stackImage.translatesAutoresizingMaskIntoConstraints = false
stackImage.leadingAnchor.constraint(equalTo: cell.leadingAnchor, constant: 10).isActive = true
stackImage.topAnchor.constraint(greaterThanOrEqualTo: cell.topAnchor, constant: 10).isActive = true
// stackImage.topAnchor.constraint(equalTo: cell.topAnchor, constant: 10).isActive = true
stackImage.trailingAnchor.constraint(equalTo: cell.restaurantHeartImage.leadingAnchor, constant: -10).isActive = true
// stackImage.bottomAnchor.constraint(equalTo: cell.bottomAnchor, constant: -10).isActive = true
stackImage.bottomAnchor.constraint(greaterThanOrEqualTo: cell.bottomAnchor, constant: -10).isActive = true
cell.restaurantName.text = restaurants[indexPath.row].name
cell.restaurantType.text = restaurants[indexPath.row].type
cell.restaurantLocation.text = restaurants[indexPath.row].location
cell.restaurantHeartImage.image = UIImage(named: "heart-tick")
if let restaurantImage = restaurants[indexPath.row].image {
cell.restaurantMiniImage.image = UIImage(data: restaurantImage as Data)
}
return cell
default:
fatalError("no data found")
}
}
//MARK:- Make custom navigation bar large font size and use rubik fonts
override func viewWillAppear(_ animated: Bool) {
navigationController?.navigationBar.prefersLargeTitles = true
self.navigationController?.navigationBar.topItem?.title = "LaViva Hotel"
navigationController?.navigationBar.setBackgroundImage(UIImage(), for: .default)
navigationController?.navigationBar.shadowImage = UIImage()
navigationController?.hidesBarsOnSwipe = true
if let customFont = UIFont(name: "Rubik-Medium", size: 40) {
navigationController?.navigationBar.largeTitleTextAttributes = [NSAttributedString.Key.foregroundColor: UIColor(red: 200/255, green: 70/255, blue: 70/255, alpha: 1), NSAttributedString.Key.font: customFont]
}
//MARK:- for empty table
if restaurants.count > 0 {
self.restaurantMainTable.backgroundView?.isHidden = true
self.restaurantMainTable.separatorStyle = .singleLine
}
else {
self.restaurantMainTable.backgroundView?.isHidden = false
self.restaurantMainTable.separatorStyle = .none
}
//MARK:- make an + button appear on top left
let addButton = UIBarButtonItem(image: UIImage(named: "plus"), style: .plain, target: self, action: #selector(addNewRestaurant))
//navigationController?.navigationItem.rightBarButtonItem = addButton
self.navigationItem.rightBarButtonItem = addButton
}
//MARK:- addNewRestaurant function
#objc func addNewRestaurant() {
let pushController = RestaurantAddController()
navigationController?.pushViewController(pushController, animated: true)
}
//MARK:- try and show cell and tower as default or dark done here
override var preferredStatusBarStyle: UIStatusBarStyle {
return .default
}
//add update delete
func controllerWillChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
restaurantMainTable.beginUpdates()
}
func controller(_ controller: NSFetchedResultsController<NSFetchRequestResult>, didChange anObject: Any, at indexPath: IndexPath?, for type: NSFetchedResultsChangeType, newIndexPath: IndexPath?) {
switch type {
case .insert:
if let newIndexPath = newIndexPath {
restaurantMainTable.insertRows(at: [newIndexPath], with: .fade)
}
case .delete:
if let indexPath = indexPath {
restaurantMainTable.deleteRows(at: [indexPath], with: .fade)
}
case .update:
if let indexPath = indexPath {
restaurantMainTable.reloadRows(at: [indexPath], with: .fade)
}
default:
restaurantMainTable.reloadData()
}
if let fetchedObjects = controller.fetchedObjects {
restaurants = fetchedObjects as! [Restaurant]
}
}
func controllerDidChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
restaurantMainTable.endUpdates()
}
//MARK:- left swipr delete
func tableView(_ tableView: UITableView, trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? {
let deleteAction = UIContextualAction(style: .destructive, title: "Delete") { (action, view, completionHandler) in
if let appDelegate = (UIApplication.shared.delegate as? AppDelegate) {
let context = appDelegate.persistentContainer.viewContext
let restaurantsToDelete = self.fetchResultController.object(at: indexPath)
context.delete(restaurantsToDelete)
appDelegate.saveContext()
}
completionHandler(true)
}
let swipeConfiguration: UISwipeActionsConfiguration
swipeConfiguration = UISwipeActionsConfiguration(actions: [deleteAction])
return swipeConfiguration
}
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
return UITableView.automaticDimension
}
func tableView(_ tableView: UITableView, estimatedHeightForRowAt indexPath: IndexPath) -> CGFloat {
return UITableView.automaticDimension
}
}
UPDATE
import UIKit
class RestaurantMainViewCells: UITableViewCell {
var restaurantMiniImage = UIImageView()
var restaurantHeartImage = UIImageView()
var restaurantName = UILabel()
var restaurantType = UILabel()
var restaurantLocation = UILabel()
override func awakeFromNib() {
super.awakeFromNib()
// Initialization code
}
override func setSelected(_ selected: Bool, animated: Bool) {
super.setSelected(selected, animated: animated)
// Configure the view for the selected state
}
}
All of your cell setup - adding and constraining UI elements - should be done in your cell class. Absolutely NOT in cellForRowAt.
You would do well to go through a few tutorials on creating dynamic cells.
But, to give you an idea, here is your code modified so you can see what's happening:
struct Restaurant {
var name: String = ""
var type: String = ""
var location: String = ""
// however you have your image information stored
//var image
}
class RestaurantMainViewCells: UITableViewCell {
var restaurantMiniImage = UIImageView()
var restaurantHeartImage = UIImageView()
var restaurantName = UILabel()
var restaurantType = UILabel()
var restaurantLocation = UILabel()
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
commonInit()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func commonInit() -> Void {
// so we can see the image view frames without actual images...
restaurantMiniImage.backgroundColor = .green
restaurantHeartImage.backgroundColor = .red
var font: UIFont = UIFont.systemFont(ofSize: 18)
if let f = UIFont(name: "Rubik-Medium", size: 18) {
font = f
}
let fontMetrics = UIFontMetrics(forTextStyle: .body)
let labels = [restaurantName, restaurantLocation, restaurantType]
labels.forEach { label in
label.font = fontMetrics.scaledFont(for: font)
// so we can see label frames...
label.backgroundColor = .yellow
}
let stackLabels = UIStackView()
stackLabels.alignment = .fill
stackLabels.distribution = .fill
stackLabels.spacing = 5
stackLabels.axis = .vertical
stackLabels.addArrangedSubview(restaurantName)
stackLabels.addArrangedSubview(restaurantType)
stackLabels.addArrangedSubview(restaurantLocation)
let stackImage = UIStackView()
stackImage.alignment = .top
stackImage.distribution = .fill
stackImage.axis = .horizontal
stackImage.spacing = 5
restaurantMiniImage.layer.cornerRadius = 30
restaurantMiniImage.clipsToBounds = true
stackImage.addArrangedSubview(restaurantMiniImage)
stackImage.addArrangedSubview(stackLabels)
contentView.addSubview(stackImage)
contentView.addSubview(restaurantHeartImage)
restaurantHeartImage.translatesAutoresizingMaskIntoConstraints = false
stackImage.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
// mini image 60x60
restaurantMiniImage.heightAnchor.constraint(equalToConstant: 60),
restaurantMiniImage.widthAnchor.constraint(equalToConstant: 60),
// heart image 20 x 20
restaurantHeartImage.heightAnchor.constraint(equalToConstant: 20),
restaurantHeartImage.widthAnchor.constraint(equalToConstant: 20),
// heart image top+20 trailing-10
restaurantHeartImage.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 20),
restaurantHeartImage.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -10),
// horizontal stack top / leading / bottom and trailinh to heart image
// all with 10-pts "padding"
stackImage.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 10),
stackImage.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 10),
stackImage.trailingAnchor.constraint(equalTo: restaurantHeartImage.leadingAnchor, constant: -10),
stackImage.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -10),
])
}
}
class RestaurantMainController: UIViewController, UITableViewDelegate, UITableViewDataSource {
var restaurants: [Restaurant] = []
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return restaurants.count
}
func numberOfSections(in tableView: UITableView) -> Int {
return 1
}
let restaurantMainTable = UITableView()
override func viewDidLoad() {
super.viewDidLoad()
view.addSubview(restaurantMainTable)
//MARK:- add delegates as self, always, else no contact with model will take place
restaurantMainTable.estimatedRowHeight = 60
self.restaurantMainTable.delegate = self
self.restaurantMainTable.dataSource = self
self.restaurantMainTable.separatorStyle = .singleLine
//MARK:- Add constraints to table
self.restaurantMainTable.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
restaurantMainTable.topAnchor.constraint(equalTo: view.topAnchor),
restaurantMainTable.bottomAnchor.constraint(equalTo: view.bottomAnchor),
restaurantMainTable.leadingAnchor.constraint(equalTo: view.leadingAnchor),
restaurantMainTable.trailingAnchor.constraint(equalTo: view.trailingAnchor),
])
//MARK:- register RestaurantMainViewCells
self.restaurantMainTable.register(RestaurantMainViewCells.self, forCellReuseIdentifier: String(describing: RestaurantMainViewCells.self))
//MARK:- Get fetch request
// I don't have your "fetch" data, so I'm just adding a couple restaurants here
restaurants.append(Restaurant(name: "Cafe De Loir", type: "Chinese Cousine", location: "Hong Kong"))
restaurants.append(Restaurant(name: "Bob's Cafe", type: "Japanese Cousine", location: "Tokyo"))
restaurants.append(Restaurant(name: "Mary's Restaurant", type: "Home Cooking", location: "Dallas, Texas"))
// let fetchRequest: NSFetchRequest<Restaurant> = Restaurant.fetchRequest()
// let sortDescriptor = NSSortDescriptor(key: "name", ascending: true)
// fetchRequest.sortDescriptors = [sortDescriptor]
//
// if let appDelegate = (UIApplication.shared.delegate as? AppDelegate) {
// let context = appDelegate.persistentContainer.viewContext
// fetchResultController = NSFetchedResultsController(fetchRequest: fetchRequest, managedObjectContext: context, sectionNameKeyPath: nil, cacheName: nil)
// fetchResultController.delegate = self
//
// do {
// try fetchResultController.performFetch()
// if let fetchObject = fetchResultController.fetchedObjects {
// restaurants = fetchObject
// }
// }
//
// catch {
// print(error)
// }
// }
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: RestaurantMainViewCells.self), for: indexPath) as! RestaurantMainViewCells
let r = restaurants[indexPath.row]
cell.restaurantName.text = r.name
cell.restaurantType.text = r.type
cell.restaurantLocation.text = r.location
//if let restaurantImage = restaurants[indexPath.row].image {
// cell.restaurantMiniImage.image = UIImage(data: restaurantImage as Data)
//}
cell.restaurantHeartImage.image = UIImage(named: "heart-tick")
return cell
}
}
The result (I don't have your images so the image views have green or red background color):

Collection View Diffable Data Source cells disappearing and not resizing properly?

I'm having a really weird issue with my collection view. I'm using the Compositional Layout and Diffable Data Source APIs for iOS 13+, but I'm getting some really weird behavior. As seen in the video below, when I update the data source, the first cell that is added to the top section doesn't resize properly, then when I add the second cell both cells disappear, and then when I add a third cell, all load in with the proper sizes and appear. When I unadd all the cells and add them back in a similar fashion a second time, that initial issue doesn't happen again.
Video of Error
I have tried using the following solutions in some fashion:
collectionView.collectionViewLayout.invalidateLayout()
cell.contentView.setNeedsLayout() followed by cell.contentView.layoutIfNeeded()
collectionView.reloadData()
I can't seem to figure out what might be causing this issue. Perhaps it could be that I have two different cells registered with the collection view and dequeueing them improperly or my data types aren't correctly conforming to hashable. I believe I've fixed both of those issues, but I will also provide my code to help. Also the data controller mentioned is a simple class that stores an array of view models for the cells to use for configuration (there shouldn't be any issue there). Thanks!
Collection View Controller
import UIKit
class PartyInvitesViewController: UIViewController {
private var collectionView: UICollectionView!
private lazy var layout = createLayout()
private lazy var dataSource = createDataSource()
private let searchController = UISearchController(searchResultsController: nil)
private let dataController = InvitesDataController()
override func loadView() {
super.loadView()
collectionView = UICollectionView(frame: .zero, collectionViewLayout: layout)
collectionView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(collectionView)
NSLayoutConstraint.activate([
collectionView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
collectionView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor),
collectionView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor),
collectionView.bottomAnchor.constraint(equalTo: view.bottomAnchor)
])
}
override func viewDidLoad() {
super.viewDidLoad()
let backButton = UIBarButtonItem(title: "", style: .plain, target: nil, action: nil)
backButton.tintColor = UIColor.Fiesta.primary
navigationItem.backBarButtonItem = backButton
let titleView = UILabel()
titleView.text = "invite"
titleView.textColor = .white
titleView.font = UIFont.Fiesta.Black.header
navigationItem.titleView = titleView
navigationItem.searchController = searchController
navigationItem.hidesSearchBarWhenScrolling = false
// definesPresentationContext = true
navigationItem.largeTitleDisplayMode = .never
navigationController?.navigationBar.isTranslucent = true
extendedLayoutIncludesOpaqueBars = true
collectionView.register(InvitesCell.self, forCellWithReuseIdentifier: InvitesCell.reuseIdentifier)
collectionView.register(InvitedCell.self, forCellWithReuseIdentifier: InvitedCell.reuseIdentifier)
collectionView.register(InvitesSectionHeaderReusableView.self, forSupplementaryViewOfKind: UICollectionView.elementKindSectionHeader, withReuseIdentifier: InvitesSectionHeaderReusableView.reuseIdentifier)
collectionView.delegate = self
collectionView.dataSource = dataSource
dataController.cellPressed = { [weak self] in
self?.update()
}
dataController.start()
update(animate: false)
view.backgroundColor = .secondarySystemBackground
collectionView.backgroundColor = .secondarySystemBackground
}
}
extension PartyInvitesViewController: UICollectionViewDelegate {
func collectionView(_ collectionView: UICollectionView, willDisplay cell: UICollectionViewCell, forItemAt indexPath: IndexPath) {
// cell.contentView.setNeedsLayout()
// cell.contentView.layoutIfNeeded()
}
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
if indexPath.section == InvitesSection.unselected.rawValue {
let viewModel = dataController.getAll()[indexPath.item]
dataController.didSelect(viewModel, completion: nil)
}
}
}
extension PartyInvitesViewController {
func update(animate: Bool = true) {
var snapshot = NSDiffableDataSourceSnapshot<InvitesSection, InvitesCellViewModel>()
snapshot.appendSections(InvitesSection.allCases)
snapshot.appendItems(dataController.getTopSelected(), toSection: .selected)
snapshot.appendItems(dataController.getSelected(), toSection: .unselected)
snapshot.appendItems(dataController.getUnselected(), toSection: .unselected)
dataSource.apply(snapshot, animatingDifferences: animate) {
// self.collectionView.reloadData()
// self.collectionView.collectionViewLayout.invalidateLayout()
}
}
}
extension PartyInvitesViewController {
private func createDataSource() -> InvitesCollectionViewDataSource {
let dataSource = InvitesCollectionViewDataSource(collectionView: collectionView, cellProvider: { collectionView, indexPath, viewModel -> UICollectionViewCell? in
switch indexPath.section {
case InvitesSection.selected.rawValue:
guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: InvitedCell.reuseIdentifier, for: indexPath) as? InvitedCell else { return nil }
cell.configure(with: viewModel)
cell.onDidCancel = { self.dataController.didSelect(viewModel, completion: nil) }
return cell
case InvitesSection.unselected.rawValue:
guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: InvitesCell.reuseIdentifier, for: indexPath) as? InvitesCell else { return nil }
cell.configure(with: viewModel)
return cell
default:
return nil
}
})
dataSource.supplementaryViewProvider = { collectionView, kind, indexPath -> UICollectionReusableView? in
guard kind == UICollectionView.elementKindSectionHeader else { return nil }
guard let view = collectionView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: InvitesSectionHeaderReusableView.reuseIdentifier, for: indexPath) as? InvitesSectionHeaderReusableView else { return nil }
switch indexPath.section {
case InvitesSection.selected.rawValue:
view.titleLabel.text = "Inviting"
case InvitesSection.unselected.rawValue:
view.titleLabel.text = "Suggested"
default: return nil
}
return view
}
return dataSource
}
}
extension PartyInvitesViewController {
private func createLayout() -> UICollectionViewLayout {
let layout = UICollectionViewCompositionalLayout { section, _ -> NSCollectionLayoutSection? in
switch section {
case InvitesSection.selected.rawValue:
return self.createSelectedSection()
case InvitesSection.unselected.rawValue:
return self.createUnselectedSection()
default: return nil
}
}
return layout
}
private func createSelectedSection() -> NSCollectionLayoutSection {
let width: CGFloat = 120
let height: CGFloat = 60
let layoutSize = NSCollectionLayoutSize(widthDimension: .estimated(width), heightDimension: .absolute(height))
let item = NSCollectionLayoutItem(layoutSize: layoutSize)
let group = NSCollectionLayoutGroup.horizontal(layoutSize: layoutSize, subitems: [item])
let headerSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: .absolute(60))
let sectionHeader = NSCollectionLayoutBoundarySupplementaryItem(layoutSize: headerSize, elementKind: UICollectionView.elementKindSectionHeader, alignment: .top)
let section = NSCollectionLayoutSection(group: group)
section.boundarySupplementaryItems = [sectionHeader]
section.orthogonalScrollingBehavior = .continuous
// for some reason content insets breaks the estimation process idk why
section.contentInsets = NSDirectionalEdgeInsets(top: 20, leading: 20, bottom: 20, trailing: 20)
section.interGroupSpacing = 20
return section
}
private func createUnselectedSection() -> NSCollectionLayoutSection {
let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: .fractionalHeight(1.0))
let item = NSCollectionLayoutItem(layoutSize: itemSize)
let groupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: .estimated(60))
let group = NSCollectionLayoutGroup.vertical(layoutSize: groupSize, subitems: [item])
let headerSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: .absolute(60))
let sectionHeader = NSCollectionLayoutBoundarySupplementaryItem(layoutSize: headerSize, elementKind: UICollectionView.elementKindSectionHeader, alignment: .top)
let section = NSCollectionLayoutSection(group: group)
section.boundarySupplementaryItems = [sectionHeader]
section.contentInsets = NSDirectionalEdgeInsets(top: 20, leading: 20, bottom: 20, trailing: 20)
section.interGroupSpacing = 20
return section
}
}
Invites Cell (First Cell Type)
class InvitesCell: FiestaGenericCell {
static let reuseIdentifier = "InvitesCell"
var stackView = UIStackView()
var userStackView = UIStackView()
var userImageView = UIImageView()
var nameStackView = UIStackView()
var usernameLabel = UILabel()
var nameLabel = UILabel()
var inviteButton = UIButton()
override func layoutSubviews() {
super.layoutSubviews()
userImageView.layer.cornerRadius = 28
}
override func arrangeSubviews() {
stackView.translatesAutoresizingMaskIntoConstraints = false
contentView.addSubview(stackView)
stackView.addArrangedSubview(userStackView)
stackView.addArrangedSubview(inviteButton)
userStackView.addArrangedSubview(userImageView)
userStackView.addArrangedSubview(nameStackView)
nameStackView.addArrangedSubview(usernameLabel)
nameStackView.addArrangedSubview(nameLabel)
setNeedsUpdateConstraints()
}
override func loadConstraints() {
// Stack view constraints
NSLayoutConstraint.activate([
stackView.widthAnchor.constraint(equalTo: contentView.widthAnchor),
stackView.heightAnchor.constraint(equalTo: contentView.heightAnchor)
])
// User image view constraints
NSLayoutConstraint.activate([
userImageView.heightAnchor.constraint(equalToConstant: 56),
userImageView.widthAnchor.constraint(equalToConstant: 56)
])
}
override func configureSubviews() {
// Stack view configuration
stackView.axis = .horizontal
stackView.alignment = .center
stackView.distribution = .equalSpacing
// User stack view configuration
userStackView.axis = .horizontal
userStackView.alignment = .center
userStackView.spacing = Constants.inset
// User image view configuration
userImageView.image = UIImage(named: "Image-4")
userImageView.contentMode = .scaleAspectFill
userImageView.clipsToBounds = true
// Name stack view configuration
nameStackView.axis = .vertical
nameStackView.alignment = .leading
nameStackView.spacing = 4
nameStackView.distribution = .fillProportionally
// Username label configuration
usernameLabel.textColor = .white
usernameLabel.font = UIFont.Fiesta.Black.text
// Name label configuration
nameLabel.textColor = .white
nameLabel.font = UIFont.Fiesta.Light.footnote
// Invite button configuration
let configuration = UIImage.SymbolConfiguration(weight: .heavy)
inviteButton.setImage(UIImage(systemName: "circle", withConfiguration: configuration), for: .normal)
inviteButton.tintColor = .white
}
}
extension InvitesCell {
func configure(with viewModel: InvitesCellViewModel) {
usernameLabel.text = viewModel.username
nameLabel.text = viewModel.name
let configuration = UIImage.SymbolConfiguration(weight: .heavy)
if viewModel.isSelected {
inviteButton.setImage(UIImage(systemName: "checkmark.circle.fill", withConfiguration: configuration), for: .normal)
inviteButton.tintColor = .green
} else {
inviteButton.setImage(UIImage(systemName: "circle", withConfiguration: configuration), for: .normal)
inviteButton.tintColor = .white
}
}
}
Invited Cell (Second Cell Type)
import UIKit
class InvitedCell: FiestaGenericCell {
static let reuseIdentifier = "InvitedCell"
var mainView = UIView()
var usernameLabel = UILabel()
// var cancelButton = UIButton()
var onDidCancel: (() -> Void)?
override func layoutSubviews() {
super.layoutSubviews()
mainView.layer.cornerRadius = 8
}
override func arrangeSubviews() {
mainView.translatesAutoresizingMaskIntoConstraints = false
usernameLabel.translatesAutoresizingMaskIntoConstraints = false
contentView.addSubview(mainView)
mainView.addSubview(usernameLabel)
}
override func loadConstraints() {
// Main view constraints
NSLayoutConstraint.activate([
mainView.widthAnchor.constraint(equalTo: contentView.widthAnchor),
mainView.heightAnchor.constraint(equalTo: contentView.heightAnchor)
])
// Username label constraints
NSLayoutConstraint.activate([
usernameLabel.topAnchor.constraint(equalTo: mainView.topAnchor, constant: 20),
usernameLabel.leftAnchor.constraint(equalTo: mainView.leftAnchor, constant: 20),
usernameLabel.rightAnchor.constraint(equalTo: mainView.rightAnchor, constant: -20),
usernameLabel.bottomAnchor.constraint(equalTo: mainView.bottomAnchor, constant: -20)
])
}
override func configureSubviews() {
// Main view configuration
mainView.backgroundColor = .tertiarySystemBackground
// Username label configuration
usernameLabel.textColor = .white
usernameLabel.font = UIFont.Fiesta.Black.text
}
}
extension InvitedCell {
func configure(with viewModel: InvitesCellViewModel) {
usernameLabel.text = viewModel.username
}
#objc func cancel() {
onDidCancel?()
}
}
Invites Cell View Model (model for the cells)
import Foundation
struct InvitesCellViewModel {
var id = UUID()
private var model: User
init(_ model: User, selected: Bool) {
self.model = model
self.isSelected = selected
}
var username: String?
var name: String?
var isSelected: Bool
mutating func toggleIsSelected() {
isSelected = !isSelected
}
}
extension InvitesCellViewModel: Hashable {
func hash(into hasher: inout Hasher) {
hasher.combine(id)
hasher.combine(isSelected)
}
static func == (lhs: InvitesCellViewModel, rhs: InvitesCellViewModel) -> Bool {
lhs.id == rhs.id && lhs.isSelected == rhs.isSelected
}
}
If I need to provide anything else to better assist in answering this question, please let me know in the comments!
This may not be a solution for everyone, but I ended up fully switching over to RxSwift. For those who are debating the switch, I now use RxDataSources and the UICollectionViewCompositionalLayout with virtually no problems (outside of the occasional bug or two). I know this may not be the answer most are looking for, but looking back, this issue seems to be on Apple's end, so I figured it was best to find another path. If anybody has found a solution that is simpler than completely jumping over to Rx, please feel free to add your answer as well.

Resources