1

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.

  1. CustomSubview is a UIView created through code
  2. BasePostCell is a UITableViewCell is a UITableViewCell subclass that is used as a base for some other cells
  3. 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
  4. 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
    }
}
Zonily Jame
  • 5,053
  • 3
  • 30
  • 56
  • Use time profiler to detect what actually causes lagging.. otherwise you'll risk optimizing things that are not really your problem – Milan Nosáľ Feb 23 '18 at 13:37
  • @MilanNosáľ ok I'll do that, but still the `configure` of my `CustomView` still looks like a pretty bad implementation, since everytime the cell appears it gets called. – Zonily Jame Feb 23 '18 at 13:42
  • OK, I see what might be the problem.. still I would check the profiler – Milan Nosáľ Feb 23 '18 at 13:44
  • 1
    I don't think you need a BasePostCell here, to make it simpler just configure your custom view before setting up each cell. – Achu22 Feb 23 '18 at 13:54

1 Answers1

1

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
    }
}
Milan Nosáľ
  • 19,169
  • 4
  • 55
  • 90
  • One question though. If I call `self.customSubview?.configure(withType: )` on `awakeFromNib` will `configure(withPost post: Post)` be called first on `cellForRow` before `awakeFromNib` life cycle speaking? – Zonily Jame Feb 26 '18 at 02:30
  • @ZonilyJame while I always do everything in code, I believe that `awakeFromNib` will be always called before `cellForRowAt` (in `dequeueReusableCell`), so I stand by my answer :). One similar question on SO recommends basically the same thing: https://stackoverflow.com/questions/35161109/ios-where-to-put-custom-cell-design-awakefromnib-or-cellforrowatindexpath – Milan Nosáľ Feb 26 '18 at 08:47
  • Yeah, I actually did what you suggested. It seems to be faster by a few ms now. – Zonily Jame Feb 26 '18 at 11:14