【发布时间】:2015-09-30 15:21:41
【问题描述】:
我在 UITableViewCell 中实现了 UIScrollView,它使用户能够以与 iOS 邮件应用程序相同的方式左右滚动以显示按钮。明确设置框架和位置的原始实现运行良好,但我重构了代码以在整个过程中使用自动布局。隐藏/显示左侧按钮(辅助按钮)的“容器”的动画效果很好,但是当右侧容器(编辑按钮)在达到所需偏移量之前减慢时,滚动视图停止的动画在进入最终状态之前位置。
所有计算都使用刚刚转换的相同数学(例如,+ 而不是 - 值,> 而不是
class SwipeyTableViewCell: UITableViewCell {
// MARK: Constants
private let thresholdVelocity = CGFloat(0.6)
private let maxClosureDuration = CGFloat(40)
// MARK: Properties
private var buttonContainers = [ButtonContainerType: ButtonContainer]()
private var leftContainerWidth: CGFloat {
return buttonContainers[.Accessory]?.containerWidthWhenOpen ?? CGFloat(0)
}
private var rightContainerWidth: CGFloat {
return buttonContainers[.Edit]?.containerWidthWhenOpen ?? CGFloat(0)
}
private var buttonContainerRightAnchor = NSLayoutConstraint()
private var isOpen = false
// MARK: Subviews
private let scrollView = UIScrollView()
// MARK: Lifecycle methods
override func awakeFromNib() {
super.awakeFromNib()
// Initialization code
scrollView.delegate = self
scrollView.showsHorizontalScrollIndicator = false
scrollView.showsVerticalScrollIndicator = false
contentView.addSubview(scrollView)
scrollView.translatesAutoresizingMaskIntoConstraints = false
scrollView.topAnchor.constraintEqualToAnchor(contentView.topAnchor).active = true
scrollView.leftAnchor.constraintEqualToAnchor(contentView.leftAnchor).active = true
scrollView.rightAnchor.constraintEqualToAnchor(contentView.rightAnchor).active = true
scrollView.bottomAnchor.constraintEqualToAnchor(contentView.bottomAnchor).active = true
let scrollContentView = UIView()
scrollContentView.backgroundColor = UIColor.cyanColor()
scrollView.addSubview(scrollContentView)
scrollContentView.translatesAutoresizingMaskIntoConstraints = false
scrollContentView.topAnchor.constraintEqualToAnchor(scrollView.topAnchor).active = true
scrollContentView.leftAnchor.constraintEqualToAnchor(scrollView.leftAnchor).active = true
scrollContentView.rightAnchor.constraintEqualToAnchor(scrollView.rightAnchor).active = true
scrollContentView.bottomAnchor.constraintEqualToAnchor(scrollView.bottomAnchor).active = true
scrollContentView.widthAnchor.constraintEqualToAnchor(contentView.widthAnchor, constant: 10).active = true
scrollContentView.heightAnchor.constraintEqualToAnchor(contentView.heightAnchor).active = true
buttonContainers[.Accessory] = ButtonContainer(type: .Accessory, scrollContentView: scrollContentView)
buttonContainers[.Edit] = ButtonContainer(type: .Edit, scrollContentView: scrollContentView)
for bc in buttonContainers.values {
scrollContentView.addSubview(bc)
bc.widthAnchor.constraintEqualToAnchor(contentView.widthAnchor).active = true
bc.heightAnchor.constraintEqualToAnchor(scrollContentView.heightAnchor).active = true
bc.topAnchor.constraintEqualToAnchor(scrollContentView.topAnchor).active = true
bc.containerToContentConstraint.active = true
}
scrollView.contentInset = UIEdgeInsetsMake(0, leftContainerWidth, 0, rightContainerWidth)
}
func closeContainer() {
scrollView.contentOffset.x = CGFloat(0)
}
}
extension SwipeyTableViewCell: UIScrollViewDelegate {
func scrollViewWillEndDragging(scrollView: UIScrollView, withVelocity velocity: CGPoint,
targetContentOffset: UnsafeMutablePointer<CGPoint>) {
let xOffset: CGFloat = scrollView.contentOffset.x
isOpen = false
for bc in buttonContainers.values {
if bc.isContainerOpen(xOffset, thresholdVelocity: thresholdVelocity, velocity: velocity) {
targetContentOffset.memory.x = bc.offsetRequiredToOpenContainer()
NSLog("Target offset \(targetContentOffset.memory.x)")
isOpen = true
break /// only one container can be open at a time so cn exit here
}
}
if !isOpen {
NSLog("Closing container")
targetContentOffset.memory.x = CGFloat(0)
let ms: CGFloat = xOffset / velocity.x /// if the scroll isn't on a fast path to zero, animate it closed
if (velocity.x == 0 || ms < 0 || ms > maxClosureDuration) {
NSLog("Animating closed")
dispatch_async(dispatch_get_main_queue()) {
scrollView.setContentOffset(CGPointZero, animated: true)
}
}
}
}
/**
Defines the position of the container view for buttons assosicated with a SwipeyTableViewCell
- Edit: Identifier for a UIView that acts as a container for buttons to the right of the cell
- Accessory: Identifier for a UIView that acts as a container for buttons to the left of the vell
*/
enum ButtonContainerType {
case Edit, Accessory
}
extension ButtonContainerType {
func getConstraints(scrollContentView: UIView, buttonContainer: UIView) -> NSLayoutConstraint {
switch self {
case Edit:
return buttonContainer.leftAnchor.constraintEqualToAnchor(scrollContentView.rightAnchor)
case Accessory:
return buttonContainer.rightAnchor.constraintGreaterThanOrEqualToAnchor(scrollContentView.leftAnchor)
}
}
func containerOpenedTest() -> ((scrollViewOffset: CGFloat, containerFullyOpenWidth: CGFloat, thresholdVelocity: CGFloat, velocity: CGPoint) -> Bool) {
switch self {
case Edit:
return {(scrollViewOffset: CGFloat, containerFullyOpenWidth: CGFloat, thresholdVelocity: CGFloat, velocity: CGPoint) -> Bool in
(scrollViewOffset > containerFullyOpenWidth || (scrollViewOffset > 0 && velocity.x > thresholdVelocity))
}
case Accessory:
return {(scrollViewOffset: CGFloat, containerFullyOpenWidth: CGFloat, thresholdVelocity: CGFloat, velocity: CGPoint) -> Bool in
(scrollViewOffset < -containerFullyOpenWidth || (scrollViewOffset < 0 && velocity.x < -thresholdVelocity))
}
}
}
func transformOffsetForContainerSide(containerWidthWhenOpen: CGFloat) -> CGFloat {
switch self {
case Edit:
return containerWidthWhenOpen
case Accessory:
return -containerWidthWhenOpen
}
}
}
/// A UIView subclass that acts as a container for buttongs associated with a SwipeyTableCellView
class ButtonContainer: UIView {
private let scrollContentView: UIView
private let type: ButtonContainerType
private let maxNumberOfButtons = 3
let buttonWidth = CGFloat(65)
private var buttons = [UIButton]()
var containerWidthWhenOpen: CGFloat {
// return CGFloat(buttons.count) * buttonWidth
return buttonWidth // TODO: Multiple buttons not yet implements - this will cause a bug!!
}
var containerToContentConstraint: NSLayoutConstraint {
return type.getConstraints(scrollContentView, buttonContainer: self)
}
var offsetFromContainer = CGFloat(0) {
didSet {
let delta = abs(oldValue - offsetFromContainer)
containerToContentConstraint.constant = offsetFromContainer
if delta > (containerWidthWhenOpen * 0.5) { /// this number is arbitary - can it be more formal?
animateConstraintWithDuration(0.1, delay: 0, options: UIViewAnimationOptions.CurveEaseOut, completion: nil) /// ensure large changes are animated rather than snapped
}
}
}
// MARK: Initialisers
init(type: ButtonContainerType, scrollContentView: UIView) {
self.type = type
self.scrollContentView = scrollContentView
super.init(frame: CGRectZero)
backgroundColor = UIColor.blueColor()
translatesAutoresizingMaskIntoConstraints = false
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
// MARK: Public methods
func isContainerOpen(scrollViewOffset: CGFloat, thresholdVelocity: CGFloat, velocity: CGPoint) -> Bool {
let closure = type.containerOpenedTest()
return closure(scrollViewOffset: scrollViewOffset, containerFullyOpenWidth: containerWidthWhenOpen, thresholdVelocity: thresholdVelocity, velocity: velocity)
}
func offsetRequiredToOpenContainer() -> CGFloat {
return type.transformOffsetForContainerSide(containerWidthWhenOpen)
}
}
【问题讨论】:
-
更新 - 只有在 contentOffset 比 targetContentOffset 大 10pt 或更多时,才会显示到最终偏移的“捕捉”(而不是平滑减速)。低于 10pt 动画很流畅。我会继续寻找,看看是否有一些虚假的添加发生在某个地方,但看起来这可能是 iOS 中的一个错误
标签: ios uitableview animation uiscrollview autolayout