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)
}
}
}