Search code examples
objective-cnsdictionarycgaffinetransformcgrectcgpoint

Restoring CGRect with Transform values from NSDictionary


I need to be able to save the frame after transformation of a UIImageView. In the below example, the original frame is when the image is added to the superview. The user then has the ability to rotate, scale and pan the image anywhere in the gray area (superview).

I take these images and save their coordinates to an NSDictionary (which is not the problem). The problem is that if I get the frame after the rotation, the frame is completely off. I need to be able to store the new frame with transform in the dictionary, so that when the user comes back to this view and the images are loaded, the frames and saved transformations are just like they intended.

Panning

CGPoint translation = [gestureRecognizer translationInView:[object superview]];
if (CGRectContainsPoint(self.frame, CGPointMake([object center].x + translation.x,  [object center].y + translation.y))) {
    [object setCenter:CGPointMake([object center].x + translation.x, [object center].y + translation.y)];
    [gestureRecognizer setTranslation:CGPointZero inView:[object superview]];
}

Rotating

self.transformRotation = CGAffineTransformRotate([[gestureRecognizer view] transform], [gestureRecognizer rotation]);

[gestureRecognizer view].transform = self.transformRotation;
if ( [gestureRecognizer rotation] != 0 ) {
    self.rotate = [gestureRecognizer rotation];
}
[gestureRecognizer setRotation:0];

Scaling

self.transformScale = CGAffineTransformScale([[gestureRecognizer view] transform], [gestureRecognizer scale], [gestureRecognizer scale]);
[gestureRecognizer view].transform = self.transformScale;
if ( [gestureRecognizer scale] != 1 ) {
    self.scale = [gestureRecognizer scale];
}
[gestureRecognizer setScale:1];

Using the Center point of the view keeps the image closer to the original location when it was saved, the first time it is loaded. Each time it is saved after that the position is the same, because the transform did not change during that session.

- (CGPoint)centerOnCanvas {
    CGPoint originalCenter = self.center;
    return originalCenter;
}

- (CGRect)frameOnCanvas {
    return CGRectMake(
        self.preTransformedFrame.origin.x,
        self.preTransformedFrame.origin.y,
        self.preTransformedFrame.size.width,
        self.preTransformedFrame.size.height
    );
}

- (CGRect)preTransformedFrame {
    CGAffineTransform currentTransform = self.transform;
    self.transform = CGAffineTransformIdentity;
    CGRect originalFrame = self.bounds;
    self.transform = currentTransform;
    return originalFrame;
}

enter image description here

UPDATE: Slightly off and a little larger than the original

enter image description here


Solution

  • According to Apple document for UIView's transform property

    When the value of this property is anything other than the identity transform, the value in the frame property is undefined and should be ignored.

    That's the reason why you can't get frame after changing transform.

    As I understand, after rotating, scaling or panning you want to save current state to restore later. In my opinion, all you need to do it is saving transform and center of UIImageView each time they are changed. You don't need frame in this case.

    For example, _transformTarget is your UIImageView, to save its current state you can use below method (Instead of saving in a NSDictionary, I use NSUserDefaults. You can change it to NSDictionary)

    - (void)saveCurrentState {
        [[NSUserDefaults standardUserDefaults] setObject:NSStringFromCGAffineTransform(_transformTarget.transform) forKey:@"_transformTarget.transform"];
        [[NSUserDefaults standardUserDefaults] setObject:NSStringFromCGPoint(_transformTarget.center) forKey:@"_transformTarget.center"];
    }
    

    At the end of each handling gesture method, save current state by using saveCurrentState.

    - (void)handlePanGesture:(UIPanGestureRecognizer*)gesture {
        CGPoint translation = [gesture translationInView:self.view];
        CGPoint newCenter = CGPointMake(_transformTarget.center.x + translation.x, _transformTarget.center.y + translation.y);
    
        if (CGRectContainsPoint(self.view.frame, newCenter)) {
            _transformTarget.center = newCenter;
            [gesture setTranslation:CGPointZero inView:self.view];
    
            [self saveCurrentState];  // Save current state when center is changed
        }
    }
    
    - (void)handleRotationGesture:(UIRotationGestureRecognizer*)gesture {
        _transformTarget.transform = CGAffineTransformRotate(_transformTarget.transform, gesture.rotation);
        gesture.rotation = 0;
    
        [self saveCurrentState];  // Save current state when transform is changed
    }
    
    - (void)handlePinchGesture:(UIPinchGestureRecognizer*)gesture {
        _transformTarget.transform = CGAffineTransformScale(_transformTarget.transform, gesture.scale, gesture.scale);
        gesture.scale = 1;
    
        [self saveCurrentState];  // Save current state when transform is changed
    }
    

    Now, the information about UIImageView is saved every time it's changed. At the next time user comes back, get info about center and transform from your dictionary and set them again.

    - (void)restoreFromSavedState {
        NSString *transformString = [[NSUserDefaults standardUserDefaults] objectForKey:@"_transformTarget.transform"];
        CGAffineTransform transform = transformString ? CGAffineTransformFromString(transformString) : CGAffineTransformIdentity;
    
        NSString *centerString = [[NSUserDefaults standardUserDefaults] objectForKey:@"_transformTarget.center"];
        CGPoint center = centerString ? CGPointFromString(centerString) : self.view.center;
    
        _transformTarget.center = center;
        _transformTarget.transform = transform;
    }
    

    Result

    For more detail, you can take a look at my sample repo https://github.com/trungducc/stackoverflow/tree/restore-view-transform