Swipeable table view cell with custom editable views in iOS

Hello guys, I decided to write a short post related to one of the UI issues, some of my colleges faced during implementing UITableView cell actions with custom UIViews. For my convenience, I wrote the sample project in Swift 5. iOS has given the developer to edit the table cells, but it’s hard to change the editable view. As an example, if we were asked to have lengthy text in single line or image and a label in side by side etc…

You can come up with various designs for editable actions. But I’m going to implement the above mentioned two approaches. The final outcome will be like below.

Swipe-able UITableViewCell with custom edit views

Swipe-able UITableViewCell with custom edit views

As for the start, just create an iOS project and in your ViewController in the storyboard, add an UITableView and set it’s constraints to margin of the super view. Next thing you have to do is, create a custom UITableViewCell class. As for my convenience, I’ll put the whole code related to that class in below. I have described what each function will perform in the comments.

import UIKit

class SwipableTableViewCell: UITableViewCell, UIScrollViewDelegate {

    var scrollView: UIScrollView?
    var scrollViewContentView: UIView?
    var scrollViewLabel: UILabel?
    let kCellCloseNotifyEvent = "SwipeableTableViewCellClose"
    var leftInset: CGFloat?
    var rightInset: CGFloat?

    required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
    }

    override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
        super.init(style: style, reuseIdentifier: reuseIdentifier)
        
        setUp()
    }
    
    override func layoutSubviews() {
        self.scrollView?.contentSize = self.contentView.bounds.size
        self.scrollView?.contentOffset = CGPoint.zero
    }

    //    MARK: UIScrollViewDelegate
    
    /*
     *  When a cell's editable action reveals, other cells editable actions should be clase
     */
    func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
        closeAllCellsExcept(cell: self)
    }
    
    /*
     *  Restricting scroll to right side where there is not action elements
     */
    func scrollViewDidScroll(_ scrollView: UIScrollView) {
        
        if (scrollView.contentOffset.x < 0) {
            scrollView.contentOffset = CGPoint(x: 0, y: 0);
        }
    }

    //    MARK: Notification selectors
    
    @objc func handleCloseEvent(notification: Notification) {
        self.scrollView?.setContentOffset(CGPoint.zero, animated: true)
    }

    //    MARK: Supportive methods

    /*
     *  Create cell the cell with horizontally scorllable behaviour
     */
    func setUp() {
        
        let scrollView = UIScrollView(frame: self.contentView.bounds)
        scrollView.autoresizingMask = AutoresizingMask(rawValue: AutoresizingMask.flexibleWidth.rawValue | AutoresizingMask.flexibleHeight.rawValue)
        scrollView.contentSize = self.contentView.bounds.size
        scrollView.delegate = self
        scrollView.scrollsToTop = false
        scrollView.showsHorizontalScrollIndicator = false
        scrollView.showsVerticalScrollIndicator = false
        self.contentView.addSubview(scrollView)
        self.scrollView = scrollView

        let contentView = UIView(frame: scrollView.bounds)
        contentView.autoresizingMask = AutoresizingMask(rawValue: AutoresizingMask.flexibleWidth.rawValue | AutoresizingMask.flexibleHeight.rawValue)
        scrollView.addSubview(contentView)
        self.scrollViewContentView = contentView
        
        let label = UILabel(frame: CGRect(x: 20, y: contentView.bounds.size.height/2, width: contentView.bounds.size.width, height: 0))
        label.autoresizingMask = AutoresizingMask(rawValue: AutoresizingMask.flexibleWidth.rawValue | AutoresizingMask.flexibleHeight.rawValue)
        self.scrollViewContentView?.addSubview(label)
        self.scrollViewLabel = label
        
        NotificationCenter.default.addObserver(self, selector: #selector(self.handleCloseEvent(notification:)), name: NSNotification.Name(rawValue: kCellCloseNotifyEvent), object: nil)
    }

    /*
     *  View that holds the editable actions
     */
    func createCustomViewContainer() -> UIView {
        let view = UIView(frame: CGRect(x: 0, y: 0, width: 0, height: self.contentView.bounds.size.height))
        view.autoresizingMask = AutoresizingMask(rawValue: AutoresizingMask.flexibleHeight.rawValue)
        view.backgroundColor = UIColor.lightGray
        self.scrollView?.addSubview(view)
        return view
    }

    /*
     *  Create cell editable action that contains only a button
     */
    func createButtonWithWidth(width: CGFloat) -> UIButton {

        let container: UIView = createCustomViewContainer()
        let size = container.bounds.size
        
        let button = UIButton(type: UIButton.ButtonType.custom)
        button.autoresizingMask = AutoresizingMask(rawValue: AutoresizingMask.flexibleHeight.rawValue)
        button.frame = CGRect(x: 0, y: 0, width: width, height: size.height)
        
        container.frame = CGRect(x: UIScreen.main.bounds.size.width, y: 0, width: size.width + width, height: size.height)
        container.addSubview(button)
        
        self.scrollView?.contentInset = UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 160)
        
        return button
    }

    /*
     *  Create cell editable action that contains an image and a label
     */
    func createViewWithImageAndLabel(width: CGFloat) -> UIView {

        let container: UIView = createCustomViewContainer()
        let size = container.bounds.size

        let imageName = "Notification"
        let image = UIImage(named: imageName)
        let imageView = UIImageView(image: image!)
        imageView.contentMode = .scaleAspectFit
        imageView.frame = CGRect(x: 0, y: 10, width: 100, height: size.height)

        let label = UILabel(frame: CGRect(x: imageView.bounds.size.width, y: 10, width: 100, height: size.height))
        label.textAlignment = .center
        label.text = "Notify Me"

        container.frame = CGRect(x: UIScreen.main.bounds.size.width, y: 0, width: width, height: size.height)
        container.addSubview(imageView)
        container.addSubview(label)

        self.scrollView?.contentInset = UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 200)
        
        return imageView
    }

    /*
     *  Close all opened cell
     *      If argument comes as self, keep that cell open but close the others
     */
    func closeAllCellsExcept(cell: SwipableTableViewCell) {
        NotificationCenter.default.post(name: NSNotification.Name(rawValue: kCellCloseNotifyEvent), object: cell)
    }

    /*
     *  When table start scrolling, we need to close all the opened cells
     */
    func closeAllCells() {
        closeAllCellsExcept(cell: SwipableTableViewCell())
    }
}

Yep, that’s the magic. Now you have to call that custom cell inside your tableView cellForRowAt function like below.

 func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {

        let cell = SwipableTableViewCell(style: UITableViewCell.CellStyle.default, reuseIdentifier: cellIdentifier)
        cell.backgroundColor = UIColor.white
        
        cell.scrollViewLabel?.text = dataArray[indexPath.row]
        
        if (indexPath.row % 2 == 0) {
            let moreButton = cell.createButtonWithWidth(width: 160)
            moreButton.setTitle("Do not notify me", for: UIControl.State.normal)
            moreButton.setTitleColor(UIColor.white, for: UIControl.State.normal)
            moreButton.titleLabel?.textAlignment = NSTextAlignment.center
            moreButton.addTarget(self, action: #selector(notifyMe), for: UIControl.Event.touchUpInside)
        }
        else {
            _ = cell.createViewWithImageAndLabel(width: 200)
        }
        
        return cell
    }

To enable cell edit, add following code in your controller, where you are declaring UITableViewDelegates.

func tableView(_ tableView: UITableView, canEditRowAt indexPath: IndexPath) -> Bool {
    return true
}

To close the open cells when you scroll the table, just add following function by implementing UIScrollViewDelegate in your ViewController class.

func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
    SwipableTableViewCell().closeAllCells()
}

I think that is pretty much that you need to know. This is not a complex code. What’s difficult was think out of the box and create the cell behaviour as in the default UITableViewCell. But my point is, we need to think twice when we add this kind of custom components in industry level apps. Because the effort and the time we spent on this should be worth. For those who needs full project, can access here. Have a interesting coding time till we meet again 🙂

 

Advertisements

About AnujAroshA

Working as an Associate Technical Lead. Specialized in iOS application development. A simple person :)
This entry was posted in iOS, Swift and tagged , , . Bookmark the permalink.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s