Value of has no member - ios

I tried to get a CustomCell trough watching a Youtube Tutorial but im getting this problem at my Tableview.It says
Value of type 'CustomCell' has no member 'mainImage'. I hope some of you can help me to solve this problem.
The mainImage I want to get in my Cell is in my CustomCell.swift.
My TableViewController:
import UIKit
struct CellData {
let image : UIImage?
let message : String?
}
var data = [CellData]()
class TableViewController : UITableViewController{
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
data = [CellData.init(image: #imageLiteral(resourceName: "Bildschirmfoto 2018-09-20 um 22.17.11"), message: "The Avengers")]
self.tableView.register(CustomCell.self, forCellReuseIdentifier: "custom")
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = self.tableView.dequeueReusableCell(withIdentifier: "custom") as! CustomCell
cell.mainImage = data[indexPath.row].image
return cell
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
<#code#>
}
}
My CustomCell
import Foundation
import UIKit
var message : String?
var mainImage : UIImage?
var messageView: UITextView = {
var textView = UITextView()
textView.translatesAutoresizingMaskIntoConstraints = false
return textView
}()
var mainImageView: UIImageView = {
var imageView = UIImageView()
imageView.translatesAutoresizingMaskIntoConstraints = false
return imageView
}()
class CustomCell: UITableViewCell {
override init(style: UITableViewCellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier:reuseIdentifier)
self.addSubview(mainImageView)
self.addSubview(messageView)
mainImageView.leftAnchor.constraint(equalTo:self.leftAnchor).isActive = true
mainImageView.topAnchor.constraint(equalTo:self.topAnchor).isActive = true
mainImageView.bottomAnchor.constraint(equalTo:self.bottomAnchor).isActive = true
mainImageView.widthAnchor.constraint(equalTo:self.heightAnchor).isActive = true
messageView.leftAnchor.constraint(equalTo:mainImageView.rightAnchor).isActive = true
messageView.rightAnchor.constraint(equalTo:self.rightAnchor).isActive = true
messageView.bottomAnchor.constraint(equalTo:self.bottomAnchor).isActive = true
messageView.topAnchor.constraint(equalTo:self.topAnchor).isActive = true
}
override func layoutSubviews() {
super.layoutSubviews()
if let message = message {
messageView.text = message
}
if let image = mainImage {
mainImageView.image = image
}
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}

You have to put your subviews into the cell class:
class CustomCell: UITableViewCell {
var messageView: UITextView = {
var textView = UITextView()
textView.translatesAutoresizingMaskIntoConstraints = false
return textView
}()
var mainImageView: UIImageView = {
var imageView = UIImageView()
imageView.translatesAutoresizingMaskIntoConstraints = false
return imageView
}()
...
}

Related

UITableView CustomCell Reuse (ImageView in CustomCell)

I'm pretty new to iOS dev and I have an issue with UITableViewCell.
I guess it is related to dequeuing reusable cell.
I added an UIImageView to my custom table view cell and also added a tap gesture to make like/unlike function (image changes from an empty heart(unlike) to a filled heart(like) as tapped and reverse). The problem is when I scroll down, some of the cells are automatically tapped. I found out why this is happening, but still don't know how to fix it appropriately.
Below are my codes,
ViewController
import UIKit
struct CellData {
var title: String
var done: Bool
}
class ViewController: UIViewController, UITableViewDelegate, UITableViewDataSource {
var models = [CellData]()
private let tableView: UITableView = {
let table = UITableView()
table.register(TableViewCell.self, forCellReuseIdentifier: TableViewCell.identifier)
return table
}()
override func viewDidLoad() {
super.viewDidLoad()
view.addSubview(tableView)
tableView.frame = view.bounds
tableView.delegate = self
tableView.dataSource = self
configure()
}
private func configure() {
self.models = Array(0...50).compactMap({
CellData(title: "\($0)", done: false)
})
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return models.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let model = models[indexPath.row]
guard let cell = tableView.dequeueReusableCell(withIdentifier: TableViewCell.identifier, for: indexPath) as? TableViewCell else {
return UITableViewCell()
}
cell.textLabel?.text = model.title
return cell
}
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
tableView.deselectRow(at: indexPath, animated: true)
tableView.reloadData()
}
}
TableViewCell
import UIKit
class TableViewCell: UITableViewCell {
let mainVC = ViewController()
static let identifier = "TableViewCell"
let likeImage: UIImageView = {
let imageView = UIImageView()
imageView.image = UIImage(systemName: "heart")
imageView.tintColor = .darkGray
imageView.isUserInteractionEnabled = true
imageView.translatesAutoresizingMaskIntoConstraints = false
return imageView
}()
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
contentView.addSubview(likeImage)
layout()
//Tap Gesture Recognizer 실행하기
let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(didTapImageView(_:)))
likeImage.addGestureRecognizer(tapGestureRecognizer)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func layoutSubviews() {
super.layoutSubviews()
}
override func prepareForReuse() {
super.prepareForReuse()
}
private func layout() {
likeImage.widthAnchor.constraint(equalToConstant: 30).isActive = true
likeImage.heightAnchor.constraint(equalToConstant: 30).isActive = true
likeImage.centerYAnchor.constraint(equalTo: contentView.centerYAnchor).isActive = true
likeImage.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -20).isActive = true
}
#objc func didTapImageView(_ sender: UITapGestureRecognizer) {
if likeImage.image == UIImage(systemName: "heart.fill"){
likeImage.image = UIImage(systemName: "heart")
likeImage.tintColor = .darkGray
} else {
likeImage.image = UIImage(systemName: "heart.fill")
likeImage.tintColor = .systemRed
}
}
}
This gif shows how it works now.
enter image description here
I've tried to use "done" property in CellData structure to capture the status of the uiimageview but failed (didn't know how to use that in the correct way).
I would be so happy if anyone can help this!
You've already figured out that the problem is cell reuse.
When you dequeue a cell to be shown, you are already setting the cell label's text based on your data:
cell.textLabel?.text = model.title
you also need to tell the cell whether to show the empty or filled heart image.
And, when the user taps that image, your cell needs to tell the controller to update the .done property of your data model.
That can be done with either a protocol/delegate pattern or, more commonly (particularly with Swift), using a closure.
Here's a quick modification of the code you posted... the comments should give you a good idea of what's going on:
struct CellData {
var title: String
var done: Bool
}
class ShinViewController: UIViewController, UITableViewDelegate, UITableViewDataSource {
var models = [CellData]()
private let tableView: UITableView = {
let table = UITableView()
table.register(ShinTableViewCell.self, forCellReuseIdentifier: ShinTableViewCell.identifier)
return table
}()
override func viewDidLoad() {
super.viewDidLoad()
view.addSubview(tableView)
tableView.frame = view.bounds
tableView.delegate = self
tableView.dataSource = self
configure()
}
private func configure() {
self.models = Array(0...50).compactMap({
CellData(title: "\($0)", done: false)
})
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return models.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: ShinTableViewCell.identifier, for: indexPath) as! ShinTableViewCell
let model = models[indexPath.row]
cell.myLabel.text = model.title
// set the "heart" to true/false
cell.isLiked = model.done
// closure
cell.callback = { [weak self] theCell, isLiked in
guard let self = self,
let pth = self.tableView.indexPath(for: theCell)
else { return }
// update our data
self.models[pth.row].done = isLiked
}
return cell
}
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
tableView.deselectRow(at: indexPath, animated: true)
}
}
class ShinTableViewCell: UITableViewCell {
// we'll use this closure to communicate with the controller
var callback: ((UITableViewCell, Bool) -> ())?
static let identifier = "TableViewCell"
let likeImageView: UIImageView = {
let imageView = UIImageView()
imageView.image = UIImage(systemName: "heart")
imageView.tintColor = .darkGray
imageView.isUserInteractionEnabled = true
imageView.translatesAutoresizingMaskIntoConstraints = false
return imageView
}()
let myLabel: UILabel = {
let v = UILabel()
v.translatesAutoresizingMaskIntoConstraints = false
return v
}()
// we'll load the heart images once in init
// instead of loading them every time they change
var likeIMG: UIImage!
var unlikeIMG: UIImage!
var isLiked: Bool = false {
didSet {
// update the image in the image view
likeImageView.image = isLiked ? likeIMG : unlikeIMG
// update the tint
likeImageView.tintColor = isLiked ? .systemRed : .darkGray
}
}
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
// make sure we load the heart images
guard let img1 = UIImage(systemName: "heart"),
let img2 = UIImage(systemName: "heart.fill")
else {
fatalError("Could not load the heart images!!!")
}
unlikeIMG = img1
likeIMG = img2
// add label and image view
contentView.addSubview(myLabel)
contentView.addSubview(likeImageView)
layout()
//Tap Gesture Recognizer 실행하기
let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(didTapImageView(_:)))
likeImageView.addGestureRecognizer(tapGestureRecognizer)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func layoutSubviews() {
super.layoutSubviews()
}
override func prepareForReuse() {
super.prepareForReuse()
}
private func layout() {
// let's use the "built-in" margins guide
let g = contentView.layoutMarginsGuide
// image view bottom constraint
let bottomConstraint = likeImageView.bottomAnchor.constraint(equalTo: g.bottomAnchor)
// this will avoid auto-layout complaints
bottomConstraint.priority = .required - 1
NSLayoutConstraint.activate([
// constrain label leading
myLabel.leadingAnchor.constraint(equalTo: g.leadingAnchor),
// center the label vertically
myLabel.centerYAnchor.constraint(equalTo: g.centerYAnchor),
// constrain image view trailing
likeImageView.trailingAnchor.constraint(equalTo: g.trailingAnchor),
// constrain image view to 30 x 30
likeImageView.widthAnchor.constraint(equalToConstant: 30),
likeImageView.heightAnchor.constraint(equalTo: likeImageView.widthAnchor),
// constrain image view top
likeImageView.topAnchor.constraint(equalTo: g.topAnchor),
// activate image view bottom constraint
bottomConstraint,
])
}
#objc func didTapImageView(_ sender: UITapGestureRecognizer) {
// toggle isLiked (true/false)
isLiked.toggle()
// inform the controller, so it can update the data
callback?(self, isLiked)
}
}

Swift UISwitch Button with Label

I am receiving data from API . The data field is displayed with two Label control, Id and status . Here is the screenshot .
I have another view where I created switch button and label programatically. I want to change the status to false when user turn on to switch button but it now updating the value . Here is the code and function i defined .
class DetailsViewController : UIViewController{
#IBOutlet private weak var tableView: UITableView!
var changeStatus: ((Bool, String) -> Void)?
var identifier = ""
private let switchControl: UISwitch = {
let switchControl = UISwitch()
switchControl.translatesAutoresizingMaskIntoConstraints = false
switchControl.isOn = false
return switchControl
}()
#IBOutlet weak var customSwitch: UISwitch!
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
view.addSubview(switchControl)
switchControl.addTarget(self, action: #selector(changeSwitchControl), for: .valueChanged)
let safeArea = view.safeAreaLayoutGuide
switchControl.topAnchor.constraint(equalTo: safeArea.topAnchor).isActive = true
switchControl.leadingAnchor.constraint(equalTo: safeArea.leadingAnchor).isActive = true
setUpUI()
}
#objc
private func changeSwitchControl() {
changeStatus?(switchControl.isOn, identifier)
}
}
Here is code in View Controller .
class ViewController: UIViewController {
// ! Mark is means it not null the value will in story board
// use lazy property to tell compiler instance value of the datasource self class and execuate the controller code to set the value of tableview datasource
private let viewModel = ViewModel()
private var subcriber = Set<AnyCancellable>()
private var storiesTrue = [String]()
#Published private(set) var stories = [Rover]()
private var datasourceStories = [Rover]()
private lazy var tableView: UITableView = {
let tableview = UITableView()
tableview.translatesAutoresizingMaskIntoConstraints = false// adding constrains
tableview.dataSource = self
tableview.showsVerticalScrollIndicator = false
tableview.register(StoryCell.self, forCellReuseIdentifier: StoryCell.identifier)
return tableview
}()
override func viewDidLoad() {
super.viewDidLoad()
setUpUI()
setUpBinding()
tableView.dataSource = self
tableView.delegate = self
self.tableView.rowHeight = 44;
// display the second view controller using push methods
/*let detail = DetailsViewController()
detail.name "Mohammad"
navigationController?.pushViewController(detail, animated: true)*/
}
private func setUpUI() {
view.backgroundColor = .white
view.addSubview(tableView)// adding hierracy key like adding into story board
// creating constrains with safe area
let safeArea = view.safeAreaLayoutGuide
tableView.topAnchor.constraint(equalTo: safeArea.topAnchor).isActive = true
tableView.leadingAnchor.constraint(equalTo: safeArea.leadingAnchor).isActive = true
tableView.trailingAnchor.constraint(equalTo: safeArea.trailingAnchor).isActive = true
tableView.bottomAnchor.constraint(equalTo: safeArea.bottomAnchor).isActive = true
}
private func setUpBinding(){
viewModel
.$rovers
.receive(on : RunLoop.main)
.sink {[weak self]_ in
self?.tableView.reloadData()
}
.store(in: &subcriber)
viewModel.getStories()
}
private func getStatus(by identifier: String) -> Bool {
return storiesTrue.contains(identifier)
}
}
extension ViewController: UITableViewDataSource{
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return viewModel.rovers.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
guard let cell = tableView.dequeueReusableCell(withIdentifier: StoryCell.identifier , for: indexPath) as? StoryCell
else{ return UITableViewCell()}
let row = indexPath.row
let Id = viewModel.getId(by: row)
let title = viewModel.getTitle(by: row)
let identifier = viewModel.getIdentifier(by: indexPath.row)
let status = getStatus(by: identifier) ? "active" : "false"
cell.configureCell(Id: Id,title: title,statusString: status)
return cell
}
}
extension ViewController: UITableViewDataSourcePrefetching {
func tableView(_ tableView: UITableView, prefetchRowsAt indexPaths: [IndexPath]) {
viewModel.getStories()
}
}
extension ViewController : UITableViewDelegate{
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath)
{
let second = DetailsViewController()
navigationController?.pushViewController(second, animated: true)
}
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
return 44;
}
}
Here is the view controller to defined the UI properties .
class StoryCell: UITableViewCell {
static let identifier = "StoryCell"
private lazy var storyIdLabel: UILabel = {
let label = UILabel()
label.translatesAutoresizingMaskIntoConstraints = false
label.numberOfLines = 0
label.textAlignment = .left
return label
}()
public lazy var storyTitleLabel: UILabel = {
let label = UILabel()
label.translatesAutoresizingMaskIntoConstraints = false
label.numberOfLines = 0
label.textAlignment = .left
return label
}()
private lazy var statusStory: UILabel = {
let label = UILabel()
label.translatesAutoresizingMaskIntoConstraints = false
label.numberOfLines = 0
label.textAlignment = .left
return label
}()
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
setUpUI()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func configureCell(Id: Int,title: String,statusString: String) {
storyIdLabel.text = "Id: \(String(Id))"
storyTitleLabel.text = "Status :\(title)"
statusStory.text = "Status:\(statusString)"
}
/* func configureCell(Id: Int) {
storyIdLabel.text = "Id: \(String(Id))"
}*/
override func awakeFromNib() {
super.awakeFromNib()
// Initialization code
}
private func setUpUI() {
contentView.addSubview(storyTitleLabel)
// constraints
let safeArea = contentView.safeAreaLayoutGuide
storyTitleLabel.topAnchor.constraint(equalTo: safeArea.topAnchor).isActive = true
storyTitleLabel.leadingAnchor.constraint(equalTo: safeArea.leadingAnchor).isActive = true
storyTitleLabel.trailingAnchor.constraint(equalTo: safeArea.trailingAnchor).isActive = true
contentView.addSubview(storyIdLabel)
let safeArea1 = contentView.safeAreaLayoutGuide
storyIdLabel.topAnchor.constraint(equalTo: storyTitleLabel.topAnchor).constant = 5
storyIdLabel.bottomAnchor.constraint(equalTo: safeArea1.bottomAnchor).isActive = true
storyIdLabel.leadingAnchor.constraint(equalTo: safeArea1.leadingAnchor).isActive = true
storyIdLabel.trailingAnchor.constraint(equalTo: safeArea1.trailingAnchor).isActive = true
/* contentView.addSubview(statusStory)
let safeArea2 = contentView.safeAreaLayoutGuide
statusStory.topAnchor.constraint(equalTo: statusStory.topAnchor).isActive = true
statusStory.bottomAnchor.constraint(equalTo: safeArea2.bottomAnchor).isActive = true
statusStory.leadingAnchor.constraint(equalTo: safeArea2.leadingAnchor).isActive = true
statusStory.trailingAnchor.constraint(equalTo: safeArea2.trailingAnchor).isActive = true*/
}
}
You have to use tableView.reloadData() after updating info that you want to show. Without running this function, data in table view won't be updated.

Adding double tap gesture recognizer to UIImageView in an UITableViewCell Swift 4+

(Edited with working solution)
So I'm trying to add a double tap gesture to an UIImageView I created in a custom UITableViewCell but can't seem to get it working.
Here is my custom UITableViewCell:
protocol CustomCellDelegate: class {
func didTapImage()
}
class CustomCell: UITableViewCell {
//change let to lazy var
lazy var userImage: UIImageView = {
let newView = UIIMageView()
newView.layer.cornerRadius = 24
newView.layer.masksToBounds = true
newView.image = UIImage(named: "samplePic")
newView.contentMode = .scaleAspectFill
newView.isUserInteractionEnabled = true
let doubleTap = UITapGestureRecognizer(target: self, action: #selector(myFunc))
doubleTap.numberOfTouchesRequired = 1
doubleTap.numberOfTapsRequired = 2
newView.addGestureRecognizer(doubleTap)
newView.translatesAutoresizingMaskIntoConstraints = false
return newView
}
weak var delegate: CustomCellDelegate?
#objc func myFunc() {
delegate?.didTapImage()
}
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: .subTitle, reuseIdentifier: reuseIdentifier)
self.selectionStyle = .none
//changed addSubView(userImage) to self.contentView.addSubView(userImage)
self.contentView.addSubView(userImage)
NSLayoutConstraint.activate([
userImage.centerYAnchor.constraint(equalTo: self.centerYAnchor),
userImage.leftAnchor.constraint(equalTo: self.leftAnchor),
userImage.widthAnchor.constraint(equalToConstant: 48),
userImage.heightAnchor.constraint(equalToConstant: 48),
}
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
Here is my custom UITableViewController:
class customTableViewController: UITableViewController, CustomCellDelegate {
fileprivate let cellId = "cellId"
func didTapImage() {
print("Tapped Image")
}
override func viewDidLoad() {
super.viewDidLoad()
tableView.register(CustomCell.self, forCellReuseIdentifier: cellId)
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 5
}
override func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
return 72
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: cellId, for: indexPath) as! CustomCell
cell.delegate = self
return cell
}
}
Any ideas as to why this isn't working? What am I doing wrong? Also how do I avoid having the same tap gestures recognizer added multiple times as cells are dequeue?
You may need
userImage.translatesAutoresizingMaskIntoConstraints = false
as you create constraints programmatically
lazy var userImage: UIImageView = {
let newView = UIIMageView()
userImage.translatesAutoresizingMaskIntoConstraints = false
newView.layer.cornerRadius = 24
newView.layer.masksToBounds = true
newView.image = UIImage(named: "samplePic")
newView.contentMode = .scaleAspectFill
newView.isUserInteractionEnabled = true
let doubleTap = UITapGestureRecognizer(target: self, action: #selector(myFunc))
doubleTap.numberOfTouchesRequired = 1
doubleTap.numberOfTapsRequired = 2
newView.addGestureRecognizer(doubleTap)
return newView
}()
also make it a lazy var not a computed property for being 1 instance every access , add the imageView to
self.contentView.addSubView(userImage)
and set the constraints with it

TableView inside a custom UITableViewCell not appearing for all of the cells of that custom cellviewtype

I am trying to create a table of services such that, if service has a couple of sub-services, then the cell associated with that service then has another table view showing those sub-services under the said service.
I tried implementing such a table by looking at the example shown in the link: table within a tableviewcell
I am posting related source codes associated with the tableview
BookingServiceChargeViewCell.swift
import UIKit
import PineKit
import SwiftMoment
class BookingServiceChargeViewCell: UITableViewCell, UITableViewDelegate, UITableViewDataSource {
var service : Service? = nil
var subServices : [Service] = []
let content = PineCardView()
var cover = UIImageView()
let serviceName = PineLabel.Bold(text: " ... ")
var itemIndex = -1
var chosen = false
var parentView : OnboardingChosenServicesViewController? = nil
var anchor = UIView()
let table = UITableView()
override init(style: UITableViewCellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
self.selectionStyle = .none
layout()
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func layout() {
self.addSubview(content)
content.snp.makeConstraints { (make) in
make.top.left.right.equalTo(self).inset(5)
make.bottom.equalTo(self)
}
layoutContent()
}
func layoutContent() {
content.addSubviews([cover, serviceName])
self.cover.image = UIImage(named: "gray-card")
cover.clipsToBounds = true
cover.snp.makeConstraints { (make) in
make.width.equalTo(content).multipliedBy(0.15)
make.left.equalTo(content).offset(10)
make.top.equalTo(content.snp.top).offset(15)
make.size.equalTo(50)
}
serviceName.textColor = UIColor.black
serviceName.font = Config.Font.get(.Bold, size: 17.5)
serviceName.snp.makeConstraints { (make) in
make.centerY.equalTo(cover)
make.left.equalTo(cover.snp.right).offset(20)
}
table.delegate = self
table.dataSource = self
table.register(BookingSubServicesChargeViewCell.self, forCellReuseIdentifier: "cell")
table.separatorStyle = .none
self.content.addSubview(table)
table.snp.makeConstraints { (make) in
make.top.equalTo(self.cover.snp.bottom).offset(15)
make.left.equalTo(self.cover.snp.right).offset(10)
make.right.equalTo(self.content.snp.right).offset(-10)
make.height.equalTo(450)
}
}
func configure(_ service: Service, subServices: [Service], index: Int, parentView: OnboardingChosenServicesViewController) {
self.service = service
self.subServices = subServices
self.itemIndex = index
self.parentView = parentView
if (self.service!.defaultImage != nil){
ImageLoader.sharedLoader.imageForUrl(urlString: self.service!.defaultImage!) { (image, url) in
self.cover.image = image
}
}
self.serviceName.text = self.service!.name!
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return self.subServices.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath) as! BookingSubServicesChargeViewCell
cell.configure(self.subServices[indexPath.row], index: indexPath.row, parentView: self.parentView!)
return cell
}
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
return 80
}
}
BookingSubServicesChargeViewCell.swift
import UIKit
import PineKit
import SwiftMoment
class BookingSubServicesChargeViewCell: UITableViewCell {
var service : Service? = nil
let content = PineCardView()
var cover = UIImageView()
let serviceName = PineLabel.Bold(text: " ... ")
var itemIndex = -1
var chosen = false
var parentView : OnboardingChosenServicesViewController? = nil
var anchor = UIView()
override init(style: UITableViewCellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
self.selectionStyle = .none
layout()
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func layout() {
self.addSubview(content)
content.snp.makeConstraints { (make) in
make.top.left.right.equalTo(self).inset(5)
make.bottom.equalTo(self)
}
layoutContent()
}
func layoutContent() {
content.addSubviews([cover, serviceName])
self.cover.image = UIImage(named: "gray-card")
cover.clipsToBounds = true
cover.snp.makeConstraints { (make) in
make.width.equalTo(content).multipliedBy(0.15)
make.left.equalTo(content).offset(10)
make.top.equalTo(content.snp.top).offset(15)
make.size.equalTo(50)
}
serviceName.textColor = UIColor.black
serviceName.font = Config.Font.get(.Bold, size: 17.5)
serviceName.snp.makeConstraints { (make) in
make.centerY.equalTo(cover)
make.left.equalTo(cover.snp.right).offset(20)
}
self.anchor = self.serviceName
}
func configure(_ service: Service, index: Int, parentView: OnboardingChosenServicesViewController) {
self.service = service
self.itemIndex = index
self.parentView = parentView
if (self.service!.defaultImage != nil){
ImageLoader.sharedLoader.imageForUrl(urlString: self.service!.defaultImage!) { (image, url) in
self.cover.image = image
}
}
self.serviceName.text = self.service!.name!
}
}
Here a couple of screenshots of the situation that has arisen:-
As you can see in the screenshots, some of the tables which have sub-services are not being shown, but sometimes they are being shown.
Can someone tell me what is it that I am missing here? What changes am I supposed to make in the source codes shown above?
FYI: I am not using storyboards in any way and I do not know what nib files are. I have constructed this programmatically and I am hoping that I can get a code snippet based solution as soon as possible.
Thanks.
In BookingServiceChargeViewCell class, call self.table.reloadData() at the end of configure method as below.
func configure(_ service: Service, subServices: [Service], index: Int, parentView: OnboardingChosenServicesViewController) {
self.service = service
self.subServices = subServices
self.itemIndex = index
self.parentView = parentView
if (self.service!.defaultImage != nil){
ImageLoader.sharedLoader.imageForUrl(urlString: self.service!.defaultImage!) { (image, url) in
self.cover.image = image
}
}
self.serviceName.text = self.service!.name!
self.table.reloadData()
}

Best practice for re-rendering a subview on a UITableViewCell

I have a UITableViewCell subclass which has a custom subview which is created through code. Now the problem is I'm trying to make the scrolling of the UITableView less jumpy.
Here's how this is setup.
CustomSubview is a UIView created through code
BasePostCell is a UITableViewCell is a UITableViewCell subclass that is used as a base for some other cells
UserPostCell, TextPostCell, and DiscussionPostCell are BasePostCell subclasses which are made using xibs and so far since I don't know if it is possible to somehow inherit an xib to another xib I just used viewWithTag and awakeFromNib to connect the subviews to their respective variables, which you will see on the sample code below
All of these are setup with NSLayoutConstraints which from what I've read/researched is significantly slower than if I create the view's through code and then just manually calculate the height, and width of each cell. I would if I could but right now I don't have the luxury of doing so because there are about 20+ different cells in the real code base. (this is just a sample code)
The class I want to change somehow is either CustomSubview or BasePostCell; or if there is a better way to do this please tell me.
Here's my code
The Model
class Post {
var type: PostType = .text
var text: String = ""
var title: String = ""
var displayPhoto: String?
// ... there are other attributes here
enum PostType {
case text, user, discussion
}
}
The Base Classes
class CustomSubview: UIView {
lazy var likeButton: UIButton = {
let button = UIButton()
button.translatesAutoresizingMaskIntoConstraints = false
button.backgroundColor = .black
button.titleLabel?.font = UIFont(face: .helveticaNeue, style: .regular, size: 14) // this is a helper function of mine
button.setTitleColor(UIColor.white, for: .normal)
button.setTitleColor(UIColor.gray, for: .selected)
return button
}()
// The rest are more or less the same as how likeButton is created
// the most important part is `translatesAutoresizingMaskIntoConstraints`
// should be set to true since I use `NSLayoutConstraints`
lazy var commentButton: UIButton = { ... }()
lazy var shareButton: UIButton = { ... }()
lazy var followButton: UIButton = { ... }()
lazy var answerButton: UIButton = { ... }()
func configure(withType type: PostType) {
// I don't know if this is the right way to do this
self.subviews.forEach { $0.removeFromSuperview() }
switch type {
case .text:
[ self.likeButton, self.commentButton, self.shareButton ].forEach { self.addSubview($0) }
// constraints code block
// code goes something like this
self.addConstraints(NSLayoutConstraint.constraints(
withVisualFormat: "H:|-0-[btnLike(==btnComment)]-0-[btnComment]-0-[btnShare(==btnComment)]-0-|",
options: NSLayoutFormatOptions(),
metrics: nil,
views: ["btnLike": self.likeButton,
"btnComment": self.commentButton,
"btnShare": self.shareButton]))
case .user:
[ self.followButton, self.shareButton ].forEach { self.addSubview($0) }
// insert more constraints code block here
case .discussion:
[ self.answerButton, self.commentButton, self.shareButton ].forEach { self.addSubview($0) }
// insert more constraints code block here
}
}
}
class BasePostCell: UITableViewCell {
// ... there are other subviews but
// only this view is modularly created
var customSubview: CustomSubview?
override func awakeFromNib() {
super.awakeFromNib()
self.customSubview = self.viewWithTag(990) as? CustomSubview
}
func configure(withPost post: Post) {
self.customSubview?.configure(withType: post.type)
}
}
The subclasses of the BasePostCell
class UserPostCell: BasePostCell {
var imgDisplayPhoto: UIImageView?
override func awakeFromNib() {
super.awakeFromNib()
self.imgDisplayPhoto = self.viewWithTag(0) as? UIImageView
}
override func configure(withPost post: Post) {
super.configure(withPost: post)
self.imgDisplayPhoto?.image = post.image
}
}
class TextPostCell: BasePostCell {
var lblContent: UILabel?
override func awakeFromNib() {
super.awakeFromNib()
self.lblContent = self.viewWithTag(1) as? UILabel
}
override func configure(withPost post: Post) {
super.configure(withPost: post)
self.lblContent?.text = post.text
}
}
class DiscussionPostCell: BasePostCell {
var lblContent: UILabel?
var lblDiscussionTitle: UILabel?
override func awakeFromNib() {
super.awakeFromNib()
self.lblContent = self.viewWithTag(1) as? UILabel
self.lblDiscussionTitle = self.viewWithTag(2) as? UILabel
}
override func configure(withPost post: Post) {
super.configure(withPost: post)
self.lblContent?.text = post.text
self.lblDiscussionTitle?.text = post.title
}
}
And finally the implementation on a SampleViewController
class SomeViewController: UIViewController {
#IBOutlet var tableView: UITableView!
var posts: [Post] = []
var heightForPost: [IndexPath: CGFloat] = [:]
override func viewDidLoad() {
super.viewDidLoad()
// let's just say I initialized the posts
self.posts = <SomePostsArrayHere>
// ... register nib to tableview codes here.
self.tableView.delegate = self
self.tableView.dataSource = self
self.tableView.reloadData()
}
// ... other implementations
}
// Here is the delegate and dataSource
extension SomeViewController: UITableViewDelegate, UITableViewDataSource {
func numberOfSections(in tableView: UITableView) -> Int {
return 1
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return self.posts.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let post = self.posts[indexPath.row]
var postCell: BasePostCell
switch post.type {
case .text:
postCell = tableView.dequeueReusableCell(withIdentifier: "TextPostCell", for: indexPath) as! TextPostCell
case .user:
postCell = tableView.dequeueReusableCell(withIdentifier: "UserPostCell", for: indexPath) as! UserPostCell
case .discussion:
postCell = tableView.dequeueReusableCell(withIdentifier: "DiscussionPostCell", for: indexPath) as! DiscussionPostCell
}
postCell.configure(withPost: post)
return postCell
}
func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
self.heightForPost[IndexPath] = cell.frame.size.height
}
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
return self.heightForPost[indexPath] ?? UITableViewAutomaticDimension
}
func tableView(_ tableView: UITableView, estimatedHeightForRowAt indexPath: IndexPath) -> CGFloat {
return 300
}
}
I have already suggested using time profiler to identify the problem code, but still I see one no-no in your code.
In configuring your cells, you always call configure(withType type: PostType) on your CustomSubview. And there, you remove the subviews and "rebuild" them. That's not something you should be doing in reusable cells - you don't want to touch their view hierarchy, all you want to do is to change their contents, e.g., change the text in a label, change an image in an imageView, etc. Otherwise you are not using the full power of reusable cells.
Just change the BaseClass to configure the subviews hierarchy just once, and then in cellForRowAt set just the contents of subviews:
class BasePostCell: UITableViewCell {
// ... there are other subviews but
// only this view is modularly created
var customSubview: CustomSubview?
override func awakeFromNib() {
super.awakeFromNib()
self.customSubview = self.viewWithTag(990) as? CustomSubview
}
func configure(withPost post: Post) {
// don't reconfigure the customView view hierarchy here, it gets called everytime cellForRowAt is called
}
}
class UserPostCell: BasePostCell {
var imgDisplayPhoto: UIImageView?
override func awakeFromNib() {
super.awakeFromNib()
// subviews setup just once here, because for the UserPostCell
// the type of the post will never change
self.customSubview?.configure(withType: .user)
self.imgDisplayPhoto = self.viewWithTag(0) as? UIImageView
}
override func configure(withPost post: Post) {
super.configure(withPost: post)
self.imgDisplayPhoto?.image = post.image
}
}
class TextPostCell: BasePostCell {
var lblContent: UILabel?
override func awakeFromNib() {
super.awakeFromNib()
self.customSubview?.configure(withType: .text)
self.lblContent = self.viewWithTag(1) as? UILabel
}
override func configure(withPost post: Post) {
super.configure(withPost: post)
self.lblContent?.text = post.text
}
}
class DiscussionPostCell: BasePostCell {
var lblContent: UILabel?
var lblDiscussionTitle: UILabel?
override func awakeFromNib() {
super.awakeFromNib()
self.customSubview?.configure(withType: .discussion)
self.lblContent = self.viewWithTag(1) as? UILabel
self.lblDiscussionTitle = self.viewWithTag(2) as? UILabel
}
override func configure(withPost post: Post) {
super.configure(withPost: post)
self.lblContent?.text = post.text
self.lblDiscussionTitle?.text = post.title
}
}

Resources