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
}
}
Related
I want to an Shimmer animation like this: https://gph.is/g/amWgbvj.
(This one created by using library: https://github.com/dvtng/react-loading-skeleton in WebApp)
I tried to use GradientLayer with opacity to create Shimmer animation through all subviews:
gradientLayer.colors = [Colors.tokenDark20.cgColor, Colors.tokenDark10.cgColor, Colors.tokenDark20.cgColor]
gradientLayer.opacity = 0.5
But I got the animation: https://gph.is/g/4L3K01P.
More effort:
I tried to use the library: https://github.com/gonzalonunez/Skeleton, tried to chain animation from left to right, but I cant make the same length of gradient shape for all subview:
extension ShimmerExampleCell: SkeletonOwner {
var gradientLayers: [CAGradientLayer] {
return [imagePlaceholderView.gradientLayer, titlePlaceholderView.gradientLayer, subtitlePlaceholderView.gradientLayer]
}
func slide(to dir: SkeletonDirection, group: ((CAAnimationGroup) -> Void) = { _ in }) {
imagePlaceholderView.gradientLayer.slide(to: .right) { (animationGroup) in
animationGroup.beginTime = 0
}
titlePlaceholderView.gradientLayer.slide(to: .right) { (animationGroup) in
animationGroup.beginTime = 1.1
subtitlePlaceholderView.gradientLayer.add(animationGroup, forKey: CAGradientLayer.kSlidingAnimationKey)
}
}
}
I got the animation in here: https://gph.is/g/ZPgPlXV
am I wrong to approach the way to make the Shimmer animation?
Help me please! Thank you in advance!
You can try to use library SkeletonView; it should help you easily implement shimmer animation wherever you want.
View Controller:
class ViewController: UIViewController {
#IBOutlet weak var tableView: UITableView!
var data: [String] = []
override func viewDidLoad() {
super.viewDidLoad()
tableView.register(UINib(nibName: "TableViewCell", bundle: nil), forCellReuseIdentifier: "cell")
tableView.delegate = self
tableView.dataSource = self
for value in 0..<20 {
self.data.append("\(value) data")
self.tableView.reloadData()
}
}
Extension Of ViewController:
extension ViewController: UITableViewDelegate, UITableViewDataSource {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return data.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath) as! TableViewCell
cell.labelName.text = data[indexPath.row]
return cell
}
UITableViewCell: Here I set the show or hide status for SkeletonView
class TableViewCell: UITableViewCell {
#IBOutlet weak var labelName: UILabel!
#IBOutlet weak var imgView: UIImageView!
override func awakeFromNib() {
super.awakeFromNib()
// Initialization code
let views = [labelName, imgView]
views.forEach {$0?.showHideSkeletonView(show: true)}
DispatchQueue.main.asyncAfter(deadline: .now()+3) {
views.forEach {$0?.showHideSkeletonView(show: false)}
}
}
ConfigSkeleton: Here I add a function for setup, animation and color for my skeletonView.
import SkeletonView
extension UIView {
func setSkeletonView() {
self.isSkeletonable = true
}
func showHideSkeletonView(show: Bool) {
if show {
let gradient = SkeletonGradient(baseColor: UIColor.clouds)
let animation = SkeletonAnimationBuilder().makeSlidingAnimation(withDirection: .topLeftBottomRight)
self.showAnimatedGradientSkeleton(usingGradient: gradient, animation: animation)
} else {
self.hideSkeleton()
}
}
I am trying to manage two buttons in same custom tableview cell.
Added two buttons named Yes and No. If yes button is selected the No button will be inactive and Yes button became active.
Here is the image what I need
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "tableCell") as! TableViewCell
cell.yesButton.tag = 101
cell.noButton.tag = 102
cell.yesButton.addTarget(self, action: #selector(buttonClicked(sender:)), for: UIControl.Event.touchUpInside)
cell.noButton.addTarget(self, action: #selector(buttonClicked(sender:)), for: UIControl.Event.touchUpInside)
return cell
}
#objc func buttonClicked(sender: AnyObject) {
let buttonPosition = (sender as AnyObject).convert(CGPoint.zero, to: tableList)
let indexPath = tableList.indexPathForRow(at: buttonPosition)
if sender.tag == 101 {
if indexPath != nil {
print("Cell indexpath = \(String(describing: indexPath?.row))")
}
}
if sender.tag == 102 {
if indexPath != nil {
print("Cell indexpath = \(String(describing: indexPath?.row))")
}
}
}
Create a model to main the state of yesButton and noButton for each tableViewCell, i.e.
class Model {
var isYesSelected = false
var isNoSelected = false
}
Create a custom UITableViewCell with Outlets of yesButton and noButton.
Create a single #IBAction for both the buttons and handle their UI based on which button is tapped.
Also, use a buttonTapHandler to identify the row in which the button is tapped. It will be called everytime a button is tapped. We'll be setting this when creating the instance of TableViewCell in tableView(_:cellForRowAt:).
class TableViewCell: UITableViewCell {
#IBOutlet weak var yesButton: UIButton!
#IBOutlet weak var noButton: UIButton!
var buttonTapHandler: (()->())?
var model: Model?
override func prepareForReuse() {
super.prepareForReuse()
yesButton.backgroundColor = .gray
noButton.backgroundColor = .gray
}
func configure(with model: Model) {
self.model = model
self.updateUI()
}
#IBAction func onTapButton(_ sender: UIButton) {
model?.isYesSelected = (sender == yesButton)
model?.isNoSelected = !(sender == yesButton)
self.updateUI()
}
func updateUI() {
yesButton.backgroundColor = (model?.isYesSelected ?? false) ? .green : .gray
noButton.backgroundColor = (model?.isNoSelected ?? false) ? .green : .gray
}
}
UITableViewDataSource's tableView(_:cellForRowAt:) method goes like,
let numberOfCells = 10
var models = [Model]()
override func viewDidLoad() {
super.viewDidLoad()
(0..<numberOfCells).forEach { _ in
self.models.append(Model())
}
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return numberOfCells
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "tableCell", for: indexPath) as! TableViewCell
cell.configure(with: models[indexPath.row])
cell.buttonTapHandler = {
print(indexPath.row)
}
return cell
}
To get the totalPoints, count the models with isYesSelected = true, i.e.
let totalPoints = models.reduce(0) { (result, model) -> Int in
if model.isYesSelected {
return result + 1
}
return 0
}
print(totalPoints)
Get that Button using your Tag like below and after that, you can change the value as per you want.
var tmpButton = self.view.viewWithTag(tmpTag) as? UIButton
Simple 3 step process...!!
Define Model Class
Prepare tableView Cell & handle actions
Set up tableView in view controller
Let's start implementation:
1) Define Model Class
In UI, we have a information like question & it's answer (Yes/No). So design model respectively.
//MARK:- Class Declaration -
class Question {
let questionText: String
var answerState: Bool?
init(question: String) {
self.questionText = question
}
}
2. Prepare tableView Cell & handle actions
Create a custom tableView cell with Question Label, Yes Button & No Button. Link that view with respected #IBOutlets & #IBActions.
import UIKit
class TableViewCell: UITableViewCell {
#IBOutlet weak var questionLabel: UILabel!
#IBOutlet weak var yesButton: UIButton!
#IBOutlet weak var noButton: UIButton!
var question: Question?
var toggle: Bool? {
didSet {
question?.answerState = toggle
//Do buttons operations like...
if let isToggle = toggle {
yesButton.backgroundColor = isToggle ? .green : .gray
noButton.backgroundColor = isToggle ? .gray : .green
} else {
yesButton.backgroundColor = .gray
noButton.backgroundColor = .gray
}
}
}
func prepareView(forQuestion question: Question) {
self.question = question
questionLabel.text = question.questionText
toggle = question.answerState
}
//Yes Button - IBAction Method
#IBAction func yesButtonTapped(_ sender: UIButton) {
toggle = true
}
//No Button - IBAction Method
#IBAction func noButtonTapped(_ sender: UIButton) {
toggle = false
}
}
3. Set up tableView in view controller
class ViewController: UIViewController {
//Prepare questions model array to design our tableView data source
let arrQuestions: [Question] = [Question(question: "Do you speak English?"), Question(question: "Do you live in Chicago?")]
}
//MARK:- UITableView Data Source & Delegate Methods -
extension ViewController: UITableViewDataSource, UITableViewDelegate {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return arrQuestions.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
guard let tableViewCell = tableView.dequeueReusableCell(withIdentifier: "TableViewCell") as? TableViewCell else {
return UITableViewCell()
}
tableViewCell.prepareView(forQuestion: arrQuestions[indexPath.row])
return tableViewCell
}
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
return 80.0
}
}
Create basic tableView and configure dataSource functions
Create tableView cell with two buttons
Create cell class with buttons outlets and actions
Result of this code
Enjoy!
I want to include a button in each table cell that opens a URL.
I've created tables (using an array) with images and labels just fine, however I'm confused how to create a button
Here's what I have so far
class ExploreCell: UITableViewCell {
#IBOutlet weak var exploreImageView: UIImageView!
#IBOutlet weak var exploreTitleView: UILabel!
#IBOutlet weak var exploreDescriptionView: UILabel!
#IBOutlet weak var exploreButton: UIButton!
func setExplore(explore: Explore) {
exploreImageView.image = explore.image
exploreTitleView.text = explore.title
exploreDescriptionView.text = explore.description
exploreButton.addTarget(self, action: "connected:", for: .touchUpInside) = explore.button
}
My Class for the array looks like this
class ExploreListScreen: UIViewController {
#IBOutlet weak var tableView: UITableView!
var explores: [Explore] = []
override func viewDidLoad() {
super.viewDidLoad()
explores = createArray ()
tableView.delegate = self
tableView.dataSource = self
}
func createArray() -> [Explore] {
var tempExplores: [Explore] = []
let explore1 = Explore(image: #imageLiteral(resourceName: "test"), title: "Demo", description: "Essential", button: "")
tempExplores.append(explore1)
return tempExplores
}
Finally I have another file which contains the declared variables
class Explore {
var image: UIImage
var title: String
var description: String
var button: UIButton
init(image: UIImage, title: String, description: String, button: UIButton) {
self.image = image
self.title = title
self.description = description
self.button = button
}
Any advice and guidance would be fantastic. Thank-you!
Here's how I usually solve this. Create a delegate for your UITableViewCell subclass, and set the view controller owning the tableView as its delegate. Add methods for the interactions that happens inside the cell.
protocol YourTableViewCellDelegate: class {
func customCellDidPressUrlButton(_ yourTableCell: YourTableViewCell)
}
class YourTableViewCell: UITableViewCell {
weak var delegate: YourTableViewCellDelegate?
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
let button = UIButton()
button.addTarget(self, action: #selector(buttonTapped), for: .touchUpInside)
addSubview(button)
}
required init?(coder _: NSCoder) {
return nil
}
#objc func buttonTapped() {
delegate?.customCellDidPressUrlButton(self)
}
}
Then, in the controller, set itself as a delegate and get the indexPath trough the proper method, indexPath(for:)
class YourTableViewController: UITableViewController {
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath) as! YourTableViewCell
cell.delegate = self
return cell
}
}
extension YourTableViewController: YourTableViewCellDelegate {
func customCellDidPressUrlButton(_ yourTableCell: YourTableViewCell) {
guard let indexPath = tableView.indexPath(for: yourTableCell) else { return }
print("Link button pressed at \(indexPath)")
}
}
Then use that indexPath to grab the correct URL and present it from your table viewcontroller with a SFSafariViewController.
Swift 4
This is best way to get indexPath using touchPoint
class YourTableViewController: UITableViewController {
// ...
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "SwiftyCell", for: indexPath) as! SwiftyTableViewCell
cell.label.text = "This is cell number \(indexPath.row)"
// WRONG! When cells get reused, these actions will get added again! That's not what we want.
// Of course, we could get around this by jumping through some hoops, but maybe there's a better solution...
cell.yourButton.addTarget(self, action: #selector(self.yourButtonTapped(_:)), for: .touchUpInside)
return cell
}
func yourButtonTapped(_ sender: Any?) {
let point = tableView.convert(sender.center, from: sender.superview!)
if let wantedIndexPath = tableView.indexPathForItem(at: point) {
let cell = tableView.cellForItem(at: wantedIndexPath) as! SwiftyCell
}
}
// ...
}
For more details you can follow this tutorials
Just create UIButton object in viewDidLoad and add this button as a sub view on cell in cellForRowAtIndexPath function. Take Burton's frame as per your requirement.
I have followed this tutorial to create a custom .xib, which I plan to use in a table view's cell:
https://medium.com/#brianclouser/swift-3-creating-a-custom-view-from-a-xib-ecdfe5b3a960
Here is the .xib's class I created:
class UserView: UIView {
#IBOutlet var view: UIView!
#IBOutlet weak var username: UILabel!
override init(frame: CGRect) {
super.init(frame: frame)
initialize()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
initialize()
}
private func initialize() {
Bundle.main.loadNibNamed("UserView", owner: self, options: nil)
addSubview(view)
view.frame = self.bounds
view.autoresizingMask = [.flexibleHeight, .flexibleWidth]
}
}
Previously, I was creating my table view cell within the storyboard, but I've come to realize that I want a more flexible view so that I can use it in different parts of my app, so I created the above custom .xib, UserView.
I have updated the table view cell in the storyboard to use the custom .xib:
https://i.stack.imgur.com/t7Tr7.png
Here is what my table view controller class looked like prior to creating the custom .xib (i.e. making the layout in the storyboard):
class UserTableViewController: UITableViewController {
// MARK: Properties
let provider = MoyaProvider<ApiService>()
var users = [User]()
override func viewDidLoad() {
super.viewDidLoad()
tableView.estimatedRowHeight = 100
tableView.rowHeight = UITableViewAutomaticDimension
// Fetch the user by their username
provider.request(.getUsers()) { result in
switch result {
case let .success(response):
do {
let results = try JSONDecoder().decode(Pagination<[User]>.self, from: response.data)
self.users.append(contentsOf: results.data)
self.tableView.reloadData()
} catch {
print(error)
}
case let .failure(error):
print(error)
break
}
}
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
}
override func numberOfSections(in tableView: UITableView) -> Int {
return 1
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return users.count
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cellIdentifier = "UserTableViewCell"
guard let cell = tableView.dequeueReusableCell(withIdentifier: cellIdentifier, for: indexPath) as? UserTableViewCell else {
fatalError("The dequeued cell is not an instance of UserTableViewCell.")
}
let user = users[indexPath.row]
cell.username.text = user.username
return cell
}
}
Here is the table view cell class:
class UserTableViewCell: UITableViewCell {
//MARK: Properties
#IBOutlet weak var userView: UserView!
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
}
}
My question is, how do I update the above table view controller class to use my custom .xib, instead of using the storyboard layout?
You can use 2 ways:
Create UITableViewCell (better)
1) Change UIView to UITableViewCell
class CustomTableViewCell: UITableViewCell {
...
class var identifier: String {
return String(describing: self)
}
}
2) Register your cell
override func viewDidLoad() {
super.viewDidLoad()
self.tableView.registerNib(UINib(nibName: CustomTableViewCell.identifier, bundle: nil), forCellReuseIdentifier: CustomTableViewCell.identifier)
...
}
3) Use cellForRow(at:)
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: CustomTableViewCell.identifier) as! CustomTableViewCell
cell.username.text = user.username
return cell
}
OR Add view as subview to cell (only in rare cases)
1) Add this to UserView
class UserView: UIView {
...
class func fromNib() -> UserView {
return UINib(nibName: String(describing: self), bundle: nil).instantiate(withOwner: nil, options: nil)[0] as! UserView
}
}
2) Use cellForRow(at:)
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cellIdentifier = "UserTableViewCell"
guard let cell = tableView.dequeueReusableCell(withIdentifier: cellIdentifier, for: indexPath) as? UserTableViewCell else {
fatalError("The dequeued cell is not an instance of UserTableViewCell.")
}
let userView = UserView.fromNib()
let user = users[indexPath.row]
userView.username.text = user.username
//Use frame size, but for me better to add 4 constraints
userView.frame = CGRect(x: 0, y: 0, width: cellWidth, height: cellHeight)
cell.contentView.addSubview(UserView)
return cell
}
I have a UITableView and in its prototype cell have a UICollectionView.
MainViewController is delegate for UITableView and
MyTableViewCell class is delegate for UICollectionView.
On updating each TableViewCell contents I call cell.reloadData() to make the collectionView inside the cell reloads its contents.
When I use reusable cells, as each cell appears, it has contents of the last cell disappeared!. Then it loads the correct contents from a URL.
I'll have 5 to 10 UITableViewCells at most. So I decided not to use reusable cells for UITableView.
I changed the cell creation line in tableView method to this:
let cell = MyTableViewCell(style: .default, reuseIdentifier:nil)
Then I got an error in MyTableViewCell class (which is delegate for UICollectionView), in this function:
override func layoutSubviews() {
myCollectionView.dataSource = self
}
EXC_BAD_INSTRUCTION CODE(code=EXC_I386_INVOP, subcode=0x0)
fatal error: unexpectedly found nil while unwrapping an Optional value
MyTableViewCell.swift
import UIKit
import Kingfisher
import Alamofire
class MyTableViewCell: UITableViewCell, UICollectionViewDataSource {
struct const {
struct api_url {
static let category_index = "http://example.com/api/get_category_index/";
static let category_posts = "http://example.com/api/get_category_posts/?category_id=";
}
}
#IBOutlet weak var categoryCollectionView: UICollectionView!
var category : IKCategory?
var posts : [IKPost] = []
override func awakeFromNib() {
super.awakeFromNib()
// Initialization code
if category != nil {
self.updateData()
}
}
override func setSelected(_ selected: Bool, animated: Bool) {
super.setSelected(selected, animated: animated)
// Configure the view for the selected state
}
override func layoutSubviews() {
categoryCollectionView.dataSource = self
}
func updateData() {
if let id = category?.id! {
let url = const.api_url.category_posts + "\(id)"
Alamofire.request(url).responseObject { (response: DataResponse<IKPostResponse>) in
if let postResponse = response.result.value {
if let posts = postResponse.posts {
self.posts = posts
self.categoryCollectionView.reloadData()
}
}
}
}
}
internal func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "postCell", for: indexPath as IndexPath) as! MyCollectionViewCell
let post = self.posts[indexPath.item]
cell.postThumb.kf.setImage(with: URL(string: post.thumbnail!))
cell.postTitle.text = post.title
return cell
}
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
//You would get something like "model.count" here. It would depend on your data source
return self.posts.count
}
func numberOfSectionsInCollectionView(collectionView: UICollectionView) -> Int {
return 1
}
}
MainViewController.swift
import UIKit
import Alamofire
class MainViewController: UITableViewController {
struct const {
struct api_url {
static let category_index = "http://example.com/api/get_category_index/";
static let category_posts = "http://example.com/api/get_category_posts/?category_id=";
}
}
var categories : [IKCategory] = []
override func viewDidLoad() {
super.viewDidLoad()
self.updateData()
}
func updateData() {
Alamofire.request(const.api_url.category_index).responseObject { (response: DataResponse<IKCategoryResponse>) in
if let categoryResponse = response.result.value {
if let categories = categoryResponse.categories {
self.categories = categories
self.tableView.reloadData()
}
}
}
}
// MARK: - Table view data source
override func numberOfSections(in tableView: UITableView) -> Int {
return self.categories.count
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 1
}
override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
return self.categories[section].title
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
// let cell = tableView.dequeueReusableCell(withIdentifier: "CollectionHolderTableViewCell") as! MyTableViewCell
let cell = MyTableViewCell(style: .default, reuseIdentifier:nil)
cell.category = self.categories[indexPath.section]
cell.updateData()
return cell
}
}
MyCollectionViewCell.swift
import UIKit
class MyCollectionViewCell: UICollectionViewCell {
#IBOutlet weak var postThumb: UIImageView!
#IBOutlet weak var postTitle: UILabel!
var category : IKCategory?
}
Why not reusing cells caused this? Why am I doing wrong?
There are a few things to do that should get you up to speed.
First, uncomment the line that uses reusable cells and remove the line of code that creates the non-reusable cells. It is safe to use reusable cells here.
Second, in MyTableViewCell, set the dataSource for the collection view right after the super.awakeFromNib() call. You only need to set the dataSource once, but layoutSubviews() will potentially get called multiple times. It's not the right place to set the dataSource for your needs.
override func awakeFromNib() {
super.awakeFromNib()
categoryCollectionView.dataSource = self
}
I have removed the call to updateData() from awakeFromNib(), as you are already calling it at cell creation. You can also delete the layoutSubviews() override, but as a general rule, you should be careful to call super.layoutSubviews() when overriding it.
Lastly, the reason the posts seemed to re-appear in the wrong cells is that the posts array wasn't being emptied as the cells were reused. To fix this issue, add the following method to MyTableViewCell:
func resetCollectionView {
guard !posts.isEmpty else { return }
posts = []
categoryCollectionView.reloadData()
}
This method empties the array and reloads your collection view. Since there are no posts in the array now, the collection view will be empty until you call updateData again. Last step is to call that function in the cell's prepareForReuse method. Add the following to MyTableViewCell:
override func prepareForReuse() {
super.prepareForReuse()
resetCollectionView()
}
Let me know how it goes!