How to keep a changing view centered? - swift

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

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 update NSView layer position and anchor point for transform simultaneously with NSView frame for mouse events

I am currently trying to implement some custom NSViews that will be animate by certain events in a macOS app. The controls are along the lines of slider you would find in an AudioUnit Plugin.
The question of animation has been answered in this question.
What I am uncertain of is how to alter a view's CALayer position and anchor point for the rotation and updating its frame for mouse events.
As a basic example, I wish to draw a square by creating an NSView and setting it's background colour. I then want to animate the rotation of the view, a la the previous link, on a mouseDown event.
With following setup, the view is moved to the centre of window, its anchor point is altered so that the view rotates around it and not around the origin of the window.contentView!. However, moving the view.layer.position and setting the view.layer!.anchorPoint = CGPoint(x: 0.5,y: 0.5) does not also move the frame that will detect events.
#IBOutlet weak var window: NSWindow!
func applicationDidFinishLaunching(_ aNotification: Notification)
{
let view = customView(frame: NSRect(x: 0, y: 0, width: 50, height: 50))
window.contentView!.wantsLayer = true
view.wantsLayer = true
view.layer?.backgroundColor = NSColor.blue.cgColor
window.contentView?.addSubview(view)
let containerFrame = window.contentView!.frame
let center = CGPoint(x: containerFrame.midX, y: containerFrame.midY)
view.layer?.position = center
view.layer?.anchorPoint = CGPoint(x: 0.5,y: 0.5)
}
The customView in this case is simply an NSView with the rotation animation from the previous link executed on mouseDown
override func mouseDown(with event: NSEvent)
{
let timeToRotate = 1
let rotateAnimation = CABasicAnimation(keyPath: "transform.rotation")
rotateAnimation.fromValue = 0.0
rotateAnimation.toValue = angle
rotateAnimation.duration = timeToRotate
rotateAnimation.repeatCount = .infinity
self.layer?.add(rotateAnimation, forKey: nil)
}
Moving the frame by setting view.frame = view.layer.frame results in the view rotating around one of its corners. So, instead I have altered the view.setFrameOrigin()
#IBOutlet weak var window: NSWindow!
func applicationDidFinishLaunching(_ aNotification: Notification)
{
let view = customView(frame: NSRect(x: 0, y: 0, width: 50, height: 50))
window.contentView!.wantsLayer = true
view.wantsLayer = true
view.layer?.backgroundColor = NSColor.blue.cgColor
window.contentView?.addSubview(view)
let containerFrame = window.contentView!.frame
let center = CGPoint(x: containerFrame.midX, y: containerFrame.midY)
let offsetFrameOrigin = CGPoint(x: center.x - view.bounds.width/2,
y: center.y - view.bounds.height/2)
view.setFrameOrigin(offsetFrameOrigin)
view.layer?.position = center
view.layer?.anchorPoint = CGPoint(x: 0.5,y: 0.5)
}
Setting the view.frame origin to an offset of the centre achieves what I want, but I cannot help but feel this is a little 'hacky' and that I may be approaching this the wrong way. Especially since any further change to view.layer or view.frame will result in either the animation being incorrect or events being detected outside what is drawn.
How can I alter NSView.layer so that it rotates around its centre at the same time as setting the NSView.frame so that mouse events are detected in the correct area?
Also, is altering NSView.layer.anchorPoint a correct way to set it up for rotation around its centre?
I think I'm in much the same position as you. Fortunately, I stumbled across a gist that extends NSView with a setAnchorPoint function that does the job for me (keeping the layer's anchor in sync with the main frame).
There's a fork for that gist for Swift 4. Here's the code itself:
extension NSView
{
func setAnchorPoint (anchorPoint:CGPoint)
{
if let layer = self.layer
{
var newPoint = CGPoint(x: self.bounds.size.width * anchorPoint.x, y: self.bounds.size.height * anchorPoint.y)
var oldPoint = CGPoint(x: self.bounds.size.width * layer.anchorPoint.x, y: self.bounds.size.height * layer.anchorPoint.y)
newPoint = newPoint.applying(layer.affineTransform())
oldPoint = oldPoint.applying(layer.affineTransform())
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 = anchorPoint
}
}
}
You'd then use it like this on the NSView directly (i.e. not its layer):
func applicationDidFinishLaunching(_ aNotification: Notification)
{
// ... //
let view = customView(frame: NSRect(x: 0, y: 0, width: 50, height: 50))
view.wantsLayer = true
view.setAnchorPoint(anchorPoint: CGPoint(x: 0.5, y: 0.5))
// ... //
}

How to properly transform UIView's scale on UIScrollView movement

To have a similar effect to Snapchat's HUD movement, I have created a movement of the HUD elements based on UIScollView's contentOffset. Edit: Link to the Github project.
func scrollViewDidScroll(_ scrollView: UIScrollView) {
self.view.layoutIfNeeded()
let factor = scrollView.contentOffset.y / self.view.frame.height
self.transformElements(self.playButton,
0.45 + 0.55 * factor, // 0.45 = desired scale + 0.55 = 1.0 == original scale
Roots.screenSize.height - 280, // 280 == original Y
Roots.screenSize.height - 84, // 84 == minimum desired Y
factor)
}
func transformElements(_ element: UIView?,
_ scale: CGFloat,
_ originY: CGFloat,
_ desiredY: CGFloat,
_ factor: CGFloat) {
if let e = element {
e.transform = CGAffineTransform(scaleX: scale, y: scale) // this line lagging
let resultY = desiredY + (originY - desiredY) * factor
var frame = e.frame
frame.origin.y = resultY
e.frame = frame
}
}
With this code implemented the scroll as well as the transition appeared to be "laggy"/not smooth. (Physical iPhone 6S+ and 7+).
Deleting the following line: e.transform = CGAffineTransform(scaleX: scale, y: scale) erased the issue. The scroll as well as the Y-movement of the UIView object is smooth again.
What's the best approach to transform the scale of an object?
There are no Layout Constraints.
func setupPlayButton() {
let rect = CGRect(x: Roots.screenSize.width / 2 - 60,
y: Roots.screenSize.height - 280,
width: 120,
height: 120)
self.playButton = UIButton(frame: rect)
self.playButton.setImage(UIImage(named: "playBtn")?.withRenderingMode(.alwaysTemplate), for: .normal)
self.playButton.tintColor = #colorLiteral(red: 1, green: 1, blue: 1, alpha: 1)
self.view.addSubview(playButton)
}
This is happening because you are applying both: transform and frame. It will be smoother, if you apply only transform. Update your transformElements function as below:
func transformElements(_ element: UIView?,
_ scale: CGFloat,
_ originY: CGFloat,
_ desiredY: CGFloat,
_ factor: CGFloat) {
if let e = element {
e.transform = CGAffineTransform(scaleX: scale, y: scale).translatedBy(x: 0, y: desiredY * (1 - factor))
}
}
You can make these kinds of animation smoother by creating an animation then setting the speed of the layer to 0 and then changing the timeOffset of the layer.
first add the animation in the setupPlayButton method
let animation = CABasicAnimation.init(keyPath: "transform.scale")
animation.fromValue = 1.0
animation.toValue = 0.45
animation.duration = 1.0
//Set the speed of the layer to 0 so it doesn't animate until we tell it to
self.playButton.layer.speed = 0.0;
self.playButton.layer.add(animation, forKey: "transform");
next in the scrollViewDidScroll change the timeOffset of the layer and move the center of the button.
if let btn = self.playButton{
var factor:CGFloat = 1.0
if isVertically {
factor = scrollView.contentOffset.y / self.view.frame.height
} else {
factor = scrollView.contentOffset.x / Roots.screenSize.width
var transformedFractionalPage: CGFloat = 0
if factor > 1 {
transformedFractionalPage = 2 - factor
} else {
transformedFractionalPage = factor
}
factor = transformedFractionalPage;
}
//This will change the size
let timeOffset = CFTimeInterval(1-factor)
btn.layer.timeOffset = timeOffset
//now change the positions. only use center - not frame - so you don't mess up the animation. These numbers aren't right I don't know why
let desiredY = Roots.screenSize.height - (280-60);
let originY = Roots.screenSize.height - (84-60);
let resultY = desiredY + (originY - desiredY) * (1-factor)
btn.center = CGPoint.init(x: btn.center.x, y: resultY);
}
I couldn't quite get the position of the button correct - so something is wrong with my math there, but I trust you can fix it.
If you want more info about this technique see here: http://ronnqvi.st/controlling-animation-timing/

Swift: How to anchor a layer to view?

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.

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