8

Below is the code for a custom Card View. The problem is, when I add the subviews to this in Interface builder it doesn't apply the corner radius to the subview. For the most part, I can get away with this by making subviews have a clear background color but I'm struggling with UIImageView. When I add that to a card it ends up with pointy corners and I've not been able to fix it.

Various solutions on here have suggested adding a second layer to display the shadow. I've attempted this but it still doesn't work as intended. What I'm trying to achieve is a view with rounded corners, drop shadow and adding any subviews (such as UIImageView) should also maintain the corner radius and not pointing out.

I've tried various settings with layer.masksToBounds and self.clipsToBounds and I always seem to get subviews with a corner radius but no shadow or the shadow visible and views not clipping.

@IBDesignable class CardView: UIView {

    @IBInspectable dynamic var cornerRadius: CGFloat = 6
    @IBInspectable dynamic var shadowOffsetWidth: Int = 2
    @IBInspectable dynamic var shadowOffsetHeight: Int = 2
    @IBInspectable dynamic var shadowColor: UIColor? = UIColor(netHex: 0x333333)
    @IBInspectable dynamic var shadowOpacity: Float = 0.5

    override init(frame: CGRect) {
        super.init(frame: frame)
        commonInit()
    }

    required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
    }
    override func layoutSubviews() {
        commonInit()
    }

    override func prepareForInterfaceBuilder() {
        commonInit()
    }

    func commonInit() {

        layer.cornerRadius = cornerRadius
        let shadowPath = UIBezierPath(roundedRect: bounds, cornerRadius: cornerRadius)
        layer.masksToBounds = false

        layer.shadowColor = shadowColor?.cgColor
        layer.shadowOffset = CGSize(width: shadowOffsetWidth, height: shadowOffsetHeight)
        layer.shadowOpacity = shadowOpacity
        layer.shadowPath = shadowPath.cgPath

        // This was how I tried to add a seperate shadow layer
//        let shadowView = UIView(frame: self.frame)
//        shadowView.layer.shadowColor = shadowColor?.cgColor
//        shadowView.layer.shadowOffset = CGSize(width: shadowOffsetWidth, height: shadowOffsetHeight)
//        shadowView.layer.shadowOpacity = shadowOpacity
//        shadowView.layer.shadowPath = shadowPath.cgPath
//        shadowView.layer.masksToBounds = false
//
//        self.addSubview(shadowView)

    }

}
koen
  • 5,383
  • 7
  • 50
  • 89
StartPlayer
  • 485
  • 4
  • 21

2 Answers2

8

The way you were trying to implement a second view to handle shadows is almost correct, you just didn't keep the right order.

Your CardView class already handles displaying a shadow. Leave that view as it is and instead add a UIView called "ContentView" as a subview. That content view has the same frame and corner radius as your CardView.

On the "ContentView", you don't need to do any work with shadows. Instead, set its layer's masksToBounds property to true. Now add all the content you want to display in your Card to the "ContentView" and it should clip correctly.

func commonInit() {

    layer.cornerRadius = cornerRadius
    let shadowPath = UIBezierPath(roundedRect: bounds, cornerRadius: cornerRadius)
    layer.masksToBounds = false

    layer.shadowColor = shadowColor?.cgColor
    layer.shadowOffset = CGSize(width: shadowOffsetWidth, height: shadowOffsetHeight)
    layer.shadowOpacity = shadowOpacity
    layer.shadowPath = shadowPath.cgPath

    let contentView = UIView()
    contentView.frame = self.frame
    contentView.layer.cornerRadius = cornerRadius
    contentView.layer.masksToBounds = true

    // any content you add should now be added to the contentView:
    // contentView.addSubview(aView)
}
TomQDRS
  • 875
  • 1
  • 7
  • 20
  • This doesn't seem to have worked for me. I'm adding subviews in interface builder rather than programatically. I'm now trying to add contentView above the card view and seeing if this will mean subviews are added to the content view rather than the cardView itself – StartPlayer Aug 01 '18 at 11:29
  • Im trying contentView.frame = self.bounds contentView.layer.cornerRadius = cornerRadius contentView.layer.masksToBounds = true contentView.backgroundColor = .red self.insertSubview(contentView, at: 0). I've had to set frame to bounds (otherwise contentView.frame.origin is off equal to how cardView is on screen. I've made it red so I can see it. if I do add subview it appears over everything in interface builder. if I insert at index 1 I can see it but the subviews still see to be adding to card view. – StartPlayer Aug 01 '18 at 11:40
  • If you're using the interface builder then add the content view inside your CardView and connect them via an `@IBOutlet` property. From there you can make the same modifications with the content view (`corner Radius`, `masksToBounds`) without having to initialize its frame. Then you can add all the content to the contentView subview in the interface builder. – TomQDRS Aug 01 '18 at 11:44
  • would that means doing to for every cardView in my app (there are a lot and I've trying to avoid going through everything single one) could I recreate cardView with a nib and add the contentView that way once. would that work? – StartPlayer Aug 01 '18 at 11:49
  • I'm not sure about using nibs, i don't really work with them, sorry. If your CardViews all display content in a similar way, I'd suggest using a CollectionView or TableView. – TomQDRS Aug 01 '18 at 11:53
  • 1
    I'm going to try and and make a nib with two views, one styled with drop shadow and a subview which clips to bounds. Its not something that would suit table view as cardView is a UI features I've used in many different places including with in tableViewCells. – StartPlayer Aug 01 '18 at 12:03
  • That hasn't worked for me either. I think the issue is that when I use interface builder and add a view inside CardView or is that adds the subview to card view rather than say the uppermost subview I've got added. – StartPlayer Aug 01 '18 at 12:26
  • The problem with this approach is that it doesn’t work with Interface Builder, unless you override `addSubview()` to add the subviews as children of `contentView`. But then you get in trouble with layout constraints: You set up a constraint in IB between a subview and the parent, but the subview is actually a child of `containerView` so at runtime you get a layout error. – Nicolas Miari Nov 27 '20 at 09:06
  • The only solution I've found to using this in the Interface Builder is to create two CardViews and stack them on top of one another. One has the shadow and the other doesn't. – Gavin Wright Jul 02 '22 at 06:56
1

furthermore, you can specific corners.

layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMaxYCorner]
Zev003
  • 85
  • 4