Unwanted image animation when resizing UITableView - ios

For an iPad project I implemented an NSBrowser-like interface wich supports a dynamic number of columns. Each column is represented by an UITableView.
When adding or removing columns, I'm using UIView's animateWithDuration:animations: to change the width of the UITableView.
The problem I'm running into:
The system adds an unwanted frame size animation for the imageView of each table view cell, which resizes to imageView from very large to it's initial size. This looks really awkward, so I'm looking for ways to get rid off it - while keeping the animated frame size change for the enclosing tableViews.
Any ideas what I might be doing wrong?
I posted a sample project demonstration the issue here:
https://github.com/iljaiwas/TableViewFrameTest
Here is where I setup the cell:
https://github.com/iljaiwas/TableViewFrameTest/blob/master/TableViewFrameTest/TestTableViewController.m#L61
Here is where I trigger the animation:
https://github.com/iljaiwas/TableViewFrameTest/blob/master/TableViewFrameTest/TestViewController.m#L46

I was having the same issue and found this link (http://www.objc.io/issue-5/iOS7-hidden-gems-and-workarounds.html) which has a section on Blocking Animations.
To get your example working add the following at the top of TestTableViewController.m after the import statement:
#interface MyTableViewCell : UITableViewCell
#end
#implementation MyTableViewCell
- (void) layoutSubviews {
[UIView performWithoutAnimation:^{
[super layoutSubviews];
}];}
#end
Then change the following line in viewDidLoad to use MyTableViewCell:
[self.tableView registerClass:[MyTableViewCell class] forCellReuseIdentifier:#"Cell"];
Now run the example and you will no longer have your unwanted image animation.

this hepled to me:
set UIImageView ** (in TableView cell) **contentMode to aspect Fit.
do not know why, but works for me.

I was having a similar issue with the (pretty large) image shrinking down from its original size when the editing animation fired to show the delete button. The picture would fly across my screen as it shrank. Pretty crazy. Anyway, I fixed it by using this category on UIImage to resize the image before putting it in the UIImageView:
- (UIImage *)imageWithImage:(UIImage *)image scaledToSize:(CGSize)scaledSize {
UIGraphicsBeginImageContext(scaledSize);
[image drawInRect:CGRectMake(0, 0, scaledSize.width, scaledSize.height)];
UIImage *scaledImage = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
return scaledImage;
}
Edit: here's another category that does a much better job of scaling down the image, depending on how good you need it to look. I originally found these on another SO question a while back but can't seem to find it now to link to, but I used them as a starting point and this one was especially helpful:
- (UIImage *)imageScaledToFitRect:(CGRect)rect {
// compute scale factor for imageView
CGFloat widthScaleFactor = CGRectGetWidth(rect) / self.size.width;
CGFloat heightScaleFactor = CGRectGetHeight(rect) / self.size.height;
NSLog(#"Rect width: %f, Image width: %f, WidthFactor: %f", rect.size.width, self.size.width, widthScaleFactor);
NSLog(#"Rect height: %f, Image height: %f, HeightFactor: %f", rect.size.height, self.size.height, heightScaleFactor);
CGFloat imageViewXOrigin = 0;
CGFloat imageViewYOrigin = 0;
CGFloat imageViewWidth = 0;
CGFloat imageViewHeight = 0;
// if image is narrow and tall, scale to width and align vertically to the top
if (widthScaleFactor > heightScaleFactor) {
imageViewWidth = self.size.width * widthScaleFactor;
imageViewHeight = self.size.height * widthScaleFactor;
}
// else if image is wide and short, scale to height and align horizontally centered
else {
imageViewWidth = self.size.width * heightScaleFactor;
imageViewHeight = self.size.height * heightScaleFactor;
imageViewXOrigin = - (imageViewWidth - CGRectGetWidth(rect))/2;
}
return [self imageScaledToRect:CGRectMake(imageViewXOrigin, imageViewYOrigin, imageViewWidth, imageViewHeight)];
}
Hope this can help someone else.

Related

Simple way to redraw view programatically when device orientation changes

I'm new to iOS development and for my assignment, I'm tasked changing updating the ViewController programmatically when the device's orientation changes. I've found a snippet of an answer here, but it doesn't get the job done.
I tried adding this to the viewWillLayoutSubviews of my View Controller, but all I get is an unused variable warning.
CGRect rotatedFrame = [self.view convertRect:self.view.frame fromView:self.view.superview];
viewWillLayoutSubviews and rotation
As a "hint", I've been told it's simple to implement in viewWillLayoutSubviews. Going through and changing all the CGRects in my VC doesn't sound like a couple of lines of code. There's got to be a simpler, more efficient way to do this, but I've only found snippets of solutions digging around on this site. Thanks for reading.
The line of code you are using is assigning a CGRect to the rotatedFrame variable, it's not updating anything on your view controller.
There's many ways to approach this but it depends on what is contained in your view and how it's been configured. Things like Auto Layout for example could let you configure almost everything in Interface Builder and let you avoid doing most things in code.
You've been tasked to do this programatically and since we know that viewWillLayoutSubviews is called every time the device is rotated that's a good place to start. Here's a lazy way I've gone about rotating a video to fit a new orientation using a transformation:
//Vertical
CGSize size = self.view.frame.size;
someView.transform = CGAffineTransformMakeRotation((M_PI * (0) / 180.0))
someView.frame = CGRectMake(0, 0, MIN(size.width, size.height), MAX(size.width, size.height));
//Horizontal
CGSize size = [UIScreen mainScreen].bounds.size;
int directionModifier = ([UIDevice currentDevice].orientation == UIInterfaceOrientationLandscapeLeft) ? -1 : 1;
someView.bounds = CGRectMake(0, 0, MAX(size.width, size.height), MIN(size.width, size.height));
someView.transform = CGAffineTransformMakeRotation((M_PI * (90) / 180.0) *directionModifier);
someView.transform = CGAffineTransformTranslate(someView.transform,0,0);
How many subviews are in your view? Are they grouped? If you're using auto-resizing masks you might get away with only adjusting the frames of one or two views. If your root view has a number of subviews you can loop through views that need similar adjustments to avoid having to write excess code. It really depends on how everything has been set up.
I figured out how to determine the viewWidth and viewHeight and set those as CGFloats. I then added an if-else statement which figures out if the display is in portrait or landscape and sets the problematic calculateButton accordingly.
Apologies for lengthy code, but I've found in searching this site I find "snippets" of answers, but being new to iOS it's difficult to figure out what goes where. Hopefully, this helps someone later. (and hopefully it's correct)
- (void) viewWillLayoutSubviews {
[super viewWillLayoutSubviews];
CGFloat viewWidth = self.view.bounds.size.width;
CGFloat viewHeight = self.view.bounds.size.height;
CGFloat padding = 20;
CGFloat itemWidth = viewWidth - padding - padding;
CGFloat itemHeight = 44;
// Bunch of setup code for layout items
// HOMEWORK: created if-else statement to deal w/ portrait vs. landscape placement of calculateButton.
if (viewWidth > viewHeight) {
// portrait
CGFloat bottomOfLabel = viewHeight;
self.calculateButton.frame = CGRectMake(padding, bottomOfLabel - itemHeight, itemWidth, itemHeight);
} else {
// landscape
CGFloat bottomOfLabel = CGRectGetMaxY(self.resultLabel.frame);
self.calculateButton.frame = CGRectMake(padding, bottomOfLabel + padding, itemWidth, itemHeight);
}
}

Combine 2 UIImage views with zoom and offset

thanks for reading.
I have a background image and a foreground image. The foreground image is in a UIScrollView so can be resized and repositioned over the background image. The background image is set as Aspect Fit. I have a function that combines the two UIImages into a new UIImage. That works fine, but what I can't get right is the x,y co-ordinates of one view over the other.
Here's some code:
CGFloat bgImageScale = self.backgroundImageView.bounds.size.height / self.bgImage.size.height; // Gives me the AspectFit scale.
CGFloat bgOffsetX = (self.backgroundImageView.bounds.size.width - self.bgImage.size.width * bgImageScale) / 2.0;
CGFloat bgOffsetY = 0.0;
CGFloat fgImageScale = self.fgImageScrollView.zoomScale;
CGFloat fgOffsetX = -self.fgImageScrollView.contentOffset.x;
CGFloat fgOffsetY = -self.fgImageScrollView.contentOffset.y;
CGPoint imageOffset = CGPointMake((fgOffsetX - bgOffsetX) * bgImageScale, (fgOffsetY - bgOffsetY) * bgImageScale);
[self.delegate completedOverlayImage:
[self mergeImage:self.fgImage
withImage:self.bgImage
usingAlpha:0.5f
withOffset:imageOffset
andScale:fgImageScale / bgImageScale
]];
In brief, the compeletedOverlayImage code does the following relevant bit:
[bottomImage drawInRect:CGRectMake(0,0,newSize.width,newSize.height)];
[topImage drawInRect:CGRectMake(imageOffset.x, imageOffset.y, newSize.width*imageScale, newSize.height*imageScale) blendMode:kCGBlendModeNormal alpha:alpha];
So I just can't get the imageOffset stuff right to get the new image overlaid the same as it appeared on-screen.
By the way, this app is iOS 7 and up only.
Can anyone help? Thanks.
Take a look at the UIView convertRect:toView: method.
Assuming that your view hierarchy looks like this
SomeViewController
View // the view controller's main view
BgView // the background view (UIImageView)
ScrollView
FgView // the foreground view (UIImageView)
If the foreground image is a UIImageView that's a child of the scroll view, then you can convert the FgView frame coordinates to the main view coordinate system with a line of code like this
CGRect foregroundFrame = [self.foregroundImageView convertRect:self.foregroundImageView.bounds toView:self.view];
Since the BgView's frame is already in mainView coordinates, this will give you both frames in the same coordinate system.
OK, I solved it myself. So for anyone else attempting the same thing, here's the code:
CGFloat bgImageScale = self.backgroundImageView.bounds.size.height / self.bgImage.size.height;
CGFloat bgOffsetX = (self.backgroundImageView.bounds.size.width - self.bgImage.size.width * bgImageScale) / 2.0;
CGFloat bgOffsetY = 0.0;
CGFloat fgImageScale = self.fgImageScrollView.zoomScale;
CGFloat fgRelativeZoom = fgImageScale / bgImageScale; // How much is fg zoomed compared to bg?
CGFloat fgOffsetX = -self.fgImageScrollView.contentOffset.x; // We want the offset of the (0,0), not the offset of the viewport. Hence, negative.
CGFloat fgOffsetY = -self.fgImageScrollView.contentOffset.y;
CGPoint imageOffset = CGPointMake(fgOffsetX / bgImageScale - bgOffsetX, fgOffsetY / bgImageScale - bgOffsetY);
[self.delegate completedOverlayImage:
[self mergeImage:self.fgImage
withImage:self.bgImage
usingAlpha:0.5f
withOffset:imageOffset
andScale:fgRelativeZoom
]];
and the function to combine the images (assuming iOS 7 which lets you set scale to zero in the UIGraphicsBeginContextWithOptions() call):
- (UIImage*) mergeImage:(UIImage*)topImage withImage:(UIImage*)bottomImage usingAlpha:(CGFloat)alpha withOffset:(CGPoint)imageOffset andScale:(CGFloat)imageScale {
int width = bottomImage.size.width;
int height = bottomImage.size.height;
CGSize newSize = CGSizeMake(width, height);
UIGraphicsBeginImageContextWithOptions(newSize, NO, 0.0);
[bottomImage drawInRect:CGRectMake(0,0,newSize.width,newSize.height)];
[topImage drawInRect:CGRectMake(imageOffset.x, imageOffset.y, newSize.width*imageScale, newSize.height*imageScale) blendMode:kCGBlendModeNormal alpha:alpha];
UIImage *newImage = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
return newImage;
}
Hope that helps someone else out there.

Auto-resize UIView to fit iPhone 5 screen while maintaining proportion

My StoryBoard is configured for iPhone4 resolution, and when running on iPhone 5 I'd like a UIView to get bigger (both height&width).
The problem right now is that the view is only getting higher and not wider. What should be the auto-resize configuration in order to achieve this?
You will probably need to use a subclass of UIView with the setFrame: method overridden to catch frame changes.
- (void)setFrame:(CGRect)frame
{
frame.size.width = frame.size.height; // Make the *width* always equal to the *height*. Logic could go here, etc.
[super setFrame:frame]
}
Reference the height of the screen and use some padding values to set your view frame. Below is code to set the frame of a UIView called yourShapeView:
// Get the frame of the screen
CGRect screenFrame = [[UIScreen mainScreen] bounds];
// Set padding from the top and bottom of the shape
CGFloat verticalPadding = 40.0f;
CGFloat horizontalPadding = 20.0f;
// Get the height/width values
CGFloat height = screenFrame.size.height - (verticalPadding * 2);
CGFloat width = screenFrame.size.width - (horizontalPadding * 2);
// Set the size of your shape based on the height and padding
yourShapeView.frame = CGRectMake(horizontalPadding, verticalPadding, width, height);

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.

How to merge images and keep orginal transformations

I have a problem with a simple iPad image collage app. I have a UIImageView where it's possible to drag'n'drop different UIImageViews into. The dragged views can be rotated and stretched. When the user have finished editing the image, I'll merge all the images in on final image. My solution have been to use UIGraphics and draw the large image, then loop through all subviews and draw them in the context:
- (UIImage*)mergeImages
{
CGSize imageSize = imageView.frame.size;
UIGraphicsBeginImageContext(imageSize);
[imageView.image drawInRect:CGRectMake(0, 0, imageSize.width, imageSize.height)];
[[imageView subviews] enumerateObjectsUsingBlock:^(id object, NSUInteger index, BOOL *stop) {
UIImage *imageOriginal = ((UIImageView*)object).image;
CGRect rect = ((UIImageView*)object).frame;
UIImage *imageToAdd = [self scaleImage:imageOriginal :rect.size];
// Find the w/h ratioes
float widthRatio = rect.size.width / imageOriginal.size.width;
float heightRatio = rect.size.height / imageOriginal.size.height;
// Calculate point to draw at
CGPoint pointToDrawAt;
if (widthRatio > heightRatio)
pointToDrawAt = CGPointMake(rect.origin.x + (rect.size.width / 2) - (imageToAdd.size.width / 2), rect.origin.y);
else
pointToDrawAt = CGPointMake(rect.origin.x, rect.origin.y + (rect.size.width / 2) - (imageToAdd.size.height / 2));
[imageToAdd drawAtPoint:pointToDrawAt];
}];
UIImage *newImage = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
// Save image in photogalery
UIImageWriteToSavedPhotosAlbum(newImage, nil, nil, nil);
return newImage;
}
The above works fines as long the images isn't rotated. So my question is:
Is there a way I can find the rotation angle from the subview, or do I have to subclass UIView and make it a property? Or is there another way I can merge images and get the drawing specific properties like angle and filters?
If I can help with further code or anything just comment. And thanks in advance :)
UIImages have a property called "imageOrientation" which can be queried. Once you know this, you can apply a CGAffineTransform to the context to get the image to render as you see it. There was a recent posting on this - in the past few days - if you search on 'CGAffineTransform' you should be able to find it (or a similar post) and see how its done.

Resources