【问题标题】:iOS card swipe animation & scroll viewiOS 刷卡动画和滚动视图
【发布时间】:2017-04-12 07:11:03
【问题描述】:

我正在实现一个 iPhone 应用程序,在该应用程序中我正在尝试构建结合卡片刷卡(与 Tinder 相同)和滚动效果的功能。请查看以下详细信息。

当用户打开应用程序时,它会显示其他用户配置文件。因此,登录用户可以向左(不)或向右(喜欢)刷卡,该卡将从列表中删除,并出现下一张用户卡。现在,如果用户不想刷卡,那么他可以向上或向下滚动以查看其他用户的个人资料。

那么,是否可以实现刷卡和滚动功能的结合。

【问题讨论】:

    标签: ios iphone


    【解决方案1】:

    我知道这个问题现在有点老了,但是是的,这正是我过去几个月一直在构建的。

    我基本上将UIPanGestureRecognizer 添加到具有自定义流程布局的UICollectionView(用于卡片分页等)。

    这就是它的样子:

    主要组件逻辑:

    import Foundation
    
    /**
     The VerticalCardSwiper is a subclass of `UIView` that has a `VerticalCardSwiperView` embedded.
    
     To use this, you need to implement the `VerticalCardSwiperDatasource`.
    
     If you want to handle actions like cards being swiped away, implement the `VerticalCardSwiperDelegate`.
     */
    public class VerticalCardSwiper: UIView {
    
        /// The collectionView where all the magic happens.
        public var verticalCardSwiperView: VerticalCardSwiperView!
    
        /// Indicates if side swiping on cards is enabled. Default value is `true`.
        @IBInspectable public var isSideSwipingEnabled: Bool = true
        /// The inset (spacing) at the top for the cards. Default is 40.
        @IBInspectable public var topInset: CGFloat = 40 {
            didSet {
                setCardSwiperInsets()
            }
        }
        /// The inset (spacing) at each side of the cards. Default is 20.
        @IBInspectable public var sideInset: CGFloat = 20 {
            didSet {
                setCardSwiperInsets()
            }
        }
        /// Sets how much of the next card should be visible. Default is 50.
        @IBInspectable public var visibleNextCardHeight: CGFloat = 50 {
            didSet {
                setCardSwiperInsets()
            }
        }
        /// Vertical spacing between CardCells. Default is 40.
        @IBInspectable public var cardSpacing: CGFloat = 40 {
            willSet {
                flowLayout.minimumLineSpacing = newValue
            }
        }
    
        /// The transform animation that is shown on the top card when scrolling through the cards. Default is 0.05.
        @IBInspectable public var firstItemTransform: CGFloat = 0.05 {
            willSet {
                flowLayout.firstItemTransform = newValue
            }
        }
    
        public weak var delegate: VerticalCardSwiperDelegate?
        public weak var datasource: VerticalCardSwiperDatasource? {
            didSet{
                numberOfCards = datasource?.numberOfCards(verticalCardSwiperView: self.verticalCardSwiperView) ?? 0
            }
        }
    
        /// The amount of cards in the collectionView.
        fileprivate var numberOfCards: Int = 0
        /// We use this horizontalPangestureRecognizer for the vertical panning.
        fileprivate var horizontalPangestureRecognizer: UIPanGestureRecognizer!
        /// Stores a `CGRect` with the area that is swipeable to the user.
        fileprivate var swipeAbleArea: CGRect!
        /// The `CardCell` that the user can (and is) moving.
        fileprivate var swipedCard: CardCell! {
            didSet {
                setupCardSwipeDelegate()
            }
        }
    
        /// The flowlayout used in the collectionView.
        fileprivate lazy var flowLayout: VerticalCardSwiperFlowLayout = {
            let flowLayout = VerticalCardSwiperFlowLayout()
            flowLayout.firstItemTransform = firstItemTransform
            flowLayout.minimumLineSpacing = cardSpacing
            flowLayout.isPagingEnabled = true
            return flowLayout
        }()
    
        public override init(frame: CGRect) {
            super.init(frame: frame)
    
            commonInit()
        }
    
        public required init?(coder aDecoder: NSCoder) {
            super.init(coder: aDecoder)
    
            commonInit()
        }
    
        fileprivate func commonInit() {
    
            setupVerticalCardSwiperView()
            setupConstraints()
            setCardSwiperInsets()
            setupGestureRecognizer()
        }
    }
    
    extension VerticalCardSwiper: CardDelegate {
    
        internal func willSwipeAway(cell: CardCell, swipeDirection: SwipeDirection) {
    
            verticalCardSwiperView.isUserInteractionEnabled = false
    
            if let index = verticalCardSwiperView.indexPath(for: cell)?.row {
                self.delegate?.willSwipeCardAway?(card: cell, index: index, swipeDirection: swipeDirection)
            }
        }
    
        internal func didSwipeAway(cell: CardCell, swipeDirection direction: SwipeDirection) {
    
            if let indexPathToRemove = verticalCardSwiperView.indexPath(for: cell){
    
                self.numberOfCards -= 1
                swipedCard = nil
    
                self.verticalCardSwiperView.performBatchUpdates({
                    self.verticalCardSwiperView.deleteItems(at: [indexPathToRemove])
                }) { [weak self] (finished) in
                    if finished {
                        self?.verticalCardSwiperView.collectionViewLayout.invalidateLayout()
                        self?.verticalCardSwiperView.isUserInteractionEnabled = true
                        self?.delegate?.didSwipeCardAway?(card: cell, index: indexPathToRemove.row ,swipeDirection: direction)
                    }
                }
            }
        }
    
        internal func didDragCard(cell: CardCell, swipeDirection: SwipeDirection) {
    
            if let index = verticalCardSwiperView.indexPath(for: cell)?.row {
                self.delegate?.didDragCard?(card: cell, index: index, swipeDirection: swipeDirection)
            }
        }
    
        fileprivate func setupCardSwipeDelegate() {
            swipedCard?.delegate = self
        }
    }
    
    extension VerticalCardSwiper: UIGestureRecognizerDelegate {
    
        public func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
    
            if let panGestureRec = gestureRecognizer as? UIPanGestureRecognizer {
    
                // When a horizontal pan is detected, we make sure to disable the collectionView.panGestureRecognizer so that it doesn't interfere with the sideswipe.
                if panGestureRec == horizontalPangestureRecognizer, panGestureRec.direction!.isX {
                    return false
                }
            }
            return true
        }
    
        /// We set up the `horizontalPangestureRecognizer` and attach it to the `collectionView`.
        fileprivate func setupGestureRecognizer(){
    
            horizontalPangestureRecognizer = UIPanGestureRecognizer(target: self, action: #selector(handlePan))
            horizontalPangestureRecognizer.maximumNumberOfTouches = 1
            horizontalPangestureRecognizer.delegate = self
            verticalCardSwiperView.addGestureRecognizer(horizontalPangestureRecognizer)
            verticalCardSwiperView.panGestureRecognizer.maximumNumberOfTouches = 1
        }
    
        /**
         This function is called when a pan is detected inside the `collectionView`.
         We also take care of detecting if the pan gesture is inside the `swipeAbleArea` and we animate the cell if necessary.
         - parameter sender: The `UIPanGestureRecognizer` that detects the pan gesture. In this case `horizontalPangestureRecognizer`.
         */
        @objc fileprivate func handlePan(sender: UIPanGestureRecognizer){
    
            guard isSideSwipingEnabled else {
                return
            }
    
            /// The taplocation relative to the superview.
            let location = sender.location(in: self)
            /// The taplocation relative to the collectionView.
            let locationInCollectionView = sender.location(in: verticalCardSwiperView)
            /// The translation of the finger performing the PanGesture.
            let translation = sender.translation(in: self)
    
            if swipeAbleArea.contains(location) && !verticalCardSwiperView.isScrolling {
                if let swipedCardIndex = verticalCardSwiperView.indexPathForItem(at: locationInCollectionView) {
                    /// The card that is swipeable inside the SwipeAbleArea.
                    swipedCard = verticalCardSwiperView.cellForItem(at: swipedCardIndex) as? CardCell
                }
            }
    
            if swipedCard != nil && !verticalCardSwiperView.isScrolling {
    
                /// The angle we pass for the swipe animation.
                let maximumRotation: CGFloat = 1.0
                let rotationStrength = min(translation.x/swipedCard.frame.width, maximumRotation)
                let angle = (CGFloat.pi/10.0) * rotationStrength
    
                switch (sender.state) {
    
                case .began:
                    break
    
                case .changed:
                    swipedCard.animateCard(angle: angle, horizontalTranslation: translation.x)
                    break
    
                case .ended:
                    swipedCard.endedPanAnimation(angle: angle)
                    swipedCard = nil
                    break
    
                default:
                    swipedCard.resetToCenterPosition()
                    swipedCard = nil
                }
            }
        }
    }
    
    extension VerticalCardSwiper: UICollectionViewDelegate, UICollectionViewDataSource {
    
        /**
         Reloads all of the data for the VerticalCardSwiperView.
    
         Call this method sparingly when you need to reload all of the items in the VerticalCardSwiper. This causes the VerticalCardSwiperView to discard any currently visible items (including placeholders) and recreate items based on the current state of the data source object. For efficiency, the VerticalCardSwiperView only displays those cells and supplementary views that are visible. If the data shrinks as a result of the reload, the VerticalCardSwiperView adjusts its scrolling offsets accordingly.
        */
        public func reloadData(){
            verticalCardSwiperView.reloadData()
        }
    
        /**
         Register a class for use in creating new CardCells.
         Prior to calling the dequeueReusableCell(withReuseIdentifier:for:) method of the collection view,
         you must use this method or the register(_:forCellWithReuseIdentifier:) method
         to tell the collection view how to create a new cell of the given type.
         If a cell of the specified type is not currently in a reuse queue,
         the VerticalCardSwiper uses the provided information to create a new cell object automatically.
         If you previously registered a class or nib file with the same reuse identifier,
         the class you specify in the cellClass parameter replaces the old entry.
         You may specify nil for cellClass if you want to unregister the class from the specified reuse identifier.
         - parameter cellClass: The class of a cell that you want to use in the VerticalCardSwiper
         identifier
         - parameter identifier: The reuse identifier to associate with the specified class. This parameter must not be nil and must not be an empty string.
         */
        public func register(_ cellClass: AnyClass?, forCellWithReuseIdentifier identifier: String) {
            verticalCardSwiperView.register(cellClass, forCellWithReuseIdentifier: identifier)
        }
    
        /**
         Register a nib file for use in creating new collection view cells.
         Prior to calling the dequeueReusableCell(withReuseIdentifier:for:) method of the collection view,
         you must use this method or the register(_:forCellWithReuseIdentifier:) method
         to tell the collection view how to create a new cell of the given type.
         If a cell of the specified type is not currently in a reuse queue,
         the collection view uses the provided information to create a new cell object automatically.
         If you previously registered a class or nib file with the same reuse identifier,
         the object you specify in the nib parameter replaces the old entry.
         You may specify nil for nib if you want to unregister the nib file from the specified reuse identifier.
         - parameter nib: The nib object containing the cell object. The nib file must contain only one top-level object and that object must be of the type UICollectionViewCell.
         identifier
         - parameter identifier: The reuse identifier to associate with the specified nib file. This parameter must not be nil and must not be an empty string.
        */
        public func register(nib: UINib?, forCellWithReuseIdentifier identifier: String) {
            verticalCardSwiperView.register(nib, forCellWithReuseIdentifier: identifier)
        }
    
        public func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
    
            return self.numberOfCards
        }
    
        public func numberOfSections(in collectionView: UICollectionView) -> Int {
    
            return 1
        }
    
        public func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
    
            return (datasource?.cardForItemAt(verticalCardSwiperView: verticalCardSwiperView, cardForItemAt: indexPath.row))!
        }
    
        public func scrollViewDidScroll(_ scrollView: UIScrollView) {
            delegate?.didScroll?(verticalCardSwiperView: verticalCardSwiperView)
        }
    
        fileprivate func setupVerticalCardSwiperView(){
    
            verticalCardSwiperView = VerticalCardSwiperView(frame: self.frame, collectionViewLayout: flowLayout)
            verticalCardSwiperView.decelerationRate = UIScrollViewDecelerationRateFast
            verticalCardSwiperView.backgroundColor = UIColor.clear
            verticalCardSwiperView.showsVerticalScrollIndicator = false
            verticalCardSwiperView.delegate = self
            verticalCardSwiperView.dataSource = self
    
            self.numberOfCards = datasource?.numberOfCards(verticalCardSwiperView: verticalCardSwiperView) ?? 0
    
            self.addSubview(verticalCardSwiperView)
        }
    
        fileprivate func setupConstraints(){
    
            verticalCardSwiperView.translatesAutoresizingMaskIntoConstraints = false
    
            NSLayoutConstraint.activate([
                verticalCardSwiperView.leadingAnchor.constraint(equalTo: self.leadingAnchor),
                verticalCardSwiperView.trailingAnchor.constraint(equalTo: self.trailingAnchor),
                verticalCardSwiperView.topAnchor.constraint(equalTo: self.topAnchor),
                verticalCardSwiperView.bottomAnchor.constraint(equalTo: self.bottomAnchor)
                ])
        }
    
        fileprivate func setCardSwiperInsets(){
    
            verticalCardSwiperView.contentInset = UIEdgeInsets(top: topInset, left: sideInset, bottom: topInset + flowLayout.minimumLineSpacing + visibleNextCardHeight, right: sideInset)
        }
    }
    
    extension VerticalCardSwiper: UICollectionViewDelegateFlowLayout {
    
        public func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
    
            let itemSize = calculateItemSize(for: indexPath.row)
    
            // set cellHeight in the custom flowlayout, we use this for paging calculations.
            flowLayout.cellHeight = itemSize.height
    
            if swipeAbleArea == nil {
                // Calculate and set the swipeAbleArea. We use this to determine wheter the cell can be swiped to the sides or not.
                let swipeAbleAreaOriginY = collectionView.frame.origin.y + collectionView.contentInset.top
                swipeAbleArea = CGRect(x: 0, y: swipeAbleAreaOriginY, width: self.frame.width, height: itemSize.height)
            }
            return itemSize
        }
    
        fileprivate func calculateItemSize(for index: Int) -> CGSize {
    
            let cellWidth: CGFloat!
            let cellHeight: CGFloat!
            let xInsets = sideInset * 2
            let yInsets = cardSpacing + visibleNextCardHeight + topInset
    
            // get size from delegate if the sizeForItem function is called.
            if let customSize = delegate?.sizeForItem?(verticalCardSwiperView: verticalCardSwiperView, index: index) {
                // set custom sizes and make sure sizes are not negative, if they are, don't subtract the insets.
                cellWidth = customSize.width - (customSize.width - xInsets > 0 ? xInsets : 0)
                cellHeight = customSize.height - (customSize.height - yInsets > 0 ? yInsets : 0)
            } else {
                cellWidth = verticalCardSwiperView.frame.size.width - xInsets
                cellHeight = verticalCardSwiperView.frame.size.height - yInsets
            }
            return CGSize(width: cellWidth, height: cellHeight)
        }
    }
    

    自定义流布局:

    import UIKit
    
    /// Custom `UICollectionViewFlowLayout` that provides the flowlayout information like paging and `CardCell` movements.
    internal class VerticalCardSwiperFlowLayout: UICollectionViewFlowLayout {
    
        /// This property sets the amount of scaling for the first item.
        internal var firstItemTransform: CGFloat?
        /// This property enables paging per card. The default value is true.
        internal var isPagingEnabled: Bool = true
        /// Stores the height of a CardCell.
        internal var cellHeight: CGFloat!
    
        internal override func prepare() {
            super.prepare()
    
            assert(collectionView!.numberOfSections == 1, "Number of sections should always be 1.")
            assert(collectionView!.isPagingEnabled == false, "Paging on the collectionview itself should never be enabled. To enable cell paging, use the isPagingEnabled property of the VerticalCardSwiperFlowLayout instead.")
        }
    
        internal override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
            let items = NSArray (array: super.layoutAttributesForElements(in: rect)!, copyItems: true)
    
            items.enumerateObjects(using: { (object, index, stop) -> Void in
                let attributes = object as! UICollectionViewLayoutAttributes
    
                self.updateCellAttributes(attributes)
            })
            return items as? [UICollectionViewLayoutAttributes]
        }
    
        // We invalidate the layout when a "bounds change" happens, for example when we scale the top cell. This forces a layout update on the flowlayout.
        internal override func shouldInvalidateLayout(forBoundsChange newBounds: CGRect) -> Bool {
            return true
        }
    
        // Cell paging
        internal override func targetContentOffset(forProposedContentOffset proposedContentOffset: CGPoint, withScrollingVelocity velocity: CGPoint) -> CGPoint {
    
            // If the property `isPagingEnabled` is set to false, we don't enable paging and thus return the current contentoffset.
            guard isPagingEnabled else {
                let latestOffset = super.targetContentOffset(forProposedContentOffset: proposedContentOffset, withScrollingVelocity: velocity)
                return latestOffset
            }
    
            // Page height used for estimating and calculating paging.
            let pageHeight = cellHeight + self.minimumLineSpacing
    
            // Make an estimation of the current page position.
            let approximatePage = self.collectionView!.contentOffset.y/pageHeight
    
            // Determine the current page based on velocity.
            let currentPage = (velocity.y < 0.0) ? floor(approximatePage) : ceil(approximatePage)
    
            // Create custom flickVelocity.
            let flickVelocity = velocity.y * 0.3
    
            // Check how many pages the user flicked, if <= 1 then flickedPages should return 0.
            let flickedPages = (abs(round(flickVelocity)) <= 1) ? 0 : round(flickVelocity)
    
            // Calculate newVerticalOffset.
            let newVerticalOffset = ((currentPage + flickedPages) * pageHeight) - self.collectionView!.contentInset.top
    
            return CGPoint(x: proposedContentOffset.x, y: newVerticalOffset)
        }
    
        internal override func finalLayoutAttributesForDisappearingItem(at itemIndexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
    
            // make sure the zIndex of the next card is higher than the one we're swiping away.
            let nextIndexPath = IndexPath(row: itemIndexPath.row + 1, section: itemIndexPath.section)
            let nextAttr = self.layoutAttributesForItem(at: nextIndexPath)
            nextAttr?.zIndex = nextIndexPath.row
    
            // attributes for swiping card away
            let attr = self.layoutAttributesForItem(at: itemIndexPath)
    
            return attr
        }
    
        /**
         Updates the attributes.
         Here manipulate the zIndex of the cards here, calculate the positions and do the animations.
         - parameter attributes: The attributes we're updating.
         */
        fileprivate func updateCellAttributes(_ attributes: UICollectionViewLayoutAttributes) {
            let minY = collectionView!.bounds.minY + collectionView!.contentInset.top
            let maxY = attributes.frame.origin.y
    
            let finalY = max(minY, maxY)
            var origin = attributes.frame.origin
            let deltaY = (finalY - origin.y) / attributes.frame.height
    
            if let itemTransform = firstItemTransform {
                let scale = 1 - deltaY * itemTransform
                attributes.transform = CGAffineTransform(scaleX: scale, y: scale)
                // TODO: add card stack effect (like Shazam)
            }
            origin.y = finalY
            attributes.frame = CGRect(origin: origin, size: attributes.frame.size)
            attributes.zIndex = attributes.indexPath.row
        }
    }
    

    CardCell 代码(UICollectionViewCell 的子类):

    import UIKit
    
    /**
     The CardCell that the user can swipe away. Based on `UICollectionViewCell`.
    
     The cells will be recycled by the `VerticalCardSwiper`,
     so don't forget to override `prepareForReuse` when needed.
     */
    @objc open class CardCell: UICollectionViewCell {
    
        internal weak var delegate: CardDelegate?
    
        open override func layoutSubviews() {
    
            self.layer.shouldRasterize = true
            self.layer.rasterizationScale = UIScreen.main.scale
    
            super.layoutSubviews()
        }
    
        open override func apply(_ layoutAttributes: UICollectionViewLayoutAttributes) {
            super.apply(layoutAttributes)
    
            self.layer.zPosition = CGFloat(layoutAttributes.zIndex)
        }
    
        open override func prepareForReuse() {
            super.prepareForReuse()
    
            // need to unhide a cell for reuse (cell is hidden when swiped away)
            self.isHidden = false
        }
    
        /**
         This function animates the card. The animation consists of a rotation and translation.
         - parameter angle: The angle the card rotates while animating.
         - parameter horizontalTranslation: The horizontal translation the card animates in.
         */
        public func animateCard(angle: CGFloat, horizontalTranslation: CGFloat){
    
            delegate?.didDragCard(cell: self, swipeDirection: determineCardSwipeDirection())
    
            var transform = CATransform3DIdentity
            transform = CATransform3DRotate(transform, angle, 0, 0, 1)
            transform = CATransform3DTranslate(transform, horizontalTranslation, 0, 1)
    
            self.layer.transform = transform
        }
    
        /**
         Resets the CardCell back to the center of the VerticalCardSwiperView.
         */
        public func resetToCenterPosition(){
    
            let cardCenterX = self.frame.midX
            let centerX = self.bounds.midX
            let initialSpringVelocity = fabs(cardCenterX - centerX)/100
    
            UIView.animate(withDuration: 0.5,
                           delay: 0,
                           usingSpringWithDamping: 0.6,
                           initialSpringVelocity: initialSpringVelocity,
                           options: .allowUserInteraction,
                           animations: { [weak self] in
                            self?.layer.transform = CATransform3DIdentity
            })
        }
    
        /**
         Called when the pan gesture is ended.
         Handles what happens when the user stops swiping a card.
         If a certain treshold of the screen is swiped, the `animateOffScreen` function is called,
         if the threshold is not reached, the card will be reset to the center by calling `resetToCenterPosition`.
         - parameter angle: The angle of the animation, depends on the direction of the swipe.
         */
        internal func endedPanAnimation(angle: CGFloat){
    
            let swipePercentageMargin = self.bounds.width * 0.4
            let cardCenterX = self.frame.midX
            let centerX = self.bounds.midX
    
            // check for left or right swipe and if swipePercentageMargin is reached or not
            if (cardCenterX < centerX - swipePercentageMargin || cardCenterX > centerX + swipePercentageMargin){
                animateOffScreen(angle: angle)
            } else {
                self.resetToCenterPosition()
            }
        }
    
        /**
         Animates to card off the screen and calls the `willSwipeAway` and `didSwipeAway` functions from the `CardDelegate`.
         - parameter angle: The angle that the card will rotate in (depends on direction). Positive means the card is swiped to the right, a negative angle means the card is swiped to the left.
         */
        fileprivate func animateOffScreen(angle: CGFloat){
    
            var transform = CATransform3DIdentity
            let direction = determineCardSwipeDirection()
    
            transform = CATransform3DRotate(transform, angle, 0, 0, 1)
    
            switch direction {
    
            case .Left:
    
                transform = CATransform3DTranslate(transform, -(self.frame.width * 2), 0, 1)
                break
    
            case .Right:
    
                transform = CATransform3DTranslate(transform, (self.frame.width * 2), 0, 1)
                break
    
            default: break
    
            }
    
            self.delegate?.willSwipeAway(cell: self, swipeDirection: direction)
    
            UIView.animate(withDuration: 0.2, animations: { [weak self] in
                self?.layer.transform = transform
            }){ (completed) in
                self.isHidden = true
                self.delegate?.didSwipeAway(cell: self, swipeDirection: direction)
            }
        }
    
        fileprivate func determineCardSwipeDirection() -> SwipeDirection {
    
            let cardCenterX = self.frame.midX
            let centerX = self.bounds.midX
    
            if cardCenterX < centerX {
                return .Left
            } else if cardCenterX > centerX {
                return .Right
            } else {
                return .None
            }
        }
    }
    

    你可以在 github 上找到它: VerticalCardSwiper

    如果您想要此答案中的更多代码,请告诉我,然后我会添加它,但它很多,因此访问 repo 可能更容易。

    【讨论】:

    • 太棒了兄弟???
    【解决方案2】:

    您需要实现滑动手势和滚动视图。在少数情况下可能会很棘手,设置以下条件会有所帮助

    scrollView.panGestureRecognizer.requireGestureRecognizerToFail(< UISwipeGestureRecognizer instance>)
    

    为了更好地理解,请参考:

    how-to-recognize-swipe-gesture-in-uiscrollview

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2015-05-27
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2021-11-06
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多