Swift: How to anchor a layer to view? - swift

I've been trying to anchor a CAShapeLayer to a view for a few days already. I couldn't find the solution for this. This is a facetracker project. I want to create a round CAShapeLayer that I could place in a fixed position in the eyeglassesView. However, the view resizes when you turn your head, so the CAShapeLayer couldn't stay on top of the eyeglasses to serve as the lens. I plan to create some animation but I need the CAShapeLayer to stick to its place first regardless of which angle I move my face.
func faceTrackerDidUpdate(points: FacePoints?) {
let eyeCornerDist = sqrt(pow(points.leftEye[0].x - points.rightEye[5].x, 2) + pow(points.leftEye[0].y - points.rightEye[5].y, 2))
let eyeToEyeCenter = CGPointMake((points.leftEye[0].x + points.rightEye[5].x) / 2, (points.leftEye[0].y + points.rightEye[5].y) / 2)
let eyeglassesWidth = 1.5 * eyeCornerDist
let eyeglassesHeight = (eyeglassesView.image!.size.height / eyeglassesView.image!.size.width) * eyeglassesWidth
eyeglassesView.transform = CGAffineTransformIdentity
eyeglassesView.frame = CGRectMake(eyeToEyeCenter.x - eyeglassesWidth / 2, eyeToEyeCenter.y - 0.5 * eyeglassesHeight, eyeglassesWidth, eyeglassesHeight)
let layer = CAShapeLayer()
layer.anchorPoint = CGPointMake(0.0, 1.0)
layer.position = CGPointMake(0.0, 150.0)
layer.path = UIBezierPath(roundedRect: CGRect(x: 0, y: 0, width: 80, height: 80), cornerRadius: 80).CGPath
layer.fillColor = UIColor(white: 1, alpha: 0.08).CGColor
eyeglassesView.layer.addSublayer(layer)
eyeglassesView.hidden = false
setAnchorPoint(CGPointMake(0.5, 1.0), forView: eyeglassesView)
let angle = atan2(points.rightEye[5].y - points.leftEye[0].y, points.rightEye[5].x - points.leftEye[0].x)
eyeglassesView.transform = CGAffineTransformMakeRotation(angle)
}
func setAnchorPoint(anchorPoint: CGPoint, forView view: UIView) {
var newPoint = CGPointMake(view.bounds.size.width * anchorPoint.x, view.bounds.size.height * anchorPoint.y)
var oldPoint = CGPointMake(view.bounds.size.width * view.layer.anchorPoint.x, view.bounds.size.height * view.layer.anchorPoint.y)
newPoint = CGPointApplyAffineTransform(newPoint, view.transform)
oldPoint = CGPointApplyAffineTransform(oldPoint, view.transform)
var position = view.layer.position
position.x -= oldPoint.x
position.x += newPoint.x
position.y -= oldPoint.y
position.y += newPoint.y
view.layer.position = position
view.layer.anchorPoint = anchorPoint
}
I tried different things, position, anchorpoint, masktobounds, transform but I really don't know how to make it work. Any help would be greatly appreciated. Let me know if there's anything I'm missing.

Related

Animating a 360 degree rotation around another view's center point

I'm making a loading spinner animation that pushes a view out from the middle, and then rotates all the way around the center view back to it's original location. This is what I am trying to achieve:
The inner arrow moves the view away from the center. I've already achieved this, the part I am stuck on is then rotating the view around the center view. I've read various other StackOverflow posts but have not been close to achieving this.
Code so far:
UIView.animate(withDuration: 0.5) {
self.topView.transform = CGAffineTransform(translationX: 20, y: -20)
} completion: { _ in
self.topView.setAnchorPoint(self.centerView.center)
// Rotate
}
}
Here is how I am setting the anchor point of the view. I'm using this as the view disappears when setting its anchor point otherwise.
func setAnchorPoint(_ point: CGPoint) {
var newPoint = CGPoint(x: bounds.size.width * point.x, y: bounds.size.height * point.y)
var oldPoint = CGPoint(x: bounds.size.width * layer.anchorPoint.x, y: bounds.size.height * layer.anchorPoint.y);
newPoint = newPoint.applying(transform)
oldPoint = oldPoint.applying(transform)
var position = layer.position
position.x -= oldPoint.x
position.x += newPoint.x
position.y -= oldPoint.y
position.y += newPoint.y
layer.position = position
layer.anchorPoint = point
}
Once the full 360 rotation is complete I would then need to move the view back in towards the center, completing the animation.
For the part when the loading view rotates around the circle view, you can use a UIBezierPath and create a CAKeyframeAnimation based on its path.
Take a look at this implementation. Hope it helps.
class LoadingViewController: UIViewController {
var circlePath: UIBezierPath!
lazy var loader = makeLoader()
lazy var centerView = makeCenterView()
override func viewDidLoad() {
super.viewDidLoad()
setup()
}
func makeLoader() -> UIButton {
let padding: CGFloat = 100
let width = self.view.frame.width - (2 * padding)
let b = UIButton(frame: CGRect(x: padding, y: 200, width: width, height: 50))
b.addTarget(self, action: #selector(didTap), for: .touchUpInside)
b.backgroundColor = .blue
return b
}
func makeCenterView() -> UIView {
let width: CGFloat = 20
let height: CGFloat = 20
let x = self.view.center.x - width/2
let y = self.view.center.y - height/2
let view = UIView(frame: CGRect(x: x, y: y, width: width, height: height))
view.backgroundColor = .green
return view
}
func setup() {
//create a UIBezierPath with a center that is at the center of the green view and a radius that has a length as the distance between the green view and the blue view.
let arcCenterX = centerView.center.x
let arcCenterY = centerView.center.y
let arcCenter = CGPoint(x: arcCenterX, y: arcCenterY)
let radius = arcCenterY - loader.center.y
let startAngle = -CGFloat.pi/2
let endAngle = CGFloat.pi*(1.5)
let arcPath = UIBezierPath(arcCenter: arcCenter, radius: radius, startAngle: startAngle, endAngle: endAngle, clockwise: true)
self.circlePath = arcPath
self.view.addSubview(loader)
self.view.addSubview(centerView)
}
#objc func didTap() {
//create a CAKeyframeAnimation with a path that matches the UIBezierPath above.
let loadAnimation = CAKeyframeAnimation(keyPath: "position")
loadAnimation.path = self.circlePath.cgPath
loadAnimation.calculationMode = .paced
loadAnimation.duration = 2.0
loadAnimation.rotationMode = .rotateAuto
loadAnimation.repeatCount = Float(CGFloat.greatestFiniteMagnitude)
loader.layer.add(loadAnimation, forKey: "circleAnimation")
}
}

How to keep a changing view centered?

I am creating a UIView that is basically a transparent UIView with a border.
This view is being animated using this code
UIView.animate(withDuration: 1.0, delay: 0, options: [.repeat, .autoreverse], animations: {
var frame = areaScanRect.frame
frame.size.width = frame.size.width * 0.8
frame.size.height = frame.size.height * 1.2
areaScanRect.frame = frame
}, completion: nil)
The view has its frame like (0,0,200,100).
I need this view to be centered on the main view.
If I simply add the rectangle to the main view, it appears at (0,0), not at the screen center obviously.
Then I add constraints, to make it center.
areaScanRect.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
areaScanRect.centerXAnchor.constraint(equalTo: view.centerXAnchor),
areaScanRect.centerYAnchor.constraint(equalTo: view.centerYAnchor),
areaScanRect.widthAnchor.constraint(equalToConstant: areaScanRect.bounds.size.width),
areaScanRect.heightAnchor.constraint(equalToConstant: areaScanRect.bounds.size.height)
])
areaScanRect shows at the correct position but as the animation goes, is not being centered.
Why?
May be this code will work:
'''
func setMyView(_point: CGPoint) {
var newView = CGPoint(x: bounds.size.width * point.x, y: bounds.size.height * point.y)
var oldView = CGPoint(x: bounds.size.width * layer.anchorPoint.x, y: bounds.size.height * layer.anchorPoint.y);
newView = newView.applying(transform)
oldView = oldView.applying(transform)
var position = layer.position
position.x -= oldView.x
position.x += newView.x
position.y -= oldView.y
position.y += newView.y
layer.position = position
layer.anchorView = point
}
}
Try this:
myView.center.x = view.center.x
myView.center.y = view.center.y
It will centre your view.
However I'd suggest only placing your view inside the view setting its Y and X coordinates instead of using constraints

How to generate UIImages in a hexagon "map" view

I'm building an app where I want to display profilePictures of people "near you" in a hexagon beehive style.
The full beehive should be draggable, like google maps for example.
My question is if this is something I can do with just using UIKit, or if it would be easier to use UIKit and SpriteKit together.
I hope someone could point me at the right direction and or have some ideas on how this could be made. Thank you for your time!
Update:
Just to make my question a bit more clear.
This is how my view looks like atm
And this is what I want to achieve
In the first image I´ve just set the X and Y pos of the UIImage center middle.
I want to create some sort of function that can get an array of different profilePics and then put the out in this pattern.
UIKit alone can do the job: you should try to setup a mask with CALayer on a UIImageView for instance.
The draggable behavior thing can be achieved either with a UIScrollView by adding and arranging all your image subviews in it, or with a UICollectionView with a custom flow, but it may be much harder to set up.
For the hexagon views, you'll find here an interesting example you can adapt for your usage: http://sapandiwakar.in/make-hexagonal-view-on-ios/
Here is an adaption of Sapan Diwakar solution in Swift 4.2 and using extensions:
extension UIBezierPath {
convenience init(roundedPolygonPathInRect rect: CGRect, lineWidth: CGFloat, sides: NSInteger, cornerRadius: CGFloat = 0, rotationOffset: CGFloat = 0) {
self.init()
let theta: CGFloat = 2.0 * CGFloat.pi / CGFloat(sides) // How much to turn at every corner
let width = min(rect.size.width, rect.size.height) // Width of the square
let center = CGPoint(x: rect.origin.x + width / 2.0, y: rect.origin.y + width / 2.0)
// Radius of the circle that encircles the polygon
// Notice that the radius is adjusted for the corners, that way the largest outer
// dimension of the resulting shape is always exactly the width - linewidth
let radius = (width - lineWidth + cornerRadius - (cos(theta) * cornerRadius)) / 2.0
// Start drawing at a point, which by default is at the right hand edge
// but can be offset
var angle = CGFloat(rotationOffset)
let corner = CGPoint(x: center.x + (radius - cornerRadius) * cos(angle), y: center.y + (radius - cornerRadius) * sin(angle))
move(to: CGPoint(x: corner.x + cornerRadius * cos(angle + theta), y: corner.y + cornerRadius * sin(angle + theta)))
for _ in 0 ..< sides {
angle += theta
let corner = CGPoint(x: center.x + (radius - cornerRadius) * cos(angle), y: center.y + (radius - cornerRadius) * sin(angle))
let tip = CGPoint(x: center.x + radius * cos(angle), y: center.y + radius * sin(angle))
let start = CGPoint(x: corner.x + cornerRadius * cos(angle - theta), y: corner.y + cornerRadius * sin(angle - theta))
let end = CGPoint(x: corner.x + cornerRadius * cos(angle + theta), y: corner.y + cornerRadius * sin(angle + theta))
addLine(to: start)
addQuadCurve(to: end, controlPoint: tip)
}
close()
}
}
extension UIImageView {
func setupHexagonMask(lineWidth: CGFloat, color: UIColor, cornerRadius: CGFloat) {
let path = UIBezierPath(roundedPolygonPathInRect: bounds, lineWidth: lineWidth, sides: 6, cornerRadius: cornerRadius, rotationOffset: CGFloat.pi / 2.0).cgPath
let mask = CAShapeLayer()
mask.path = path
mask.lineWidth = lineWidth
mask.strokeColor = UIColor.clear.cgColor
mask.fillColor = UIColor.white.cgColor
layer.mask = mask
let border = CAShapeLayer()
border.path = path
border.lineWidth = lineWidth
border.strokeColor = color.cgColor
border.fillColor = UIColor.clear.cgColor
layer.addSublayer(border)
}
}
And then you can just use it like that:
let image = UIImageView(frame: CGRect(x: 30, y: 30, width: 300, height: 300))
image.contentMode = .scaleAspectFill
image.image = UIImage(named: "lenna.png")
image.setupHexagonMask(lineWidth: 5.0, color: .white, cornerRadius: 20.0)
view.addSubview(image)
EDIT: As I told you, the easiest way is to use a UIScrollView to display your map, and with simple math you can display your hexagons the way you want.
Here is a small example you must adapt to match your requirements. For example you should be extra careful with performance. This example should not be used as is, if you have many images, you should load them on the fly and remove them when you don't show them. And you can even think using a background rendering if it takes too much fps...
Assuming view is a UIScrollView:
let hexaDiameter : CGFloat = 150
let hexaWidth = hexaDiameter * sqrt(3) * 0.5
let hexaWidthDelta = (hexaDiameter - hexaWidth) * 0.5
let hexaHeightDelta = hexaDiameter * 0.25
let spacing : CGFloat = 5
let rows = 10
let firstRowColumns = 6
view.contentSize = CGSize(width: spacing + CGFloat(firstRowColumns) * (hexaWidth + spacing),
height: spacing + CGFloat(rows) * (hexaDiameter - hexaHeightDelta + spacing) + hexaHeightDelta)
for y in 0..<rows {
let cellsInRow = y % 2 == 0 ? firstRowColumns : firstRowColumns - 1
let rowXDelta = y % 2 == 0 ? 0.0 : (hexaWidth + spacing) * 0.5
for x in 0..<cellsInRow {
let image = UIImageView(frame: CGRect(x: rowXDelta + CGFloat(x) * (hexaWidth + spacing) + spacing - hexaWidthDelta,
y: CGFloat(y) * (hexaDiameter - hexaHeightDelta + spacing) + spacing,
width: hexaDiameter,
height: hexaDiameter))
image.contentMode = .scaleAspectFill
image.image = UIImage(named: "lenna.png")
image.setupHexagonMask(lineWidth: 5.0, color: .white, cornerRadius: 10.0)
view.addSubview(image)
}
}

Drawing hexagon image using Swift 4

public func roundedPolygonPath(rect: CGRect, lineWidth: CGFloat, sides: NSInteger, cornerRadius: CGFloat, rotationOffset: CGFloat = 0) -> UIBezierPath {
let path = UIBezierPath()
let theta: CGFloat = CGFloat(2.0 * M_PI) / CGFloat(sides) // How much to turn at every corner
let offset: CGFloat = cornerRadius * tan(theta / 2.0) // Offset from which to start rounding corners
let width = min(rect.size.width, rect.size.height) // Width of the square
let center = CGPoint(x: rect.origin.x + width / 2.0, y: rect.origin.y + width / 2.0)
// Radius of the circle that encircles the polygon
// Notice that the radius is adjusted for the corners, that way the largest outer
// dimension of the resulting shape is always exactly the width - linewidth
let radius = (width - lineWidth + cornerRadius - (cos(theta) * cornerRadius)) / 2.0
// Start drawing at a point, which by default is at the right hand edge
// but can be offset
var angle = CGFloat(rotationOffset)
let corner = CGPoint(x: center.x + (radius - cornerRadius) * cos(angle),y: center.y + (radius - cornerRadius) * sin(angle))
path.move(to: CGPoint(x: corner.x + cornerRadius * cos(angle + theta),y: corner.y + cornerRadius * sin(angle + theta)))
for _ in 0..<sides {
angle += theta
let corner = CGPoint(x: center.x + (radius - cornerRadius) * cos(angle),y: center.y + (radius - cornerRadius) * sin(angle))
let tip = CGPoint(x: center.x + radius * cos(angle),y: center.y + radius * sin(angle))
let start = CGPoint(x: corner.x + cornerRadius * cos(angle - theta),y: corner.y + cornerRadius * sin(angle - theta))
let end = CGPoint(x: corner.x + cornerRadius * cos(angle + theta), y: corner.y + cornerRadius * sin(angle + theta))
path.addLine(to: start)
path.addQuadCurve(to: end, controlPoint: tip)
}
path.close()
// Move the path to the correct origins
let bounds = path.bounds
let transform = CGAffineTransform(translationX: -bounds.origin.x + rect.origin.x + lineWidth / 2.0, y: -bounds.origin.y + rect.origin.y + lineWidth / 2.0)
path.apply(transform)
return path
}
public func createImage(layer: CALayer) -> UIImage {
let size = CGSizeMake(CGRect.maxX(layer.frame), CGRect.maxY(layer.frame))
UIGraphicsBeginImageContextWithOptions(size, layer.isOpaque, 0.0)
let ctx = UIGraphicsGetCurrentContext()
layer.render(in: ctx!)
let image = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
return image!
}
let lineWidth = CGFloat(7.0)
let rect = CGRectMake(0.0, 0.0, 150.0, 150.0)
let sides = 6
var path = roundedPolygonPath(rect, lineWidth, sides, 15.0, rotationOffset: CGFloat(-M_PI / 2.0))
let borderLayer = CAShapeLayer()
borderLayer.frame = CGRectMake(0.0, 0.0, path.bounds.width + lineWidth, path.bounds.height + lineWidth)
borderLayer.path = path.CGPath
borderLayer.lineWidth = lineWidth
borderLayer.lineJoin = kCALineJoinRound
borderLayer.lineCap = kCALineCapRound
borderLayer.strokeColor = UIColor.blackColor().CGColor
borderLayer.fillColor = UIColor.whiteColor().CGColor
var image = createImage(borderLayer)}[enter image description here][1]
I cannot convert the Swift 3 with CGRect and CGSizeMake functions to latest Swift 4.
The following errors is due to the conversion of Swift 3 example. I have found,
'CGRectMake' is unavailable in Swift
Extensions may not contain stored properties
'CGSizeMake' is unavailable in Swift
*Im trying to draw the path **
Instead of CGSizeMake you now can use CGSize.init, e.g. CGSize(width: 100, height: 50).
Instead of the static function CGRect.maxX you now can use the member method of the same name: CGRect.maxX(layer.frame) becomes layer.frame.maxX.

Scale with CGAffineTransform and set the anchor

If I understand correctly scaling a UIView with CGAffineTransform anchors the transformation to its center.
In particular:
self.frame = CGRectMake(0,0,100,100);
self.transform = CGAffineTransformMakeScale(2, 2);
NSLog(#"%f;%f;%f;%f", self.frame.origin.x, self.frame.origin.y, self.frame.size.width, self.frame.size.height);
Prints:
-50;-50;200;200
How do you create a CGAffineTransform scale that uses a specific anchor point (say 0;0)?
(a)
Scale and then translate?
Something like :
CGAffineTransform t = CGAffineTransformMakeScale(2, 2);
t = CGAffineTransformTranslate(t, width/2, height/2);
self.transform = t;
(b)
Set the anchor point (which is probably what you want really)
[self layer].anchorPoint = CGPointMake(0.0f, 0.0f);
self.transform = CGAffineTransformMakeScale(2, 2);
(c)
Set the center again to make sure it's in the same place?
CGPoint center = self.center;
self.transform = CGAffineTransformMakeScale(2, 2);
self.center = center;
Firstly #import <QuartzCore/QuartzCore.h> and then set the anchor points of your view:
[[self layer] setAnchorPoint:CGPointMake(0, 0)];
This is the way I found the scale a view while keeping it's origin in place (0,0) in Swift5
func animate() {
myView.setAnchorPoint(CGPoint(x: 0, y: 0))
myView.transform = CGAffineTransform(translationX: -0.5, y: -0.5)
let animator = UIViewPropertyAnimator(duration: 0.3, curve: .easeOut) {
self.myView.transform = CGAffineTransform(scaleX: 0.25, y: 0.25)
}
}
extension UIView {
func setAnchorPoint(_ point: CGPoint) {
var newPoint = CGPoint(x: bounds.size.width * point.x, y: bounds.size.height * point.y)
var oldPoint = CGPoint(x: bounds.size.width * layer.anchorPoint.x, y: bounds.size.height * layer.anchorPoint.y);
newPoint = newPoint.applying(transform)
oldPoint = oldPoint.applying(transform)
var position = layer.position
position.x -= oldPoint.x
position.x += newPoint.x
position.y -= oldPoint.y
position.y += newPoint.y
layer.position = position
layer.anchorPoint = point
}
}
credit to Hacking with Swift