Search code examples
iosswiftuiviewuikitclipstobounds

Is it possible to unclip just a single subview from its superview's bounds?


I have a situation where I want a single subview to not be clipped to the superview's bounds, while the rest of the superview's subviews are clipped to its bounds. Is there a way to exclude a certain UIView from being clipped to its superview's bounds. Similarly, can a UIView specify which subviews it does/doesn't want to clip to its own bounds?

As far as code goes, I am using Koloda to create UIViews that are similar to Tinder's swiping cards. I have a collection view whose cells are essentially Koloda views. Whenever a card starts panning, I set its cell's collection view's clipsToBounds property to false. This allows the cell to be panned above the navigation bar without being clipped. When the pan ends, the view's cell's collection view's clipsToBounds property is set back to true.

import UIKit
import Koloda

class LikedUserCollectionViewCell: UICollectionViewCell {
    @IBOutlet var swipeView: KolodaView!
    var parentVC: MyViewController!
}

extension LikedUserCollectionViewCell: KolodaViewDelegate, KolodaViewDataSource {
    func koloda(_ koloda: KolodaView, viewForCardAt index: Int) -> UIView {
        //return a KolodaView, this code isn't important to the issue
        return KolodaView()
    }

    func kolodaNumberOfCards(_ koloda: KolodaView) -> Int {
        return 1
    }

    func kolodaPanBegan(_ koloda: KolodaView, card: DraggableCardView) {
        self.parentVC.collectionView.bringSubviewToFront(self)
        //Here, instead of unclipping all of the collection view's subviews, I would like to only unclip the one containing the subview that is being panned
        self.parentVC.collectionView.clipsToBounds = false
    }

    func kolodaPanFinished(_ koloda: KolodaView, card: DraggableCardView) {
        //Once the pan is finished, all of the collection view's subviews can be clipped again
        self.parentVC.collectionView.clipsToBounds = true
    }

    func koloda(_ koloda: KolodaView, didSwipeCardAt index: Int, in direction: SwipeResultDirection) {
        //Not relevant to the question
    }
}

The problem is, when there are cells that are partially scrolled off the collection view, but not yet removed from the collection view as they have not completely scrolled off the collection view, they now become visible as the collection view's clipsToBounds is now set to true. This is why I want to be able to dictate which of the collection view's subviews are clipped to its bounds. This behavior can be seen in the below gif:

enter image description here

So, in summary, is there a way to, instead of unclipping all subviews from their superview's bounds via superview.clipsToBounds = false, I could only unclip a single subview from its superview's bounds?


Solution

  • The simple solution here is that since you want to make an exception for a single view, don't move the collectionView cell. take a snapshotView and move the snapshot instead; it can be anywhere else in the hierarchy and does not need to be a child of the collection view. You can use UIView.convert(from:) to convert the cells coordinates into the appropriate parent view's coordinate space to properly place your snapshot.

    Edit from OP:

    I was able to solve my issue by taking Josh's advice and updating my code to the following:

    import UIKit
    import Koloda
    
    class LikedUserCollectionViewCell: UICollectionViewCell {
        @IBOutlet var swipeView: KolodaView!
        var parentVC: MyViewController!
        //DraggableCardView is a custom Koloda class
        var draggableCardView: DraggableCardView!
    }
    
    extension LikedUserCollectionViewCell: KolodaViewDelegate, KolodaViewDataSource {
        func koloda(_ koloda: KolodaView, viewForCardAt index: Int) -> UIView {
            //return a KolodaView, this code isn't important to the issue
            return KolodaView()
        }
    
        func kolodaNumberOfCards(_ koloda: KolodaView) -> Int {
            return 1
        }
    
        func kolodaPanBegan(_ koloda: KolodaView, card: DraggableCardView) {
            let snapshotView = self.swipeView.snapshotView(afterScreenUpdates: false)!
            snapshotView.accessibilityIdentifier = "snapshotView"
            self.parentVC.view.addSubview(snapshotView)
            snapshotView.frame.origin = self.parentVC.view.convert(swipeView.viewForCard(at: 0)!.frame.origin, from: swipeView)
            draggableCardView = card
            swipeView.viewForCard(at: 0)?.isHidden = true
        }
    
        func koloda(_ koloda: KolodaView, draggedCardWithPercentage finishPercentage: CGFloat, in direction: SwipeResultDirection) {
            let snapshotView = self.parentVC.view.subviews.first(where: { $0.accessibilityIdentifier == "snapshotView" })!
            snapshotView.frame.origin = self.parentVC.view.convert(draggableCardView.frame.origin, from: swipeView)
        }
    
        func kolodaPanFinished(_ koloda: KolodaView, card: DraggableCardView) {
            let snapshotView = self.parentVC.view.subviews.first(where: { $0.accessibilityIdentifier == "snapshotView" })!
            draggableCardView = nil
            snapshotView.removeFromSuperview()
            swipeView.viewForCard(at: 0)?.isHidden = false
        }
    
        func koloda(_ koloda: KolodaView, didSwipeCardAt index: Int, in direction: SwipeResultDirection) {
            //Not relevant to the question
        }
    }