UIScrollView动画到targetContentOffset不稳定

时间:2015-09-30 15:21:41

标签: ios uitableview animation uiscrollview autolayout

我在UITableViewCell中实现了一个UIScrollView,使用户能够以与iOS Mail应用程序相同的方式向左和向右滚动以显示按钮。设置框架和位置的原始实现明确地运行良好,但我重构了代码以使用autolayout。动画隐藏/显示'容器'对于左侧的按钮(附件按钮)效果很好,但是当右侧容器(编辑按钮)在到达其最终位置之前到达所需偏移之前减慢时,动画会使滚动视图停止。

所有计算都使用刚刚转换的相同数学(例如+而不是 - 值,>而不是<在测试中),具体取决于容器所在的一侧以及记录显示的值是否正确。我无法看到任何明显的代码错误,并且对IB中设置的单元没有任何限制。这是一个错误还是有一些显而易见的东西我通过盯着最后一小时的代码而错过了?

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

1 个答案:

答案 0 :(得分:0)

确定 - 发现错误,这是早期UIScrollView实验中遗留下来的错误。我在之前关于“快照”的评论中提到了线索。发生在所需targetContentOffset的10pt内...

scrollContentView宽度约束设置不正确,如下所示:

scrollContentView.widthAnchor.constraintEqualToAnchor(contentView.widthAnchor, constant: 10).active = true

在我发现我可以通过设置其contentInset强制UIScrollView滚动之前,我只是将子视图放大到UIScrollView被固定到的单元格内容视图。由于我一直在重构代码以使用新的锚属性,旧的代码传播了,我得到了我的错误!

所以,不是iOS有问题......只是我不注意。学习到教训了!我现在有一些关于如何实现可能更整洁的事情的其他想法。