UIView bounds.applying but with rotation
Asked Answered
M

3

6

I'd like to create a dash border around a view, which can be moved/rotated/scaled.

Here's my code:

func addBorder() {        
    let f = selectedObject.bounds.applying(selectedObject.transform)
    borderView.backgroundColor = UIColor(red: 1, green: 0, blue: 0, alpha: 0.5) //just for testing
    borderView.frame = f
    borderView.center = selectedObject.center
    borderView.transform = CGAffineTransform(translationX: selectedObject.transform.tx, y: selectedObject.transform.ty)

    removeBorder() //remove old border

    let f2 = CGRect(x: 0, y: 0, width: borderView.frame.width, height: borderView.frame.height)
    let dashedBorder = CAShapeLayer()
    dashedBorder.strokeColor = UIColor.black.cgColor
    dashedBorder.lineDashPattern = [2, 2]
    dashedBorder.frame = f2
    dashedBorder.fillColor = nil
    dashedBorder.path = UIBezierPath(rect: f2).cgPath
    dashedBorder.name = "border"
    borderView.layer.addSublayer(dashedBorder)
}

And it looks like this: enter image description here

It's not bad, but I want the border to be rotated as well, because it may be misleading for the user as touch area is only on the image.

I've tried to apply rotation to the transform:

func addBorder() {        
    let f = selectedObject.bounds.applying(selectedObject.transform)
    borderView.backgroundColor = UIColor(red: 1, green: 0, blue: 0, alpha: 0.5) //just for testing
    borderView.frame = f
    borderView.center = selectedObject.center
    let rotation = atan2(selectedObject.transform.b, selectedObject.transform.a)

    borderView.transform = CGAffineTransform(rotationAngle: rotation).translatedBy(x: selectedObject.transform.tx, y: selectedObject.transform.ty)

    removeBorder() //remove old border

    let f2 = CGRect(x: 0, y: 0, width: borderView.frame.width, height: borderView.frame.height)
    let dashedBorder = CAShapeLayer()
    dashedBorder.strokeColor = UIColor.black.cgColor
    dashedBorder.lineDashPattern = [2, 2]
    dashedBorder.frame = f2
    dashedBorder.fillColor = nil
    dashedBorder.path = UIBezierPath(rect: f2).cgPath
    dashedBorder.name = "border"
    borderView.layer.addSublayer(dashedBorder)
}

But after rotating it looks like this:

enter image description here

How can I fix this?

Metencephalon answered 1/10, 2018 at 10:58 Comment(0)
M
0

Even though I've accepted the answer, because it helped me understand the issue I'm posting the final answer, because it's more to it. And I think it can be helpful for someone else, because I couldn't find this solution on Stackoverflow or somewhere else.

The idea is to create a borderView with bounds same as selectedObject. This was the solution from @Incredible_dev, however there was one issue: the line itself stretches as the borderView is scaled in any direction. And I want to keep the line size and just it want to be around selectedObject. So, I multiply selectedObject bounds with scale extracted from selectedObject.transform. Then I copy translation and rotation from the selectedObject.

Here's the final code:

var borderView: UIView!
var selectedObject: UIView?

extension CGAffineTransform { //helper extension

    func getScale() -> CGFloat {
        return (self.a * self.a + self.c * self.c).squareRoot()
    }

    func getRotation() -> CGFloat {
        return atan2(self.b, self.a)
    }

}

func removeBorder() { //remove the older border
    if borderView != nil {
        borderView.removeFromSuperview()
    }
}

func addBorder() {
    guard let selectedObject = selectedObject else { return }

    removeBorder() //remove old border

    let t = selectedObject.transform
    let s = t.getScale()
    let r = t.getRotation()

    borderView = UIView(frame: CGRect(x: 0, y: 0, width: selectedObject.bounds.width * s, height: selectedObject.bounds.height * s)) //multiply bounds with selectedObject's scale
    dividerImageView.addSubview(borderView) //add borderView to the "scene"

    borderView.transform = CGAffineTransform(translationX: t.tx, y: t.ty).rotated(by: r) //copy translation and rotation, order is important
    borderView.center = selectedObject.center

    let dashedBorder = CAShapeLayer() //create 2-point wide dashed line
    dashedBorder.lineWidth = 2
    dashedBorder.strokeColor = UIColor.black.cgColor
    dashedBorder.lineDashPattern = [2, 2]
    dashedBorder.fillColor = nil
    dashedBorder.path = UIBezierPath(rect: borderView.bounds).cgPath
    borderView.layer.addSublayer(dashedBorder)
}
Metencephalon answered 11/10, 2018 at 10:2 Comment(0)
A
2

Here is a sample based on your code that should do:

//initial transforms
selectedObject.transform = CGAffineTransform.init(rotationAngle: .pi / 4).translatedBy(x: 150, y: 15)

func addBorder() {
    let borderView = UIView.init(frame: selectedObject.bounds)
    self.view.addSubview(borderView)
    borderView.backgroundColor = UIColor(red: 1, green: 0, blue: 0, alpha: 0.5) //just for testing
    borderView.center = selectedObject.center
    borderView.transform = selectedObject.transform

    removeBorder() //remove old border

    let dashedBorder = CAShapeLayer()
    dashedBorder.strokeColor = UIColor.black.cgColor
    dashedBorder.lineDashPattern = [2, 2]
    dashedBorder.fillColor = nil
    dashedBorder.path = UIBezierPath(rect: borderView.bounds).cgPath
    dashedBorder.name = "border"
    borderView.layer.addSublayer(dashedBorder)
}
Antipater answered 1/10, 2018 at 12:1 Comment(1)
Doesn't work. It looks like this: imgur.com/a/OSIAaWw in my app you can scale as well. I don't want to stretch border, I just want to resize it and rotate to the right place.Metencephalon
C
2

Here is the solution of for problem:

func addBorder() {

    borderView.backgroundColor = UIColor(red: 1, green: 0, blue: 0, alpha: 0.5) //just for testing
    let degrees: CGFloat = 20.0 //the value in degrees for rotation
    let radians: CGFloat = degrees * (.pi / 180)
    borderView.transform = CGAffineTransform(rotationAngle: radians)

    removeBorder()

    let dashedBorder = CAShapeLayer()
    dashedBorder.strokeColor = UIColor.black.cgColor
    dashedBorder.lineDashPattern = [2, 2]
    dashedBorder.frame = borderView.bounds
    dashedBorder.fillColor = nil
    dashedBorder.path = UIBezierPath(roundedRect: borderView.bounds, cornerRadius:0).cgPath
    dashedBorder.name = "border"
    borderView.layer.addSublayer(dashedBorder)
}

The above code is tested in Xcode 10 with Swift 4.2

Chapfallen answered 10/10, 2018 at 9:14 Comment(5)
But it just uses rotation alone. Try applying scale or translation. And even if you scale it'll stretch the CAShapeLayer path. The idea is to calculate the final border, which will have a default scale and is only rotated.Metencephalon
Okay, my apologies. I've made a serious tests and got it worked. In my case, I was just updating the same borderView, not creating a new one each time. It somehow messes up the frame after second transform change. But when I'm creating this again each time it works. Now, the only issue is the visual size of the border. Because my "selectObject" stats with just 0.25 scale border is already very thin and almost invisible.Metencephalon
I've finally did it. This finishes the trick: dashedBorder.lineWidth = CGFloat(abs(1 / scale(from: t))) where t is transform and scale method is scale extraction from transform.Metencephalon
Good to know. All the best.Chapfallen
Your answer helped me, however final code is a little more complex so I've posted it anyway. lineWidth solution wasn't perfect.Metencephalon
M
0

Even though I've accepted the answer, because it helped me understand the issue I'm posting the final answer, because it's more to it. And I think it can be helpful for someone else, because I couldn't find this solution on Stackoverflow or somewhere else.

The idea is to create a borderView with bounds same as selectedObject. This was the solution from @Incredible_dev, however there was one issue: the line itself stretches as the borderView is scaled in any direction. And I want to keep the line size and just it want to be around selectedObject. So, I multiply selectedObject bounds with scale extracted from selectedObject.transform. Then I copy translation and rotation from the selectedObject.

Here's the final code:

var borderView: UIView!
var selectedObject: UIView?

extension CGAffineTransform { //helper extension

    func getScale() -> CGFloat {
        return (self.a * self.a + self.c * self.c).squareRoot()
    }

    func getRotation() -> CGFloat {
        return atan2(self.b, self.a)
    }

}

func removeBorder() { //remove the older border
    if borderView != nil {
        borderView.removeFromSuperview()
    }
}

func addBorder() {
    guard let selectedObject = selectedObject else { return }

    removeBorder() //remove old border

    let t = selectedObject.transform
    let s = t.getScale()
    let r = t.getRotation()

    borderView = UIView(frame: CGRect(x: 0, y: 0, width: selectedObject.bounds.width * s, height: selectedObject.bounds.height * s)) //multiply bounds with selectedObject's scale
    dividerImageView.addSubview(borderView) //add borderView to the "scene"

    borderView.transform = CGAffineTransform(translationX: t.tx, y: t.ty).rotated(by: r) //copy translation and rotation, order is important
    borderView.center = selectedObject.center

    let dashedBorder = CAShapeLayer() //create 2-point wide dashed line
    dashedBorder.lineWidth = 2
    dashedBorder.strokeColor = UIColor.black.cgColor
    dashedBorder.lineDashPattern = [2, 2]
    dashedBorder.fillColor = nil
    dashedBorder.path = UIBezierPath(rect: borderView.bounds).cgPath
    borderView.layer.addSublayer(dashedBorder)
}
Metencephalon answered 11/10, 2018 at 10:2 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.