Swift - scale background while retaining top anchor - ios

How can I scale a UIImageView while retaining the top anchor?
I tried:
UIView.animate(withDuration: 0.3, animations: {
self.backGroundImage.contentMode = .scaleToFill
self.backGroundImage.transform = CGAffineTransform(scaleX: 3, y: 3)
self.backGroundImage.topAnchor.constraint(equalTo: self.view.topAnchor).isActive = true
})
That scales the image but the anchor point is the middle of the image. It seems like setting the topAnchor doesn't do anything. Is there a way to set some sort of "scaleAnchor" or some other solution?
In the end I would like to achieve this:
Update
This is how I constrain the view:
NSLayoutConstraint.activate([
// constrain background image
backGroundImage.topAnchor.constraint(equalTo: view.topAnchor),
backGroundImage.bottomAnchor.constraint(equalTo: view.bottomAnchor),
backGroundImage.leadingAnchor.constraint(equalTo: view.leadingAnchor),
backGroundImage.trailingAnchor.constraint(equalTo: view.trailingAnchor), )]

This works for me in Swift 5.1:
public extension UIView {
func applyTransform(withScale scale: CGFloat, anchorPoint: CGPoint) {
let xPadding = (scale - 1) * (anchorPoint.x - 0.5) * bounds.width
let yPadding = (scale - 1) * (anchorPoint.y - 0.5) * bounds.height
transform = CGAffineTransform(scaleX: scale, y: scale).translatedBy(x: xPadding, y: yPadding)
}
}
Example:
yourView.applyTransform(withScale: 3, anchorPoint: CGPoint(x: 0.5, y: 0))

The transform matrix is applied after the frame has been calculated from the constraints. If you want to scale things up consistently I suggest just setting a multiple on the height and width constraints instead. Alternatively you can move the anchor point by using CALayers.anchorPoint and then apply you transform so that it scales from the top middle instead of the center (which is the default).
EDIT -- example:
import PlaygroundSupport
class V: UIViewController {
let square = UIView()
override func viewDidLoad() {
super.viewDidLoad()
square.translatesAutoresizingMaskIntoConstraints = false
square.backgroundColor = .red
view.addSubview(square)
NSLayoutConstraint.activate([
square.centerXAnchor.constraint(equalTo: view.centerXAnchor),
square.centerYAnchor.constraint(equalTo: view.centerYAnchor),
square.widthAnchor.constraint(equalToConstant: 100),
square.heightAnchor.constraint(equalToConstant: 100),
])
square.layer.anchorPoint = .init(x: 0.5, y: 0)
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
UIView.animate(withDuration: 10) {
self.square.transform = .init(scaleX: 3, y: 3)
}
}
}
PlaygroundPage.current.liveView = V()

Related

I have an UIImageView in an UIScrollView. How do I center at x,y of the image programmatically?

I have an UIImageView in an UIScrollView, and the image has size say 2000x3000. I can pinch to zoom and drag to pan on the screen. (I followed this video https://www.youtube.com/watch?v=0Tz0vI721c8).
My question is if I want to center the display at a point on the image programmatically (i.e. 600, 800), I think I need to use scrollView.setContentOffset(CGPoint), how do I figure out the CGPoint?
You have to take a couple things into account, beyond just setting the .contentOffset to match the "target center point" ...
First, the goal...
Using this 2000 x 3000 image:
"
With this set of "center points":
let points: [CGPoint] = [
CGPoint(x: 100, y: 100),
CGPoint(x: 800, y: 600),
CGPoint(x: 1600, y: 1200),
CGPoint(x: 600, y: 2000),
CGPoint(x: 1900, y: 2900),
]
We want to programmatically set the centers to the center of the scroll view:
If all we did was set the .contentOffset to the "target point" for #3, we'd get this:
So, we need to subtract one-half of the width and height of the scroll view frame:
scrollView.contentOffset.x = points[2].x - (scrollView.frame.width * 0.5)
scrollView.contentOffset.y = points[2].y - (scrollView.frame.height * 0.5)
That puts our "target point" in the center of the scroll view, but...
If we've zoomed out to, say, 90% (scrollView.zoomScale = 0.9), we get this:
So, we need to translate the "target point" to the zoom scale:
// translate target point to zoomScale
var x: CGFloat = points[2].x * scrollView.zoomScale
var y: CGFloat = points[2].y * scrollView.zoomScale
x = x - (scrollView.frame.width * 0.5)
y = y - (scrollView.frame.height * 0.5)
scrollView.contentOffset.x = x
scrollView.contentOffset.y = y
and, woo hoo, we have this:
The next problem, though, is that we don't want to center the "target point" if that would exceed the scroll view limits.
For example, if we try to center on "1" or "5" we get:
and:
the Points are centered in the scroll view, but as soon as we touch it to scroll or zoom, it will snap to the corner.
We need to limit the .contentOffset to avoid that.
So, for point "1" (points[0]):
// translate target point to zoomScale
var x: CGFloat = points[0].x * scrollView.zoomScale
var y: CGFloat = points[0].y * scrollView.zoomScale
// don't want to set offset below Zero
let minOffset: CGPoint = .zero
// don't want to set offset greater than what will fit in the scroll view
let maxOffset: CGPoint = CGPoint(x: imgView.frame.width - scrollView.frame.width, y: imgView.frame.height - scrollView.frame.height)
// this prevents x and from being negative
x = max(minOffset.x, x - (scrollView.frame.width * 0.5))
y = max(minOffset.y, y - (scrollView.frame.height * 0.5))
// this prevents x and y from exceeding the frame of the scroll view
x = min(maxOffset.x, x)
y = min(maxOffset.y, y)
scrollView.contentOffset.x = x
scrollView.contentOffset.y = y
Here's a complete example... as in the above images, tapping a "Number Button" will scroll that center point to the center of the scroll view (or as close as possible, if it would exceed the limits). All you need is to add that 2000x3000 image (named "img2000x3000") ... no #IBOutlet or #IBAction connections needed:
class ScrollPosViewController: UIViewController, UIScrollViewDelegate {
let points: [CGPoint] = [
CGPoint(x: 100, y: 100),
CGPoint(x: 800, y: 600),
CGPoint(x: 1600, y: 1200),
CGPoint(x: 600, y: 2000),
CGPoint(x: 1900, y: 2900),
]
let scrollView: UIScrollView = {
let v = UIScrollView()
v.backgroundColor = .yellow
v.translatesAutoresizingMaskIntoConstraints = false
return v
}()
let imgView: UIImageView = {
let v = UIImageView()
v.translatesAutoresizingMaskIntoConstraints = false
return v
}()
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .systemGreen
// make sure we can load the image
guard let img = UIImage(named: "img2000x3000") else {
print("Could not load image!!!")
return
}
// assing image to image view
imgView.image = img
// create a buttons stack view
let stack: UIStackView = {
let v = UIStackView()
v.distribution = .fillEqually
v.spacing = 20
v.translatesAutoresizingMaskIntoConstraints = false
return v
}()
for i in 0..<points.count {
let b = UIButton()
b.setTitle("\(i + 1)", for: [])
b.setTitleColor(.white, for: .normal)
b.setTitleColor(.lightGray, for: .highlighted)
b.backgroundColor = .systemBlue
b.layer.cornerRadius = 8
b.addTarget(self, action: #selector(centerOn(_:)), for: .touchUpInside)
stack.addArrangedSubview(b)
}
// add image view to scroll view
scrollView.addSubview(imgView)
// add scroll view to view
view.addSubview(scrollView)
// add buttons stack view to view
view.addSubview(stack)
let g = view.safeAreaLayoutGuide
let c = scrollView.contentLayoutGuide
NSLayoutConstraint.activate([
// constrain background image view
// Leading / Trailing at 20-pts
scrollView.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 20.0),
scrollView.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -20.0),
// height proportional to image size
scrollView.heightAnchor.constraint(equalTo: scrollView.widthAnchor, multiplier: img.size.height / img.size.width),
// centered vertically
scrollView.centerYAnchor.constraint(equalTo: g.centerYAnchor),
// constrain image view to all 4 sides of scroll view's Content Layout Guide
imgView.topAnchor.constraint(equalTo: c.topAnchor),
imgView.leadingAnchor.constraint(equalTo: c.leadingAnchor),
imgView.trailingAnchor.constraint(equalTo: c.trailingAnchor),
imgView.bottomAnchor.constraint(equalTo: c.bottomAnchor),
// constrain image view's width/height to image width/height
imgView.widthAnchor.constraint(equalToConstant: img.size.width),
imgView.heightAnchor.constraint(equalToConstant: img.size.height),
// constrain buttons stack view at bottom
stack.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 20.0),
stack.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -20.0),
stack.bottomAnchor.constraint(equalTo: g.bottomAnchor, constant: -20.0),
])
scrollView.delegate = self
scrollView.minimumZoomScale = 1.0
scrollView.maximumZoomScale = 2.0
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
// update min zoom scale so we can only "zoom out" until
// the content view fits the scroll view frame
if scrollView.minimumZoomScale == 1.0 {
let xScale = scrollView.frame.width / imgView.frame.width
let yScale = scrollView.frame.height / imgView.frame.height
scrollView.minimumZoomScale = min(xScale, yScale)
}
}
#objc func centerOn(_ sender: Any?) -> Void {
guard let btn = sender as? UIButton,
let t = btn.currentTitle,
let n = Int(t),
n > 0,
n <= points.count
else {
return
}
// translate target point to zoomScale
var x: CGFloat = points[n - 1].x * scrollView.zoomScale
var y: CGFloat = points[n - 1].y * scrollView.zoomScale
// don't want to set offset below Zero
let minOffset: CGPoint = .zero
// don't want to set offset greater than what will fit in the scroll view
let maxOffset: CGPoint = CGPoint(x: imgView.frame.width - scrollView.frame.width, y: imgView.frame.height - scrollView.frame.height)
// this prevents x and from being negative
x = max(minOffset.x, x - (scrollView.frame.width * 0.5))
y = max(minOffset.y, y - (scrollView.frame.height * 0.5))
// this prevents x and y from exceeding the frame of the scroll view
x = min(maxOffset.x, x)
y = min(maxOffset.y, y)
// if we want to animate the point to the new offset
UIView.animate(withDuration: 0.5, delay: 0.0, options: [.curveEaseInOut], animations: {
// set the new content offset
self.scrollView.contentOffset = CGPoint(x: x, y: y)
}, completion: nil)
// or, without animation
// set the new content offset
//scrollView.contentOffset = CGPoint(x: x, y: y)
}
func viewForZooming(in scrollView: UIScrollView) -> UIView? {
return imgView
}
}
// Assuming your image is the same width and height of
// the scrollview, the CGPoint for the center of the image will be
let scrollViewCenterX: CGFloat = scrollView.bounds.width / 2
let scrollViewCenterY: CGFloat = scrollView.bounds.height / 2
let imageCenterPoint = CGPoint(x: scrollViewCenterX, y: scrollViewCenterY)
scrollView.setContentOffset(imageCenterPoint)
// if we want to center the scrollview on `point`
let point = CGPoint(x: 600, y: 800)
let newPoint = CGPoint(x: point.x / 2, y: point.y / 2)
scrollView.setContentOffset(newPoint)

Swift 5 and UIKit draw, animate and split lines between 2 to 3 UIViews [closed]

Closed. This question needs to be more focused. It is not currently accepting answers.
Want to improve this question? Update the question so it focuses on one problem only by editing this post.
Closed 1 year ago.
Improve this question
I have a view that may contains 2 or 3 UIViews.
I want to draw (and possibly animate a line from the bottom MidX of the higher view to the bottom one.
If I have 3 views I want the line to split and animate to both of them.
If I have a single view I want the line to go directly to the middle top of the bottom view all this using UIBezierPath and CAShapeLayer
All this considering screen height (4.7" -> 6.2") I have attached images to illustrate what I want to achieve.
Thanks for the help.
You're on the right track...
The problem with drawing a "split" line is that there is one start point and TWO end points. So, the resulting animation may not be what you really want.
Another approach would be to use TWO layers - one with the "left-side" split line and one with the "right-side" split line, then animate them together.
Here's an example of wrapping things into a "Connect" view subclass.
We'll use 3 layers: 1 for the single vertical connecting line and one each for the right-side and left-side lines.
We can also set the path points to the center of the view, and the left and right edges. That way we can constrain the Leading edge to the center of the left-box, and the trailing edge to the center of the right-box.
This view, by itself, will look like this (with a yellow background so we can see its frame):
or:
With the lines will be animated from the top.
class ConnectView: UIView {
// determines whether we want a single box-to-box line, or
// left and right split / stepped lines to two boxes
public var single: Bool = true
private let singleLineLayer = CAShapeLayer()
private let leftLineLayer = CAShapeLayer()
private let rightLineLayer = CAShapeLayer()
private var durationFactor: CGFloat = 0
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
commonInit()
}
func commonInit() -> Void {
// add and configure sublayers
[singleLineLayer, leftLineLayer, rightLineLayer].forEach { lay in
layer.addSublayer(lay)
lay.lineWidth = 4
lay.strokeColor = UIColor.blue.cgColor
lay.fillColor = UIColor.clear.cgColor
}
}
override func layoutSubviews() {
super.layoutSubviews()
// for readablility, define the points for our lines
let topCenter = CGPoint(x: bounds.midX, y: 0)
let midCenter = CGPoint(x: bounds.midX, y: bounds.midY)
let botCenter = CGPoint(x: bounds.midX, y: bounds.maxY)
let midLeft = CGPoint(x: bounds.minX, y: bounds.midY)
let midRight = CGPoint(x: bounds.maxX, y: bounds.midY)
let botLeft = CGPoint(x: bounds.minX, y: bounds.maxY)
let botRight = CGPoint(x: bounds.maxX, y: bounds.maxY)
let singleBez = UIBezierPath()
let leftBez = UIBezierPath()
let rightBez = UIBezierPath()
// vertical line
singleBez.move(to: topCenter)
singleBez.addLine(to: botCenter)
// split / stepped line to the left
leftBez.move(to: topCenter)
leftBez.addLine(to: midCenter)
leftBez.addLine(to: midLeft)
leftBez.addLine(to: botLeft)
// split / stepped line to the right
rightBez.move(to: topCenter)
rightBez.addLine(to: midCenter)
rightBez.addLine(to: midRight)
rightBez.addLine(to: botRight)
// set the layer paths
// initializing strokeEnd to 0 for all three
singleLineLayer.path = singleBez.cgPath
singleLineLayer.strokeEnd = 0
leftLineLayer.path = leftBez.cgPath
leftLineLayer.strokeEnd = 0
rightLineLayer.path = rightBez.cgPath
rightLineLayer.strokeEnd = 0
// calculate total line lengths (in points)
// so we can adjust the "draw speed" in the animation
let singleLength = botCenter.y - topCenter.y
let doubleLength = singleLength + (midCenter.x - midLeft.x)
durationFactor = singleLength / doubleLength
}
public func doAnim() -> Void {
// reset the animations
[singleLineLayer, leftLineLayer, rightLineLayer].forEach { lay in
lay.removeAllAnimations()
lay.strokeEnd = 0
}
let animation = CABasicAnimation(keyPath: "strokeEnd")
animation.fromValue = 0.0
animation.toValue = 1.0
animation.duration = 2.0
animation.fillMode = .forwards
animation.isRemovedOnCompletion = false
if self.single {
// we want the apparent drawing speed to be the same
// for a single line as for a split / stepped line
// so change the animation duration
animation.duration *= durationFactor
// animate the single line layer
self.singleLineLayer.add(animation, forKey: animation.keyPath)
} else {
// animate the both left and right line layers
self.leftLineLayer.add(animation, forKey: animation.keyPath)
self.rightLineLayer.add(animation, forKey: animation.keyPath)
}
}
}
and a sample view controller showing it in action:
class ConnectTestViewController: UIViewController {
let vTop = UIView()
let vLeft = UIView()
let vCenter = UIView()
let vRight = UIView()
let testConnectView = ConnectView()
override func viewDidLoad() {
super.viewDidLoad()
// give the 4 views different background colors
// add them as subviews
// make them all 100x100 points
let colors: [UIColor] = [
.systemYellow,
.systemRed, .systemGreen, .systemBlue,
]
for (v, c) in zip([vTop, vLeft, vCenter, vRight], colors) {
v.backgroundColor = c
v.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(v)
v.widthAnchor.constraint(equalToConstant: 100.0).isActive = true
v.heightAnchor.constraint(equalTo: v.widthAnchor).isActive = true
}
// add the clear-background Connect View
testConnectView.backgroundColor = .clear
testConnectView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(testConnectView)
let g = view.safeAreaLayoutGuide
NSLayoutConstraint.activate([
// horizontally center the top box near the top
vTop.topAnchor.constraint(equalTo: g.topAnchor, constant: 40.0),
vTop.centerXAnchor.constraint(equalTo: g.centerXAnchor),
// horizontally center the center box, 200-pts below the top box
vCenter.topAnchor.constraint(equalTo: vTop.bottomAnchor, constant: 200.0),
vCenter.centerXAnchor.constraint(equalTo: g.centerXAnchor),
// align tops of left and right boxes with center box
vLeft.topAnchor.constraint(equalTo: vCenter.topAnchor),
vRight.topAnchor.constraint(equalTo: vCenter.topAnchor),
// position left and right boxes to left and right of center box
vLeft.trailingAnchor.constraint(equalTo: vCenter.leadingAnchor, constant: -20.0),
vRight.leadingAnchor.constraint(equalTo: vCenter.trailingAnchor, constant: 20.0),
// constrain Connect View
// Top to Bottom of Top box
testConnectView.topAnchor.constraint(equalTo: vTop.bottomAnchor),
// Bottom to Top of the row of 3 boxes
testConnectView.bottomAnchor.constraint(equalTo: vCenter.topAnchor),
// Leading to CenterX of Left box
testConnectView.leadingAnchor.constraint(equalTo: vLeft.centerXAnchor),
// Trailing to CenterX of Right box
testConnectView.trailingAnchor.constraint(equalTo: vRight.centerXAnchor),
])
// add a couple buttons at the bottom
let stack = UIStackView()
stack.spacing = 20
stack.distribution = .fillEqually
stack.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(stack)
["Run Anim", "Show/Hide"].forEach { str in
let b = UIButton()
b.setTitle(str, for: [])
b.backgroundColor = .red
b.setTitleColor(.white, for: .normal)
b.setTitleColor(.lightGray, for: .highlighted)
b.addTarget(self, action: #selector(buttonTap(_:)), for: .touchUpInside)
stack.addArrangedSubview(b)
}
NSLayoutConstraint.activate([
stack.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 20.0),
stack.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -20.0),
stack.bottomAnchor.constraint(equalTo: g.bottomAnchor, constant: -20.0),
stack.heightAnchor.constraint(equalToConstant: 50.0),
])
}
#objc func buttonTap(_ sender: Any?) -> Void {
guard let b = sender as? UIButton,
let t = b.currentTitle
else {
return
}
if t == "Run Anim" {
// tap button to toggle between
// Top-to-Middle box line or
// Top-to-SideBoxes split / stepped line
testConnectView.single.toggle()
// run the animation
testConnectView.doAnim()
} else {
// toggle background of Connect View between
// clear and yellow
testConnectView.backgroundColor = testConnectView.backgroundColor == .clear ? .yellow : .clear
}
}
}
Running that will give this result:
The first button at the bottom will toggle the connection between Top-Center and Top-Left-Right (re-running the animation each time). The second button will toggle the view's background color between clear and yellow so we can see its frame.
Edit
If you want rounded "step" corners that look like this:
replace the layoutSubviews() code above with this:
override func layoutSubviews() {
super.layoutSubviews()
// for readablility, define the points for our lines
let topCenter = CGPoint(x: bounds.midX, y: 0)
let midCenter = CGPoint(x: bounds.midX, y: bounds.midY)
let botCenter = CGPoint(x: bounds.midX, y: bounds.maxY)
let midLeft = CGPoint(x: bounds.minX, y: bounds.midY)
let midRight = CGPoint(x: bounds.maxX, y: bounds.midY)
let botLeft = CGPoint(x: bounds.minX, y: bounds.maxY)
let botRight = CGPoint(x: bounds.maxX, y: bounds.maxY)
let singleBez = UIBezierPath()
let leftBez = UIBezierPath()
let rightBez = UIBezierPath()
// vertical line
singleBez.move(to: topCenter)
singleBez.addLine(to: botCenter)
// rounded "step" corners
let radius: CGFloat = 20.0
let leftArcP = CGPoint(x: midLeft.x + radius, y: midLeft.y)
let leftArcC = CGPoint(x: midLeft.x + radius, y: midLeft.y + radius)
let rightArcP = CGPoint(x: midRight.x - radius, y: midRight.y)
let rightArcC = CGPoint(x: midRight.x - radius, y: midRight.y + radius)
// split / stepped line to the left
leftBez.move(to: topCenter)
leftBez.addLine(to: midCenter)
leftBez.addLine(to: leftArcP)
leftBez.addArc(withCenter: leftArcC, radius: radius, startAngle: .pi * 1.5, endAngle: .pi, clockwise: false)
leftBez.addLine(to: botLeft)
// split / stepped line to the right
rightBez.move(to: topCenter)
rightBez.addLine(to: midCenter)
rightBez.addLine(to: rightArcP)
rightBez.addArc(withCenter: rightArcC, radius: radius, startAngle: .pi * 1.5, endAngle: 0, clockwise: true)
rightBez.addLine(to: botRight)
// set the layer paths
// initializing strokeEnd to 0 for all three
singleLineLayer.path = singleBez.cgPath
singleLineLayer.strokeEnd = 0
leftLineLayer.path = leftBez.cgPath
leftLineLayer.strokeEnd = 0
rightLineLayer.path = rightBez.cgPath
rightLineLayer.strokeEnd = 0
// calculate total line lengths (in points)
// so we can adjust the "draw speed" in the animation
let singleLength = botCenter.y - topCenter.y
let doubleLength = singleLength + (midCenter.x - midLeft.x)
durationFactor = singleLength / doubleLength
}
Well after some research I have come up with this solution for Swift 5:
class ViewController: UIViewController {
#IBOutlet weak var someView: UIView!
override func viewDidLoad() {
super.viewDidLoad()
let start = CGPoint(x: self.someView.bounds.midX, y: self.someView.bounds.maxY)
let end = CGPoint(x: self.someView.layer.bounds.midX, y: (UIScreen.main.bounds.height / 2) - 100)
let linePath = UIBezierPath()
linePath.move(to: start)
linePath.addLine(to: end)
linePath.addLine(to: CGPoint(x: -50, y: (UIScreen.main.bounds.height / 2) - 100))
linePath.move(to: end)
linePath.addLine(to: CGPoint(x: 250, y: (UIScreen.main.bounds.height / 2) - 100))
linePath.addLine(to: CGPoint(x: lowerViewA.x, y: lowerViewA.y))
linePath.move(to: CGPoint(x: -50, y: (UIScreen.main.bounds.height / 2) - 100))
linePath.addLine(to: CGPoint(x: lowerViewB.x, y: lowerViewB.y))
let shapeLayer = CAShapeLayer()
shapeLayer.path = linePath.cgPath
shapeLayer.fillColor = UIColor.clear.cgColor
shapeLayer.strokeColor = UIColor.green.cgColor
shapeLayer.lineWidth = 2
shapeLayer.lineJoin = CAShapeLayerLineJoin.bevel
self.someView.layer.addSublayer(shapeLayer)
//Basic animation if you want to animate the line drawing.
let pathAnimation: CABasicAnimation = CABasicAnimation(keyPath: "strokeEnd")
pathAnimation.duration = 4.0
pathAnimation.fromValue = 0.0
pathAnimation.toValue = 1.0
//Animation will happen right away
shapeLayer.add(pathAnimation, forKey: "strokeEnd")
}
}

Scale down UIView and it's subview simultaneously in swift 5?

I have created UIView and it has one subview and it's containing shape object which are creating using UIBezierPath, and UIView has fixed height and width (image 1). When i click the blue color button, it should be scale down to another fixed width and height. I have applied CGAffineTransform to subview and, i guess it is scale down properly, but since topAnchor and leftAchor has constant values, after scale down it is not displaying properly (image 2).I will attach my source code here and screenshots, please, analyze this and can someone suggest better approach for how to do this ? and highly appreciate ur feedback and comments.
code:
import UIKit
class ViewController: UIViewController {
var screenView: UIView!
var button: UIButton!
let widthOfScaleDownView: CGFloat = 200
let heightOfScaleDownView: CGFloat = 300
override func viewDidLoad() {
screenView = UIView(frame: .zero)
screenView.translatesAutoresizingMaskIntoConstraints = false
screenView.backgroundColor = UIColor.red
self.view.addSubview(screenView)
NSLayoutConstraint.activate([
screenView.heightAnchor.constraint(equalToConstant: 800),
screenView.widthAnchor.constraint(equalToConstant: 600),
screenView.centerYAnchor.constraint(equalTo: self.view.centerYAnchor, constant: 0),
screenView.centerXAnchor.constraint(equalTo: self.view.centerXAnchor, constant: 0)
])
button = UIButton()
button.translatesAutoresizingMaskIntoConstraints = false
button.backgroundColor = UIColor.blue
self.view.addSubview(button)
NSLayoutConstraint.activate([
button.heightAnchor.constraint(equalToConstant: 50),
button.widthAnchor.constraint(equalToConstant: 100),
button.topAnchor.constraint(equalTo: self.view.topAnchor, constant: 950),
button.leftAnchor.constraint(equalTo: self.view.leftAnchor, constant: 350)
])
button.setTitle("click", for: .normal)
button.setTitleColor(UIColor.white, for: .normal)
button.addTarget(self, action: #selector(scaleDownView(_:)), for: .touchUpInside)
let shapeView = UIView(frame: .zero)
shapeView.translatesAutoresizingMaskIntoConstraints = false
let rect = CGRect(x: 0, y: 0, width: 300, height: 300)
let shape = UIBezierPath(roundedRect: rect, cornerRadius: 50)
let layer = CAShapeLayer()
layer.path = shape.cgPath
layer.lineWidth = 2
shapeView.layer.addSublayer(layer)
screenView.addSubview(shapeView)
NSLayoutConstraint.activate([
shapeView.topAnchor.constraint(equalTo: screenView.topAnchor, constant: 250),
shapeView.leftAnchor.constraint(equalTo: screenView.leftAnchor, constant: 150)
])
}
#IBAction func scaleDownView(_ sender: UIButton) {
let widthScale = widthOfScaleDownView/screenView.frame.width
let heightScale = heightOfScaleDownView/screenView.frame.height
screenView.subviews.forEach{ view in
view.transform = CGAffineTransform(scaleX: widthScale, y: heightScale)
}
NSLayoutConstraint.activate([
screenView.heightAnchor.constraint(equalToConstant: heightOfScaleDownView),
screenView.widthAnchor.constraint(equalToConstant: widthOfScaleDownView),
screenView.centerYAnchor.constraint(equalTo: self.view.centerYAnchor, constant: 0),
screenView.centerXAnchor.constraint(equalTo: self.view.centerXAnchor, constant: 0)
])
}
}
Couple things...
1 - When you use CGAffineTransform to change a view, it will automatically affect the view's subviews. So no need for a loop.
2 - Transforms are cumulative, so when "going back" to the original scale, use .identity instead of another scale transform.
3 - You cannot set a constraint multiple times. So if you do:
screenView.heightAnchor.constraint(equalToConstant: 800)
and then you also do:
screenView.heightAnchor.constraint(equalToConstant: heightOfScaleDownView)
you will get auto-layout conflicts... the view cannot be both 800-pts tall and
300-pts tall at the same time.
Take a look at the changes I made to your code. I also added a 1-second animation to the scale transform so you can see what it's doing:
class ViewController: UIViewController {
var screenView: UIView!
var button: UIButton!
let widthOfScaleDownView: CGFloat = 200
let heightOfScaleDownView: CGFloat = 300
override func viewDidLoad() {
screenView = UIView(frame: .zero)
screenView.translatesAutoresizingMaskIntoConstraints = false
screenView.backgroundColor = UIColor.red
self.view.addSubview(screenView)
// constrain screenView
// width: 800 height: 600
// centered X and Y
NSLayoutConstraint.activate([
screenView.heightAnchor.constraint(equalToConstant: 800),
screenView.widthAnchor.constraint(equalToConstant: 600),
screenView.centerYAnchor.constraint(equalTo: self.view.centerYAnchor, constant: 0),
screenView.centerXAnchor.constraint(equalTo: self.view.centerXAnchor, constant: 0)
])
button = UIButton()
button.translatesAutoresizingMaskIntoConstraints = false
button.backgroundColor = UIColor.blue
self.view.addSubview(button)
NSLayoutConstraint.activate([
button.heightAnchor.constraint(equalToConstant: 50),
button.widthAnchor.constraint(equalToConstant: 100),
button.topAnchor.constraint(equalTo: self.view.topAnchor, constant: 950),
button.leftAnchor.constraint(equalTo: self.view.leftAnchor, constant: 350)
])
button.setTitle("click", for: .normal)
button.setTitleColor(UIColor.white, for: .normal)
button.addTarget(self, action: #selector(scaleDownView(_:)), for: .touchUpInside)
let shapeView = UIView(frame: .zero)
shapeView.translatesAutoresizingMaskIntoConstraints = false
let rect = CGRect(x: 0, y: 0, width: 300, height: 300)
let shape = UIBezierPath(roundedRect: rect, cornerRadius: 50)
let layer = CAShapeLayer()
layer.path = shape.cgPath
layer.lineWidth = 2
shapeView.layer.addSublayer(layer)
screenView.addSubview(shapeView)
// constrain shapeView
// width: 300 height: 300
// centered X and Y in screenView
NSLayoutConstraint.activate([
shapeView.widthAnchor.constraint(equalToConstant: 300.0),
shapeView.heightAnchor.constraint(equalToConstant: 300.0),
shapeView.centerXAnchor.constraint(equalTo: screenView.centerXAnchor),
shapeView.centerYAnchor.constraint(equalTo: screenView.centerYAnchor),
])
}
#IBAction func scaleDownView(_ sender: UIButton) {
let widthScale = widthOfScaleDownView/screenView.frame.width
let heightScale = heightOfScaleDownView/screenView.frame.height
UIView.animate(withDuration: 1.0) {
// if we have already been scaled down
if widthScale == 1 {
// animate the scale transform back to original (don't add another transform)
self.screenView.transform = .identity
} else {
// animate the scale-down transform
self.screenView.transform = CGAffineTransform(scaleX: widthScale, y: heightScale)
}
}
// do NOT change screenView constraints!
}
}

Create a layer with increasing height - swift

I want to make a custom slider that has increasing height i.e it's height starts from 4.0 and goes to 6.0.
I have written code for creating a layer but I cannot find a way to increase its height in this manner. Here is my code :
let path = UIBezierPath(roundedRect: bounds, cornerRadius: cornerRadius)
ctx.addPath(path.cgPath)
ctx.setFillColor(UIColor.red.cgColor)
ctx.fillPath()
let lowerValuePosition = slider.positionForValue(slider.lowerValue)
let upperValuePosition = slider.positionForValue(slider.upperValue)
let rect = CGRect(x: 0, y: 0,
width: (bounds.width - 36),
height: bounds.height)
ctx.fill(rect)
Because the Minimum and Maximum (left-side & right-side) "Track" images stretch, you may not be able to get what you want with a default UISlider.
Not too tough to get around it though.
Basically:
create a custom view with your "rounded wedge" shape
overlay a UISlider on that custom view
"fill" the percentage of the shape when the slider value changes
Here's the idea, before overlaying them:
When we want to overlay the slider on the wedge, set the slider Min/Max track images to clear and it looks like this:
We can use a little trick to handle "filling" the shape by percentage:
use a gradient background layer
mask it with the shape
set the gradient colors to red, red, gray, gray
set the color locations to [0.0, pct, pct, 1.0]
That way we get a clean edge, instead of a gradient fade.
Here's a complete example -- no #IBOutlet or #IBAction connections, so just set a view controller's custom class to WedgeSliderViewController:
class RoundedWedgeSliderView: UIView {
var leftRadius: CGFloat = 4.0
var rightRadius: CGFloat = 6.0
// mask shape
private var cMask = CAShapeLayer()
var pct: Float = 0.0 {
didSet {
let p = pct as NSNumber
// disable layer built-in animation so the update won't "lag"
CATransaction.begin()
CATransaction.setDisableActions(true)
// update gradient locations
gradientLayer.locations = [
0.0, p, p, 1.0
]
CATransaction.commit()
}
}
// allows self.layer to be a CAGradientLayer
override class var layerClass: AnyClass { return CAGradientLayer.self }
private var gradientLayer: CAGradientLayer {
return self.layer as! CAGradientLayer
}
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
commonInit()
}
func commonInit() -> Void {
// gradient colors will be
// red, red, gray, gray
let colors = [
UIColor.red.cgColor,
UIColor.red.cgColor,
UIColor(white: 0.9, alpha: 1.0).cgColor,
UIColor(white: 0.9, alpha: 1.0).cgColor,
]
gradientLayer.colors = colors
// initial gradient color locations
gradientLayer.locations = [
0.0, 0.0, 0.0, 1.0
]
// horizontal gradient
gradientLayer.startPoint = CGPoint(x: 0.0, y: 0.5)
gradientLayer.endPoint = CGPoint(x: 1.0, y: 0.5)
}
override func layoutSubviews() {
super.layoutSubviews()
let r = bounds
// define the "Rounded Wedge" shape
let leftCenter = CGPoint(x: r.minX + leftRadius, y: r.midY)
let rightCenter = CGPoint(x: r.maxX - rightRadius, y: r.midY)
let bez = UIBezierPath()
bez.addArc(withCenter: leftCenter, radius: leftRadius, startAngle: .pi * 0.5, endAngle: .pi * 1.5, clockwise: true)
bez.addArc(withCenter: rightCenter, radius: rightRadius, startAngle: .pi * 1.5, endAngle: .pi * 0.5, clockwise: true)
bez.close()
// set the mask layer's path
cMask.path = bez.cgPath
// mask self's layer
layer.mask = cMask
}
}
class WedgeSliderViewController: UIViewController {
let mySliderView = RoundedWedgeSliderView()
let theSlider = UISlider()
override func viewDidLoad() {
super.viewDidLoad()
view.addSubview(mySliderView)
view.addSubview(theSlider)
mySliderView.translatesAutoresizingMaskIntoConstraints = false
theSlider.translatesAutoresizingMaskIntoConstraints = false
// respect safe area
let g = view.safeAreaLayoutGuide
NSLayoutConstraint.activate([
// constrain slider 100-pts from top, 40-pts on each side
theSlider.topAnchor.constraint(equalTo: g.topAnchor, constant: 100.0),
theSlider.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 40.0),
theSlider.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -40.0),
// constrain mySliderView width to the slider width minus 16-pts
// (so we have 8-pt "padding" on each side for the thumb to cover)
mySliderView.widthAnchor.constraint(equalTo: theSlider.widthAnchor, constant: -16.0),
// constrain mySliderView to same height as the slider, centered X & Y
mySliderView.heightAnchor.constraint(equalTo: theSlider.heightAnchor),
mySliderView.centerXAnchor.constraint(equalTo: theSlider.centerXAnchor),
mySliderView.centerYAnchor.constraint(equalTo: theSlider.centerYAnchor),
])
// set left- and right-side "track" images to empty images
theSlider.setMinimumTrackImage(UIImage(), for: .normal)
theSlider.setMaximumTrackImage(UIImage(), for: .normal)
// add target for the slider
theSlider.addTarget(self, action: #selector(self.sliderValueChanged(_:)), for: .valueChanged)
// set intitial values
theSlider.value = 0.0
mySliderView.pct = 0.0
// end-radii of mySliderView defaults to 4.0 and 6.0
// un-comment next line to see the difference
//mySliderView.rightRadius = 10.0
}
#objc func sliderValueChanged(_ sender: Any) {
if let s = sender as? UISlider {
// update mySliderView when the slider changes
mySliderView.pct = s.value
}
}
}
You have to override the trackRect method of your superclass UISlider in the subclass like below and return the bounds and size that you require:
/**
UISlider Subclass
*/
class CustomSlider: UISlider {
override func trackRect(forBounds bounds: CGRect) -> CGRect {
return CGRect(origin: #oigin#, size: CGSize(width: #width#, height: #height#))
}
}

Circle UIView resize with animation keeping Circle shape

I have a UIView which I am making the background a circular shape with:
self.colourView.layer.cornerRadius = 350
self.colourView.clipsToBounds = true
With the view being a 700 by 700 square.
I am then trying to change the size of this view using:
UIView.animate(withDuration: zoomLength,
delay: 0,
options: .curveEaseIn,
animations: {
self.colorViewWidth.constant = 100
self.colorViewHeight.constant = 100
self.colourView.layer.cornerRadius = 50
self.colourView.clipsToBounds = true
self.view.layoutIfNeeded()
},
completion: { finished in
})
})
The problem with this is the corner radius of the view switch to 50 instantly. So that as the view shrinks in size it doesn't look like a circle shrinking but a square with rounded corners until it reaches the 100 by 100 size.
How can I take a view that circule in shape and shrink it down while maintaining the circlare shape during the animation.
You could always animate the corner radius as well with a CABasicAnimation along side the UIView animations. See example below.
import UIKit
class ViewController: UIViewController {
//cause 'merica
var colorView = UIView()
var button = UIButton()
var colorViewWidth : NSLayoutConstraint!
var colorViewHeight : NSLayoutConstraint!
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
colorView.frame = CGRect(x: 0, y: 0, width: 750, height: 750)
colorView.center = self.view.center
colorView.backgroundColor = .blue
colorView.layer.cornerRadius = 350
colorView.clipsToBounds = true
colorView.layer.allowsEdgeAntialiasing = true
colorView.translatesAutoresizingMaskIntoConstraints = false
//set up constraints
colorViewWidth = NSLayoutConstraint(item: colorView, attribute: .width, relatedBy: .equal, toItem: nil, attribute: .notAnAttribute, multiplier: 1, constant: 750)
colorViewWidth.isActive = true
colorViewHeight = NSLayoutConstraint(item: colorView, attribute: .height, relatedBy: .equal, toItem: nil, attribute: .notAnAttribute, multiplier: 1, constant: 750)
colorViewHeight.isActive = true
self.view.addSubview(colorView)
colorView.centerXAnchor.constraint(equalTo: self.view.centerXAnchor).isActive = true
colorView.centerYAnchor.constraint(equalTo: self.view.centerYAnchor).isActive = true
//button for action
button = UIButton(frame: CGRect(x: 0, y: 0, width: self.view.bounds.width - 20, height: 50))
button.center = self.view.center
button.center.y = self.view.bounds.height - 70
button.addTarget(self, action: #selector(ViewController.pressed(sender:)), for: .touchUpInside)
button.setTitle("Animate", for: .normal)
button.setTitleColor(.blue, for: .normal)
self.view.addSubview(button)
}
func pressed(sender:UIButton) {
self.colorViewWidth.constant = 100
self.colorViewHeight.constant = 100
UIView.animate(withDuration: 1.0,
delay: 0,
options: .curveEaseIn,
animations: {
self.view.layoutIfNeeded()
},
completion: { finished in
})
//animate cornerRadius and update model
let animation = CABasicAnimation(keyPath: "cornerRadius")
animation.duration = 1.0
//super important must match
animation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseIn)
animation.fromValue = colorView.layer.cornerRadius
animation.toValue = 50
colorView.layer.add(animation, forKey: nil)
//update model important
colorView.layer.cornerRadius = 50
}
}
Result:
Try to put your image inside a viewDidLayoutSubviews()
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
colourView.layer.cornerRadius = colourView.frame.size.width/2
colourView.layer.masksToBounds = true
colourView.layer.borderWidth = 3.0
colourView.layer.borderColor = UIColor(red: 142.0/255.5, green: 142.0/255.5, blue: 142.0/255.5, alpha: 1.0).cgColor
}
let me know how it goes
Cheers
Regarding animation you need to create the CAShapeLayer object and then set the cgpath of the layer object.
let circleLayer = CAShapeLayer()
let radius = view.bounds.width * 0.5
//Value of startAngle and endAngle should be in the radian.
let path = UIBezierPath(arcCenter: center, radius: radius, startAngle: startAngle, endAngle: endAngle, clockwise: true)
circleLayer.path = path.cgPath
The above will draw the circle for you. After that you can apply the animation on layer object.
let animation = CABasicAnimation(keyPath: "strokeEnd")
animation.fromValue = fromValue//you can update the value here by default it will be 0
animation.toValue = toValue //you can update the value here by default it will be 1
animation.duration = CFTimeInterval(remainingTime)
circleLayer
circleLayer.removeAnimation(forKey: "stroke")
circleLayer.add(animation, forKey: "stroke")

Resources