skip to Main Content

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!

3

Answers


  1. 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.

    Login or Signup to reply.
  2. 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

    Login or Signup to reply.
  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.

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search