2

I have cobbled together a UITableView subclass in swift that replicates some of the functionality of the swipeable buttons used in Apple's own apps. It was working fine in Xcode 7.0 beta 4 but since using beta 5, the cells are initially displayed with an incorrect contentOffset (equal to the contentInset used to hide the buttons) when the table is first displayed. Rotating the view or scrolling cells off screen corrects the issue - this error only occurs on initial presentation of the table view.

Checking the contentOffset for the cells as they're created shows that the contentOffset is set to zero but somewhere in the lifecycle, this value is overwritten. I can hack it to correct the issue by adding the following to the table view controller:

override func viewDidAppear(animated: Bool) {
    super.viewDidAppear(animated)
    for cell in table.visibleCells {
        (cell as! SwipeableTableViewCell).scrollView.contentOffset.x = CGFloat(0)
    }
}

However, it's not very elegant as I'm obviously fighting the system. Can anyone explain what's changed since Beta 4?

Source code for the UITableViewCell subclass below (apologies for code style in this early implementation - I'm a newbie and am still in the process of refactoring)

class SwipeableTableViewCell: UITableViewCell, UITableViewDelegate {

    //MARK: Constants
    private let thresholdVelocity = CGFloat(0.6)
    private let maxClosureDuration = CGFloat(40)
    private let buttonCount = 3
    private let buttonWidth = CGFloat(50)

    //MARK: Computed properties
    private var buttonContainerWidth: CGFloat {
        return CGFloat(buttonCount) * buttonWidth
    }

    //MARK: Properties
    let scrollView = UIScrollView() // TODO: was originally private but have had to allow superclass access to correct contentOffset glitch
    private let scrollContentView = UIView() /// positioned using constraints
    private var buttonContainers: (left: ButtonContainer!, right: ButtonContainer!)
    private let labelView = UILabel() /// positioned using constraints
    private var buttonsLeft = [SwipeableCellButton]()
    private var buttonsRight = [SwipeableCellButton]()
    private var viewDictionary: [String: UIView]!
    private let buttonColors = [UIColor.redColor(), UIColor.orangeColor(), UIColor.purpleColor()]


    //MARK: Initialisers
    required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
        viewDictionary = ["cv": contentView, "sv" : scrollView, "scv" : scrollContentView, "lv" : labelView]
    }


    //MARK: Lifecycle methods
    override func awakeFromNib() {
        super.awakeFromNib()

        // setup scrollView display characteristics
        scrollView.delegate = self
        scrollView.translatesAutoresizingMaskIntoConstraints = false
        scrollView.showsHorizontalScrollIndicator = false
        scrollView.showsVerticalScrollIndicator = false

        // build cell's view hierachy
        contentView.addSubview(scrollView)
        contentView.addConstraints(NSLayoutConstraint.constraintsWithVisualFormat("H:|[sv]|", options: [], metrics: nil, views: viewDictionary))
        contentView.addConstraints(NSLayoutConstraint.constraintsWithVisualFormat("V:|[sv]|", options: [], metrics: nil, views: viewDictionary))

        // setup scrollContentView
        scrollContentView.translatesAutoresizingMaskIntoConstraints = false
        scrollContentView.backgroundColor = UIColor.greenColor()
        scrollView.addSubview(scrollContentView)
        contentView.addConstraints(NSLayoutConstraint.constraintsWithVisualFormat("H:[scv(==cv)]", options: [], metrics: nil, views: viewDictionary)) // match width of cell contentView
        contentView.addConstraints(NSLayoutConstraint.constraintsWithVisualFormat("V:[scv(==cv)]", options: [], metrics: nil, views: viewDictionary)) // match height of cell contentView
        scrollView.addConstraints(NSLayoutConstraint.constraintsWithVisualFormat("H:|[scv]|", options: [], metrics: nil, views: viewDictionary)) // pin L+R edges to scrollview
        scrollView.addConstraints(NSLayoutConstraint.constraintsWithVisualFormat("V:|[scv]|", options: [], metrics: nil, views: viewDictionary)) // pin top+bottom edges to scrollview

        labelView.backgroundColor = UIColor.blueColor()
        labelView.textColor = UIColor.whiteColor()
        labelView.text = "TEST LABEL VIEW"
        labelView.translatesAutoresizingMaskIntoConstraints = false
        scrollContentView.addSubview(labelView)
        scrollContentView.addConstraints(NSLayoutConstraint.constraintsWithVisualFormat("H:|[lv]|", options: [], metrics: nil, views: viewDictionary)) /// pin to L&R edges of superview
        scrollContentView.addConstraints(NSLayoutConstraint.constraintsWithVisualFormat("V:|[lv]|", options: [], metrics: nil, views: viewDictionary)) /// pin to top and bottom of superview

        // setup button containers
        buttonContainers.left = ButtonContainer(properties: ContainerProperties.Left(width: buttonContainerWidth, height: contentView.frame.height, buttonWidth: buttonWidth))
        buttonContainers.right = ButtonContainer(properties: ContainerProperties.Right(width: buttonContainerWidth, height: contentView.frame.height, buttonWidth: buttonWidth))
        buttonContainers.right.frame.origin.x = contentView.frame.width
        viewDictionary.updateValue(buttonContainers.left, forKey: "bcl")
        viewDictionary.updateValue(buttonContainers.right, forKey: "bcr")

        for i in 1...buttonCount {
            var endX = buttonContainerWidth - (CGFloat(i) * buttonWidth) // calc for left button container
            let buttonL = SwipeableCellButton(container: buttonContainers.left, width: buttonWidth, endX: endX)
            buttonL.setAppearance("BUT\(i)", titleColor: UIColor.whiteColor(), backgroundColor: buttonColors[i - 1])

            endX = CGFloat(i - 1) * buttonWidth // re-calc for right button container
            let buttonR = SwipeableCellButton(container: buttonContainers.right, width: buttonWidth, endX: endX)
            buttonR.setAppearance("BUT\(i)", titleColor: UIColor.whiteColor(), backgroundColor: buttonColors[i - 1])

            buttonsLeft.append(buttonL)
            buttonsRight.append(buttonR)
            buttonContainers.left.addSubview(buttonL)
            buttonContainers.right.addSubview(buttonR)
        }
        scrollContentView.addSubview(buttonContainers.left)
        scrollContentView.addSubview(buttonContainers.right)

        // enable scrolling by adding an inset
        scrollView.contentInset = UIEdgeInsetsMake(0, buttonContainers.left.frame.width, 0, buttonContainers.right.frame.width)

    }

    override func layoutSubviews() {
        super.layoutSubviews()
        /// ensure x origin of R button container frmme is set correcntly
        buttonContainers.right.frame.origin.x = contentView.frame.width
        /// reset offset
        scrollView.contentOffset = CGPointZero
        NSLog("x offset after layoutSubviews: \(scrollView.contentOffset.x)")
    }


    //MARK: Private methods
    private func updateButtonPositions(buttonContainerVisibleWidth: CGFloat) {
        for s in buttonContainers.left.subviews {
            if let b = s as? SwipeableCellButton {
                b.updatePosition(buttonContainerVisibleWidth)
            }
        }
        for s in buttonContainers.right.subviews {
            if let b = s as? SwipeableCellButton {
                b.updatePosition(buttonContainerVisibleWidth)
            }
        }
    }
}


//MARK: Extension  - UIScrollViewDelegate
extension SwipeableTableViewCell : UIScrollViewDelegate {

    func scrollViewWillBeginDragging(scrollView: UIScrollView) {
        NSLog("Will begin dragging")
    }


    func scrollViewWillEndDragging(scrollView: UIScrollView, withVelocity velocity: CGPoint,
        targetContentOffset: UnsafeMutablePointer<CGPoint>) {
            NSLog("viewWillEndDragging")
            let x: CGFloat = scrollView.contentOffset.x
            NSLog("X offset \(x)")
            let left = buttonContainers.left.frame.width, right = buttonContainers.right.frame.width
            if (left > 0 && (x < -left || (x < 0 && velocity.x < -thresholdVelocity))) {
                targetContentOffset.memory.x = -left
            } else if (right > 0 && (x > right || (x > 0 && velocity.x > thresholdVelocity))) {
                targetContentOffset.memory.x = right
            } else {
                targetContentOffset.memory = CGPointZero

                // if the scroll isn't on a fast path to zero, animate it closed
                let ms: CGFloat = x / velocity.x
                if (velocity.x == 0 || ms < 0 || ms > maxClosureDuration) {
                    dispatch_async(dispatch_get_main_queue()) {
                        scrollView.setContentOffset(CGPointZero, animated: true)
                    }
                }
            }
    }


    func scrollViewDidScroll(scrollView: UIScrollView) {
        NSLog("viewDidScroll x offset \(scrollView.contentOffset.x)")
        let buttonContainerVisibleWidth = min(buttonContainerWidth, abs(scrollView.contentOffset.x))
        updateButtonPositions(buttonContainerVisibleWidth)
        /// stop the left button container moving away from the left margin of the cell
        if (scrollView.contentOffset.x < -buttonContainers.left.frame.width) {
            // Make the left buttonsLeft stay in place.
            buttonContainers.left.frame = CGRectMake(scrollView.contentOffset.x, 0, buttonContainers.left.frame.width, buttonContainers.left.frame.size.height)
            /// prevent right button container moving away from the right margin of the cell
        } else if (scrollView.contentOffset.x > buttonContainers.right.frame.width) {
            buttonContainers.right.frame = CGRectMake(scrollView.frame.size.width - buttonContainerWidth + scrollView.contentOffset.x, 0, buttonContainers.right.frame.width, buttonContainers.right.frame.height)
        }
    }
}
rustproofFish
  • 931
  • 10
  • 32

0 Answers0