1

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?

David Chopin
  • 2,780
  • 2
  • 19
  • 40

2 Answers2

1

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

David Chopin
  • 2,780
  • 2
  • 19
  • 40
Josh Homann
  • 15,933
  • 3
  • 30
  • 33
0

Looking at your interface, why is the collectionView above the (navigation bar)?

You shouldn't need any clipping in this situation, as long as the collectionView is behind the navigation bar.

Either call sendSubview(toBack: collectionView) or set the collectionView.layer.zPosition to a larger value.

  • I have it above the navigation bar as I want the cells to be able to be above the navigation bar when dragging them. – David Chopin Nov 08 '19 at 00:22
  • In that case I'd say it's best to have everything in the same superview stack without having to really hack away. Add everything as a subview to a UINavigationController's view and control the layer hierarchy from one place. – Samuel Huang Nov 08 '19 at 00:26