Bottom inner shadow with rounded corners - ios

I'm trying to reproduce the following design, and I'm struggling with the bottom inner shadow.
I didn't found any solution that fulfill the requirements:
bottom only
contained in a rounded view
and ideally, a solution working with UIButton.

While this is possible, it's a bit tricky and it will require you to draw shadows using CGPath: https://developer.apple.com/documentation/quartzcore/calayer/1410771-shadowpath
Probably a simpler way would be to use a resizable image: https://developer.apple.com/documentation/uikit/uiimage/1624102-resizableimage
You would make a smaller image like this mock below, and then simply resize it to increase the frame like so:
let resized = mockImage.resizableImage(withCapInsets: UIEdgeInsets(top: 16, left: 16, bottom: 16, right: 56), resizingMode: .stretch)

I have created a button extension for you to add the bottom shadow. Please check it out. You just need to call the "bottomShadow()" function with your button refrence
e.g.
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
self.button?.bottomShadow()
}
/*Button Extension*/
extension UIButton {
func bottomShadow() {
let shadowHeight: CGFloat = 5.0
let shadowframe = CGRect.init(x: 0, y: self.bounds.height - shadowHeight, width: self.bounds.width, height: shadowHeight)
let path = UIBezierPath(roundedRect: shadowframe, byRoundingCorners: [.topLeft , .topRight], cornerRadii: CGSize(width: self.layer.cornerRadius, height: self.layer.cornerRadius))
let mask = CAShapeLayer()
mask.fillColor = UIColor.lightGray.cgColor
mask.path = path.cgPath
self.layer.addSublayer(mask)
}
}

Related

BezierPath sublayer does not wrap around the whole UIView

I'm currently trying to make a dotted border around my UIView. I referred to a previous post: Dashed line border around UIView
The left side of the UIView gets the red dotted lines but the right side misses the edge.
This is left side
This is right side
Here is my code which I am executing in viewDidLoad:
myview.backgroundColor = UIColor.lightGray
myview.layer.cornerRadius = 4
let dottedBorder = CAShapeLayer()
dottedBorder.strokeColor = UIColor.red.cgColor
dottedBorder.lineDashPattern = [4, 4]
dottedBorder.frame = myview.bounds
dottedBorder.fillColor = nil
dottedBorder.path = UIBezierPath(roundedRect: myview.bounds, byRoundingCorners: .allCorners, cornerRadii: CGSize(width: 4, height: 4)).cgPath
myview.layer.addSublayer(dottedBorder)
Assuming you're using autolayout, you shouldn't depend on the view size inside viewDidLoad, because usually it equals you storyboard device size(selected is SB editor), not the real device one.
Anyway, it may change in future. All calculations depending on frame/bounds needs to be done inside viewDidLayoutSubviews. Something like this:
private let dottedBorder = CAShapeLayer()
override func viewDidLoad() {
super.viewDidLoad()
dottedBorder.strokeColor = UIColor.red.cgColor
dottedBorder.lineDashPattern = [4, 4]
dottedBorder.fillColor = nil
myview.layer.addSublayer(dottedBorder)
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
dottedBorder.frame = myview.bounds
dottedBorder.path = UIBezierPath(roundedRect: myview.bounds, byRoundingCorners: .allCorners, cornerRadii: CGSize(width: 4, height: 4)).cgPath
}

top right is not working on byRoundingcorners in swift 4

im trying to make rounded corners for the sub view in the top left and top right using the following code
maskLayer.path = UIBezierPath(roundedRect: view.bounds, byRoundingCorners: [.topRight, .topLeft], cornerRadii: CGSize(width: 10, height: 10)).cgPath
also tried in viewDidLayoutSubviews(), but only left border radius is working... the right border radius is not working.
can someone help me out
You may miss configure something , here maskedCorners rounds top left && right --- and roundCorners rounds bottom left && right
class ViewController: UIViewController {
let redBox = UIView(frame: CGRect(x: 100, y: 100, width: 128, height: 128))
let blueBox = UIView(frame: CGRect(x: 100, y: 300, width: 128, height: 128))
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
redBox.backgroundColor = .red
redBox.layer.cornerRadius = 25
redBox.layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner]
view.addSubview(redBox)
blueBox.backgroundColor = .blue
view.addSubview(blueBox)
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
blueBox.roundCorners(corners: [.bottomLeft, .bottomRight], radius: 25.0)
}
}
extension UIView {
func roundCorners(corners: UIRectCorner, radius: CGFloat) {
let path = UIBezierPath(roundedRect: bounds, byRoundingCorners: corners, cornerRadii: CGSize(width: radius, height: radius))
let mask = CAShapeLayer()
mask.path = path.cgPath
layer.mask = mask
}
}
That often happens when your maskLayer is wider than the view it masks.
See the diagram below. Overlapped area is a rectangle with one corner rounded.
try this
override func layoutSubviews() {
super.layoutSubviews()
yourView.roundCorners(topLeft: 25, topRight: 25)
bookImage.clipsToBounds = true
}

Auto Resizing CAShapeLayer while Animating UIView

Apologies, if this question has already been answered elsewhere. I tried searching in multiple places but could not find a good solution. I am a beginner to Swift development.
As per the code below, I am creating a SubView, adding an oval ShapeLayer to it and then animating the SubView by moving its center and increasing its size.
The SubView is animating correctly, however the ShapeLayer inside the SubView is not changing size. I would like the Red Oval to increase in size, similar to the SubView. I would really appreciate it if could let me know what I am missing.
class playGroundView: UIView {
override func draw(_ rect: CGRect) {
// Add a blue rectangle as subview
let startFrame = CGRect(x: self.frame.midX, y: self.frame.midY, width: 10, height: 20)
self.addSubview(UIView(frame: startFrame))
self.subviews[0].backgroundColor = UIColor.blue
// Create red oval shape that is bounded by blue rectangle
// Add red oval shape as sub-layer to blue rectangle view
let subView = self.subviews[0]
let ovalSymbol = UIBezierPath(ovalIn: subView.bounds)
let shapeLayer = CAShapeLayer()
shapeLayer.path = ovalSymbol.cgPath
shapeLayer.fillColor = UIColor.red.cgColor
shapeLayer.strokeColor = UIColor.red.cgColor
subView.layer.addSublayer(shapeLayer)
// Animate movement and increase in size of blue rectangle view
UIViewPropertyAnimator.runningPropertyAnimator(withDuration: 3, delay: 0, options: [], animations: {
let endFrame = CGRect(x:self.frame.midX - 50, y:self.frame.midY - 50, width: 20, height: 40)
self.subviews[0].frame = endFrame
self.setNeedsLayout()
self.layoutIfNeeded()
})
}
}
Image of Incorrect Output
Ok, after spending more time researching and getting a better understanding of what goes inside a ViewController and UIView class, entering the following in those classes works for me:
Inside class ViewController: UIViewController
override func viewDidLoad() {
super.viewDidLoad()
let startFrame = CGRect(x: playGround.frame.midX, y: playGround.frame.midY, width: 30, height: 60)
cardSubView = cardView(frame: startFrame)
playGround.addSubview(cardSubView!)
let endFrame = CGRect(x: playGround.frame.midX - 100, y: playGround.frame.midY - 100, width: 60, height: 120)
UIViewPropertyAnimator.runningPropertyAnimator(withDuration: 10, delay: 0, options: [], animations: {
self.cardSubView?.frame = endFrame
self.cardSubView?.layoutIfNeeded()
})
}
Inside a UIView class:
class cardView: UIView {
override func draw(_ rect: CGRect) {
let backGroundPath = UIBezierPath(rect: rect)
UIColor.blue.setFill()
backGroundPath.fill()
let ovalSymbol = UIBezierPath(ovalIn: rect)
UIColor.red.setFill()
ovalSymbol.fill()
}
}
This still results in a weird frame shadowing towards the end of the animation in my iPhone simulator, however when running on device there is no issue.

IOS - How to create Facebook reaction bar with blur background?

Although it may not be the week to replicate some design of Facebook, I would like to be able to design my own version of the reaction indicator view below.
I have three UIImageViews lined in the same positions as above. The problem is that, unlike Facebook, the background color may change (i.e is on top of a UIBlurEffect) and therefore I am unable to set the border color to white.
I thought it would make sense to set the borderColor like so:
imageViewOne.layer.borderColor = UIColor.clear.cgColor
imageViewOne.layer.borderWidth = 2
However, the underlying imageViewTwo is displayed in the border instead of the background color.
So far, I have this:
Would appreciate some help/ideas on how to make this work - I'm thinking of masks but not sure whether a. this is the correct solution and b. how to achieve the desired effect. To clarify, I am not able to set the border color as a constant as it will change with the UIBlurEffect.
In my opinion, there are 2 way to resolve your problem.
Create and use clipped image for Wow and Love like below Love image.
Another way is using mask property of UIView. Creating a mask image and apply it for mask property.
Mask image looks like.
Code for applying mask.
let imvLoveMask = UIImageView.init(image: UIImage.init(named: "MASK_IMAGE_NAME"));
imvLoveMask.frame = CGRect.init(x: 0, y: 0, width: imvLove.frame.size.width, height: imvLove.frame.size.height);
imvLove.mask = imvLoveMask;
Both of 2 above way can help you achieve what you want in the question. Background of icons in below image is an UIVisualEffectView.
In my opinion, the first way with clipped image is better and faster because you don't need to apply mask for your imageView. But if you don't want to create a clipped image for some reason, you can use the second way.
For more detail, you can take a look at my demo repo
You need to clip part of the image in order to let underlying content be visible in the gaps between images. See playground sample.
Add smile_1, smile_2, smile_3 images to playground resources. I took emoji images from https://emojipedia.org/facebook/.
import UIKit
import PlaygroundSupport
class EmojiView: UIView {
var imageView = UIImageView()
var imageInset = UIEdgeInsets(top: 3.0, left: 3.0, bottom: 3.0, right: 3.0)
var shapeLayer = CAShapeLayer()
var overlap: CGFloat = 0.0 {
didSet {
self.updateShape()
}
}
override init(frame: CGRect) {
super.init(frame: frame)
self.setup()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
// MARK: UIView
override func awakeFromNib() {
super.awakeFromNib()
self.setup()
}
override func layoutSubviews() {
super.layoutSubviews()
self.imageView.frame = UIEdgeInsetsInsetRect(self.bounds, self.imageInset)
self.shapeLayer.frame = self.bounds
self.updateShape()
}
// MARK: Private
private func setup() {
self.addSubview(self.imageView)
self.layer.mask = self.shapeLayer
}
private func updateShape() {
let frame = self.bounds
let path = UIBezierPath(rect: frame)
// Cut off part of the image if overlap more then > 0
if 0 < self.overlap {
path.usesEvenOddFillRule = true
path.append(UIBezierPath(ovalIn: CGRect(x: -frame.width + self.overlap, y: 0.0, width: frame.width, height: frame.height)).reversing())
}
self.shapeLayer.path = path.cgPath
}
}
let overlap: CGFloat = 10 // Amount of pixels emojis overlap each other
// Create emoji views
let emojiView_1 = EmojiView(frame: CGRect(x: 5.0, y: 5.0, width: 40.0, height: 40.0))
emojiView_1.imageView.image = UIImage(named: "smile_1")
let emojiView_2 = EmojiView(frame: CGRect(x: emojiView_1.frame.maxX - overlap, y: 5.0, width: 40.0, height: 40.0))
emojiView_2.imageView.image = UIImage(named: "smile_2")
emojiView_2.overlap = overlap
let emojiView_3 = EmojiView(frame: CGRect(x: emojiView_2.frame.maxX - overlap, y: 5.0, width: 40.0, height: 40.0))
emojiView_3.imageView.image = UIImage(named: "smile_3")
emojiView_3.overlap = overlap
let holderView = UIView(frame: CGRect(x: 0.0, y: 0.0, width: emojiView_3.frame.maxX + 5, height: 50.0))
// Add gradient layer
let gradientLayer = CAGradientLayer()
gradientLayer.frame = holderView.bounds
gradientLayer.colors = [UIColor.red.cgColor, UIColor.green.cgColor]
gradientLayer.startPoint = CGPoint(x: 0.0, y: 0.5)
gradientLayer.endPoint = CGPoint(x: 1.0, y: 0.5)
holderView.layer.addSublayer(gradientLayer)
// Add emoji views
holderView.addSubview(emojiView_1)
holderView.addSubview(emojiView_2)
holderView.addSubview(emojiView_3)
// Present the view controller in the Live View window
PlaygroundPage.current.liveView = holderView
use this :
self.imageViewOne.layer.cornerRadius = self.imageViewOne.layer.bounds.width/2
self.imageViewOne.layer.masksToBounds = true
Simple suggestion: As you are setting border color programatically, you have a control to change it, according to background color (if background color is solid (not a gradient)).
imageViewOne.layer.borderColor = imageViewOne.superview?.backgroundColor ?? UIColor.white
imageViewOne.layer.borderWidth = 2.0
Actually instead of masking, you can put your images in a view which has white background and round(set corner radius). Then you can put these views (which has white background and images in it) via settings their zPosition or on storyboard with view hierarchy.
I've prepared a little playground for you. You can see the result in the screenshot. I've put a view inside the containerViews instead you can use uiimageview etc. It's a bit ugly but solves your issue I guess it's up to you to decide how use it.
Here is the code, you can just copy and paste it to a new playground and test it.
import UIKit
import PlaygroundSupport
PlaygroundPage.current.needsIndefiniteExecution = true
var mainContainerView = UIView(frame: CGRect(x: 0, y: 0, width: 140, height: 80))
mainContainerView.backgroundColor = UIColor.white
var containerView = UIView(frame: CGRect(x: 0, y: 0, width: 80, height: 80))
containerView.backgroundColor = UIColor.white
containerView.layer.cornerRadius = containerView.frame.width / 2
var innerView = UIView(frame: CGRect(x: 10, y: 10, width: 60, height: 60))
innerView.backgroundColor = UIColor.red
innerView.layer.cornerRadius = innerView.frame.width / 2
containerView.addSubview(innerView)
var containerView2 = UIView(frame: CGRect(x: 60, y: 0, width: 80, height: 80))
containerView2.backgroundColor = UIColor.yellow
containerView2.layer.cornerRadius = containerView2.frame.width / 2
var innerView2 = UIView(frame: CGRect(x: 10, y: 10, width: 60, height: 60))
innerView2.backgroundColor = UIColor.green
innerView2.layer.cornerRadius = innerView2.frame.width / 2
containerView2.addSubview(innerView2)
containerView.layer.zPosition = 2
containerView2.layer.zPosition = 1
mainContainerView.addSubview(containerView)
mainContainerView.addSubview(containerView2)
PlaygroundPage.current.liveView = mainContainerView

Smooth only top angles and imageview in Swift

I'm trying to round only the top top corners (TopLeft and TopRight) of a view and imageview. With the code below I was able to round up only the left corner (although there is also specified TopRight). Maybe he doesn't see the constraint placed on the storyboard.
How do I fix?
Thank you.
let rectShape = CAShapeLayer()
rectShape.bounds = self.CopertinaImage1.frame
rectShape.position = self.CopertinaImage1.center
rectShape.path = UIBezierPath(roundedRect: self.CopertinaImage1.bounds, byRoundingCorners: [.TopRight, .TopLeft], cornerRadii: CGSize(width: 10, height: 10)).CGPath
self.CopertinaImage1.layer.backgroundColor = UIColor.greenColor().CGColor
self.CopertinaImage1.layer.mask = rectShape
The right corner is always the same:
Edit:
I've tried placing the following code in viewDidLayoutSubviews:
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
let rectShape = CAShapeLayer()
rectShape.bounds = self.CopertinaImage1.frame
rectShape.position = self.CopertinaImage1.center
rectShape.path = UIBezierPath(roundedRect: self.CopertinaImage1.bounds, byRoundingCorners: [.TopRight, .TopLeft], cornerRadii: CGSize(width: 10, height: 10)).CGPath
self.CopertinaImage1.layer.backgroundColor = UIColor.greenColor().CGColor
self.CopertinaImage1.layer.mask = rectShape
}
But it didn't work.
I've tried the code you've posted. For whatever reason when I enter your constraints and recreate your view hierarch, the auto layout engine does not call viewDidLayoutSubviews after it does it's second pass. I could force it to but calling self.view.layoutIfNeeded() from viewWillAppear. For example:
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
let rectShape = CAShapeLayer()
rectShape.frame = self.imageView.bounds
rectShape.path = UIBezierPath(roundedRect: self.imageView.bounds, byRoundingCorners: [.TopRight, .TopLeft], cornerRadii: CGSize(width: 10, height: 10)).CGPath
self.imageView.layer.backgroundColor = UIColor.greenColor().CGColor
self.imageView.layer.mask = rectShape
}
override func viewWillAppear(animated: Bool) {
super.viewWillAppear(animated)
self.view.layoutIfNeeded()
}
Results in:
With both corners rounded.
I would say a better, more elegant solution that avoids this auto layout glitch is to just set the corner radius of the image view container, as it seems like that what you're doing anyways. For example:
override func viewDidLoad() {
super.viewDidLoad()
self.imageView.clipsToBounds = true
self.imageContainerView.layer.cornerRadius = 10.0
self.imageContainerView.layer.masksToBounds = true
}
You can also do this from viewDidLoad because it doesn't depend on any of your views' frames being set.
I just tried your code in a playground. Worked fine for me. Maybe I am misunderstanding the problem.

Resources