I have created this width animation which creates a border-bottom for my TextField and I want to animate it so that when the user clicks on the TextField the CALayer goes from left to right and then appear. the problem is that the animation start from the center and then the width grows to each side while I want it to start from left to right... What am I missing ?
override func didAddSubview(_ subview: UIView) {
let shape = CALayer()
shape.frame = CGRect(x: 0, y: self.height, width: self.width, height: 1)
shape.frame.origin.x = 0
shape.backgroundColor = UIColor.red.cgColor
self.layer.addSublayer(shape)
let boundsAnim:CABasicAnimation = CABasicAnimation(keyPath: "bounds.size.width")
boundsAnim.fromValue = 0
boundsAnim.toValue = self.width
boundsAnim.duration = 2
shape.add(boundsAnim, forKey: "bounds.size.width")
}
Set the anchor point to the left side
shape.anchorPoint = CGPoint(x: 0, y: 0.5)
Related
I created an arbitrary view
let middleView = UIView(
frame: CGRect(x: 0.0,
y: view.frame.height/4,
width: view.frame.width,
height: view.frame.height/4))
middleView.backgroundColor = UIColor.blue
view.addSubview(middleView)
Then I created a circle using UIBezierPath; however when I set the position to middleView.center, the circle is far off to the bottom of the view. Can you set the position in the center of a subview?
let shapeLayer = CAShapeLayer()
let circlePath = UIBezierPath(
arcCenter: .zero,
radius: 100,
startAngle: CGFloat(0).toRadians(),
endAngle: CGFloat(360).toRadians(),
clockwise: true)
shapeLayer.path = circlePath.cgPath
shapeLayer.strokeColor = UIColor.purple.cgColor
shapeLayer.fillColor = UIColor.clear.cgColor
shapeLayer.position = middleView.center
middleView.layer.addSublayer(shapeLayer)
How do I center this circle in that view?
You have two problems.
First, you are setting shapeLayer.position = middleView.center. The center of a view is is the superview's geometry. In other words, middleView.center is relative to view, not to middleView. But then you're adding shapeLayer as a sublayer of middleView.layer, which means shapeLayer needs a position that is in middleView's geometry, not in view's geometry. You need to set shapeLayer.position to the center of middleView.bounds:
shapeLayer.position = CGPoint(x: middleView.bounds.midX, y: middleView.bounds.midY)
Second, you didn't say where you're doing all this. My guess is you're doing it in viewDidLoad. But that is too early. In viewDidLoad, the views loaded from the storyboard still have the frames they were given in the storyboard, and haven't been laid out for the current device's screen size yet. So it's a bad idea to look at frame (or bounds or center) in viewDidLoad if you don't do something to make sure that things will be laid out correctly during the layout phase. Usually you do this by setting the autoresizingMask or creating constraints. Example:
let middleView = UIView(
frame: CGRect(x: 0.0,
y: view.frame.height/4,
width: view.frame.width,
height: view.frame.height/4))
middleView.backgroundColor = UIColor.blue
middleView.autoresizingMask = [.flexibleWidth, .flexibleHeight, .flexibleTopMargin, .flexibleBottomMargin]
view.addSubview(middleView)
However, shapeLayer doesn't belong to a view, so it doesn't have an autoresizingMask and can't be constrained. You have to lay it out in code. You could do that, but it's better to just use a view to manage the shape layer. That way, you can use autoresizingMask or constraints to control the layout of the shape, and you can set it up in viewDidLoad.
let circleView = CircleView(frame: CGRect(x: 0, y: 0, width: 200, height: 200))
circleView.center = CGPoint(x: middleView.bounds.midX, y: middleView.bounds.midY)
circleView.autoresizingMask = [.flexibleLeftMargin, .flexibleRightMargin, .flexibleTopMargin, .flexibleBottomMargin]
circleView.shapeLayer.strokeColor = UIColor.purple.cgColor
circleView.shapeLayer.fillColor = nil
middleView.addSubview(circleView)
...
class CircleView: UIView {
override class var layerClass: AnyClass { return CAShapeLayer.self }
var shapeLayer: CAShapeLayer { return layer as! CAShapeLayer }
override func layoutSubviews() {
super.layoutSubviews()
shapeLayer.path = UIBezierPath(ovalIn: bounds).cgPath
}
}
Result:
And after rotating to landscape:
I'm trying to centre a UIView in another view (so like a pop up view), but whatever I do, I just cannot align it centrally!
func loadPopUpView() {
let customView = CGRect(x: view.center.x, y: view.center.y, width: 100, height: 100)
popUpView = UIView(frame: customView)
popUpView.backgroundColor = UIColor.black
view.addSubview(popUpView)
popUpView.isHidden = false
}
I've changed the background colour to black just so I know when it appears.
I cannot do this with storyboard because it's going on a tableView, so I'm trying to do it programmatically. Result Image
You also need to minus half value of your custom view height and width from x and y like below:
let customView = CGRect(x: view.center.x - 50, y: view.center.y - 50, width: 100, height: 100)
You are telling it to put top left corner in the center, hence you need to let it get half size in position.
let customView = CGRect(x: view.center.x-50, y: view.center.y-50, width: 100, height: 100)
Another solution is the use anchorpoints.
customView.centerXAnchor.constraint(equalTo: self.centerXAnchor).isActive = true
customView.centerYAnchor.constraint(equalTo: self.centerYAnchor).isActive = true
customView.heightAnchor.constraint(equalToConstant: 100).isActive = true
customView.widthAnchor.constraint(equalToConstant: 100).isActive = true
On CGRect(x:, y:, width:, height:), the point (x,y) is the origin. In iOS, that's the top left point.
On the CGRect doc:
In the default Core Graphics coordinate space, the origin is located
in the lower-left corner of the rectangle and the rectangle extends
towards the upper-right corner. If the context has a
flipped-coordinate space—often the case on iOS—the origin is in the
upper-left corner and the rectangle extends towards the lower-right
corner.
So to fix this:
let width = 100.0
let height = 100.0
let customViewFrame = CGRect(x: view.center.x - width/2.0, y: view.center.y - height/2.0, width: width, height: height)
Another solution would be to apply the center once the frame (especially the width/size) have been set.
let customViewFrame = CGRect(x: 0, y: 0, width: 100, height: 100)
customViewFrame = view.center
i'm trying to learn how to draw shapes and animate it, i succeeded at both and i managed to draw shapes in the center of my screen and in alignment to each other, but when i change my simulator to any device other than my view as device they jump out of the center of the screen, is there any way to set constraints to my drawn shapes so that they are always in the center?
here is my code:
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
// rect
let layer = CAShapeLayer()
let bounds = CGRect(x: 60, y: 200, width: 250, height: 250)
layer.path = UIBezierPath(roundedRect: bounds, byRoundingCorners: .allCorners, cornerRadii: CGSize(width: 20, height: 20)).cgPath
layer.strokeColor = UIColor.white.cgColor
layer.fillColor = UIColor.clear.cgColor
layer.lineWidth = 4
view.layer.addSublayer(layer)
let rectAnimation = CABasicAnimation(keyPath: "strokeEnd")
rectAnimation.fromValue = 0
rectAnimation.toValue = 1
rectAnimation.duration = 2
layer.add(rectAnimation, forKey: "line")
}
A powerful tool to give constraint is Masonry, you will really appreciate it. It's super easy for setting constraints and for animating them;
Else you can set your x and y "bounds" coordinates to be at centerX and centerY too
I wrote the below code to make the textfield only have bottom line,
the result show as the below image (the bottom line is not long enough to the right edge.)
func setBottomBorder() {
self.borderStyle = .none
self.layer.backgroundColor = UIColor.clear.cgColor
self.attributedPlaceholder = NSAttributedString(string:self.placeholder != nil ? self.placeholder! : "", attributes: [NSAttributedStringKey.foregroundColor: UIColor.white])
let border = CALayer()
let width = CGFloat(2.0)
border.borderColor = UIColor.white.cgColor
border.frame = CGRect(x: 0, y: self.frame.size.height - width, width: self.frame.size.width, height: self.frame.size.height)
border.borderWidth = width
self.layer.addSublayer(border)
self.layer.masksToBounds = true
}
As per My suggestion instead of progrmatically.
you should set it using StoryBoard:
A textfield with Hidden Border.
Below that you can take a label with white background and it is done.
Note:- Do not forget to give correct constraints to both so that it looks good
I'm creating a bare-bones window programmatically and I just need to draw some lines in its content view. The size of the area the lines fall in (worldBounds) needs to be scaled down and I assume I need the contentView bounds origin to be the same as worldBounds origin so it can draw everything. The approach is to set the frame to 1/8 the size, then use setBoundsOrigin and setBoundsSize on the contentView, which I understand scales the coordinate system.
The problem is no drawing appears. The window appears with the correct size. I have a function drawTestLines set up just to test it. If I remove the calls to setBounds, everything draws fine, but of course then the bounds don't match worldBounds. Any help is much appreciated!
var worldBounds = NSRect()
let scale: CGFloat = 0.125
func drawMap() {
boundLineStore(lineStore, &worldBounds)
worldBounds.origin.x -= 8
worldBounds.origin.y -= 8
worldBounds.size.width += 16
worldBounds.size.height += 16
if !draw {
return
}
let scaled = NSRect(x: 300.0, // to set the window size/position
y: 80,
width: worldBounds.size.width*scale, // 575
height: worldBounds.size.height*scale) // 355
window = NSWindow(contentRect: scaled,
styleMask: .titled,
backing: .buffered,
defer: false)
window.display()
window.orderFront(nil)
window.contentView!.setBoundsSize(worldBounds.size) // (4593, 2833)
window.contentView!.setBoundsOrigin(worldBounds.origin) // (-776, -4872)
// Draw map lines
window.contentView!.lockFocus()
drawTestLines(in: window.contentView!)
window.contentView!.unlockFocus()
}
// for testing
func drawTestLines(in view: NSView) {
let bottom = view.bounds.minY
let top = view.bounds.maxY
let left = view.bounds.minX
let right = view.bounds.maxX
NSColor.black.setStroke()
for i in Int(left)..<Int(right) { // draw vertical lines across the view just to see if it works!
if i%64 == 0 {
NSBezierPath.strokeLine(from: NSPoint(x: CGFloat(i), y: bottom), to: NSPoint(x: CGFloat(i), y: top))
}
}
NSBezierPath.strokeLine(from: NSPoint(x: left, y: bottom), to: NSPoint(x: right, y: top))
NSBezierPath.strokeLine(from: NSPoint.zero, to: NSPoint(x: 50.0, y: 50.0))
}
Experiment: Create a new Xcode project. Change applicationDidFinishLaunching:
func applicationDidFinishLaunching(_ aNotification: Notification) {
if let contentBounds = window.contentView?.bounds {
let scale: CGFloat = 0.5
let worldBounds = CGRect(x: 0.0, y: 0.0, width: contentBounds.size.width * scale, height: contentBounds.size.height * scale)
window.contentView!.bounds = worldBounds
}
}
Change the class of the content view of the window in IB to MyView. Add a new class MyView, subclass of NSView. Change draw to:
override func draw(_ dirtyRect: NSRect) {
super.draw(dirtyRect)
let bottom = self.bounds.minY
let top = self.bounds.maxY
let left = self.bounds.minX
let right = self.bounds.maxX
NSColor.black.setStroke()
for i in Int(left)..<Int(right) { // draw vertical lines across the view just to see if it works!
if i%64 == 0 {
NSBezierPath.strokeLine(from: NSPoint(x: CGFloat(i), y: bottom), to: NSPoint(x: CGFloat(i), y: top))
}
}
NSBezierPath.strokeLine(from: NSPoint(x: left, y: bottom), to: NSPoint(x: right, y: top))
NSBezierPath.strokeLine(from: NSPoint.zero, to: NSPoint(x: 50.0, y: 50.0))
}
See what happens when you change scale or the origin of worldBounds.