Centering a CAShapeLayer within a UIView? - ios

There are probably more than 5 questions just like this one, but none of them solve the problem.
I want to center a circle on a view. It's not happening despite trying all kinds of methods through similar questions like this one.
I'm not sure if this happens because I set the translatesAutoresizingMaskIntoConstraints = false. I tried playing around with the center and anchor points of the shape layer but crazy behavior happens. I tried putting this code in viewDidLayouSubviews. Didn't work. What else can I do?
colorSizeGuide is the view of which I'm trying to center my layer in.
func setupConstraints() {
// other constraints set up here
colorSizeGuide.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true
colorSizeGuide.topAnchor.constraint(equalTo: view.topAnchor, constant: 30).isActive = true
colorSizeGuide.widthAnchor.constraint(equalToConstant: 30).isActive = true
colorSizeGuide.heightAnchor.constraint(equalToConstant: 30).isActive = true
}
func setupColorSizeGuide() {
shape.path = UIBezierPath(ovalIn: colorSizeGuide.frame).cgPath
shape.position = self.colorSizeGuide.center
shape.anchorPoint = CGPoint(x: 0.5, y: 0.5)
shape.strokeColor = (UIColor.black).cgColor
shape.fillColor = (UIColor.clear).cgColor
shape.lineWidth = 1.0
colorSizeGuide.layer.addSublayer(shape)
}

There are a few options you can do.
If you're not in a scrolling environment (e.g. tableView) use corner radius of half of the width of the rectangle on the view, then the view will be cropped to a circle - this is not particularly fast or customizable, but it gets the job done.
Playground Code:
import UIKit
import PlaygroundSupport
let frame: CGRect = CGRect(x: 0, y: 0, width: 100, height: 100)
let view: UIView = UIView(frame: frame)
view.backgroundColor = .magenta
view.layer.cornerRadius = frame.size.width / 2
PlaygroundPage.current.liveView = view
You can make your custom UIView subclass implement the layerClass method and return CAShapeLayer there. This will mean that self.layer of your view will actually be a shape layer. You can set a path of a circle there and there you go. If you want to center it in a view just make sure to use bounds coordinates of the view when defining the UIBezierPath.
Playground code:
import UIKit
import PlaygroundSupport
let frame: CGRect = CGRect(x: 0, y: 0, width: 100, height: 100)
let view: UIView = UIView(frame: frame)
PlaygroundPage.current.liveView = view
class ShapeView: UIView {
override class var layerClass: AnyClass { return CAShapeLayer.self }
override init(frame: CGRect) {
super.init(frame: frame)
(layer as? CAShapeLayer)?.fillColor = UIColor.red.cgColor
(layer as? CAShapeLayer)?.strokeColor = UIColor.green.cgColor
(layer as? CAShapeLayer)?.path = UIBezierPath(ovalIn: frame).cgPath
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
let sv = ShapeView(frame: frame)
view.addSubview(sv)
Make a non-view shape layer, by doing CAShapeLayer.init. Position your on your view in its layoutSubviews method (or viewDidLayoutSubviews if you're doing it in the view controller) and set a proper frame to the shape layer, like it was a view.
Playground Code:
import UIKit
import PlaygroundSupport
let frame: CGRect = CGRect(x: 0, y: 0, width: 100, height: 100)
let smallFrame: CGRect = CGRect(x: 0, y: 0, width: 10, height: 10)
let view: UIView = UIView(frame: frame)
view.backgroundColor = .magenta
PlaygroundPage.current.liveView = view
let sl = CAShapeLayer()
sl.path = UIBezierPath(ovalIn: smallFrame).cgPath
sl.fillColor = UIColor.red.cgColor
sl.strokeColor = UIColor.green.cgColor
sl.frame.origin.x = 30
sl.frame.origin.y = 30
view.layer.addSublayer(sl)

Related

How to animate a border shimmer in swift?

I need help with making an animation that runs a gradient trough the borders of a view, as shown in this GIF:
Any ideas how to go about it in Swift?
Thanks!
Here's my solution:
Use an animated gradient layer for the base view
Add an overlaying white view (with inset) to cover up the center of the gradient
As MadProgrammer said, you can set the gradient's startPoint and endPoint at an angle, to get it to go around the corners
class BorderShimmerView : UIView {
/// allow gradient layer to resize automatically
override class var layerClass: AnyClass { return CAGradientLayer.self }
/// boilerplate UIView initializers
init() {
super.init(frame: CGRect(x: 0, y: 0, width: 100, height: 100))
commonInit()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
commonInit()
}
/// set up everything
func commonInit() {
let overlayView = UIView() /// add a view overlaid on the gradient view
overlayView.backgroundColor = .white
overlayView.frame = bounds.insetBy(dx: 3, dy: 3) /// appears like a border
overlayView.autoresizingMask = [.flexibleWidth, .flexibleHeight] /// allow resizing
self.addSubview(overlayView)
let gradientLayer = self.layer as! CAGradientLayer
gradientLayer.locations = [0, 0.45, 0.55, 1] /// adjust this to change the colors' spacing
gradientLayer.colors = [
UIColor.white.cgColor,
UIColor.yellow.cgColor, /// yellow + orange for gold effect
UIColor.orange.cgColor,
UIColor.white.cgColor
]
let startPointAnimation = CABasicAnimation(keyPath: #keyPath(CAGradientLayer.startPoint))
startPointAnimation.fromValue = CGPoint(x: 2, y: -1) /// extreme top right
startPointAnimation.toValue = CGPoint(x: 0, y: 1) /// bottom left
let endPointAnimation = CABasicAnimation(keyPath: #keyPath(CAGradientLayer.endPoint))
endPointAnimation.fromValue = CGPoint(x: 1, y: 0) /// top right
endPointAnimation.toValue = CGPoint(x: -1, y: 2) /// extreme bottom left
let animationGroup = CAAnimationGroup() /// group animations together
animationGroup.animations = [startPointAnimation, endPointAnimation]
animationGroup.duration = 2
animationGroup.repeatCount = .infinity /// repeat animation infinitely
gradientLayer.add(animationGroup, forKey: nil)
}
}
Usage:
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
let shimmerView = BorderShimmerView()
shimmerView.frame = CGRect(x: 50, y: 50, width: 300, height: 300)
view.addSubview(shimmerView)
}
}
Result:

How to re-draw line connected between moveable two UIView

I would like to re-draw the line between two movable UIView, depending on the position of UIView.
So I found this. However, this method uses "Pan Gesture" to re-draw lines, depending on the position of the gesture.
Also found are setNeedsDisplay(), but this is a request to re-draw, not a real-time event function.
The way I want to find it is not to use gestures to redraw lines, but to re-draw lines in real time.
In a little bit more detail, I applied "UIColisionBehavior" to all the UIVviews I created. The UIView applied changes position as they are struck, and depending on the changed position, the line is being redrawn.
As if the UIView were moving in this way, the connected line was redrawn according to where it was moved:
Below is the code I'm experimenting with in the Playground. When you execute the code, you can see that the first connected purple line is connected to the falling UIView and does not fall off:
//: A UIKit based Playground for presenting user interface
import UIKit
import PlaygroundSupport
class MoveAbleView : UIView {
var outGoingLine : CAShapeLayer?
var inComingLine : CAShapeLayer?
override init(frame: CGRect) {
super.init(frame: frame)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func lineTo(connectedView: MoveAbleView) -> CAShapeLayer {
let path = UIBezierPath()
path.move(to: self.center)
path.addLine(to: connectedView.center)
let line = CAShapeLayer()
line.path = path.cgPath
line.lineWidth = 5
line.strokeColor = UIColor.purple.cgColor
connectedView.inComingLine = line
outGoingLine = line
return line
}
}
class MyViewController : UIViewController {
var dynamicAnimator = UIDynamicAnimator()
var collisionBehavior = UICollisionBehavior()
var gravityBehavior = UIGravityBehavior()
override func loadView() {
let view = UIView()
view.backgroundColor = .white
self.view = view
let viw = MoveAbleView(frame: CGRect(x: 100, y: 200, width: 50, height: 50))
viw.backgroundColor = UIColor.red
self.view.addSubview(viw)
let viw2 = MoveAbleView(frame: CGRect(x: 300, y: 100, width: 50, height: 50))
viw2.backgroundColor = UIColor.orange
self.view.addSubview(viw2)
let gravityViw = MoveAbleView(frame: CGRect(x: 100, y: 0, width: 50, height: 50))
gravityViw.backgroundColor = UIColor.green
self.view.addSubview(gravityViw)
let gravityViw2 = MoveAbleView(frame: CGRect(x: 300, y: -200, width: 50, height: 50))
gravityViw2.backgroundColor = UIColor.blue
self.view.addSubview(gravityViw2)
collisionBehavior.addItem(viw)
collisionBehavior.addItem(viw2)
collisionBehavior.addItem(gravityViw)
collisionBehavior.addItem(gravityViw2)
gravityBehavior.addItem(gravityViw)
gravityBehavior.addItem(gravityViw2)
dynamicAnimator.addBehavior(collisionBehavior)
dynamicAnimator.addBehavior(gravityBehavior)
self.view.layer.addSublayer(viw.lineTo(connectedView: viw2))
}
}
// Present the view controller in the Live View window
PlaygroundPage.current.liveView = MyViewController()
When UIView strikes and moves, how do you redraw a connected line in real time?
Actually, all what you need is another UIView to represent the lines and employ attachmentBehaviors. For instance, there is a line between two attached objects.
class MyViewController : UIViewController {
override func loadView() {
let view = UIView()
view.backgroundColor = .white
self.view = view
dynamicAnimator = UIDynamicAnimator(referenceView: view)
let viw = MoveAbleView(frame: CGRect(x: 100, y: 200, width: 50, height: 50))
viw.backgroundColor = UIColor.red
self.view.addSubview(viw)
let viw2 = MoveAbleView(frame: CGRect(x: 300, y: 200, width: 50, height: 50))
viw2.backgroundColor = UIColor.orange
self.view.addSubview(viw2)
let gravityViw = MoveAbleView(frame: CGRect(x: 100, y: 0, width: 50, height: 50))
gravityViw.backgroundColor = UIColor.green
self.view.addSubview(gravityViw)
let line1 = MoveAbleView(frame: CGRect(x: 125, y: 225, width: 200, height: 10))
line1.backgroundColor = UIColor.purple
self.view.addSubview(line1)
let l1 = UIAttachmentBehavior.init(item: viw, offsetFromCenter: UIOffset.zero, attachedTo: line1, offsetFromCenter: UIOffset.init(horizontal: -100, vertical: 0))
let l2 = UIAttachmentBehavior.init(item: viw2, offsetFromCenter: UIOffset.zero, attachedTo: line1, offsetFromCenter: UIOffset.init(horizontal: 100, vertical: 0))
collisionBehavior.addItem(viw)
collisionBehavior.addItem(viw2)
collisionBehavior.addItem(gravityViw)
gravityBehavior.addItem(gravityViw)
dynamicAnimator.addBehavior(l1)
dynamicAnimator.addBehavior(l2)
dynamicAnimator.addBehavior(collisionBehavior)
dynamicAnimator.addBehavior(gravityBehavior)
}
}

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.

How to add a value within Circle Swift

I have added a imageview which is displayed as a circle by using the code below.
ImageView.layer.cornerRadius = ImageView.frame.size.width/2
ImageView.clipsToBounds = true
how can I implement a label within this which displays a number. I am not sure how to go about doing this. I am quite new to swift therefore I am not if this can be done or if there are better ways about doing this. My goal is to create a custom circle to display within the imageview which displays a number.
Or is it better to embed a view and then use uibeziapath to construct a circle.
You can create a custom view using CAShapeLayer and UIBezierPath if you want a hollow circle and label inside it. This class does that.
class CircleView: UIView {
let shapeLayer = CAShapeLayer()
var circularPath: UIBezierPath?
// This is your label, you can its property here like textColor and Font
let breathingStatusLabel : UILabel = {
let label = UILabel()
label.textColor = UIColor.white
label.text = “1”
label.textAlignment = .center
label.font = UIFont(name: "AvenirNext-UltraLight", size: 31)
label.alpha = 1.0
return label
}()
override init(frame: CGRect) {
super.init(frame: frame)
self.frame = frame
self.setupPaths()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
func setupPaths() {
// Add label to your view and set its frame
self.addSubview(breathingStatusLabel)
breathingStatusLabel.frame = CGRect.init(x: 0, y: 0, width: self.bounds.width, height: self.bounds.height)
breathingStatusLabel.center = CGPoint.init(x: self.bounds.width/2, y: self.bounds.height/2)
circularPath = UIBezierPath(arcCenter: .zero, radius: self.bounds.height / 2, startAngle: 0, endAngle: 2 * CGFloat.pi, clockwise: true)
shapeLayer.path = circularPath?.cgPath
shapeLayer.strokeColor = UIColor.red.cgColor
shapeLayer.lineWidth = 6.0
shapeLayer.fillColor = UIColor.clear.cgColor
shapeLayer.position = CGPoint(x: self.center.x, y: self.bounds.height / 2)
shapeLayer.lineCap = kCALineCapRound
shapeLayer.strokeEnd = 1
self.layer.addSublayer(shapeLayer)
self.shapeLayer.transform = CATransform3DMakeRotation(CGFloat.pi / 2, 0, 0, 1)
}
}
To use it :
let circle = CircleView(frame: someView.frame) // Some frame is where you want to show this.
view.addSubview(circle)
My goal is to create a custom circle to display within the imageview which displays a number.
If you are not using any image in imageView but you just make a circle then its better if you use
mylabel.layer.cornerRadius = mylabel.frame.size.width/2
and set data as mylabel.text = "2" and make sure it is center aligned.

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

Resources