Programmatically adapt uiviews to iPad / iPhone screens - iphone

I make an app that has many views that subclass from UIView. The size and the orientation of these views is random and the state of a screen of the app can be saved. When the user saves a screen on the same device that he opens it, then the screen state is OK. Everything is positioned correctly. But, if the user saves the screen state on an iPhone and opens it from an iPad the views are not positioned correctly. Actually the views appear shorter or longer, the center seems to be saved correctly, but the rotation of the views and their size (bounds property) are not working OK.
These are the two methods that save and restore the state of the view
- (void)encodeWithCoder:(NSCoder *)aCoder {
// Save the screen size of the device that the view was saved on
[aCoder encodeCGSize:self.gameView.bounds.size forKey:#"saveDeviceGameViewSize"];
// ****************
// ALL properties are saved in normalized coords
// ****************
// Save the center of the view
CGPoint normCenter = CGPointMake(self.center.x / self.gameView.bounds.size.width, self.center.y / self.gameView.bounds.size.height);
[aCoder encodeCGPoint:normCenter forKey:#"center"];
// I rely on view bounds NOT frame
CGRect normBounds = CGRectMake(0, 0, self.bounds.size.width / self.gameView.bounds.size.width, self.bounds.size.height / self.gameView.bounds.size.height);
[aCoder encodeCGRect:normBounds forKey:#"bounds"];
// Here I save the transformation of the view, it has ONLY rotation info, not translation or scalings
[aCoder encodeCGAffineTransform:self.transform forKey:#"transform"];
}
- (id)initWithCoder:(NSCoder *)aDecoder {
self = [super initWithCoder:aDecoder];
if (self) {
// Restore the screen size of the device that the view was saved on
saveDeviceGameViewSize = [aDecoder decodeCGSizeForKey:#"saveDeviceGameViewSize"];
// Adjust the view center
CGPoint tmpCenter = [aDecoder decodeCGPointForKey:#"center"];
tmpCenter.x *= self.gameView.bounds.size.width;
tmpCenter.y *= self.gameView.bounds.size.height;
self.center = tmpCenter;
// Restore the transform
self.transform = [aDecoder decodeCGAffineTransformForKey:#"transform"];
// Restore the bounds
CGRect tmpBounds = [aDecoder decodeCGRectForKey:#"bounds"];
CGFloat ratio = self.gameView.bounds.size.height / saveDeviceGameViewSize.height;
tmpBounds.size.width *= (saveDeviceGameViewSize.width * ratio);
tmpBounds.size.height *= self.gameView.bounds.size.height;
self.bounds = tmpBounds;
}
return self;
}

What about separating the view states between your iPhone and iPad version? For each device, if a configuration for that device hasn't been saved, just use the default? I haven't seen your UI, but I think in general a solution like this would work well, be easier to implement, and also follow users' expectations.

Very much depends on when you apply your transformation. I would propose the following steps when you load your view:
1. load the view bounds
2. load the view centre
3. load the transformation
There is also a problem with this code. You set your bounds after you set the center. This is completely wrong, because the bounds property will not be working always. Try to do the steps I've described and report back please with the result, but it should work.

Can self.gameView.bounds change depending on what device you're using? If the answer is yes, I see a problem because you're storing the centre after normalisation by the gameView bounds size, but when you restore centre you're un-normalising it by multiplying it by the stored value of gameView.bounds, rather than the value of gameView.bounds for the current device.
Also, the bit of code for // Restore the bounds looks wrong, because in the line that sets tmpBounds.size.width = you're doing a calculation involving a width and ratio that is a ratio of two widths. In other words, there's no height involved which looks wrong. Maybe it should be the following?
// note: height, not width, used on RHS
tmpBounds.size.width *= (saveDeviceGameViewSize.height * ratio);
In order to generally proceed with fixing this (if the above doesn't help): you need to
a) verify that the thing you're implementing makes sense (i.e. the algorithm in your head), and
b) verify that you're doing it right by analysing your code and doing some judicious debugging/NSLogging on the save + restore code

Related

Smooth aspect change during orientation change

When the screen of the iPhone orientation changes, I adjust my projection matrix of my 3D rendering to the new aspect value. However, doing this in either willRotateToInterfaceOrientation or didRotateFromInterfaceOrientation would cause the aspect ratio being wrong during the transition, because at the beginning of the animation, the screen size is still the same as before and is changed to the new bounds gradually while being rotated. Therefore I want the aspect value used for my 3D projection matrix to change gradually as well. To achieve this, I retrieve start time and duration for the rotation animation in willAnimateRotationToInterfaceOrientation:
- (void)willAnimateRotationToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation duration:(NSTimeInterval)duration
{
_aspect.isChanging = YES;
_aspect.startedChanging = [[NSDate date] retain];
_aspect.changeDuration = duration;
_aspect.oldValue = self.renderer.aspect;
_aspect.currentValue = fabsf(self.view.bounds.size.width / self.view.bounds.size.height);
}
Note that the view bound size is already set to the new value that will be valid after the animation.
Then, in update, I do the following:
- (void)update
{
if (_aspect.isChanging) {
float f = MIN(1, [[NSDate date] timeIntervalSinceDate:_aspect.startedChanging] / _aspect.changeDuration);
self.renderer.aspect = _aspect.oldValue * (1-f) + _aspect.currentValue * f;
} else {
self.renderer.aspect = _aspect.currentValue;
}
[self.renderer update];
}
This already works quite well, but the render aspect change does not match the actual aspect change, which is because I'm interpolating it linearly. Therefore I tried to match the easing of the actual aspect change by throwing math functions at the problem: The best result I could get was by adding the following line:
f = 0.5f - 0.5f*cosf(f*M_PI);
This results in almost no visible stretching of the image during the rotation, however if you look closely, it still seems to be a bit unmatched somewhere in between. I guess, the end user won't notice it, but I'm asking here if there might be a better solution, so these are my questions:
What is the actual easing function used for the change in aspect ratio during the rotation change animation?
Is there a way to get the actual width and height of the view as it is displayed during the orientation change animation? This would allow me to retrieve the in-between aspect directly.
On the first bullet point, you can use CAMediaTimingFunction (probably with kCAMediaTimingFunctionEaseInEaseOut) to get the control points for the curve that defines the transition. Then just use a cubic bezier curve formula.
On the second bullet point, you can use [[view layer] presentationLayer] to get a version of that view's CALayer with all current animations applied as per their current state. So if you check the dimensions of that you should get the then current values — I guess if you act upon a CADisplayLink callback then you'll be at most one frame behind.

Creating an image magnification inside an instance of UIScrollView

I have an instance of UIScrollView with images inside it.
It is possible to scroll the scroll view horizontally, and view more images.
I want to magnify the images, according the their distance from the middle of the scroll view, so it should look like this.
I thought about the simplest way to do that, using the following code:
- (void) scrollViewDidScroll:(UIScrollView *)scrollView {
//Change the frames of all the visible images here.
}
This way does not work smoothly on some of the preceding devices, such as iPod 2G.
Is there a better way to perform the image magnification than the one I described?
If so, please let me know.
Thanks!
EDIT:
This is the implementation of the body of the method above:
-(void) magnifyImages {
//This method magnifies all the images.
for (NSString *name in [imagesDict allKeys]) {
UIImageView *imageView = [imagesDict objectForKey:name];
[self magnifyImageView:imageView];
}
}
static float kMaxMagnification = 1.5;
-(void) magnifyImageView:(UIImageView *)imageView {
CGRect frame = imageView.frame;
float imageMiddleLine = frame.origin.x+frame.size.width/2-scrollView.contentOffset.x;
//Check if the image's middle line is visible = whether it is needed to magnify the image.
if (imageMiddleLine +frame.size.width/2>0
&& imageMiddleLine-frame.size.width/2<scrollView.frame.size.width) {
float magnification = fabs(160-imageMiddleLine);
//Mathematical formula that calculates the magnification.
magnification = (kMaxMagnification-1)*(kDeviceWidth/2-magnification)/160+1;
//'normalImageSize' is a property of the image that returns the image's normal size (when not magnified).
CGSize imgSize = imageView.normalImageSize;
frame=CGRectMake(frame.origin.x-(imgSize.width*magnification-frame.size.width)/2,
frame.origin.y-(imgSize.height*magnification-frame.size.height)/2,
imgSize.width*magnification, imgSize.height*magnification);
imageView.frame=frame;
}
}

Rotating a UIView with alpha = 0

I have a UIView that I rotate with this code:
helpView.transform = CGAffineTransformMakeRotation(degreesToRadians( rotationAngle ));
Where degreeToRadians just is a macro to convert from degrees to radians.
This works fine as long as the view is visible, eg alpha = 1. When I hide it (alpha = 0, which I animate) it does not rotate any more. I guess this is a smart way for the devices to "save" on drawing time, but is there any way I can force it to be drawn even when alpha is 0? Otherwise I will have to rotate it before I show it again.
Any good ideas?
Thanks
Edit: This is the code I use to show/hide the view.
-(void)showHelp
{
bool helpAlpha = !helpView.alpha;
CGFloat newScale;
if (helpView.alpha) {
newScale = kHelpSmall;
helpView.transform = CGAffineTransformMakeScale(kHelpBig, kHelpBig);
} else {
newScale = kHelpBig;
helpView.transform = CGAffineTransformMakeScale(kHelpSmall, kHelpSmall);
}
[UIView animateWithDuration:(kAnimationTimeShort / 2) animations:^(void) {
[helpView setAlpha:helpAlpha];
helpView.transform = CGAffineTransformMakeScale(newScale, newScale);
}];
}
As you see I also scale it for a nicer effect. Works perfect when visible, does not rotate when alpha = 0. Rotation is done in another method, where I would prefer to keep it as I also rotate some other views there.
You are resetting the transform every time you use CGAffineTransformMake*. If you do this, you will get either a rotated transform or a scaled one. I am assuming the scaled one is after the rotated one and hence you aren't able to see the view rotated. If you need both the effects to remain, you will have to use CGAffineTransformRotate. So a scale and rotate will be
helpView.transform = CGAffineTransformMakeScale(kHelpSmall, kHelpSmall);
helpView.transform = CGAffineTransformRotate(helpView.transform, degreesToRadians(rotationAngle));
The order might vary.

What's the best approach to draw lines between views?

Background: I have a custom scrollview (subclassed) that has uiimageviews on it that are draggable, based on the drags I need to draw some lines dynamically in a subview of the uiscrollview. (Note I need them in a subview as at a later point i need to change the opacity of the view.)
So before I spend ages developing the code (i'm a newbie so it will take me a while) I looked into what i need to do and found some possible ways. Just wondering what the right way to do this.
Create a subclass of UIView and use the drawRect method to draw the line i need (but unsure how to make it dynamically read in the values)
On the subview use CALayers and draw on there
Create a draw line method using CGContext functions
Something else?
Cheers for the help
Conceptually all your propositions are similar. All of them would lead to the following steps (some of them done invisibly by UIKit):
Setup a bitmap context in memory.
Use Core Graphics to draw the line into the bitmap.
Copy this bitmap to a GPU buffer (a texture).
Compose the layer (view) hierarchy using the GPU.
The expensive part of the above steps are the first three points. They lead to repeated memory allocation, memory copying, and CPU/GPU communication. On the other hand, what you really want to do is lightweight: Draw a line, probably animating start/end points, width, color, alpha, ...
There's an easy way to do this, completely avoiding the described overhead: Use a CALayer for your line, but instead of redrawing the contents on the CPU just fill it completely with the line's color (setting its backgroundColor property to the line's color. Then modify the layer's properties for position, bounds, transform, to make the CALayer cover the exact area of your line.
Of course, this approach can only draw straight lines. But it can also be modified to draw complex visual effects by setting the contents property to an image. You could, for example have fuzzy edges of a glow effect on the line, using this technique.
Though this technique has its limitations, I used it quite often in different apps on the iPhone as well as on the Mac. It always had dramatically superior performance than the core graphics based drawing.
Edit: Code to calculate layer properties:
void setLayerToLineFromAToB(CALayer *layer, CGPoint a, CGPoint b, CGFloat lineWidth)
{
CGPoint center = { 0.5 * (a.x + b.x), 0.5 * (a.y + b.y) };
CGFloat length = sqrt((a.x - b.x) * (a.x - b.x) + (a.y - b.y) * (a.y - b.y));
CGFloat angle = atan2(a.y - b.y, a.x - b.x);
layer.position = center;
layer.bounds = (CGRect) { {0, 0}, { length + lineWidth, lineWidth } };
layer.transform = CATransform3DMakeRotation(angle, 0, 0, 1);
}
2nd Edit: Here's a simple test project which shows the dramatical difference in performance between Core Graphics and Core Animation based rendering.
3rd Edit: The results are quite impressive: Rendering 30 draggable views, each connected to each other (resulting in 435 lines) renders smoothly at 60Hz on an iPad 2 using Core Animation. When using the classic approach, the framerate drops to 5 Hz and memory warnings eventually appear.
First, for drawing on iOS you need a context and when drawing on the screen you cannot get the context outside of drawRect: (UIView) or drawLayer:inContext: (CALayer). This means option 3 is out (if you meant to do it outside a drawRect: method).
You could go for a CALayer, but I'd go for a UIView here. As far as I have understood your setup, you have this:
UIScrollView
| | |
ViewA ViewB LineView
So LineView is a sibling of ViewA and ViewB, would need be big enough to cover both ViewA and ViewB and is arranged to be in front of both (and has setOpaque:NO set).
The implementation of LineView would be pretty straight forward: give it two properties point1 and point2 of type CGPoint. Optionally, implement the setPoint1:/setPoint2: methods yourself so it always calls [self setNeedsDisplay]; so it redraws itself once a point has been changed.
In LineView's drawRect:, all you need to is draw the line either with CoreGraphics or with UIBezierPath. Which one to use is more or less a matter of taste. When you like to use CoreGraphics, you do it like this:
- (void)drawRect:(CGRect)rect
{
CGContextRef context = UIGraphicsGetCurrentContext();
// Set up color, line width, etc. first.
CGContextMoveToPoint(context, point1);
CGContextAddLineToPoint(context, point2);
CGContextStrokePath(context);
}
Using NSBezierPath, it'd look quite similar:
- (void)drawRect:(CGRect)rect
{
UIBezierPath *path = [UIBezierPath bezierPath];
// Set up color, line width, etc. first.
[path moveToPoint:point1];
[path addLineToPoint:point2];
[path stroke];
}
The magic is now getting the correct coordinates for point1 and point2. I assume you have a controller that can see all the views. UIView has two nice utility methods, convertPoint:toView: and convertPoint:fromView: that you'll need here. Here's dummy code for the controller that would cause the LineView to draw a line between the centers of ViewA and ViewB:
- (void)connectTheViews
{
CGPoint p1, p2;
CGRect frame;
frame = [viewA frame];
p1 = CGPointMake(CGRectGetMidX(frame), CGRectGetMidY(frame));
frame = [viewB frame];
p2 = CGPointMake(CGRectGetMidX(frame), CGRectGetMidY(frame));
// Convert them to coordinate system of the scrollview
p1 = [scrollView convertPoint:p1 fromView:viewA];
p2 = [scrollView convertPoint:p2 fromView:viewB];
// And now into coordinate system of target view.
p1 = [scrollView convertPoint:p1 toView:lineView];
p2 = [scrollView convertPoint:p2 toView:lineView];
// Set the points.
[lineView setPoint1:p1];
[lineView setPoint2:p2];
[lineView setNeedsDisplay]; // If the properties don't set it already
}
Since I don't know how you've implemented the dragging I can't tell you how to trigger calling this method on the controller. If it's done entirely encapsulated in your views and the controller is not involved, I'd go for a NSNotification that you post every time the view is dragged to a new coordinate. The controller would listen for the notification and call the aforementioned method to update the LineView.
One last note: you might want to call setUserInteractionEnabled:NO on your LineView in its initWithFrame: method so that a touch on the line will go through to the view under the line.
Happy coding !

Find the center point of a UIScrollView while zooming

I'm having difficulties getting a tiled UIScrollView to zoom in and out correctly with pinch zooming. The issue is that when a pinch-zoom occurs, the resulting view is usually not centered in the same region.
Details: The app starts with a tiled image that is 500x500. If a user zooms in, it will snap to 1000x1000 and the tiles will redraw. For all the zoom affects, etc. I am just letting the UIScrollView do it's thing. When scrollViewDidEndZooming:withView:atScale: is called, I redraw the tiles (like you can see in many examples and other questions here).
I think that I've drilled the problem down to calculating the center of the view correctly when I get to scrollViewDidEndZooming:withView:atScale: (I can center on a known point fine after I redraw).
What I'm currently using:
- (void)scrollViewDidEndZooming:(UIScrollView *)scrollView withView:(UIView *)view atScale:(float)scale {
// as an example, create the "target" content size
CGSize newZoomSize = CGSizeMake(1000, 1000);
// get the center point
CGPoint center = [scrollView contentOffset];
center.x += [scrollView frame].width / 2;
center.y += [scrollView frame].height / 2;
// since pinch zoom changes the contentSize of the scroll view, translate this point to
// the "target" size (from the current size)
center = [self translatePoint:center currentSize:[scrollView contentSize] newSize:newZoomSize];
// redraw...
}
/*
Translate the point from one size to another
*/
- (CGPoint)translatePoint:(CGPoint)origin currentSize:(CGSize)currentSize newSize:(CGSize)newSize {
// shortcut if they are equal
if(currentSize.width == newSize.width && currentSize.height == newSize.height){ return origin; }
// translate
origin.x = newSize.width * (origin.x / currentSize.width);
origin.y = newSize.height * (origin.y / currentSize.height);
return origin;
}
Does this seem correct? Is there a better way? Thanks!
The way I have solved this so far is to store the initial center point of the view when the zoom starts. I initially saving this value when the scrollViewDidScroll method is called (and the scroll view is zooming). When scrollViewDidEndZooming:withView:atScale: is called, I use that center point (and reset the saved value).
The center of the scrollview can be found by adding it's center property, and it's contentOffset property.
aView.center = CGPointMake(
self.scrollView.center.x + self.scrollView.contentOffset.x,
self.scrollView.center.y + self.scrollView.contentOffset.y);