'Clip to bounds a particular view within a subview

Problem

I have a custom UIView that has an image and selection (border) subview. I want to be able to add this custom UIView as a subview of a larger blank view. Here's the catch, the larger blank view needs to clip all of the subviews to its bounds (clipToBounds). However, the user can select one of the custom UIViews within the large blank view, where the subview is then highlighted by a border.

The problem is that because the large blank view clips to bounds, the outline for the selected subview is cut off.

I want the image in the subview to clip to the bounds of the large blank view, but still be able to see the full selection outline of the subview (which is cut off due to the large blank view's corner radius.

I am using UIKit and Swift

👎 What I Currently Have: image

👍 What I Want: image

The image part of the subview clips to the bounds (corner radius) of the large blank view, but the outline selection view in the subview should not.

Thanks in advance for all your help!



Solution 1:[1]

I think what you are looking for is not technically possible as defined by the docs

From the docs:

clipsToBounds

Setting this value to true causes subviews to be clipped to the bounds of the receiver. If set to false, subviews whose frames extend beyond the visible bounds of the receiver are not clipped. The default value is false.

So the subviews do not have control of whether they get clipped or not, it's the container view that decides.

So I believe Matic's answer is right in that the structure he proposes gives you the most flexibility.

With that being said, here are a couple of work arounds I can think of:

First, set up to recreated your scenario

Custom UIView

// Simple custom UIView with image view and selection UIView
fileprivate class CustomBorderView: UIView
{
    private var isSelected = false
    {
        willSet
        {
            toggleBorder(newValue)
        }
    }
    
    var imageView = UIImageView()
    var selectionView = UIView()
    
    init()
    {
        super.init(frame: CGRect.zero)
        configureImageView()
        configureSelectionView()
    }
    
    required init?(coder: NSCoder)
    {
        fatalError("init(coder:) has not been implemented")
    }
    
    override func layoutSubviews()
    {
        super.layoutSubviews()
    }
    
    private func configureImageView()
    {
        imageView.image = UIImage(named: "image-test")
        imageView.contentMode = .scaleAspectFill
        addSubview(imageView)
        
        imageView.translatesAutoresizingMaskIntoConstraints = false
        imageView.leadingAnchor.constraint(equalTo: leadingAnchor).isActive = true
        imageView.topAnchor.constraint(equalTo: topAnchor).isActive = true
        imageView.trailingAnchor.constraint(equalTo: trailingAnchor).isActive = true
        imageView.bottomAnchor.constraint(equalTo: bottomAnchor).isActive = true
    }
    
    private func configureSelectionView()
    {
        selectionView.backgroundColor = .clear
        selectionView.layer.borderWidth = 3
        selectionView.layer.borderColor = UIColor.clear.cgColor
        
        addSubview(selectionView)
        
        selectionView.translatesAutoresizingMaskIntoConstraints = false
        selectionView.leadingAnchor.constraint(equalTo: leadingAnchor).isActive = true
        selectionView.topAnchor.constraint(equalTo: topAnchor).isActive = true
        selectionView.trailingAnchor.constraint(equalTo: trailingAnchor).isActive = true
        selectionView.bottomAnchor.constraint(equalTo: bottomAnchor).isActive = true
        
        configureTapGestureRecognizer()
    }
    
    private func configureTapGestureRecognizer()
    {
        let tapGesture = UITapGestureRecognizer(target: self,
                                                action: #selector(didTapSelectionView))
        selectionView.addGestureRecognizer(tapGesture)
    }
    
    @objc
    private func didTapSelectionView()
    {
        isSelected = !isSelected
    }
    
    private func toggleBorder(_ on: Bool)
    {
        if on
        {
            selectionView.layer.borderColor = UIColor(red: 28.0/255.0,
                                                      green: 244.0/255.0,
                                                      blue: 162.0/255.0,
                                                      alpha: 1.0).cgColor
            
            return
        }
        
        selectionView.layer.borderColor = UIColor.clear.cgColor
    }
}

Then in the view controller

class ClippingTestViewController: UIViewController
{
    private let mainContainerView = UIView()
    private let customView = CustomBorderView()
    
    override func viewDidLoad()
    {
        super.viewDidLoad()
        view.backgroundColor = .white
        title = "Clipping view"
        configureMainContainerView()
        configureCustomBorderView()
        
        mainContainerView.layer.cornerRadius = 50
        mainContainerView.clipsToBounds = true
    }
    
    private func configureMainContainerView()
    {
        mainContainerView.backgroundColor = .white
        
        view.addSubview(mainContainerView)
        
        mainContainerView.translatesAutoresizingMaskIntoConstraints = false
        
        mainContainerView.leadingAnchor.constraint(equalTo: view.leadingAnchor,
                                                   constant: 20).isActive = true
        
        mainContainerView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor,
                                               constant: 20).isActive = true
        
        mainContainerView.trailingAnchor.constraint(equalTo: view.trailingAnchor,
                                                    constant: -20).isActive = true
        
        mainContainerView.heightAnchor.constraint(equalToConstant: 300).isActive = true
        
        view.layoutIfNeeded()
    }
    
    private func configureCustomBorderView()
    {
        mainContainerView.addSubview(customView)
        
        customView.translatesAutoresizingMaskIntoConstraints = false
        
        customView.leadingAnchor.constraint(equalTo: mainContainerView.leadingAnchor).isActive = true
        
        customView.topAnchor.constraint(equalTo: mainContainerView.safeAreaLayoutGuide.topAnchor).isActive = true
        
        customView.trailingAnchor.constraint(equalTo: mainContainerView.trailingAnchor).isActive = true
        
        customView.bottomAnchor.constraint(equalTo: mainContainerView.bottomAnchor).isActive = true
        
        view.layoutIfNeeded()
    }
}

This gives me your current experience

clipToBounds custom UIView with border cornerRadius

Work Around 1. - Shrink subviews on selection

When the view is not selected, everything looks fine. When the view is selected, you could reduce the width and height of the custom subview with some animation while adding the border.

Work Around 2. - Manually clip desired subviews

You go through each subview in your container view and:

  • Apply the clipping to any subview you desire
  • Apply the corner radius to the views you clip
  • Leaving the container view unclipped and without a corner radius

To do that, I created a custom UIView subclass for the container view

class ClippingSubView: UIView
{
    override var clipsToBounds: Bool
    {
        didSet
        {
            if clipsToBounds
            {
                clipsToBounds = false
                clipImageViews(in: self)
                layer.cornerRadius = 0
            }
        }
    }
    
    // Recursively go through all subviews
    private func clipImageViews(in view: UIView)
    {
        for subview in view.subviews
        {
            // I am only checking image view, you could check which you want
            if subview is UIImageView
            {
                print(layer.cornerRadius)
                subview.layer.cornerRadius = layer.cornerRadius
                subview.clipsToBounds = true
            }
            
            clipImageViews(in: subview)
        }
    }
}

Then make sure to adjust the following lines where you create your views:

let mainContainerView = ClippingSubView()

// Do this only after you have added all the subviews for this to work
mainContainerView.layer.cornerRadius = 50
mainContainerView.clipsToBounds = true

This gives me your desired output

UIView clipToBounds custom UIView UIImageView with border cornerRadius

Solution 2:[2]

This is a pretty common problem which may have multiple solutions. In the end though I always find it best to simply go one level higher:

ContainerView (Does not clip)
    ContentView (Clips)
    HighlightingView (Does not clip)

You would put all your current views on ContentView. Then introduce another view which represents your selection and put it on the same level as your ContentView.

In the end this will give you most flexibility. It can still get a bit more complicated when you add things like shadows. But again "more views" is usually the end solution.

Solution 3:[3]

You'll likely run into a lot of problems trying to get a subview's border to display outside its superView's clipping bounds.

One approach is to add an "Outline View" as a sibling of the "Clipping View":

enter image description here

When you select a clippingView's subview - and drag it around - set the frame of the outlineView to match the frame of that subview.

You'll want to set .isUserInteractionEnabled = false on the outlineView so it doesn't interfere with touches on the subviews.

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 Shawn Frank
Solution 2 Matic Oblak
Solution 3 DonMag