CGAffineTransformMakeRotation UIView corruption - ios

I'm a bit stuck with a problem that i've got when rotating a UIView.
I have a method called 'updatePositions' that sets the view's current frame to either the left or the right of the superview, and then calls a rotation function to rotate the entire UIView. However when I call this, the frame is set fine, but the rotation corrupts the view completely.
In my layout subviews, I create a label and bezier path, each of which are only created if the label doesn't exist and are laid out using the bounds property of the view.
Am I calling CGAffineTransformMakeRotation in the wrong place?
- (void)updatePosition
{
// If the join overview orientates to north set position
if (self.joinOverview.doesOrientateToNorth)
// If the approach track is not south west place the north indicator in the lower left of the diagram
// if approach track is south west place north indicator in lower right
if (self.joinOverview.dataItem.approachTrack != VDApproachTrackSouthWest)
[self setFrame:CGRectMake(-self.superview.frame.origin.x, self.superview.frame.size.height - VIEW_SIZE, VIEW_SIZE, VIEW_SIZE)];
else
[self setFrame:CGRectMake(self.superview.frame.size.width - VIEW_SIZE, self.superview.frame.size.height - VIEW_SIZE, VIEW_SIZE, VIEW_SIZE)];
}
else // If the join overview orientates to the approach track set rotation and position
{
// Set the position to the lower left of the view
[self setFrame:CGRectMake(-self.superview.frame.origin.x, self.superview.frame.size.height - VIEW_SIZE, VIEW_SIZE, VIEW_SIZE)];
// Rotate
self.transform = CGAffineTransformMakeRotation(DegreesToRadians(50));
}
}
- (void)layoutSubviews
{
// Super
[super layoutSubviews];
// set background colour to transparent
self.backgroundColor = [UIColor clearColor];
if (!self.northSymbol) {
// Add 'N' symbol label
self.northSymbol = [[UILabel alloc]initWithFrame:CGRectMake(0, self.bounds.size.height / 2, self.bounds.size.width, self.bounds.size.height / 2)];
[self.northSymbol setText:#"N"];
[self.northSymbol setTextAlignment:NSTextAlignmentCenter];
[self.northSymbol setFont:[UIFont boldSystemFontOfSize:18]];
[self addSubview:self.northSymbol];
self.arrow = [UIBezierPath bezierPathWithThreePointArrowFromPoint:CGPointMake(self.bounds.size.width / 2, self.bounds.size.height / 2)
toPoint:CGPointMake(self.bounds.size.width / 2, self.bounds.size.height / 20)
headWidth:self.bounds.size.width / 2.5];
}
}

Either you're using auto layout which doesn't like transforms, or you're calling updatePosition more than once, and thus you're changing frame of already transformed view, which will cause you a lot of pain. Add
self.transform = CGAffineTransformIdentity;
as the first line of updatePosition.

Related

iOS - Quartz drawing issue with parent/child views

Scenario
I have two views. One is the "parent" view which contains a "child" view that does the drawing. I refer to the child as QuartzView in the code to follow. QuartzView knows how to draw a square to it's own context.
Issue
When I tell the QuartzView on it's self to draw a square it does so as expected. When I use the parent view to tell QuartsView to draw a square on it's self it draws the square in the lower left corner of the screen at about 1/5 the expected size.
Question
I assume there's some parent/child or context issues here but I'm not sure what they are. How can I get both squares to draw in the exact same place at the exact same size?
Parent ViewController
- (void)drawASquare {
// this code draws the "goofy" square that is smaller and off in the bottom left corner
x = qv.frame.size.width / 2;
y = qv.frame.size.height / 2;
CGPoint center = CGPointMake(x, y);
[qv drawRectWithCenter:center andWidth:50 andHeight:50 andFillColor:[UIColor blueColor]];
}
Child QuartzView
- (void)drawRect:(CGRect)rect
{
self.context = UIGraphicsGetCurrentContext();
UIColor *color = [UIColor colorWithRed:0 green:1 blue:0 alpha:0.5];
// this code draws a square as expected
float w = self.frame.size.width / 2;
float h = self.frame.size.height / 2;
color = [UIColor blueColor];
CGPoint center = CGPointMake(w, h);
[self drawRectWithCenter:center andWidth:20 andHeight:20 andFillColor:color];
}
- (void)drawRectWithCenter:(CGPoint)center andWidth:(float)w andHeight:(float)h andFillColor:(UIColor *)color
{
CGContextSetFillColorWithColor(self.context, color.CGColor);
CGContextSetRGBStrokeColor(self.context, 0.0, 1.0, 0.0, 1);
CGRect rectangle = CGRectMake(center.x - w / 2, center.x - w / 2, w, h);
CGContextFillRect(self.context, rectangle);
CGContextStrokeRect(self.context, rectangle);
}
Note
The opacities are the same for both squares
I turned off "Autoresize subviews" with no noticeable difference
view.contentScaleFactor = [[UIScreen mainScreen] scale]; has not helped
Edit
I'm noticing that the x/y values of the square when drawn the parent starting from the bottom left as 0,0 whereas normally 0,0 would be the top left.
The return value from UIGraphicsGetCurrentContext() is only valid inside the drawRect method. You can not and must not use that context in any other method. So the self.context property should just be a local variable.
In the drawRectWithCenter method, you should store all of the parameters in properties, and then request a view update with [self setNeedsDisplay]. That way, the framework will call drawRect with the new information. The drawRectWithCenter method should look something like this
- (void)drawRectWithCenter:(CGPoint)center andWidth:(float)w andHeight:(float)h andFillColor:(UIColor *)color
{
self.showCenter = center;
self.showWidth = w;
self.showHeight = h;
self.showFillColor = color;
[self setNeedsDisplay];
}
And of course, the drawRect function needs to take that information, and do the appropriate drawing.
I assume there's some parent/child or context issues here but I'm not sure what they are. How can I get both squares to draw in the exact same place at the exact same size?
You normally don't need to worry about the graphics context in your -drawRect: method because Cocoa Touch will set up the context for you before calling -drawRect:. But your -drawASquare method in the view controller calls -drawRectWithCenter:... to draw outside the normal drawing process, so the context isn't set up for your view. You should really have the view do its drawing in -drawRect:. If your view controller wants to make the view redraw, it should call -setNeedsDisplay, like:
[qv setNeedsDisplay];
That will add the view to the drawing list, and the graphics system will set up the graphics context and call the view's -drawRect: for you.
I'm noticing that the x/y values of the square when drawn the parent starting from the bottom left as 0,0 whereas normally 0,0 would be the top left.
UIKit and Core Animation use an upper left origin, but Core Graphics (a.k.a. Quartz) normally uses a lower left origin. The docs say:
The default coordinate system used by Core Graphics framework is LLO-based.

How do I position my UIScrollView's image properly when switching orientation?

I'm having a lot of trouble figuring out how best to reposition my UIScrollView's image view (I have a gallery kind of app going right now, similar to Photos.app, specifically when you're viewing a single image) when the orientation switches from portrait to landscape or vice-versa.
I know my best bet is to manipulate the contentOffset property, but I'm not sure what it should be changed to.
I've played around a lot, and it seems like for whatever reason 128 works really well. In my viewWillLayoutSubviews method for my view controller I have:
if (UIInterfaceOrientationIsLandscape([UIApplication sharedApplication].statusBarOrientation)) {
CGPoint newContentOffset = self.scrollView.contentOffset;
if (newContentOffset.x >= 128) {
newContentOffset.x -= 128.0;
}
else {
newContentOffset.x = 0.0;
}
newContentOffset.y += 128.0;
self.scrollView.contentOffset = newContentOffset;
}
else {
CGPoint newContentOffset = self.scrollView.contentOffset;
if (newContentOffset.y >= 128) {
newContentOffset.y -= 128.0;
}
else {
newContentOffset.y = 0.0;
}
newContentOffset.x += 128.0;
self.scrollView.contentOffset = newContentOffset;
}
And it works pretty well. I hate how it's using a magic number though, and I have no idea where this would come from.
Also, whenever I zoom the image I have it set to stay centred (just like Photos.app does):
- (void)centerScrollViewContent {
// Keep image view centered as user zooms
CGRect newImageViewFrame = self.imageView.frame;
// Center horizontally
if (newImageViewFrame.size.width < CGRectGetWidth(self.scrollView.bounds)) {
newImageViewFrame.origin.x = (CGRectGetWidth(self.scrollView.bounds) - CGRectGetWidth(self.imageView.frame)) / 2;
}
else {
newImageViewFrame.origin.x = 0;
}
// Center vertically
if (newImageViewFrame.size.height < CGRectGetHeight(self.scrollView.bounds)) {
newImageViewFrame.origin.y = (CGRectGetHeight(self.scrollView.bounds) - CGRectGetHeight(self.imageView.frame)) / 2;
}
else {
newImageViewFrame.origin.y = 0;
}
self.imageView.frame = newImageViewFrame;
}
So I need it to keep it positioned properly so it doesn't show black borders around the image when repositioned. (That's what the checks in the first block of code are for.)
Basically, I'm curious how to implement functionality like in Photos.app, where on rotate the scrollview intelligently repositions the content so that the middle of the visible content before the rotation is the same post-rotation, so it feels continuous.
You should change the UIScrollView's contentOffset property whenever the scrollView is layouting its subviews after its bounds value has been changed. Then when the interface orientation will be changed, UIScrollView's bounds will be changed accordingly updating the contentOffset.
To make things "right" you should subclass UIScrollView and make all the adjustments there. This will also allow you to easily reuse your "special" scrollView.
The contentOffset calculation function should be placed inside UIScrollView's layoutSubviews method. The problem is that this method is called not only when the bounds value is changed but also when srollView is zoomed or scrolled. So the bounds value should be tracked to hint if the layoutSubviews method is called due to a change in bounds as a consequence of the orientation change, or due to a pan or pinch gesture.
So the first part of the UIScrollView subclass should look like this:
- (id)initWithFrame:(CGRect)frame {
self = [super initWithFrame:frame];
if (self) {
// Set the prevBoundsSize to the initial bounds, so the first time
// layoutSubviews is called we won't do any contentOffset adjustments
self.prevBoundsSize = self.bounds.size;
}
return self;
}
- (void)layoutSubviews {
[super layoutSubviews];
if (!CGSizeEqualToSize(self.prevBoundsSize, self.bounds.size)) {
[self _adjustContentOffset];
self.prevBoundsSize = self.bounds.size;
}
[self _centerScrollViewContent];
}
Here, the layoutSubviews method is called every time the UIScrollView is panned, zoomed or its bounds are changed. The _centerScrollViewContent method is responsible for centering the zoomed view when its size becomes smaller than the size of the scrollView's bounds. And, it is called every time user pans or zooms the scrollView, or rotates the device. Its implementation is very similar to the implementation you provided in your question. The difference is that this method is written in the context of UIScrollView class and therefore instead of using self.imageView property to reference the zoomed view, which may not be available in the context of UIScrollView class, the viewForZoomingInScrollView: delegate method is used.
- (void)_centerScrollViewContent {
if ([self.delegate respondsToSelector:#selector(viewForZoomingInScrollView:)]) {
UIView *zoomView = [self.delegate viewForZoomingInScrollView:self];
CGRect frame = zoomView.frame;
if (self.contentSize.width < self.bounds.size.width) {
frame.origin.x = roundf((self.bounds.size.width - self.contentSize.width) / 2);
} else {
frame.origin.x = 0;
}
if (self.contentSize.height < self.bounds.size.height) {
frame.origin.y = roundf((self.bounds.size.height - self.contentSize.height) / 2);
} else {
frame.origin.y = 0;
}
zoomView.frame = frame;
}
}
But the more important thing here is the _adjustContentOffset method. This method is responsible for adjusting the contentOffset. Such that when UIScrollView's bounds value is changed the center point before the change will remain in center. And because of the condition statement, it is called only when UIScrollView's bounds is changed (e.g.: orientation change).
- (void)_adjustContentOffset {
if ([self.delegate respondsToSelector:#selector(viewForZoomingInScrollView:)]) {
UIView *zoomView = [self.delegate viewForZoomingInScrollView:self];
// Using contentOffset and bounds values before the bounds were changed (e.g.: interface orientation change),
// find the visible center point in the unscaled coordinate space of the zooming view.
CGPoint prevCenterPoint = (CGPoint){
.x = (self.prevContentOffset.x + roundf(self.prevBoundsSize.width / 2) - zoomView.frame.origin.x) / self.zoomScale,
.y = (self.prevContentOffset.y + roundf(self.prevBoundsSize.height / 2) - zoomView.frame.origin.y) / self.zoomScale,
};
// Here you can change zoomScale if required
// [self _changeZoomScaleIfNeeded];
// Calculate new contentOffset using the previously calculated center point and the new contentOffset and bounds values.
CGPoint contentOffset = CGPointMake(0.0, 0.0);
CGRect frame = zoomView.frame;
if (self.contentSize.width > self.bounds.size.width) {
frame.origin.x = 0;
contentOffset.x = prevCenterPoint.x * self.zoomScale - roundf(self.bounds.size.width / 2);
if (contentOffset.x < 0) {
contentOffset.x = 0;
} else if (contentOffset.x > self.contentSize.width - self.bounds.size.width) {
contentOffset.x = self.contentSize.width - self.bounds.size.width;
}
}
if (self.contentSize.height > self.bounds.size.height) {
frame.origin.y = 0;
contentOffset.y = prevCenterPoint.y * self.zoomScale - roundf(self.bounds.size.height / 2);
if (contentOffset.y < 0) {
contentOffset.y = 0;
} else if (contentOffset.y > self.contentSize.height - self.bounds.size.height) {
contentOffset.y = self.contentSize.height - self.bounds.size.height;
}
}
zoomView.frame = frame;
self.contentOffset = contentOffset;
}
}
Bonus
I've created a working SMScrollView class (here is link to GitHub) implementing the above behavior and additional bonuses:
You can notice that in Photos app, zooming a photo, then scrolling it to one of its boundaries and then rotating the device does not keep the center point in its place. Instead it sticks the scrollView to that boundary. And if you scroll to one of the corners and then rotate, the scrollView will be stick to that corner as well.
In addition to adjusting contentOffset you may find that you also want to adjust the scrollView's zoomScale. For example, assume you are viewing a photo in portrait mode that is scaled to fit the screen size. Then when you rotate the device to the landscape mode you may want to upscale the photo to take advantage of the available space.

Horizontally center multiple UIViews

I want to horizontally center a number of UIViews (they happen to be circles) in the master UIView. It will end up basically looking like the dots on the standard Page Control.
I have all the code written to create the circle UIViews I just have no idea how to arrange them horizontally and dynamically at run time.
Essentially I need some kind of horizontal container where I can do this
-(void)addCircle{
[self addSubView:[CircleView init]];
}
And it will auto arrange however many children it has in the center.
I get confused with auto-layout as well from time to time but here is a way how you can do it programmatically: (I assume that you add your circle views to a containerView property of your view controller and you do not add any other views to it.)
Add these two properties to your view controller:
#property (nonatomic) CGRect circleViewFrame;
#property (nonatomic) CGFloat delta;
Initiate those properties with the desired values in your view controller's viewDidLoad method:
// the size (frame) of your circle views
self.circleViewFrame = CGRectMake(0, 0, 10, 10);
// the horizontal distance between your circle views
self.delta = 10.0;
Now we add your "automatic addCircle method":
- (void)addCircleView {
UIView *newCircleView = [self createCircleView];
[self.containerView addSubview:newCircleView];
[self alignCircleViews];
}
Of course we need to implement the createCircleView method...
- (UIView*)createCircleView {
// Create your circle view here - I use a simple square view as an example
UIView *circleView = [[UIView alloc] initWithFrame:self.circleViewFrame];
// Set the backgroundColor to some solid color so you can see the view :)
circleView.backgroundColor = [UIColor redColor];
return circleView;
}
... and the alignCircleViews method:
- (void)alignCircleViews {
int numberOfSubviews = [self.containerView.subviews count];
CGFloat totalWidth = (numberOfSubviews * self.circleViewFrame.size.width) + (numberOfSubviews - 1) * self.delta;
CGFloat x = (self.containerView.frame.size.width / 2) - (totalWidth / 2);
for (int i = 0; i < numberOfSubviews; i++) {
UIView *circleView = self.containerView.subviews[i];
circleView.frame = CGRectMake(x,
self.circleViewFrame.origin.y,
self.circleViewFrame.size.width,
self.circleViewFrame.size.height);
x += self.circleViewFrame.size.width + self.delta;
}
}
This is the most important method which will automatically realign all your subviews each time a new circleView is added. The result will look like this:
Simple steps: append circle to container view, resize container view, center align container view
-(void)addToContanerView:(CircleView*)circle{
circle.rect.frame = CGrectMake(containers_end,container_y,no_change,no_change);
[containerView addSubview:circle];
[containerView sizeToFit];
containerView.center = self.view.center;
}
Assumptions:
containers_end & containers_y you can get from CGRectMax function,
for UIView SizeToFit method check here
To take care of rotation use make sure your Autoresizing subviews are set for left, right bottom and top margin.
You can try using this library. I have used it on several of my projects and so far, it worked really well.
https://github.com/davamale/DMHorizontalView

Overriding "bitmap scaling" effect of UIScrollView zooming

In my iPad app, Universal Combat Log (new-layout branch), I have a UIView subclass (UCLLineChartView) which contains a UIScrollView and the scrollview in turn contains another UIView subclass (ChartView). ChartView has multiple sub-layers, one for each line of data that has been added to the chart. UCLLineChartView draws the axes and markers. The contents of these views/layers are entirely custom drawn, no stock views are used (e.g. UIImageView).
I'm having a problem with zooming -- it's scaling the ChartView like an image, which makes the drawn line all blurred and stretched. I want the line to stay sharp, preferably even while the user is in the act of zooming, but after 3 days of hacking at this, I cannot get it to work.
If I override setTransform on the ChartView to grab the scale factor from the transform but don't call [super setTransform], then the scrollview's zoomScale stays at 1. I tried keeping the given transform and overriding the transform method to return it. I tried replicating the effects of setTransform by changing the ChartView's center and bounds but I wasn't able to get the behaviour quite right and it still didn't seem to affect the scrollview's zoomScale. It seems that the scrollview's zoomScale depends on the effects of setTransform, but I cannot determine how.
Any assistance would be greatly appreciated.
What you will need to do is update the contentScaleFactor of the chartView. You can do that by adding the following code in either scrollViewDidEndZooming:withView:atScale: or scrollViewDidZoom:.
CGFloat newScale = scrollView.zoomScale * [[UIScreen mainScreen] scale];
[self.chartView setContentScaleFactor:newScale];
I have figured out a solution to my problem that is not too gross a hack. In your UIScrollViewDelegate:
- (void)scrollViewWillBeginZooming:(UIScrollView *)scrollView withView:(UIView *)view
{
[_contentView beginZoom];
}
- (void)scrollViewDidEndZooming:(UIScrollView *)scrollView withView:(UIView *)view atScale:(float)scale
{
CGSize size = scrollView.bounds.size;
CGPoint contentOffset = _scrollView.contentOffset;
CGFloat newScale = _contentView.scale;
newScale = MAX(newScale, kMinZoomScale);
newScale = MIN(newScale, kMaxZoomScale);
[_scrollView setZoomScale:1.0 animated:NO];
_scrollView.minimumZoomScale = kMinZoomScale / newScale;
_scrollView.maximumZoomScale = kMaxZoomScale / newScale;
_contentView.scale = newScale;
CGSize newContentSize = CGSizeMake(size.width * newScale, size.height);
_contentView.frame = CGRectMake(0, 0, newContentSize.width, newContentSize.height);
_scrollView.contentSize = newContentSize;
[_scrollView setContentOffset:contentOffset animated:NO];
[_contentView updateForNewSize];
[_contentView setNeedsDisplay];
}
In your content view, declare a scale property and the following methods:
- (void)beginZoom
{
_sizeAtZoomStart = CGSizeApplyAffineTransform(self.frame.size, CGAffineTransformMakeScale(1/self.scale, 1));
_scaleAtZoomStart = self.scale;
}
- (void)setTransform:(CGAffineTransform)transform
{
self.scale = _scaleAtZoomStart * transform.a;
self.frame = CGRectMake(0, 0, _sizeAtZoomStart.width * self.scale, _sizeAtZoomStart.height);
[self updateForNewSize];
[self setNeedsDisplay];
}
And if your content view uses sub-layers, you'll need to disable their implicit animations by adding the following to the sub-layers' delegate(s):
- (id<CAAction>)actionForLayer:(CALayer *)layer forKey:(NSString *)event
{
// prevent animation of the individual layers so that zooming doesn't cause weird jitter
return (id<CAAction>)[NSNull null];
}
The basic idea here is that the overridden setTransform uses the scale factor from the tranform matrix to calculate the new scale factor for the content view and then resizes the content view accordingly. The scrollview automatically adjusts the content offset to keep the content view centered.
The scrollViewDidEndZooming code keeps the zooming bounded.
There are further complexities for dealing with resizing the scrollview when rotating device for example.

Start new CAAnimation from current CAAnimation's state

I'm using a pair of CALayers as image masks, allowing me to animate a bulb filling or emptying at a set speed while also following the current touch position. That is, one mask jumps to follow the touch and the other slides to that position. Since I use an explicit animation I'm forced to set the position of the mask sliding mask when I add the animation. This means that if I start a fill and then start an empty before the fill completes the empty will begin from the completed fill position (the opposite is also true).
Is there a way to get the position of the animation, set the position at each step of the animation, or to have the new animation begin from the current state of the active animation?
The code handling the animating is below:
- (void)masksFillTo:(CGFloat)height {
// Clamp the height we fill to inside the bulb. Remember Y gets bigger going down.
height = MIN(MAX(BULB_TOP, height), BULB_BOTTOM);
// We can find the target Y location by subtracting the Y value for the top of the
// bulb from the height.
CGFloat targetY = height - BULB_TOP;
// Find the bottom of the transparent mask to determine where the solid fill
// is sitting. Then find how far that fill needs to move.
// TODO: This works with the new set position, so overriding old anime doesn't work
CGFloat bottom = transMask.frame.origin.y + transMask.frame.size.height;
// If the target is above the bottom of the solid, we want to fill up.
// This means the empty mask jumps and the transparent mask slides.
CALayer *jumper;
CALayer *slider;
if (bottom - targetY >= 0) {
jumper = emptyMask;
slider = transMask;
// We need to reset the bottom to the emptyMask
bottom = emptyMask.frame.origin.y + emptyMask.frame.size.height;
} else {
jumper = transMask;
slider = emptyMask;
}
[jumper removeAllAnimations];
[slider removeAllAnimations];
CGFloat dy = bottom - targetY;
[CATransaction begin];
[CATransaction setValue:(id)kCFBooleanTrue forKey:kCATransactionDisableActions];
[jumper setPosition:CGPointMake(jumper.position.x, jumper.position.y - dy)];
[self slideMaskFillTo:height withMask:slider]; // Do this inside here or an odd flash glitch appears.
[CATransaction commit];
}
// TODO: Always starts from new position, even if animation hasn't reached it.
- (void)slideMaskFillTo:(CGFloat)height withMask:(CALayer *)slider {
// We can find the target Y location by subtracting the Y value for the top of the
// bulb from the height.
CGFloat targetY = height - BULB_TOP;
// We then find the bottom of the mask.
CGFloat bottom = slider.frame.origin.y + slider.frame.size.height;
CGFloat dy = bottom - targetY;
// Do the animation. Animating with duration doesn't appear to work properly.
// Apparently "When modifying layer properties from threads that don’t have a runloop,
// you must use explicit transactions."
CABasicAnimation *a = [CABasicAnimation animationWithKeyPath:#"position"];
a.duration = (dy > 0 ? dy : -dy) / PXL_PER_SEC; // Should be 2 seconds for a full fill
a.fromValue = [NSValue valueWithCGPoint:slider.position];
CGPoint newPosition = slider.position;
newPosition.y -= dy;
a.toValue = [NSValue valueWithCGPoint:newPosition];
a.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionLinear];
[slider addAnimation:a forKey:#"colorize"];
// Update the actual position
slider.position = newPosition;
}
And an example of how this is called. Notice this means it can be called mid-animation.
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
UITouch *touch = [touches anyObject];
CGPoint point = [touch locationInView:self.view];
[self masksFillTo:point.y];
}
If anyone finds it relevant, this is the creation of the images and masks.
// Instantiate the different bulb images - empty, transparent yellow, and solid yellow. This
// includes setting the frame sizes. This approach found at http://stackoverflow.com/a/11218097/264775
emptyBulb = [[UIImageView alloc] initWithImage:[UIImage imageNamed:#"Light.png"]];
transBulb = [[UIImageView alloc] initWithImage:[UIImage imageNamed:#"Light-moving.png"]];
solidBulb = [[UIImageView alloc] initWithImage:[UIImage imageNamed:#"Light-on.png"]];
[emptyBulb setFrame:CGRectMake(10, BULB_TOP, 300, BULB_HEIGHT)]; // 298 x 280
[transBulb setFrame:CGRectMake(10, BULB_TOP, 300, BULB_HEIGHT)]; // 298 x 280
[solidBulb setFrame:CGRectMake(10, BULB_TOP, 300, BULB_HEIGHT)]; // 298 x 280
[self.view addSubview:solidBulb]; // Empty on top, then trans, then solid.
[self.view addSubview:transBulb];
[self.view addSubview:emptyBulb];
// Create a mask for the empty layer so it will cover the other layers.
emptyMask = [CALayer layer];
[emptyMask setContentsScale:emptyBulb.layer.contentsScale]; // handle retina scaling
[emptyMask setFrame:emptyBulb.layer.bounds];
[emptyMask setBackgroundColor:[UIColor blackColor].CGColor];
emptyBulb.layer.mask = emptyMask;
// Also create a mask for the transparent image.
transMask = [CALayer layer];
[transMask setContentsScale:transBulb.layer.contentsScale]; // handle retina scaling
[transMask setFrame:transBulb.layer.bounds];
[transMask setBackgroundColor:[UIColor blackColor].CGColor];
transBulb.layer.mask = transMask;
Was just led to a solution via this answer. If one looks in the right part of the docs, you'll find the following:
- (id)presentationLayer
Returns a copy of the layer containing all properties as they were at the start of the current transaction, with any active animations applied.
So if I add this code before I first check the transparent mask location (aka solid level), I grab the current animated position and can switch between fill up and fill down.
CALayer *temp;
if (transMask.animationKeys.count > 0) {
temp = transMask.presentationLayer;
transMask.position = temp.position;
}
if (emptyMask.animationKeys.count > 0) {
temp = emptyMask.presentationLayer;
emptyMask.position = temp.position;
}
layer.presentation() makes copy of original layer each time you get it, and you need to override init(layer: Any) for every custom CALayer you have.
Another possible way to start from current state is to set animation's beginTime using current animation's beginTime. It allows to start new animation with offset.
In your case it could be (Swift):
if let currentAnimation = slider.animation(forKey: "colorize") {
let currentTime = CACurrentMediaTime()
let animationElapsedTime = currentAnimation.beginTime + currentAnimation.duration - currentTime
a.beginTime = currentTime - animationElapsedTime
}
However this works great for linear timings, but for others it could be difficult to calculate the proper time.

Resources