schmidt9
schmidt9

Reputation: 4538

Zoom UIView inside UIScrollView with drawing

I have a scroll view (gray) with a zooming view inside (orange). The problem is if I zoom this view the red shape drawn on it gets zoomed too including lines width and blue squares size. What I want is to keep constant lines width and blue squares size (like on first picture) scaling just the area of the shape itself according to zoom level (drawn text is just for reference, I don't care about its size)

before zoom

enter image description here

after zoom

enter image description here

view controller

#import "ViewController.h"
#import "ZoomingView.h"

@interface ViewController ()

@property (strong, nonatomic) IBOutlet UIScrollView *scrollView;

@end

@implementation ViewController
{
    ZoomingView *_zoomingView;
}

- (void)viewDidLayoutSubviews
{
    [self setup];
}

- (void)setup
{
    CGFloat kViewSize = self.scrollView.frame.size.width - 40;

    self.scrollView.minimumZoomScale = 1;
    self.scrollView.maximumZoomScale = 10;
    self.scrollView.delegate = self;
    self.scrollView.contentSize = self.scrollView.bounds.size;

    _zoomingView = [[ZoomingView alloc] initWithFrame:
                    CGRectMake((self.scrollView.frame.size.width - kViewSize) / 2,
                               (self.scrollView.frame.size.height - kViewSize) / 2,
                               kViewSize,
                               kViewSize)];
    [self.scrollView addSubview:_zoomingView];
}

#pragma mark - UIScrollViewDelegate

- (UIView*)viewForZoomingInScrollView:(UIScrollView *)scrollView
{
    return _zoomingView;
}

- (void)scrollViewDidZoom:(UIScrollView *)scrollView
{
    // zooming view position fix

    UIView *zoomView = [scrollView.delegate viewForZoomingInScrollView:scrollView];
    CGRect zvf = zoomView.frame;

    if (zvf.size.width < scrollView.bounds.size.width) {
        zvf.origin.x = (scrollView.bounds.size.width - zvf.size.width) / 2.0f;
    } else {
        zvf.origin.x = 0.0;
    }

    if (zvf.size.height < scrollView.bounds.size.height) {
        zvf.origin.y = (scrollView.bounds.size.height - zvf.size.height) / 2.0f;
    } else {
        zvf.origin.y = 0.0;
    }

    zoomView.frame = zvf;

    [_zoomingView updateWithZoomScale:scrollView.zoomScale];
}

@end

zooming view

#import "ZoomingView.h"

@implementation ZoomingView
{
    CGFloat _zoomScale;
}

- (instancetype)initWithFrame:(CGRect)frame
{
    self = [super initWithFrame:frame];
    if (self) {
        [self setup];
    }
    return self;
}

- (void)setup
{
    self.backgroundColor = [UIColor orangeColor];

    _zoomScale = 1;
}

- (void)drawRect:(CGRect)rect
{
    const CGFloat kPointSize = 10;

    NSArray *points = @[[NSValue valueWithCGPoint:CGPointMake(30, 30)],
                        [NSValue valueWithCGPoint:CGPointMake(200, 40)],
                        [NSValue valueWithCGPoint:CGPointMake(180, 200)],
                        [NSValue valueWithCGPoint:CGPointMake(70, 180)]];

    CGContextRef context = UIGraphicsGetCurrentContext();
    CGContextSetLineWidth(context, 1);

    // points

    [[UIColor blueColor] setStroke];

    for (NSValue *value in points) {
        CGPoint point = [value CGPointValue];
        CGContextStrokeRect(context, CGRectMake(point.x - kPointSize / 2,
                                                point.y - kPointSize / 2,
                                                kPointSize,
                                                kPointSize));
    }

    // lines

    [[UIColor redColor] setStroke];

    for (NSUInteger i = 0; i < points.count; i++) {
        CGPoint point = [points[i] CGPointValue];

        if (i == 0) {
            CGContextMoveToPoint(context, point.x, point.y);
        } else {
            CGContextAddLineToPoint(context, point.x, point.y);
        }
    }

    CGContextClosePath(context);
    CGContextStrokePath(context);

    // text

    NSAttributedString *str = [[NSAttributedString alloc] initWithString:[NSString stringWithFormat:@"%f", _zoomScale] attributes:@{NSFontAttributeName : [UIFont systemFontOfSize:12]}];
    [str drawAtPoint:CGPointMake(5, 5)];
}

- (void)updateWithZoomScale:(CGFloat)zoomScale
{
    _zoomScale = zoomScale;

    [self setNeedsDisplay];
}

@end

EDIT

Based on proposed solution (which works for sure) I was interested if I could make it work using my drawRect routine and Core Graphics methods.

So I changed my code this way, applying proposed scaling and contentsScale approach from this answer. As a result, without contentsScale drawing looks very blurry and with it much better, but a light blurriness persists anyway.

So the approach with layers gives the best result, although I don't get why.

- (void)drawRect:(CGRect)rect
{
    const CGFloat kPointSize = 10;

    NSArray *points = @[[NSValue valueWithCGPoint:CGPointMake(30, 30)],
                        [NSValue valueWithCGPoint:CGPointMake(200, 40)],
                        [NSValue valueWithCGPoint:CGPointMake(180, 200)],
                        [NSValue valueWithCGPoint:CGPointMake(70, 180)]];

    CGFloat scaledPointSize = kPointSize * (1.0 / _zoomScale);
    CGFloat lineWidth = 1.0 / _zoomScale;

    CGContextRef context = UIGraphicsGetCurrentContext();
    CGContextSetLineWidth(context, lineWidth);

    // points

    [[UIColor blueColor] setStroke];

    for (NSValue *value in points) {
        CGPoint point = [value CGPointValue];
        CGContextStrokeRect(context, CGRectMake(point.x - scaledPointSize / 2,
                                                point.y - scaledPointSize / 2,
                                                scaledPointSize,
                                                scaledPointSize));
    }

    // lines

    [[UIColor redColor] setStroke];

    for (NSUInteger i = 0; i < points.count; i++) {
        CGPoint point = [points[i] CGPointValue];

        if (i == 0) {
            CGContextMoveToPoint(context, point.x, point.y);
        } else {
            CGContextAddLineToPoint(context, point.x, point.y);
        }
    }

    CGContextClosePath(context);
    CGContextStrokePath(context);

    // text

    NSAttributedString *str = [[NSAttributedString alloc] initWithString:[NSString stringWithFormat:@"%f", _zoomScale] attributes:@{NSFontAttributeName : [UIFont systemFontOfSize:12]}];
    [str drawAtPoint:CGPointMake(5, 5)];
}

- (void)updateWithZoomScale:(CGFloat)zoomScale
{
    _zoomScale = zoomScale;

    [self setNeedsDisplay];

    [CATransaction begin];
    [CATransaction setValue:[NSNumber numberWithBool:YES]
                     forKey:kCATransactionDisableActions];
    self.layer.contentsScale = zoomScale;
    [CATransaction commit];
}

Upvotes: 0

Views: 392

Answers (1)

DonMag
DonMag

Reputation: 77690

You may be better off putting your boxes and line-shape on CAShapeLayers, where you can update the line-width based on the zoom scale.

You only need to create and define your line-shape once. For your boxes, though, you'll need to re-create the path when you change the zoom (to keep the width/height of the boxes at a constant non-zoomed point size.

Give this a try. You should be able to simply replace your current ZoomingView.m class - no changes to the view controller necessary.

//
//  ZoomingView.m
//
//  modified by Don Mag
//

#import "ZoomingView.h"

@interface ZoomingView()

@property (strong, nonatomic) CAShapeLayer *shapeLayer;
@property (strong, nonatomic) CAShapeLayer *boxesLayer;

@property (strong, nonatomic) NSArray *points;

@end

@implementation ZoomingView

{
    CGFloat _zoomScale;
    CGFloat _kPointSize;
}

- (instancetype)initWithFrame:(CGRect)frame
{
    self = [super initWithFrame:frame];
    if (self) {
        [self setup];
    }
    return self;
}

- (void)setup
{
    self.backgroundColor = [UIColor orangeColor];

    _points = @[[NSValue valueWithCGPoint:CGPointMake(30, 30)],
                [NSValue valueWithCGPoint:CGPointMake(200, 40)],
                [NSValue valueWithCGPoint:CGPointMake(180, 200)],
                [NSValue valueWithCGPoint:CGPointMake(70, 180)]];

    _zoomScale = 1;

    _kPointSize = 10.0;

    // create and setup boxes layer
    _boxesLayer = [CAShapeLayer new];
    [self.layer addSublayer:_boxesLayer];
    _boxesLayer.strokeColor = [UIColor redColor].CGColor;
    _boxesLayer.fillColor = [UIColor clearColor].CGColor;
    _boxesLayer.lineWidth = 1.0;
    _boxesLayer.frame = self.bounds;

    // create and setup shape layer
    _shapeLayer = [CAShapeLayer new];
    [self.layer addSublayer:_shapeLayer];
    _shapeLayer.strokeColor = [UIColor greenColor].CGColor;
    _shapeLayer.fillColor = [UIColor clearColor].CGColor;
    _shapeLayer.lineWidth = 1.0;
    _shapeLayer.frame = self.bounds;

    // new path for shape
    UIBezierPath *thePath = [UIBezierPath new];

    for (NSValue *value in _points) {

        CGPoint point = [value CGPointValue];

        if ([value isEqualToValue:_points.firstObject]) {
            [thePath moveToPoint:point];
        } else {
            [thePath addLineToPoint:point];
        }

    }

    [thePath closePath];

    [_shapeLayer setPath:thePath.CGPath];

    // trigger the boxes update
    [self updateWithZoomScale:_zoomScale];
}

- (void)drawRect:(CGRect)rect
{
    // text

    NSAttributedString *str = [[NSAttributedString alloc] initWithString:[NSString stringWithFormat:@"%f", _zoomScale] attributes:@{NSFontAttributeName : [UIFont systemFontOfSize:12]}];
    [str drawAtPoint:CGPointMake(5, 5)];
}

- (void)updateWithZoomScale:(CGFloat)zoomScale
{
    _zoomScale = zoomScale;

    CGFloat scaledPointSize = _kPointSize * (1.0 / zoomScale);

    // create a path for the boxes
    //  needs to be done here, because the width/height of the boxes
    //  must change with the scale
    UIBezierPath *thePath = [UIBezierPath new];

    for (NSValue *value in _points) {

        CGPoint point = [value CGPointValue];

        CGRect r = CGRectMake(point.x - scaledPointSize / 2.0,
                              point.y - scaledPointSize / 2.0,
                              scaledPointSize,
                              scaledPointSize);

        [thePath appendPath:[UIBezierPath bezierPathWithRect:r]];

    }

    [_boxesLayer setPath:thePath.CGPath];

    _boxesLayer.lineWidth = 1.0 / zoomScale;
    _shapeLayer.lineWidth = 1.0 / zoomScale;

    [self setNeedsDisplay];
}

@end

Results:

enter image description here enter image description here

Note: Should go without saying, but... This is intended to be a starting point for you to work with, not "production code."

Upvotes: 1

Related Questions