I am adding a circular layer around my circular view and setting its position to be the center of the view but the layer gets added at a different position. The following is the code.
class CustomView: UIView {
let outerLayer = CAShapeLayer()
required init?(coder: NSCoder) {
super.init(coder: coder)
self.layer.addSublayer(outerLayer)
}
override func layoutSubviews() {
super.layoutSubviews()
self.backgroundColor = UIColor.systemBlue
self.layer.cornerRadius = self.bounds.height/2
self.layer.borderWidth = 2.0
self.layer.borderColor = UIColor.white.cgColor
let outerLayerFrame = self.bounds.insetBy(dx: -5.0, dy: -5.0)
outerLayer.frame = outerLayerFrame
let path = UIBezierPath(ovalIn: outerLayerFrame)
outerLayer.path = path.cgPath
outerLayer.position = self.center
outerLayer.strokeColor = UIColor.systemBlue.cgColor
outerLayer.fillColor = UIColor.clear.cgColor
outerLayer.lineWidth = 3
}
}
Can anyone please tell me what is wrong here.
You want to set your layer(s) properties when the view is instantiated / initialized, but its size can (and usually does) change between then and when auto-layout adjusts it to the current device dimensions.
Unlike the UIView itself, layers do not auto-resize.
So, you want to set framing and layer paths in layoutSubviews():
class CustomView: UIView {
let outerLayer = CAShapeLayer()
override init(frame: CGRect) {
super.init(frame: frame)
configure()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
configure()
}
// layoutSubviews is where you want to set your size
// and corner radius values
override func layoutSubviews() {
super.layoutSubviews()
// make self "round"
self.layer.cornerRadius = self.bounds.height/2
// add a round path for outer layer
let path = UIBezierPath(ovalIn: bounds)
outerLayer.path = path.cgPath
}
private func configure() {
self.backgroundColor = UIColor.systemBlue
// setup layer properties here
self.layer.borderWidth = 2.0
self.layer.borderColor = UIColor.white.cgColor
outerLayer.strokeColor = UIColor.systemBlue.cgColor
outerLayer.fillColor = UIColor.clear.cgColor
outerLayer.lineWidth = 3
// add the sublayer here
self.layer.addSublayer(outerLayer)
}
}
Result (with the view set to 120 x 120 pts):
So, I realized, it's the position that is making the layer not aligned in center.
Here is the solution, I figured-
class CustomView: UIView {
let outerLayer = CAShapeLayer()
required init?(coder: NSCoder) {
super.init(coder: coder)
self.layer.addSublayer(outerLayer)
}
override func layoutSubviews() {
super.layoutSubviews()
self.backgroundColor = UIColor.systemBlue
self.layer.cornerRadius = self.bounds.height/2
self.layer.borderWidth = 2.0
self.layer.borderColor = UIColor.white.cgColor
let outerLayerBounds = self.bounds.insetBy(dx: -5.0, dy: -5.0)
outerLayer.bounds = outerLayerBounds // should be bounds not frame
let path = UIBezierPath(ovalIn: outerLayerBounds)
outerLayer.path = path.cgPath
outerLayer.position = CGPoint(x: outerLayerBounds.midX , y: outerLayerBounds.midY) //self.center doesn't center the layer, had to calculate the center of the layer manually
outerLayer.strokeColor = UIColor.systemBlue.cgColor
outerLayer.fillColor = UIColor.clear.cgColor
outerLayer.lineWidth = 3
}
}
Related
how can I create a UIView with a shadow that would be like iOS keyboards’s keys?
I tried adding a CALayer with
view.layer.insertSublayer(shadowLayer, below: view.layer) // view is the keyboard key view
But I can’t see the new layer doing so. Could you help me?
Thank you
I think you can just use the backing CALayer that comes with every UIView for this.
func keyLetterButtonView ( havingLetter letter : String) -> UIView
{
let v = UIView()
let l = v.layer
l.cornerRadius = 4.0
l.shadowOpacity = 1.0
l.shadowRadius = 0
l.shadowColor = UIColor.systemGray.cgColor
l.shadowOffset = CGSize ( width: 0, height: 1.0)
v.backgroundColor = .systemGray6
// Key letter
let label = UILabel()
label.font = UIFont.preferredFont(forTextStyle: .title3)
label.textAlignment = .center
label.text = letter
v.addSubview ( label)
var constraints = [NSLayoutConstraint]()
for vv in [ v, label] {
vv.translatesAutoresizingMaskIntoConstraints = false
let sizeConstraints = [
vv.widthAnchor.constraint (
equalToConstant: 28.0
)
, vv.heightAnchor.constraint (
equalToConstant: 42.0
)
]
constraints.append (
contentsOf: sizeConstraints
)
}
NSLayoutConstraint.activate ( constraints)
return v
}
If you inspect the keyboard view using Debug View Hierarchy, you can see that the "key buttons" do not have a shadow ... they have a single-point line on the bottom.
Various ways to do that, but one of the easiest is to add a white CAShapeLayer, give both the view's layer and the shape layer the same size and corner radius, and then shift the shape layer up by one point.
Here's a quick, simple view subclass - marked as #IBDesignable so you can see it in Storyboard / Interface Builder:
#IBDesignable
class BottomShadowView: UIView {
let topLayer = CAShapeLayer()
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
commonInit()
}
func commonInit() {
// add the "white" layer
layer.addSublayer(topLayer)
// give both layers the same corner radius
layer.cornerRadius = 8
topLayer.cornerRadius = 8
// set top layer to white
topLayer.backgroundColor = UIColor.white.cgColor
// if background color is not set, or is clear
// set it to dark gray
if self.backgroundColor == nil || self.backgroundColor == .clear {
self.backgroundColor = .darkGray
}
}
override func prepareForInterfaceBuilder() {
super.prepareForInterfaceBuilder()
// if background color is not set, or is clear
// set it to dark gray
if self.backgroundColor == nil || self.backgroundColor == .clear {
self.backgroundColor = .darkGray
}
}
override func layoutSubviews() {
super.layoutSubviews()
// make top layer the same size as bounds
// offset up 1-point
topLayer.frame = bounds.offsetBy(dx: 0, dy: -1.0)
}
}
It looks like this (on a systemYellow background):
and zoomed-in for clarity:
We can also get a slightly "lighter weight" custom view by overriding draw() instead of adding a sublayer:
#IBDesignable
class DrawBottomShadowView: UIView {
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
commonInit()
}
func commonInit() {
// background color needs to be .clear
self.backgroundColor = .clear
}
override func prepareForInterfaceBuilder() {
super.prepareForInterfaceBuilder()
self.backgroundColor = .clear
}
override func draw(_ rect: CGRect) {
super.draw(rect)
var r: CGRect!
var pth: UIBezierPath!
// if rounded rect for "bottom shadow line"
// goes all the way to the top, we'll get
// anti-alias artifacts at the top corners
// so, we'll make it slightly smaller
r = bounds.insetBy(dx: 0, dy: 2).offsetBy(dx: 0, dy: 2)
pth = UIBezierPath(roundedRect: r, cornerRadius: 8)
UIColor.darkGray.setFill()
pth.fill()
// "filled" rounded rect should be
// 1-point shorter than height
r = bounds
r.size.height -= 1.0
pth = UIBezierPath(roundedRect: r, cornerRadius: 8)
UIColor.white.setFill()
pth.fill()
}
}
I have a custom UIView called CircleView which is essentially a colored ellipse. The color property I'm using to color the ellipse is rendered using setFillColor on the graphics context. I was wondering if there was a way to animate the color change, because when I run through the animate / transition the color changes immediately instead of being animated.
Example Setup
let c = CircleView()
c.frame = CGRect(x: 20, y: 20, width: 100, height: 100)
c.color = UIColor.blue
c.backgroundColor = UIColor.clear
self.view.addSubview(c)
UIView.transition(with: c, duration: 5, options: .transitionCrossDissolve, animations: {
c.color = UIColor.red // Not animated
})
UIView.animate(withDuration: 5) {
c.color = UIColor.yellow // Not animated
}
Circle View
class CircleView : UIView {
var color = UIColor.blue {
didSet {
setNeedsDisplay()
}
}
override init(frame: CGRect) {
super.init(frame: frame)
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
override func draw(_ rect: CGRect) {
guard let context = UIGraphicsGetCurrentContext() else {return}
context.addEllipse(in: rect)
context.setFillColor(color.cgColor)
context.fillPath()
}
}
You can use the built in animation support for the layer's backgroundColor.
While the easiest way to make a circle is to make your view a square (using aspect ratio constraints, for instance) and then set the cornerRadius to half the width or height, I assume you want something a bit more advanced, and that is why you used a path.
My solution to this would be something like:
class CircleView : UIView {
var color = UIColor.blue {
didSet {
layer.backgroundColor = color.cgColor
}
}
override init(frame: CGRect) {
super.init(frame: frame)
setup()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
setup()
}
// Setup the view, by setting a mask and setting the initial color
private func setup(){
layer.mask = shape
layer.backgroundColor = color.cgColor
}
// Change the path in case our view changes it's size
override func layoutSubviews() {
super.layoutSubviews()
let path = CGMutablePath()
// add an elipse, or what ever path/shapes you want
path.addEllipse(in: bounds)
// Created an inverted path to use as a mask on the view's layer
shape.path = UIBezierPath(cgPath: path).reversing().cgPath
}
// this is our shape
private var shape = CAShapeLayer()
}
Or if you really need a simple circle, just something like:
class CircleView : UIView {
var color = UIColor.blue {
didSet {
layer.backgroundColor = color.cgColor
}
}
override init(frame: CGRect) {
super.init(frame: frame)
setup()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
setup()
}
private func setup(){
clipsToBounds = true
layer.backgroundColor = color.cgColor
}
override func layoutSubviews() {
super.layoutSubviews()
layer.cornerRadius = bounds.height / 2
}
}
Either way, this will animate nicely:
UIView.animate(withDuration: 5) {
self.circle.color = .red
}
Strange things happens!
Your code is ok, you just need to call your animation in another method and asyncronusly
As you can see, with
let c = CircleView()
override func viewDidLoad() {
super.viewDidLoad()
c.frame = CGRect(x: 20, y: 20, width: 100, height: 100)
c.color = UIColor.blue
c.backgroundColor = UIColor.clear
self.view.addSubview(c)
changeColor()
}
func changeColor(){
DispatchQueue.main.async
{
UIView.transition(with: self.c, duration: 5, options: .transitionCrossDissolve, animations: {
self.c.color = UIColor.red // Not animated
})
UIView.animate(withDuration: 5) {
self.c.color = UIColor.yellow // Not animated
}
}
}
Work as charm.
Even if you add a button that trigger the color change, when you press the button the animation will work.
I encourage you to set this method in the definition of the CircleView
func changeColor(){
DispatchQueue.main.async
{
UIView.transition(with: self, duration: 5, options: .transitionCrossDissolve, animations: {
self.color = UIColor.red
})
UIView.animate(withDuration: 5) {
self.color = UIColor.yellow
}
}
}
and call it where you want in your ViewController, simply with
c.changeColor()
I'm building a custom view using an XIB file. However, I am facing a problem where the layers I add to the view (trackLayer) are not shown on the xib (circleLayer is an animation and I don't expect it to render in xib which is not possible to my knowledge). The code of the owner class for the XIB is shown as follows:
#IBDesignable
class SpinningView: UIView {
#IBOutlet var contentView: SpinningView!
//MARK: Properties
let circleLayer = CAShapeLayer()
let trackLayer = CAShapeLayer()
let circularAnimation: CABasicAnimation = {
let animation = CABasicAnimation()
animation.fromValue = 0
animation.toValue = 1
animation.duration = 2
animation.fillMode = kCAFillModeForwards
animation.isRemovedOnCompletion = false
return animation
}()
//MARK: IB Inspectables
#IBInspectable var strokeColor: UIColor = UIColor.red {
...
}
#IBInspectable var trackColor: UIColor = UIColor.lightGray {
...
}
#IBInspectable var lineWidth: CGFloat = 5 {
...
}
#IBInspectable var fillColor: UIColor = UIColor.clear {
...
}
#IBInspectable var isAnimated: Bool = true {
...
}
//MARK: Initialization
override init(frame: CGRect) {
super.init(frame: frame)
initSubviews()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
setup()
initSubviews()
}
//MARK: Private functions
private func setup() {
setupTrack()
setupAnimationOnTrack()
}
private func setupTrack(){
trackLayer.lineWidth = lineWidth
trackLayer.fillColor = fillColor.cgColor
trackLayer.strokeColor = trackColor.cgColor
layer.addSublayer(trackLayer)
}
private func setupAnimationOnTrack(){
circleLayer.lineWidth = lineWidth
circleLayer.fillColor = fillColor.cgColor
circleLayer.strokeColor = strokeColor.cgColor
circleLayer.lineCap = kCALineCapRound
layer.addSublayer(circleLayer)
updateAnimation()
}
private func updateAnimation() {
if isAnimated {
circleLayer.add(circularAnimation, forKey: "strokeEnd")
}
else {
circleLayer.removeAnimation(forKey: "strokeEnd")
}
}
//MARK: Layout contraints
override func layoutSubviews() {
super.layoutSubviews()
let center = CGPoint(x: bounds.midX, y: bounds.midY)
let radius = min(bounds.width / 2, bounds.height / 2) - circleLayer.lineWidth / 2
let startAngle = -CGFloat.pi / 2
let endAngle = 3 * CGFloat.pi / 2
let path = UIBezierPath(arcCenter: .zero, radius: radius, startAngle: startAngle, endAngle: endAngle, clockwise: true)
trackLayer.position = center
trackLayer.path = path.cgPath
circleLayer.position = center
circleLayer.path = path.cgPath
}
private func initSubviews() {
let bundle = Bundle(for: SpinningView.self)
let nib = UINib(nibName: "SpinningView", bundle: bundle)
nib.instantiate(withOwner: self, options: nil)
contentView.frame = bounds
addSubview(contentView)
}
}
When a view is subclassed in the Main.storyboard, I can see the image and the tracklayer as follows IB UIView but when I go over to the XIB, it does not have the trackLayer(circle around the image) XIB Image. While one can argue that it is working and why I am bothering with this, I think it is important that I design the XIB properly since another person might just see it as an view with only an image (no idea of the animating feature)
When you are designing your XIB, it is not an instance of your #IBDesignable SpinningView. You are, effectively, looking at the source of your SpinningView.
So, the code behind it is not running at that point - it only runs when you add an instance of SpinningView to another view.
Take a look at this simple example:
#IBDesignable
class SpinningView: UIView {
#IBOutlet var contentView: UIView!
#IBOutlet var theLabel: UILabel!
//MARK: Initialization
override init(frame: CGRect) {
super.init(frame: frame)
initSubviews()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
initSubviews()
}
private func initSubviews() {
let bundle = Bundle(for: SpinningView.self)
let nib = UINib(nibName: "SpinningView", bundle: bundle)
nib.instantiate(withOwner: self, options: nil)
contentView.frame = bounds
addSubview(contentView)
// this will change the label's textColor in Storyboard
// when a UIView's class is set to SpinningView
theLabel.textColor = .red
}
}
When I am designing the XIB, this is what I see:
But when I add a UIView to another view, and assign its class as SpinningView, this is what I see:
The label's text color get's changed to red, because the code is now running.
Another way to think about it... you have trackColor, fillColor, etc vars defined as #IBInspectable. When you're designing your XIB, you don't see those properties in Xcode's Attributes Inspector panel - you only see them when you've selected an instance of SpinningView that has been added elsewhere.
Hope that makes sense.
I'm having issues drawing a circle using CAShapeLayer. I have a custom view MainLogoAnimationView code as follows:
import UIKit
class MainLogoAnimationView: UIView {
#IBOutlet var customView: UIView!
var redCircle: AnimationCircleView!
// MARK: Object Lifecycle
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
setup()
}
func setup() {
_ = Bundle.main.loadNibNamed("MainLogoAnimationView", owner: self, options: nil)?[0] as! UIView
self.addSubview(customView)
customView.frame = self.bounds
let circleWidth = frame.size.width*0.25
let yPos = frame.size.height/2 - circleWidth/2
redCircle = AnimationCircleView(frame: CGRect(x: 10, y: yPos, width: circleWidth, height: circleWidth))
redCircle.backgroundColor = UIColor.yellow
addSubview(redCircle)
}
override func layoutSubviews() {
super.layoutSubviews()
}
}
In interface builder I created a light blue UIView and selected MainLogoAnimationView as the subview.
The AnimationCircleView is like so:
import UIKit
class AnimationCircleView: UIView {
// MARK: Object lifecycle
var circleColor = UIColor.red.withAlphaComponent(0.5).cgColor {
didSet {
shapeLayer.fillColor = circleColor
}
}
override init(frame: CGRect) {
super.init(frame: frame)
setup()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
setup()
}
let shapeLayer = CAShapeLayer()
func setup() {
let circlePath = UIBezierPath(ovalIn: frame)
shapeLayer.path = circlePath.cgPath
shapeLayer.fillColor = circleColor
shapeLayer.frame = self.frame
shapeLayer.backgroundColor = UIColor.gray.cgColor
layer.addSublayer(shapeLayer)
}
}
Screenshot:
Is seems the CAShapeLayer frame is incorrect and also the red circle is in the wrong position. It should be inside the yellow square. What am I doing wrong here? Any pointers would be really appreciated! Thanks!
UPDATE:
In your AnimationCircleView inside setup change
shapeLayer.frame = self.frame
to
shapeLayer.frame = self.bounds
This should ensure the gray colour is drawn inside the yellow rectangle.
Edit:
Similar to the answer above, changing
let circlePath = UIBezierPath(ovalIn: frame)
to
let circlePath = UIBezierPath(ovalIn: bounds)
will solve your problem and draw circle inside the gray area.
CAShapeLayer frame is calculated relative to the position of the view it is added on, soo setting it equal to the frame, adds the CAShapeLayer at a x and y position offset from the origin by the value of the x and y passed in self.frame.
Learning Swift.
I have an UIButton class. Here is it:
class CustomBtn: UIButton {
var shadowLayer: CAShapeLayer!
var shadowAdded: Bool = false
required init(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)!
}
override func layoutSubviews() {
super.layoutSubviews()
}
// Only override drawRect: if you perform custom drawing.
// An empty implementation adversely affects performance during animation.
override func drawRect(rect: CGRect) {
super.drawRect(rect)
shadowLayer = CAShapeLayer()
shadowLayer.path = UIBezierPath(roundedRect: bounds, cornerRadius: 5).CGPath
shadowLayer.fillColor = UIColor(netHex: Colors.Red1.value).CGColor
shadowLayer.shadowColor = UIColor.darkGrayColor().CGColor
shadowLayer.shadowPath = shadowLayer.path
shadowLayer.shadowOffset = CGSize(width: 2.0, height: 2.0)
shadowLayer.shadowOpacity = 0.8
shadowLayer.shadowRadius = 2
layer.insertSublayer(shadowLayer, atIndex: 0)
}
}
And this is my output:
But when I change orientation:
What would you do to get the button background (red color and the shadow) fill the width in any orientation?
You don't really need to override drawRect for this, so is best to heed the advice in the comment above it. But, the problem is you are setting your layers path based on your button's bounds, but those bounds change when you rotate the device, but you're not updating your layer.
Also many of the effects you're trying to achieve can be done by setting properties on your button's layer itself. I would change your class to this:
class CustomBtn: UIButton {
required init(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)!
self.commonInit()
}
override init(frame:CGRect){
super.init(frame: frame)
self.commonInit()
}
func commonInit(){
self.layer.backgroundColor = UIColor(netHex: Colors.Red1.value).CGColor
self.layer.cornerRadius = 5
self.layer.shadowColor = UIColor.darkGrayColor().CGColor
self.layer.shadowOffset = CGSize(width: 2.0, height: 2.0)
self.layer.shadowOpacity = 0.8
self.layer.shadowRadius = 2
}
}
You can get your class to be slightly more performant by adding to commonInit:
self.layer.shadowPath = UIBezierPath(roundedRect: bounds, cornerRadius: 5).CGPath
But if you do that you'll also want to add:
override var bounds: CGRect{
didSet{
self.commonInit()
}
}
So anytime the bounds change your shadow path will be updated.