TableView cells are getting empty data when scrolling quickly - ios

So I'm running a function(item.getUserBySupporterID(supporter_id: supporter_id)) in each cell to make a request in my view model for each cell to get a String and an Image for each cell. If I scroll down fast on my table view, the data at the bottom is not loaded from the requests while some requests return nil. If I scroll slow, the data is loaded fine:
https://gph.is/g/apk3N5O
import Foundation
import UIKit
class NotificationVC: Toolbar, UITableViewDelegate, UITableViewDataSource {
private var myTableView:UITableView!
var notifications:[NotificationViewModel] = [] {
didSet {
myTableView.reloadData()
}
}
var profile = SessionManager.shared.profile
override func viewDidLoad() {
view.backgroundColor = UIColor.white
navigationController?.isToolbarHidden = false
addTableView()
loadNotifications()
}
func loadNotifications() {
print("loadNotifications")
if let user_id = profile?.sub {
let getNotifications = GETNotificationsByUserID(user_id: user_id)
getNotifications.getNotifications { notifications in
self.notifications = notifications.map { notification in
let ret = NotificationViewModel()
ret.mainNotification = notification
return ret
}
}
}
}
func addTableView() {
self.myTableView = UITableView()
self.myTableView?.translatesAutoresizingMaskIntoConstraints = false
self.myTableView.frame.size.height = self.view.frame.height
self.myTableView.frame.size.width = self.view.frame.width
self.myTableView.register(NotificationCell.self, forCellReuseIdentifier: "MyCell")
self.myTableView.dataSource = self
self.myTableView.delegate = self
self.myTableView.isScrollEnabled = true
myTableView.delaysContentTouches = false
self.view.addSubview(self.myTableView)
self.myTableView?.topAnchor.constraint(equalTo: self.view.topAnchor).isActive = true
self.myTableView?.leadingAnchor.constraint(equalTo: self.view.leadingAnchor).isActive = true
self.myTableView?.trailingAnchor.constraint(equalTo: self.view.trailingAnchor).isActive = true
self.myTableView.bottomAnchor.constraint(equalTo: self.view.bottomAnchor).isActive = true
self.myTableView.estimatedRowHeight = 100
self.myTableView.rowHeight = UITableView.automaticDimension
myTableView.layoutMargins = UIEdgeInsets.zero
myTableView.separatorInset = UIEdgeInsets.zero
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return notifications.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "MyCell", for: indexPath as IndexPath) as! NotificationCell
cell.selectionStyle = .none
cell.delegate = self
let item = self.notifications[indexPath.item]
cell.viewModel = item
return cell
}
}
import Foundation
import UIKit
class NotificationCell: UITableViewCell {
static var shared = NotificationCell()
var profile = SessionManager.shared.profile
var notificationType:String?
var messageTextViewBtm:NSLayoutConstraint?
var viewModel: NotificationViewModel? {
didSet {
if let item = viewModel {
if let notificationMessage = item.mainNotification?.message {
if notificationMessage.contains("replied to your comment") {
notificationType = "reply"
} else if notificationMessage.contains("liked your comment") {
notificationType = "likedComment"
} else if notificationMessage.contains("started following you") {
notificationType = "follow"
} else if notificationMessage.contains("liked your post") {
notificationType = "likedPost"
} else if notificationMessage.contains("commented on your post") {
notificationType = "commentedPost"
}
}
if let supporter_id = item.mainNotification?.supporter_id {
if item.gotSupporter == false {
item.getUserBySupporterID(supporter_id: supporter_id)
} else {
self.user_image.image = item.supporterImage
self.username.text = item.supporterName
}
item.supporterImageDidSet = { [weak self] in self?.user_image.image = $0 }
item.supporterNameDidSet = { [weak self] in self?.username.text = $0 }
}
}
}
}
var components:URLComponents = {
var component = URLComponents()
component.scheme = "http"
component.host = "localhost"
component.port = 8000
return component
}()
lazy var username:UILabel = {
let label = UILabel()
label.text = ""
label.font = label.font.withSize(19)
label.sizeToFit()
return label
}()
lazy var user_image: UIImageView = {
let image = UIImageView()
image.isUserInteractionEnabled = true
let gesture = UITapGestureRecognizer()
gesture.addTarget(self, action: #selector(userImageClicked))
image.addGestureRecognizer(gesture)
return image
}()
lazy var messageTextView:UITextView = {
let tv = UITextView()
tv.isScrollEnabled = false
tv.isEditable = false
tv.sizeToFit()
tv.backgroundColor = UIColor.lightGray
tv.textContainer.maximumNumberOfLines = 0
tv.textContainer.lineBreakMode = .byCharWrapping
tv.font = UIFont(name: "GillSans", size: 18)
return tv
}()
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
addSubview(username)
addSubview(user_image)
addSubview(messageTextView)
user_imageContraints()
usernameContraints()
messageConstraints()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func user_imageContraints() {
user_image.translatesAutoresizingMaskIntoConstraints = false
user_image.topAnchor.constraint(equalTo:topAnchor, constant: 8).isActive = true
user_image.leadingAnchor.constraint(equalTo:leadingAnchor, constant: 8).isActive = true
user_image.heightAnchor.constraint(equalToConstant: 40).isActive = true
user_image.widthAnchor.constraint(equalToConstant: 40).isActive = true
}
func usernameContraints() {
username.translatesAutoresizingMaskIntoConstraints = false
username.topAnchor.constraint(equalTo: user_image.topAnchor).isActive = true
username.leadingAnchor.constraint(equalTo: user_image.trailingAnchor, constant: 3).isActive = true
username.heightAnchor.constraint(equalToConstant: 25).isActive = true
}
func messageConstraints() {
messageTextView.translatesAutoresizingMaskIntoConstraints = false
messageTextView.topAnchor.constraint(equalTo: username.bottomAnchor).isActive = true
messageTextViewBtm = messageTextView.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -20)
messageTextViewBtm?.isActive = true
messageTextView.leadingAnchor.constraint(equalTo: user_image.trailingAnchor, constant: 3).isActive = true
messageTextView.trailingAnchor.constraint(equalTo: post_image.leadingAnchor, constant: -5).isActive = true
}
}
import UIKit
import Foundation
class NotificationViewModel {
var mainNotification: Notifications?
var imageLoader: DownloadImage?
var supporterName: String? { didSet { supporterNameDidSet?(supporterName) } }
var supporterNameDidSet: ((String?)->())?
var supporterImage: UIImage? = UIImage() { didSet { supporterImageDidSet?(supporterImage) } }
var supporterImageDidSet: ((UIImage?)->())?
var gotSupporter:Bool = false
func getUserBySupporterID(supporter_id:String) {
GetUsersById(id: supporter_id).getAllPosts { user in
self.gotSupporter = true
self.imageLoader = DownloadImage()
self.imageLoader?.imageDidSet = { [weak self] image in
self?.supporterImage = image
self?.supporterImageDidSet?(image)
}
if let picture = user[0].picture {
self.imageLoader?.downloadImage(urlString: picture)
}
self.supporterName = user[0].username
self.supporterNameDidSet?(user[0].username)
}
}
}```

It is not a nice idea to fetch cell info from remote source one by one. It seems you need to find a way to get all supporter infos at once and put them in your notifications. After then dispylay your data with comleted data source.
Downloading image for each cell one by one can be ok with caching. Because images are too big things to download at once(and again and again). And this is not good for your customers especially if they are using cellular data with high pricing(Also most of cases users does not scrooll to bottom of your view so downloading an imagee to not even display is not a good idea). But it seems not same for your supporter info. They can be fetched at once. If there are too much notification data or fething too much supporter info at once makes your app slower, You can write a pagination mechanism.

Related

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

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.

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 row height and cell.frame.height / cell.contentView.frame.heigth doesn't same

I have a table view that I initialized with all delegate functions. I have a custom cell named FeedCell. I'm trying to take row height inside the cell. But row height and cell.frame.height / cell.contentView.frame.heigth doesn't same. Codes can be found below, any suggestions are welcome!
If you want cell to change dynamically you can try that approaching.
//will make auto height
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
return UITableView.automaticDimension
}
//register your cell
tableView.register(FeedCell.self, forCellReuseIdentifier: FeedCell.identifier)
use cell in cell for row at
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: FeedCell.identifier) as! FeedCell
cell.backgroundColor = .clear
cell.withImage = true
cell.withSubtitle = true
cell.withButton = false
cell.titleLabel.text = ""
cell.descriptionLabel.text = ""
return cell
}
and a sample feed cell code as you need
extension FeedCell {
static let identifier = "FeedCell"
}
class FeedCell: UITableViewCell {
let titleLabel = UILabel()
let subtitleLabel = UILabel()
let descriptionLabel = UILabel()
let iconImageView = UIImageView()
var button = UIButton()
var withImage: Bool = true {
didSet {
if withImage {
self.iconImageView.isHidden = false
self.iconImageWidthConstraint.constant = self.imageSide
self.iconImageHeightConstraint.constant = self.imageSide
self.titleLabelLeadingConstraint.isActive = true
self.separatorInsetStyle = .beforeImage
} else {
self.iconImageView.isHidden = true
self.iconImageWidthConstraint.constant = 0
self.iconImageHeightConstraint.constant = 0
self.titleLabelLeadingConstraint.isActive = false
self.separatorInsetStyle = .auto
}
self.updateConstraints()
}
}
var roundImage: Bool = false {
didSet {
self.layoutSubviews()
}
}
var withSubtitle: Bool = true {
didSet {
self.descriptionLabelTopConstraint.isActive = false
self.descriptionLabel.removeConstraint(self.descriptionLabelTopConstraint)
if withSubtitle {
self.subtitleLabel.isHidden = false
self.descriptionLabelTopConstraint = self.descriptionLabel.topAnchor.constraint(equalTo: self.subtitleLabel.bottomAnchor, constant: self.spaceAfterSubtitle)
} else {
self.subtitleLabel.isHidden = true
self.descriptionLabelTopConstraint = self.descriptionLabel.topAnchor.constraint(equalTo: self.titleLabel.bottomAnchor, constant: self.spaceAfterSubtitle)
}
self.descriptionLabelTopConstraint.isActive = true
self.updateConstraints()
}
}
var withButton: Bool = false {
didSet {
if withButton {
self.button.isHidden = false
self.descriptionLabelBottomConstraint.isActive = false
self.buttonTopConstraint.isActive = true
self.buttonBottomConstraint.isActive = true
} else {
self.button.isHidden = true
self.buttonTopConstraint.isActive = false
self.buttonBottomConstraint.isActive = false
self.descriptionLabelBottomConstraint.isActive = true
}
self.updateConstraints()
}
}
var centerXButton: Bool = false {
didSet {
if self.centerXButton {
self.buttonLeadingConstraint.isActive = false
self.buttonCenterXConstraint.isActive = true
} else {
self.buttonCenterXConstraint.isActive = false
self.buttonLeadingConstraint.isActive = true
}
self.updateConstraints()
}
}
var imageSide: CGFloat = 40 {
didSet {
self.iconImageWidthConstraint.constant = self.imageSide
self.iconImageHeightConstraint.constant = self.imageSide
self.updateConstraints()
}
}
var topSpace: CGFloat = 11 {
didSet {
self.iconImageTopConstraint.constant = self.topSpace
self.titleLabelTopConstraint.constant = self.topSpace - 2
self.updateConstraints()
}
}
var spaceAfterTitle: CGFloat = 2 {
didSet {
if self.withSubtitle {
self.subtitleLabelTopConstraint.constant = self.spaceAfterTitle
} else {
self.descriptionLabelTopConstraint.constant = self.spaceAfterTitle
}
self.updateConstraints()
}
}
var spaceAfterSubtitle: CGFloat = 2 {
didSet {
if self.withSubtitle {
self.descriptionLabelTopConstraint.constant = self.spaceAfterSubtitle
}
self.updateConstraints()
}
}
var spaceAfterDescribtion: CGFloat = 15 {
didSet {
self.buttonTopConstraint.constant = self.spaceAfterDescribtion
self.updateConstraints()
}
}
var bottomSpace: CGFloat = 7 {
didSet {
if self.withButton {
self.buttonBottomConstraint.constant = -self.bottomSpace
} else {
self.descriptionLabelBottomConstraint.constant = -self.bottomSpace
}
self.updateConstraints()
}
}
var spaceBetweenImageAndTitles: CGFloat = 15 {
didSet {
self.titleLabelLeadingConstraint.constant = self.spaceBetweenImageAndTitles
self.updateConstraints()
}
}
override var contentViews: [UIView] {
return [self.iconImageView, self.titleLabel, self.subtitleLabel, self.descriptionLabel]
}
//constraints
private var iconImageTopConstraint: NSLayoutConstraint!
private var iconImageBottomConstraint: NSLayoutConstraint!
private var iconImageWidthConstraint: NSLayoutConstraint!
private var iconImageHeightConstraint: NSLayoutConstraint!
private var titleLabelLeadingConstraint: NSLayoutConstraint!
private var titleLabelTopConstraint: NSLayoutConstraint!
private var subtitleLabelTopConstraint: NSLayoutConstraint!
private var descriptionLabelTopConstraint: NSLayoutConstraint!
private var descriptionLabelBottomConstraint: NSLayoutConstraint!
private var buttonTopConstraint: NSLayoutConstraint!
private var buttonLeadingConstraint: NSLayoutConstraint!
private var buttonCenterXConstraint: NSLayoutConstraint!
private var buttonBottomConstraint: NSLayoutConstraint!
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
self.commonInit()
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func awakeFromNib(){
super.awakeFromNib()
}
override func commonInit() {
super.commonInit()
let marginGuide = contentView.layoutMarginsGuide
//iconImageView
self.iconImageView.layer.cornerRadius = 10
self.contentView.addSubview(self.iconImageView)
self.iconImageView.translatesAutoresizingMaskIntoConstraints = false
self.iconImageView.leadingAnchor.constraint(equalTo: marginGuide.leadingAnchor).isActive = true
self.iconImageTopConstraint = self.iconImageView.topAnchor.constraint(equalTo: marginGuide.topAnchor, constant: self.topSpace)
self.iconImageTopConstraint.isActive = true
self.iconImageWidthConstraint = self.iconImageView.widthAnchor.constraint(equalToConstant: self.imageSide)
self.iconImageWidthConstraint.isActive = true
self.iconImageHeightConstraint = self.iconImageView.heightAnchor.constraint(equalToConstant: self.imageSide)
self.iconImageHeightConstraint.isActive = true
self.iconImageBottomConstraint = self.iconImageView.bottomAnchor.constraint(lessThanOrEqualTo: marginGuide.bottomAnchor, constant: -(self.bottomSpace))
self.iconImageBottomConstraint.priority = UILayoutPriority.init(900)
self.iconImageBottomConstraint.isActive = true
//titleLabel
self.titleLabel.numberOfLines = 0
self.titleLabel.textAlignment = .left
self.titleLabel.textColor = UIColor.white
self.contentView.addSubview(self.titleLabel)
self.titleLabel.translatesAutoresizingMaskIntoConstraints = false
self.titleLabelLeadingConstraint = self.titleLabel.leadingAnchor.constraint(equalTo: self.iconImageView.trailingAnchor, constant: self.spaceBetweenImageAndTitles)
self.titleLabelLeadingConstraint.isActive = true
let titleLabelAdditionalLeadingConstraint = self.titleLabel.leadingAnchor.constraint(equalTo: marginGuide.leadingAnchor)
titleLabelAdditionalLeadingConstraint.priority = UILayoutPriority.init(900)
titleLabelAdditionalLeadingConstraint.isActive = true
self.titleLabelTopConstraint = self.titleLabel.topAnchor.constraint(equalTo: marginGuide.topAnchor, constant: self.topSpace - 2)
self.titleLabelTopConstraint.isActive = true
self.titleLabel.trailingAnchor.constraint(equalTo: marginGuide.trailingAnchor).isActive = true
//subtitleLabel
self.subtitleLabel.numberOfLines = 0
self.subtitleLabel.font = UIFont.system(weight: UIFont.FontWeight.regular, size: 15)
self.subtitleLabel.textAlignment = .left
self.subtitleLabel.textColor = UIColor.white
self.contentView.addSubview(self.subtitleLabel)
self.subtitleLabel.translatesAutoresizingMaskIntoConstraints = false
self.subtitleLabel.leadingAnchor.constraint(equalTo: self.titleLabel.leadingAnchor).isActive = true
self.subtitleLabel.trailingAnchor.constraint(equalTo: marginGuide.trailingAnchor).isActive = true
self.subtitleLabelTopConstraint = self.subtitleLabel.topAnchor.constraint(equalTo: self.titleLabel.bottomAnchor, constant: self.spaceAfterTitle)
self.subtitleLabelTopConstraint.isActive = true
//descriptionLabel
self.descriptionLabel.numberOfLines = 0
self.descriptionLabel.font = UIFont.system(weight: UIFont.FontWeight.regular, size: 15)
self.descriptionLabel.textAlignment = .left
self.descriptionLabel.textColor = .gray
self.contentView.addSubview(self.descriptionLabel)
self.descriptionLabel.translatesAutoresizingMaskIntoConstraints = false
self.descriptionLabel.leadingAnchor.constraint(equalTo: self.titleLabel.leadingAnchor).isActive = true
self.descriptionLabel.trailingAnchor.constraint(equalTo: marginGuide.trailingAnchor).isActive = true
self.descriptionLabelTopConstraint = self.descriptionLabel.topAnchor.constraint(equalTo: self.subtitleLabel.bottomAnchor, constant: self.spaceAfterSubtitle)
self.descriptionLabelTopConstraint.isActive = true
self.descriptionLabelBottomConstraint = self.descriptionLabel.bottomAnchor.constraint(equalTo: marginGuide.bottomAnchor, constant: -self.bottomSpace)
self.descriptionLabelBottomConstraint.isActive = true
self.contentView.addSubview(self.button)
self.button.titleLabel?.numberOfLines = 0
self.button.contentHorizontalAlignment = UIControl.ContentHorizontalAlignment.left
self.button.translatesAutoresizingMaskIntoConstraints = false
self.buttonLeadingConstraint = self.button.leadingAnchor.constraint(equalTo: self.titleLabel.leadingAnchor)
self.buttonLeadingConstraint.isActive = true
self.buttonCenterXConstraint = self.button.centerXAnchor.constraint(equalTo: self.titleLabel.centerXAnchor)
self.buttonCenterXConstraint.isActive = false
self.buttonTopConstraint = self.button.topAnchor.constraint(equalTo: self.descriptionLabel.bottomAnchor, constant: self.spaceAfterDescribtion)
self.buttonTopConstraint.isActive = false
//
self.buttonBottomConstraint = self.button.bottomAnchor.constraint(equalTo: marginGuide.bottomAnchor, constant: -self.bottomSpace)
self.buttonBottomConstraint.isActive = false
self.buttonBottomConstraint.priority = UILayoutPriority.init(900)
self.setDefaultParametrs()
}
private func setDefaultParametrs() {
self.withImage = true
self.withSubtitle = true
self.withButton = true
self.accessoryType = .none
self.separatorInsetStyle = .beforeImage
self.titleLabel.text = "Title"
self.titleLabel.textAlignment = .left
self.titleLabel.textColor = UIColor.white
self.subtitleLabel.text = "Subtitle"
self.subtitleLabel.font = UIFont.system(weight: UIFont.FontWeight.regular, size: 15)
self.subtitleLabel.textAlignment = .left
self.subtitleLabel.textColor = UIColor.black
self.descriptionLabel.text = ""
self.descriptionLabel.textAlignment = .left
self.descriptionLabel.textColor = .gray
self.button.setTitle("Button")
self.imageSide = 40
self.roundImage = false
self.topSpace = 11
self.spaceAfterTitle = 2
self.spaceAfterSubtitle = 2
self.spaceAfterDescribtion = 15
self.bottomSpace = 7
self.spaceBetweenImageAndTitles = 15
}
override func prepareForReuse() {
super.prepareForReuse()
self.setDefaultParametrs()
}
override func layoutSubviews() {
super.layoutSubviews()
if self.roundImage {
self.iconImageView.layer.cornerRadius = self.imageSide / 2
} else {
self.iconImageView.layer.cornerRadius = self.imageSide * 0.158
}
switch self.separatorInsetStyle {
case .all:
self.separatorInset.left = 0
case .beforeImage:
self.separatorInset.left = self.layoutMargins.left + self.imageSide + self.spaceBetweenImageAndTitles - 2
case .none:
self.separatorInset.left = self.frame.width
case .auto:
self.separatorInset.left = self.layoutMargins.left
}
}
}

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):

Resources