4

I have a UIView, inside it I have a UIImageView. I have a UIPinchGestureRecognizer added to the UIVIew to handle the pinch and zoom and make the UIView grow with the UIImageView altogether.

My UIView has a border. I added the border this way:

self.layer.borderColor = [UIColor blackColor].CGColor;
self.layer.borderWidth = 1.0f;
self.layer.cornerRadius = 8.0f;

And the problem I'm having is I can't find a way of making my UIView bigger while keeping the same width of the border. When pinching and zooming the border gets thicker.

This is my UIPinchGestureRecognizer handler:

- (void)scale:(UIPanGestureRecognizer *)sender{

    if([(UIPinchGestureRecognizer*)sender state] == UIGestureRecognizerStateBegan) {
        _lastScale = 1.0;
    }

    CGFloat scale = 1.0 - (_lastScale - [(UIPinchGestureRecognizer*)sender scale]);

    CGAffineTransform currentTransform = self.transform;
    CGAffineTransform newTransform = CGAffineTransformScale(currentTransform, scale, scale);

    _lastScale = [(UIPinchGestureRecognizer*)sender scale];

    [self setTransform:newTransform];
}

Thanks a lot!!

I've been googling around A LOT and found this:

self.layer.borderWidth = 2.0f / scaleFactor;

Sadly is not working for me... It makes sense but not working.

Also I read the solution about adding an other view in the back and making the front view to have an offset in the position so the back view is shown and looks like a border. That's not an option because I need my image to view transparent.

I want to recreate what Aviary does in their app. You can scale up and down an "sticker" and the border always stays the same size.

Andres
  • 11,439
  • 12
  • 48
  • 87
  • Have you tried re-applying the `borderWidth` at the end of your `scale:` method? – ChrisH Jul 10 '14 at 18:50
  • the borderWidth is always the same. It doesn't get increased, if I NSLog("%f",self.layer.borderWith) it's always the value I set the first time, but the border does get bigger... – Andres Jul 10 '14 at 18:54
  • Makes sense. The layers values aren't changing, you're just applying a transform to them. So can't you scale the borderwidth down by the same ratio that you're scaling the layer up? – ChrisH Jul 10 '14 at 18:56
  • I don't think so, because the border is in the layer of the view I'm applying the transform and I'm not sure I can scale just the border... – Andres Jul 10 '14 at 19:03
  • I think you have the right idea with with self.layer.borderWidth = 1.0f / scaleFactor; (though remember there is not really such a thing as a quarter point/half a pixel) remember IOS cashes views when transforming to be efficient. Hence make sure to change the boarder before you change the transform. – Martin O'Shea Jul 15 '14 at 23:07
  • This is the correct behavior. The transform is applied to the view's backing store, so think of it as transforming just a bitmap of the view instead of individual properties. I would recommend manipulating the frame rather than the transform as LorikMalorik notes below. If frame proves difficult to manipulate, you could also try using the center and bounds properties. – axiixc Jul 16 '14 at 05:30

5 Answers5

1

I was trying to do this effect in this way as well in Swift 4.2. Ultimately I could not do it successfully using CGAffine Effects. I ultimately had to update the constraints -- as I was using anchors.

Here is the code I ultimately started using. SelectedView is the view that should be scaled.

Keep in mind that this does not break other CGAffine effects. I'm still using those for rotation.

@objc private func scaleSelectedView(_ sender: UIPinchGestureRecognizer) {
    guard let selectedView = selectedView else {
        return
    }

    var heightDifference: CGFloat?
    var widthDifference: CGFloat?

    // increase width and height according to scale
    selectedView.constraints.forEach { constraint in
        guard
            let view = constraint.firstItem as? UIView,
            view == selectedView
            else {
                return
        }
        switch constraint.firstAttribute {
        case .width:
            let previousWidth = constraint.constant
            constraint.constant = constraint.constant * sender.scale
            widthDifference = constraint.constant - previousWidth
        case .height:
            let previousHeight = constraint.constant
            constraint.constant = constraint.constant * sender.scale
            heightDifference = constraint.constant - previousHeight
        default:
            return
        }
    }

    // adjust leading and top anchors to keep view centered
    selectedView.superview?.constraints.forEach { constraint in
        guard
            let view = constraint.firstItem as? UIView,
            view == selectedView
            else {
                return
        }
        switch constraint.firstAttribute {
        case .leading:
            guard let widthDifference = widthDifference else {
                return
            }
            constraint.constant = constraint.constant - widthDifference / 2
        case .top:
            guard let heightDifference = heightDifference else {
                return
            }
            constraint.constant = constraint.constant - heightDifference / 2
        default:
            return
        }
    }

    // reset scale after applying in order to keep scaling linear rather than exponential
    sender.scale = 1.0
}

Notes: width and height anchors are on the view itself. Top and Leading anchors are on the superview -- hence the two forEach blocks.

EndersJeesh
  • 427
  • 1
  • 4
  • 20
1

I had to figure out a way of keeping my borders the same width during transforms (also creating stickers), and needed the solution to work with CGAffineTransform because my stickers rotate (and you cannot easily use frame-based solutions if you're doing rotations).

My approach was to change the layer.borderWidth in response to the transform changing, like this. Works for me.

class MyView : UIView {
    let borderWidth = CGFloat(2)

    override var transform: CGAffineTransform {
        didSet {
            self.setBorder()
        }
    }

    override func viewDidLoad() {
        super.viewDidLoad()
        self.setBorder()
    }

    fileprivate func setBorder() {
        self.layer.borderWidth = self.borderWidth / transform.scale
    }
}

extension CGAffineTransform {
    var scale: CGFloat {
        return sqrt((a * a + c * c))
    }
}
xaphod
  • 6,392
  • 2
  • 37
  • 45
0

while setting CGAffineTransformScale, you wont be able to control the border-width separately. The solution would be to use another view say borderView as subview in above view hierarchy than the view to be scaled (with backgroundColor as clearcolor)whose SIZE should be changed according to scale factor of other view.
Apply your border-width to the borderView keeping the desired borderwidth.

Best of luck!

Saurabh Passolia
  • 8,099
  • 1
  • 26
  • 41
0

Switch from CGAffineTransform to view's frame.

Sample code for pinching a view with constant border width:

@interface ViewController ()
@property (nonatomic, weak) IBOutlet UIView *testView;
@end

@implementation ViewController {
    CGFloat _lastScale;
    CGRect _sourceRect;
}

- (void)viewDidLoad
{
    [super viewDidLoad];
    self.testView.backgroundColor = [UIColor yellowColor];
    self.testView.layer.borderColor = [UIColor blackColor].CGColor;
    self.testView.layer.borderWidth = 1.0f;
    self.testView.layer.cornerRadius = 8.0f;
    _sourceRect = self.testView.frame;
    _lastScale = 1.0;
}

- (IBAction)scale:(UIPanGestureRecognizer *)sender{

    if([(UIPinchGestureRecognizer*)sender state] == UIGestureRecognizerStateBegan) {
        _lastScale = 1.0;
    }

    CGFloat scale = 1.0 - (_lastScale - [(UIPinchGestureRecognizer*)sender scale]);

    CGPoint center = self.testView.center;
    CGRect newBounds = CGRectMake(0, 0, _sourceRect.size.width * scale, _sourceRect.size.height * scale);
    self.testView.bounds = newBounds;
    self.testView.layer.transform = CATransform3DMakeRotation(M_PI * (scale - 1), 0, 0, 1);
}

@end

UPD: I have checked Aviary app: scale by frame, rotate by CGAffineTransform. You may need to implement some additional logic to make it work.

UPD: Use bounds and play with rotation

LorikMalorik
  • 2,001
  • 1
  • 14
  • 14
0

Without setting the transform, scale the size of view by changing it's bounds. This solution works for me.

You can add a method in the view class you want to scale, sample code:

- (void)scale:(CGFloat)scale {
    self.bounds = CGRectMake(0, 0, CGRectGetWidth(self.bounds) * scale, CGRectGetHeight(self.bounds) * scale);
}