In my iPad app I have a view controller with a small table view. When you tap on the table view it opens a modal view controller that is a larger and more refined version of the small table view. I would like to create an animation from a pre-rendered image of the large view controller by scaling the image down to be the size of the small table view and zoom it to full screen size and then replace the image with the "real" view controller.
Something like:
LargeViewController* lvc = [[LargeViewController alloc] init];
[self presentModalViewController:lvc byZoomingFromRect:CGRectMake(50,50,200,300)];
I know you can produce an image from a view:
- (UIImage *) imageWithView:(UIView *)view
{
UIGraphicsBeginImageContextWithOptions(view.bounds.size, view.opaque, [[UIScreen mainScreen] scale]);
[view.layer renderInContext:UIGraphicsGetCurrentContext()];
UIImage * img = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
return img;
}
But how do I make the view controller draw itself (offscreen) so I can take it's view and scale the image in an animation to fill screen?
Thanks in advance.
I suppose you want to create your very own animation. Last month I played around with something like that. My solution was adding a custom view (maybe taken from a view controller) to the current view as an overlay. This works with layers, too.
First you fetch the Image from your "future" or "present" view controller, like you did in your code example above. Normally the view controllers content should be available while rendering to the context.
Now you have the image. The manipulation of the image must be done by you.
Add the image to a UIImageView. This ImageView can be added as subview or layer. Now you have a layer where you can freely draw above your actual user interface. Sometimes you have to move the layer or view around, so that it perfectly overlays your view. This depends on your view setup. If you are dealing with Tableviews, adding a subview is not that easy. So better use the layer.
After all the work was done, present the new view controller without animation, so that it appears immediately.
Remove the layer or view from your parent view after the work was done, and clean up.
This sounds complicated, but once you've done that you have a template for that. In "WWDC 2011, Session 309 Introducing Interface Builder Storyboarding" apple introduced 'custom segues', where you'll find a mechanism for exactly what you want to do. The code below is a cut out of an older project and is somehow messy and must be cleaned up. But for showing the principle this should work:
-(void) animate {
static LargeViewController* lvc = [[LargeViewController alloc] init];
UIGraphicsBeginImageContextWithOptions(self.bounds.size, view.opaque, [[UIScreen mainScreen] scale]);
[lvc.view.layer renderInContext:UIGraphicsGetCurrentContext()];
// Create a ImageView to display your "zoomed" image
static UIImageView* displayView = [[UIImageView alloc] initWithFrame:self.view.frame];
static UIImage * img = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
// Add your image to the view
displayView.image = img;
// insert the view above your actual view, adjust coordinates in the
// frame property of displayView if overlay is misaligned
[[self.view] addSubview:displayView];
// alternatively you can use the layer
// [self.view.layer addSublayer:displayView.layer];
// draw the imageView
[displayView setNeedsDisplay];
// do something in background. You may create your own
// construction, i.e. using a timer
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSDate *now = [NSDate date];
NSTimeInterval animationDuration = 3.;
NSTimeInterval t = -[now timeIntervalSinceNow];
while (t < animationDuration) {
t = -[now timeIntervalSinceNow];
// Do some animation here, by manipulation the image
// or the displayView
// <calculate animation>, do something with img
// you have exact timing information in t,
// so you can set the scalefactor derived from t
// You must not use an UIImage view. You can create your own view
// and do sth. in draw rect. Do whatever you want,
// the results will appear
// in the view if you added a subview
// or in a layer if you are using the layer
dispatch_sync(dispatch_get_main_queue(), ^{
// display the result
displayView.image = img;
[displayView setNeedsDisplay];
});
}
});
// now the animation is done, present the real view controller
[self presentModalViewController:lvc animated:NO];
// and clean up here
}
Perhaps you could use something like
CGAffineTransform tr = CGAffineTransformScale(lvc.view.transform, 0.5, 0.5);
to embed a scaled down version of the view in your parent view controller, then present lvc modally and restore scale when the user taps the view.
UIKit takes care of most of this for you. While jbat100's solution could be made to work too, you should be able to do this simply by setting lvc's initial frame to the smaller rect you want to start out at and then when you set the frame too its full size, the implicit animation for changing the frame will handle the zooming animation for you. Each UIView has a CALayer that its content is drawn in and that layer has several implicit animtions setup to animated changes to certain properties such as the frame or position properties. Here is my untested stab at it:
.
.
lvc.view.frame = CGRectMake(50,50,200,300);
[self performSelector:#selector(setFrameToFullScreen) withObject:nil afterDelay:0];
}
- (void)setFrameToFullScreen {
lcv.view.frame = [UIScreen mainScreen].bounds;
}
The performSelector:withObject:afterDelay call will cause setFrameToFullScreen to be called on the next run loop cycle. If you don't do something like that, then only the final frame will be used and the system won't recognize the change in the frame and apply its implicit animation to the views layer.
Related
I'm in a dilemma which method to use for setting frames of custom UIViews with many subviews in it and still have animations and automatically adjust to rotations. What I usually do when I create a new viewcontroller is alloc my custom view in loadView or viewDidLoad, e.g:
-(void)viewDidLoad
{
[super viewDidLoad];
detailView = [[DetailView alloc] initWithFrame:CGRectMake(0, 0, self.view.frame.size.width, self.view.frame.size.height)];
detailView.autoresizingMask = UIViewAutoresizingFlexibleHeight|UIViewAutoresizingFlexibleWidth;
self.view = detailView;
}
Normally this width & height is not correct for an iPhone5-screen (the actual view-frame is not set until viewWillAppear) but because of the autoresizingmask it all works out.
Then in the initWithFrame of the custom UIView DetailView, I alloc all subviews with CGRectZero, e.g:
-(id)initWithFrame:(CGRect)frame
{
self = [super initWithFrame:frame];
if (self)
{
label = [[UILabel alloc] initWithFrame:CGRectZero];
[self addSubview:label];
}
}
Then I override layoutsubviews to set all frames of all subviews. This works perfectly for any screen size and any orientation, e.g:
-(void)layoutSubviews
{
[super layoutSubviews];
label.frame = CGRectMake(0, 0, self.frame.size.width, self.frame.size.height);
}
However, I just found out that layoutSubviews is not so great when you use animations, because when you use animations in an animationblock, layoutsubviews is called in the middle of the animation and it completely breaks the animation, e.g:
-(void)animateLabel
{
[UIView animateWithDuration:0.4f animations:^
{
label.transform = CGAffineTransformMakeTranslation(100, 100);
}];
}
I believe there are ugly workarounds this by using flags for each animation and in layoutsubviews use those flags to set the correct start or endframe of the animated block but I don't think I should have to create a flag for each animation I want to do.
So my problem is now: how am I supposed to have a custom UIView WITH animations that also automatically adjusts itself to rotations?
The only solution I can come up with right now (that I don't like):
Don't use layoutSubviews but use the setFrame/setBounds method of the custom UIView to set the frames of all subviews. Then check in the viewController every time a rotation occurs and then use the setFrame/setBounds method of the custom UIView to change all frames of all subviews. I don't like this solution because the rotation methods are different in iOS5 and iOS6 and I don't want to have to do this in every UIViewController with it's own custom UIView.
Any suggestions?
I have recently started overriding viewDidLayoutSubviews (many times instead of viewWillAppear) in my UIViewControllers.
Yes viewDidLayoutSubviews is called on rotations. (from comment)
The method fires after all the internal layouts have already been completed so all finalized frames should be setup, but still give you the time you need to make adjustments before the the view is visible and shouldn't have any issues with animations because you are not already inside an animation block.
viewcontroller.m
- (void)viewDidLayoutSubViews {
// At this point I know if an animation is appropriate or not.
if (self.shouldRunAnimation)
[self.fuView runPrettyAnimations];
}
fuView.m
- (void)runPrettyAnimations {
// My animation blocks for whatever layout I'd like.
}
- (void)layoutSubviews {
// My animations are not here, but non animated layout changes are.
// - Here we have no idea if our view is visible to the user or may appear/disappear
// partway through an animation.
// - This also might get called far more than we intend since it gets called on
// any frame updates.
}
To describe my project:
I have a rectangle UIImageView frame floating over a white layer. Inside the UIImageView, I'm successfully creating the illusion that it is showing a portion of a background image behind the white layer. You can drag the rectangle around, and it will "redraw" the image so that you can peer into what is behind the white. Its basically this code:
//whenever the frame is moved, updated the CGRect frameRect and run this:
self.newCroppedImage = CGImageCreateWithImageInRect([bgImage.image CGImage], frameRect);
frame.image = [UIImage imageWithCGImage:self.newCroppedImage];
Anyhow, I also have a rotation gesture recognizer that allows the user to rotate the frame (and consequentially rotates the image). This is because the CGRect sent to the CGImageCreateWithImageInRect is still oriented at its original rotation. This breaks the illusion that you're looking through a window because the image you see is rotated when only the frame should appear that way.
So ideally, I need to take the rotation of my frame and apply it to the image created from my bgImage. Does anyone have any clues or ideas on how I could apply this?
I suggest you take a different approach. Don't constantly create new images to put in your UIImageView. Instead, set up your view hierarchy like this:
White view
"Hole" view (just a regular UIView)
Image view
That is, the white view has the hole view as a subview. The hole view has the UIImageView as its subview.
The hole view must have its clipsToBounds property set to YES (you can set it with code or in your nib).
The image view should have its size set to the size of its image. This will of course be larger than the size of the hole view.
And this is very very important: the image view's center must be set to the hole view's center.
Here's the code I used in my test project to set things up. The white view is self.view. I start with the hole centered in the white view, and I set the image view's center to the hole view's center.
- (void)viewWillAppear:(BOOL)animated
{
[super viewWillAppear:YES];
CGRect bounds = self.view.bounds;
self.holeView.center = CGPointMake(CGRectGetMidX(bounds), CGRectGetMidY(bounds));
self.holeView.clipsToBounds = YES;
bounds = self.holeView.bounds;
self.imageView.center = CGPointMake(CGRectGetMidX(bounds), CGRectGetMidY(bounds));
self.imageView.bounds = (CGRect){ CGPointZero, self.imageView.image.size };
}
I also set the image view's size to the size of its image. You might want to set it to the size of the white view.
To pan and rotate the hole, I'm going to set holeView.transform. I'm not going to change holeView.frame or holeView.center. I have two instance variables, _holeOffset and _holeRotation, that I use to compute the transform. The trick to making it seem like a hole through the white view, revealing the image view, is to apply the inverse transform to the image view, undoing the effects of the hole view's transform:
- (void)updateTransforms {
CGAffineTransform holeTransform = CGAffineTransformIdentity;
holeTransform = CGAffineTransformTranslate(holeTransform, _holeOffset.x, _holeOffset.y);
holeTransform = CGAffineTransformRotate(holeTransform, _holeRotation);
self.holeView.transform = holeTransform;
self.imageView.transform = CGAffineTransformInvert(holeTransform);
}
This trick of using the inverse transform on the subview only works if the center of the subview is at the center of its superview. (Technically the anchor points have to line up, but by default the anchor point of a view is its center.)
I put a UIPanGestureRecognizer on holeView. I configured it to send panGesture: to my view controller:
- (IBAction)panGesture:(UIPanGestureRecognizer *)sender {
CGPoint offset = [sender translationInView:self.view];
[sender setTranslation:CGPointZero inView:self.view];
_holeOffset.x += offset.x;
_holeOffset.y += offset.y;
[self updateTransforms];
}
I also put a UIRotationGestureRecognizer on holeView. I configured it to send rotationGesture: to my view controller:
- (IBAction)rotationGesture:(UIRotationGestureRecognizer *)sender {
_holeRotation += sender.rotation;
sender.rotation = 0;
[self updateTransforms];
}
I have an app where it reads an XML document containing 2 fields (image url and description). If the document has a field that has a valid image URL, I want the view to show the image. Else I just want it to show the description.
The problem I am having is :
how to show the UIImageView dynamically
how to move the UITextView downwards since now I added a UIImageView
Currently I just have a View with a UITextView on it.
Any ideas?
1) -[UIView setHidden:] if the UIImageView is already in place. Otherwise, [[UIImageView alloc] initWithFrame:], then add the image view as a subview to the view.
2) -[UIView setFrame:] (is one option)
The following is what you could do.
Prepare the the view with the UIImageView and the UITextView in the viewDidLoad like this:
-(void) viewDidLoad)
{
[super viewDidLoad];
myImageView = [[UIImageView alloc] init];
myImageView.frame = CGRectMake(x,y,0,0); //you can set it at the right position and even set either the width OR the height depending on where you want the textView in my example I'm gonna assume its beneath the imageView
//other imageView settings
[myView addSubView:myImageView];
myTextView = [[myTextView alloc] init];
myTextView.frame = CGRectMake(x,y,width,heigh+CGRectGetMaxY(myImageView.frame)); //here you prepare the textView for the imageView and the space it takes. But doesn't get hindered by it if it's not there.
//other textView settings.
[myView addSubView:myTextView];
//You would want this loading function about here OR after this loading has been occured.
[self loadImageFromURL];
}
-(void)loadImageFromURL
{
//get your image here.. or not
if(imageHasLoaded) //some condition that gets set when your image has successfully loaded. (This should be done in a delegate (didLoadImageFromURL) if you have such thing prepared.
{
//myImageView.image = theLoadedImage;
myImageView.frame = CGRectMake(x,y,theLoadedImageWidth,theLoadedImageHeight);
//after you set the frame of the imageView you need to refresh the view
[myView setNeedsDisplay];
}
}
And this is how you should dynamically add an imageView with image and frame to some view.
In my app, after an image is picked with uiimagepicker, it is processed via a webservice. The web processing takes a second, so I'd like to create an grayed out overlay with an activity indicator view.
My problem is that the UIImagePickerView view seems to stay on top of everything until the processing is finished.
I have tried using [myPicker dismissModalViewControllerAnimated:YES]; before the overlay is loaded or the processing even starts, but the view is still on screen. After that, my overlay is loaded as followed
ovController = [[OverlayViewController alloc] initWithNibName:#"OverlayViewController" bundle:[NSBundle mainBundle]];
CGFloat yaxis = self.navigationController.navigationBar.frame.size.height;
CGFloat width = self.view.frame.size.width;
CGFloat height = self.view.frame.size.height;
CGRect frame = CGRectMake(0, yaxis, width, height);
ovController.view.frame = frame;
ovController.view.backgroundColor = [UIColor grayColor];
ovController.view.alpha = 0.7;
[self.view insertSubview:ovController.view aboveSubview:self.view];
I have also tried using [[myPicker cameraOverlayView] insertSubview:ovController.view]; before I dismiss myPicker, but to no avail.
To be clear, I am trying bring the "loading" overlay on the screen after the UIImagePicker goes away, but before the web processing starts.
Any input will be appreciated. Thanks
It sounds like you are doing the upload on the main thread which is locking the view from being updated. You might try doing the upload on a sperate thread so that the main thread can dismiss the image picker and display the overlay.
I need to make a snapshot or a screenshot - call it as you like- of the current view and then display it in the modal view.
Because if I just write this in the ModalView's View controller
MyAppViewController *viewController = [[MyAppViewController alloc] init];
self.view = viewController.view;
All the methods of the MyAppViewController are called as well, but I don't need it, I just need to "save" everything that was on the screen when the ModalView appeared and show it in the ModalView's view.
How can I do it?
Thanks in advance!
I would suggest doing this:
Have a method that creates an image out of the contents of the view.
-(UIImage*) makeImage {
UIGraphicsBeginImageContext(self.view.bounds.size);
[self.view.layer renderInContext:UIGraphicsGetCurrentContext()];
UIImage *viewImage = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
return viewImage;
}
Create a custom init method for your modal view, and also give your modalView an instance variable that can hold a UIImage something like...
- (id)initWithImage:(UIImage *)temp {
myImage = temp;
}
Then in your modalView, perhaps in the viewDidLoad method, create a UIImageView and set the image to myImage
Hopefully this achieves what you are trying to do.
In iOS 7, you can use one of the new methods in UIView for creating snapshots, for instance:
- (UIView *)snapshotViewAfterScreenUpdates:(BOOL)afterUpdates;
which returns a UIView which you can then add as a subview.
UIView *viewSnapShop = [self snapshotViewAfterScreenUpdates:NO];