I'm trying to convert some ObjC for a horizontally scrolling UITableViewCell to Swift. The original ObjC version used explicit sizing of the cell contents but I am trying to use autolayout for consistency across my app.
All was going well up until the point where I wanted to create buttons on the left and right of the screen that can be hidden/unhidden by swiping in the same way as the delete button in the Mail app (but I want a bit more functionality hence a homebrew solution). The original code achieved this by creating scrollview content insets at the left and right that were the same size as the buttons but I have a few conceptual and programmatic issues with this.
Firstly, I understood contentInset as a 'buffer' zone around a scroll view that people often use to prevent the content area being hidden by status or navigation bars (i.e. a way of maximising screen real-estate). As such, the content area of the scrollview should remain constant regardless of contentInsets as they are essentially a margin. However, the original coder creates a negative insets and these seem to magically increase content area but the views within the insets are hidden off-screen. What's going on? Why do we do this rather than just changing the offset to compensate for the width of the off-screen buttons?
Secondly, buttons are revealed by swiping far or fast enough so that the eventual resting point of the scrollview reveals the hidden button. The code to lock the scrollview in position and stop the button from being re-hidden is within the scrollViewWillEndDragging function and changes the targetContentOffset to achieve this. This works when using explicit view and button sizes but fails to work when using autolayout. However, if you call scrollview.setContentOffset it works. Why the difference? Surely it's the same thing but I'm guessing there must be a different sequence of method calls.
Code is below (I've edited out some of the less important material). This is proof-of-concept so not very elegant!
Creating scrollView:
let scrollView = UIScrollView()
scrollView.backgroundColor = UIColor.blueColor()
scrollView.delegate = self;
scrollView.scrollsToTop = false
scrollView.showsHorizontalScrollIndicator = false
scrollView.showsVerticalScrollIndicator = false
self.contentView.addSubview(scrollView)
// pin scrollView to cell contentView
scrollView.setTranslatesAutoresizingMaskIntoConstraints(false)
self.contentView.addConstraints(NSLayoutConstraint.constraintsWithVisualFormat("H:|[sv]|", options: nil, metrics: nil, views: ["sv" : scrollView]))
self.contentView.addConstraints(NSLayoutConstraint.constraintsWithVisualFormat("V:|[sv]|", options: nil, metrics: nil, views: ["sv" : scrollView]))
let scrollViewContent = UIView()
scrollView.addSubview(scrollViewContent)
// add content sequentially (L>>R) to the content subview of scrollView
var lastObjectAdded: UIView? = nil // allows constraints to be created between adjacent contentView subviews
// add button on the left
let leftButton = UIButton.buttonWithType(UIButtonType.Custom) as! UIButton
leftButton.backgroundColor = UIColor.purpleColor()
leftButton.setTitle("Click Me!", forState: UIControlState.Normal)
leftButton.setTitleColor(UIColor.whiteColor(), forState: UIControlState.Normal)
// width needs to be set explicitly so inset for scroll view can be set in this class
// (when using autolayout, frame dimensions are not available until after all subviews are laid out)
leftButton.frame.size.width = CGFloat(100)
scrollViewContent.addSubview(leftButton)
// size using autolayout
leftButton.setTranslatesAutoresizingMaskIntoConstraints(false)
// give button a fixed width
// TODO: Enable this to be defined by function call
scrollViewContent.addConstraints(NSLayoutConstraint.constraintsWithVisualFormat("H:[but(==100)]", options: nil, metrics: nil, views: ["but" : leftButton]))
// pin height to height of cell's contentView
self.contentView.addConstraints(NSLayoutConstraint.constraintsWithVisualFormat("V:[but(==cv)]", options: nil, metrics: nil, views: ["but" : leftButton, "cv" : self.contentView]))
// pin to top left of the scollView subview
scrollViewContent.addConstraints(NSLayoutConstraint.constraintsWithVisualFormat("V:|[but]", options: nil, metrics: nil, views: ["but" : leftButton]))
scrollViewContent.addConstraints(NSLayoutConstraint.constraintsWithVisualFormat("H:|[but]", options: nil, metrics: nil, views: ["but" : leftButton]))
// store a reference to the button to be used for positioning other subviews
lastObjectAdded = leftButton
testView = leftButton
< SNIP ---
--- SNIP>
// test that I can add a label that matches the width of the tableViewCell's contentView subview'
let specialLab = UILabel()
specialLab.setTranslatesAutoresizingMaskIntoConstraints(false)
specialLab.backgroundColor = UIColor.orangeColor()
specialLab.text = "WIDTH MATCHES TABLEVIEWCELL WIDTH"
scrollViewContent.addSubview(specialLab)
scrollViewContent.addConstraints(NSLayoutConstraint.constraintsWithVisualFormat("V:|-(10)-[lab]", options: nil, metrics: nil, views: ["lab" : specialLab]))
// this is the important constraint
self.contentView.addConstraints(NSLayoutConstraint.constraintsWithVisualFormat("H:[lab(==cv)]", options: nil, metrics: nil, views: ["lab" : specialLab, "cv" : contentView]))
scrollViewContent.addConstraints(NSLayoutConstraint.constraintsWithVisualFormat("H:[last]-(10)-[lab]", options: nil, metrics: nil, views: ["lab" : specialLab, "last" : lastObjectAdded!]))
lastObjectAdded = specialLab
// pin last label to right which dictates content size width
scrollViewContent.addConstraints(NSLayoutConstraint.constraintsWithVisualFormat("H:[lab]-10-|", options: nil, metrics: nil, views: ["lab" : lastObjectAdded!]))
// set scrollView's content view height and frame-to-superview constraints
// (width comes from subview constraints)
// content view is calculated for us
scrollViewContent.setTranslatesAutoresizingMaskIntoConstraints(false)
scrollView.addConstraints(NSLayoutConstraint.constraintsWithVisualFormat("H:|[svc]|", options: nil, metrics: nil, views: ["svc" : scrollViewContent]))
scrollView.addConstraints(NSLayoutConstraint.constraintsWithVisualFormat("V:|[svc]|", options: nil, metrics: nil, views: ["svc" : scrollViewContent]))
// create inset to hide button on left
scrollView.contentInset = UIEdgeInsetsMake(0, -leftButton.bounds.width, 0, 0)
The second bit of code relates to un-hiding the button on the left:
extension SwipeableViewCell: UIScrollViewDelegate {
func scrollViewWillEndDragging(scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>) {
let left = CGFloat(100.0) // this is the inset required to hide the left button(s)
let kSwipeableTableViewCellOpenVelocityThreshold = CGFloat(0.6) // minimum velocity req to reveal buttons
let kSwipeableTableViewCellMaxCloseMilliseconds = CGFloat(300) // max time for the button to be hidden
let x = scrollView.contentOffset.x
// check to see if swipe from left is far enough and fast enough to reveal button
if (left > 0 && (x < 0 || (x < left && velocity.x < -kSwipeableTableViewCellOpenVelocityThreshold))) {
// manually set the offset to reveal the whole button but no more
// targetContentOffset.memory = CGPointZero // this should work but doesn't - offset is not retained
dispatch_async(dispatch_get_main_queue()) {
scrollView.setContentOffset(CGPointZero, animated: true) // this works!
}
} else {
// if not, hide the button
dispatch_async(dispatch_get_main_queue()) {
scrollView.setContentOffset(CGPoint(x: CGFloat(100), y: CGFloat(0)), animated: true)
}
}
}
}