I've got a tableViewCell that I need to have an array passed into the tableViewCell but not just passed into a text label or something like that. I'll let my code show.
My TableViewController:
let subjectsDict = ["Spanish": ["Lesson 1", "Lesson 2"], "Math":["Problem set 1", "Problem set 2"], "Science": ["Lab"]]
let subjectArray = ["Spanish", "Math", "Science"]
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
guard let cell = tableView.dequeueReusableCell(withIdentifier: "subjectCell", for: indexPath) as? SubjectTableViewCell else {
return UITableViewCell()
}
cell.subjectList = subjectsDict[subjectArray[indexPath.row]]
return cell
}
And my tableViewCell looks like this.
class subjectTableViewCell: UITableViewCell {
var subjectList: [String] = []
override init(style: UITableViewCellStyle, reuseIdentifier: String?) {
super.init(style: style , reuseIdentifier: reuseIdentifier)
setUpTable()
}
required init?(coder decoder: NSCoder) {
super.init(coder: decoder)
}
override func awakeFromNib() {
super.awakeFromNib()
setUpTable()
}
func setUpTable() {
print(subjectList)
}
//other code for creating the cell
}
But when I print the subjectList from the subjectTableViewCell it prints none
Your code makes no attempt to update the cell's content with the value of subjectList. All you show is a print.
Also note that your print is called before any attempt to set subjectList is made. And remember that cells get reused. setUpTable will only be called once but subjectList will be set over and over as the cell gets used.
The simplest solution is to update the cell when subjectList is set.
var subjectList: [String] = [] {
didSet {
textLabel?.text = subjectList.joined(separator: ", ")
}
}
I'm assuming you are using the standard textLabel property. If you have your own label then update accordingly.
If you just want to invoke setUpTable() when your subjectList in the cell gets updated, try using:
var subjectList: [String] = [] {
didSet {
setUpTable()
}
}
You're trying to print subjectList in the moment you initialise your table view cell so in this moment you haven't set subjectList yet. If you want to print subjectList you can do it after you set it.
After this line gets executed:
cell.subjectList = subjectsDict[subjectArray[indexPath.row]]
Related
I have following code to display data in table view.
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
guard let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath) as? ArticleTalbeViewCell else {
fatalError("ArticleTableViewCell not found")
}
let articleVM = self.articleListVM.articleAtIndex(indexPath.row)
// cell.viewModel = articleVM
cell.titlelabel?.text = articleVM.title
cell.descriptionLabel?.text = articleVM.description
return cell
}
}
Now, my code with
cell.titlelabel?.text = articleVM.title
cell.descriptionLabel?.text = articleVM.description
work well.
Is cell.viewModel = articleVM a good practice?
Imagine I must set the date to the cell many times? This approach cell.viewModel = articleVM will save several lines.
The UITableViewCell's code is below:
class ArticleTalbeViewCell: UITableViewCell {
var titlelabel:UILabel?
var descriptionLabel:UILabel?
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
self.setupUI()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
fatalError("init(coder:) has not been implemented")
}
private func setupUI() -> () {
let label1 = UILabel()
label1.numberOfLines = 0
let label2 = UILabel()
label2.numberOfLines = 0
label2.textColor = UIColor.lightGray
label2.setContentHuggingPriority(UILayoutPriority.defaultHigh, for: NSLayoutConstraint.Axis.vertical)
titlelabel = label1
descriptionLabel = label2
let staview = UIStackView()
staview.axis = .vertical
staview.addArrangedSubview(label1)
staview.addArrangedSubview(label2)
staview.spacing = 8
staview.translatesAutoresizingMaskIntoConstraints = false // !important
self.contentView.addSubview(staview)
staview.topAnchor.constraint(equalTo: self.contentView.topAnchor,constant: 5).isActive = true
staview.bottomAnchor.constraint(equalTo: self.contentView.bottomAnchor, constant: -5).isActive = true
staview.leftAnchor.constraint(equalTo: self.contentView.leftAnchor, constant: 5).isActive = true
staview.rightAnchor.constraint(equalTo: self.contentView.rightAnchor, constant: -5).isActive = true
}
}
The ArticleListViewModel code is below:
struct ArticleListViewModel {
let articles:[Article]
}
extension ArticleListViewModel {
var numbeOfSections: Int {
return 1
}
func numOfRowsInSection(_ section: Int) -> Int {
return self.articles.count
}
func articleAtIndex(_ index: Int) -> ArticleViewModel {
let article = self.articles[index]
return ArticleViewModel(article)
}
}
struct ArticleViewModel {
private let article: Article
}
extension ArticleViewModel {
init(_ article: Article) {
self.article = article
}
}
extension ArticleViewModel {
var title: String {
return self.article.title ?? "null"
}
var description: String {
return self.article.description ?? "null"
}
}
The ArticleList code is below:
import Foundation
struct ArticleList: Decodable {
let articles: [Article]
}
struct Article: Decodable {
let title: String?
let description: String?
let chines: String?
}
How to edit the "cell" code to implement the cell.viewModel = articleVM?
First of all, I do not think giving cell.viewModel = articleVM does not change too much thing. Because of you have created ArticleListViewModel and ArticleViewModel it would be same if you create these vm's for your table view cell. However, I can give you some advices that is used by me while using MVVM approach.
You can add new property inside table view cell then using property observer give a value to it.
cell.article = articleVM
Therefore, either your cellForRowAt method won't be too long and it is almost same like giving your view model to your cell.
private var titlelabel:UILabel?
private var descriptionLabel:UILabel?
var article: ArticleViewModel? {
didSet {
guard let article = article else { return }
titlelabel?.text = article.title
descriptionLabel?.text = article.description
}
}
As far as I understand, your second question is if you have date variable and formatting inside cellForRowAt method takes too many spaces and looks ugly. That is why you need to do your business logic inside view model. Your table view cell is only responsible for showing it not to format it. I hope, it is clear for you. If you have any question you can ask.
Instead of configuring the cell in tableView(_:cellForRowAt:) method, you must configure your cell in the ArticleTalbeViewCell itself using the dataSource model, i.e.
class ArticleTalbeViewCell: UITableViewCell {
private var titlelabel: UILabel?
private var descriptionLabel: UILabel?
//rest of the code...
func configure(with article: Article) {
self.titlelabel?.text = article.title
self.descriptionLabel?.text = article.description
}
}
In the above code,
1. There is no need to expose titlelabel and descriptionLabel outside the class ArticleTalbeViewCell. So, mark both of them private.
2. You just need to call configure(with:) method along with the Article instance.
Why method and not a property of type Article?
There is no need to save the model object inside the ArticleTalbeViewCell until and unless it is required somewhere else after the cell is configured once.
In tableView(_:cellForRowAt:) method, you need to simply call configure(with:) method on your cell along with the article instance at that particular indexPath.
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
guard let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath) as? ArticleTalbeViewCell else {
fatalError("ArticleTableViewCell not found")
}
let article = self.articleListVM.articleAtIndex(indexPath.row)
cell.configure(with: article)
return cell
}
Also, I don't think there is any need to create a separate ArticleViewModel. It is not doing anything special. It's just an extra wrapper on Article.
You can simply return Article type from articleAtIndex(_:) method in extension ArticleListViewModel, i.e
func articleAtIndex(_ index: Int) -> Article {
let article = self.articles[index]
return article
}
Why configuration in custom UITableViewCell?
This is because your tableView might contain multiple custom UITableViewCells. Adding the configuration of all the cell in tableView(_:cellForRowAt:) will make the code hefty and unable to read. Its better that the cell specific configuration is handled by the cell itself instead of the ViewController.
So I am pretty new to iOS development. I try to create everything programmatically so my Storyboard is empty. I'm currently trying to get a TableView with custom cells. The TableView is running and looking fine when I use the standard UITableViewCell. I created a very simple class called "GameCell". Basically, I want to create a cell here with multiple labels and maybe some extra UIObjects in the future (imageView etc.). For some reason, the custom cells do not show up.
Game cell class:
class GameCell: UITableViewCell {
var mainTextLabel = UILabel()
var sideTextLabel = UILabel()
func setLabel() {
self.mainTextLabel.text = "FirstLabel"
self.sideTextLabel.text = "SecondLabel"
}
}
Here the additional necessary code to get the number of rows and return the cells to the TableView which I have in my ViewController. self.lastGamesCount is just an Int here and definitely not zero when I print it.
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return self.lastGamesCount
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: cellID) as! GameCell
In my viewDidLoad() I register the cells like this:
tableView.register(GameCell.self, forCellReuseIdentifier: cellID)
When I run everything the Build is successful I can see the navigation bar of my App and all but the TableView is empty. I go back to the normal UITableViewCell and the cells are showing up again. What am I missing here? Any help is appreciated.
Thanks!
The problem is you need to set constraints for these labels
var mainTextLabel = UILabel()
var sideTextLabel = UILabel()
after you add them to the cell
class GameCell: UITableViewCell {
let mainTextLabel = UILabel()
let sideTextLabel = UILabel()
override init(style: UITableViewCellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
setLabel()
}
func setLabel() {
self.mainTextLabel.translatesAutoresizingMaskIntoConstraints = false
self.sideTextLabel.translatesAutoresizingMaskIntoConstraints = false
self.contentView.addSubview(mainTextLabel)
self.contentView.addSubview(sideTextLabel)
NSLayoutConstraint.activate([
mainTextLabel.leadingAnchor.constraint(equalTo: self.contentView.leadingAnchor),
mainTextLabel.trailingAnchor.constraint(equalTo: self.contentView.trailingAnchor),
mainTextLabel.topAnchor.constraint(equalTo: self.contentView.topAnchor,constant:20),
sideTextLabel.leadingAnchor.constraint(equalTo: self.contentView.leadingAnchor),
sideTextLabel.trailingAnchor.constraint(equalTo: self.contentView.trailingAnchor),
sideTextLabel.topAnchor.constraint(equalTo: self.mainTextLabel.bottomAnchor,constant:20),
sideTextLabel.bottomAnchor.constraint(equalTo: self.contentView.bottomAnchor,constant:-20)
])
self.mainTextLabel.text = "FirstLabel"
self.sideTextLabel.text = "SecondLabel"
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
I have a cell class 'NewsCell' (subclass of UITableViewCell) that I use for two different kinds of news: OrganizationNews and ProjectNews. These news has common things, but some of elements are different. Namely, when my cell is used for ProjectNews I want to hide Organization's logo, when it is for OrganizationNews I want to hide Project's name button.
I have 'configureCell(_, forNews, ofProject)' method. I call it in 'NewsViewController'. I used 'removeFromSuperview' method, because I need to rearrange my elements in 'NewsCell'. Changing 'isHidden' value won't give me that effect.
So, that is the issue. I have 'Thread 1: Fatal error: Unexpectedly found nil while unwrapping an Optional value' exception in the lines projectNameButton.removeFromSuperview() or logoImageView.removeFromSuperview().
What should I do?
// NewsViewController.swift
func configureCell(_ cell: NewsCell, forNews news: News, ofProject project: Project? = nil) {
//...
if news is OrganizationNews {
cell.projectNameButton.removeFromSuperview()
} else if news is ProjectNews {
cell.logoImageView.removeFromSuperview()
}
// ...
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let news = newsCollection[indexPath.row]
let cell = tableView.dequeueReusableCell(withIdentifier: TableViewCellIdentifiers.newsCell, for: indexPath) as! NewsCell
configureCell(cell, forNews: news)
cell.delegate = self
return cell
}
A UITableView or UICollectionView are built on the reuse concept, where the cells are reused and repopulated when you work on it.
When you try to call dequeReusableCell(withIdentifier:), it sometimes returns something that is created before. So, suppose you dequed before something which had all controls, then removed one (removeFromSuperview), then tried to deque again, the new dequed one may NOT have the subview.
I think the best solution for you is making two different cells.
Example:
class BaseNewsCell: UITableViewCell {
// Put the common views here
}
class OrganizationNewsCell: BaseNewsCell {
// Put here things that are ONLY for OrganizationNewsCell
}
class ProjectNewsCell: BaseNewsCell {
// Put here things that are ONLY for ProjectNewsCell
}
Then deque them from 2 different identifier by two different storyboard cells, xibs.
Or
class BaseNewsCell: UITableViewCell {
// Put the common views here
}
class OrganizationNewsCell: BaseNewsCell {
// This happens when this kind of cell is created for the first time
override func awakeFromNib() {
super.awakeFromNib()
someNonCommon.removeFromSuperview()
}
}
class ProjectNewsCell: BaseNewsCell {
override func awakeFromNib() {
super.awakeFromNib()
someOtherNonCommon.removeFromSuperview()
}
}
Note: This violates Liskov's principle (one of the SOLID principles), because you remove functionality from superclass in the subclass.
Change the removing lines as below,
if news is OrganizationNews {
cell.projectNameButton?.removeFromSuperview()
} else if news is ProjectNews {
cell.logoImageView?.removeFromSuperview()
}
This will fix the issue. But a good approach would be to create separate classes for each cell. You can create a base class to keep common logic there.
You shouldn't remove the subview from the outside of the cell. Let's refactor your code.
NewsCell.swift
final class NewsCell: UITableViewCell {
enum Kind {
case organization
case project
}
var logoImageView: UIImageView?
let nameLabel = UILabel()
var kind: NewsCell.Kind {
didSet {
if kind != oldValue {
setupLogoImageView()
self.setNeedsLayout()
}
}
}
init(kind: NewsCell.Kind, reuseIdentifier: String?) {
self.kind = kind
super.init(style: .default, reuseIdentifier: reuseIdentifier)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
// MARK: - Positioning
extension NewsCell {
override func layoutSubviews() {
super.layoutSubviews()
// Your layouting
switch kind {
case .organization:
// Setup frame for organization typed NewsCell
case .project:
// Setup frame for project typed NewsCell
}
}
}
// MARK: - Setup
extension NewsCell {
private func setupLogoImageView() {
logoImageView = kind == .organization ? UIImageView() : nil
}
}
How to use:
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let news = newsCollection[indexPath.row]
var cell = tableView.dequeueReusableCell(withIdentifier: TableViewCellIdentifiers.newsCell) as? NewsCell
if cell == nil {
cell = NewsCell(kind: .organization, reuseIdentifier: TableViewCellIdentifiers.newsCell)
}
cell!.kind = news is Organization ? .organization: .project
return cell!
}
I'm have a table view and I'm using the tableView.dequeueReusableCellWithIdentifier to reuse the cells but still tableView is very slow.
and by slow, I mean it takes about 500 milliseconds to put 9 of my views in the tableView. and it's tested on apple A7 X64 processor so it must be pretty slower on older processors.
the reason that it's slow is because there are a few sub views and constraints.
but I've seen more complex tableCells with better performance, so there must be something I can do.
like caching a cell or something else??
any ideas?
sample code
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
tableView.registerNib(UINib(nibName: "ChatCell", bundle: nil), forCellReuseIdentifier: "ChatCell")
let cell = tableView.dequeueReusableCellWithIdentifier("ChatCell") as! ChatCell
return cell
}
the reason that it's slow is because there are a few sub views and constraints.
Personally, I don't suggest you use constraints in cell, especially when there're many subviews, it'll cost much CPU time and lead the scrolling lag. Instead, you can calculate manually based on cell frame.
And for more suggestion, i suggest you take time to read this post: Simple Strategies for Smooth Animation on the iPhone.
The call to registerNib is normally done only once in viewDidLoad, not every time you are asked for a cell in cellForRowAtIndexPath. Not sure how slow that call is, but it might be the reason for your slow response.
I think you are using effects (like shadow or round corners or etc) or having heavy calculations on UI
Edit: Code Sample added
//Add in your init func
tblView.registerClass(MSCustomVerticalListCell.self, forCellReuseIdentifier: NSStringFromClass(MSCustomVerticalListCell))
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tblView.dequeueReusableCellWithIdentifier(NSStringFromClass(MSCustomVerticalListCell), forIndexPath: indexPath) as! MSCustomVerticalListCell
//add data binding
cell.item = dataSource[indexPath.row]
return cell
}
Your data binding class (Data Model):
class MSC_VCItem
{
var Title:String!
var Action:String!
var SubTitle:String!
var Icon:String!
init(title:String!,subTitle:String!,icon:String!,action:String!)
{
self.Title = title
self.SubTitle = subTitle
self.Icon = icon
self.Action = action
}
}
And Finally you custom table cell:
class MSCustomVerticalListCell : UITableViewCell {
let padding = 5
let imageWidth = 50
var customImageView: UIImageView!
var customTitleLabel: UILabel!
var customSubtitleLabel: UILabel!
var item: MSC_VCItem? {
didSet {
if let it = item {
customTitleLabel.text = it.Title
customSubtitleLabel.text = it.SubTitle
UIImage.loadFromCacheOrURL(it.Icon, callback: { (image: UIImage) -> () in
self.customImageView.image = image
})
setNeedsLayout()
}
}
}
override init(style: UITableViewCellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
self.backgroundColor = UIColor.clearColor()
customTitleLabel = UILabel(frame: CGRectZero)
self.addSubview(customTitleLabel)
customSubtitleLabel = UILabel(frame: CGRectZero)
contentView.addSubview(customSubtitleLabel)
customImageView = UIImageView(frame: CGRectZero)
customImageView.image = UIImage(named: "default")
contentView.addSubview(customImageView)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func layoutSubviews() {
super.layoutSubviews()
//Write your UI here like bg color or text color
}
}
I'm creating an app with displays parse data in a table view. I downloaded a template from https://github.com/Bizzi-Body/HowToDisplayImagesInTableViewFromParse it all worked fine when I ran it but when I put my Parse app id in and Client id it just shows a loading screen (see Screenshot)
So I thought it might be a problem with the app template so downloaded a different one and edited it, but the same problem happen, so I'm think its something wrong with my parse account settings.
import UIKit
class TableViewController: PFQueryTableViewController {
// Initialise the PFQueryTable tableview
override init!(style: UITableViewStyle, className: String!) {
super.init(style: style, className: className)
}
required init(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
// Configure the PFQueryTableView
self.parseClassName = "Photo"
self.textKey = "updatedAt"
self.pullToRefreshEnabled = true
self.paginationEnabled = false
}
// Define the query that will provide the data for the table view
override func queryForTable() -> PFQuery! {
var query = PFQuery(className: "Photo")
query.orderByAscending("updatedAt")
return query
}
//override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath, object: PFObject) -> PFTableViewCell {
var cell = tableView.dequeueReusableCellWithIdentifier("Cell") as CustomCell!
if cell == nil {
cell = CustomCell(style: UITableViewCellStyle.Default, reuseIdentifier: "Cell")
}
// Extract values from the PFObject to display in the table cell
cell.username.text = object["user"] as String!
cell.itemdetail.text = object["description"] as String!
cell.price.text = object["Price"] as String!
var thumbnail = object["Image"] as PFFile
var initialThumbnail = UIImage(named: "question")
cell.productimage.image = initialThumbnail
cell.productimage.file = thumbnail
cell.productimage.loadInBackground()
return cell
}
}
If you enabled Parse LocalDatastore a quite similar issue is here:
https://github.com/ParsePlatform/ParseUI-iOS/issues/26
So try to disable LocalDatastore or update ParseSDK
I think the problem is that the Custom Cell class name does not match the Custom Cell Class detailed in the story board.
If your custom cell class is called "CustomCell" (i.e. - in "CustomCell.swift") you need to make sure that in the story board the custom class setting for the prototype cell is also "CustomCell".
Maybe this changed when you created your one custom class/cell