I’m trying to make an iOS Action app extension with minimal UI. Basically it would just show a progress indicator until the action completed. I just want to be able to animate the view so that it slides down from the top & then slides back up when the action has completed. If anyone is familiar with Instapaper’s Share extension, then that’s the kind of basic UI I’m looking for.
The problem is that when I try to duplicate this functionality - I just have a small UIView that animates down from the top - I get a black background behind my view. I can’t figure out how to make that background transparent so that the stuff behind my view is still visible. Does anyone know how to do this?
As a starting point I’m just using the default Action Extension template that’s created by Xcode...
Create a new iOS app project in Xcode.
Add a new target -> Action Extension.
In the ActionViewController.m file add a viewWillAppear method to animate the view (using a 1 second animation so that the black background is easily seen):
Code:
- (void)viewWillAppear:(BOOL)animated
{
[super viewWillAppear:animated];
CGRect currFrame = self.view.frame;
CGRect newFrame = currFrame;
newFrame.origin.y -= newFrame.size.height;
self.view.frame = newFrame;
[UIView animateWithDuration:1.0 animations:^{
self.view.frame = currFrame;
}];
}
When this is run the view is animated sliding down from the top. However rather than seeing the UI of the calling App all you see is a black background.
I’ve tried a number of things - changing the modalPresentationStyle (doesn’t seem to do anything), setting the entire view to hidden (this just makes the whole screen black), etc.
For reference this is using iOS 9.3.2 and Xcode 7.3.1.
From what I understand from Apple docs
In iOS, an Action extension:
Helps users view the current document in a different way
Always appears in an action sheet or full-screen modal view
Receives selected content only if explicitly provided by the host app
The fact that the Action extension always appear in a full-screen view on an iPhone might mean that there's no way of having a transparent background.
I am certain that a Share extension can be animated (I've done it myself) how you want it and have a transparent background. That's why Instapaper's Share extension works nicely.
You are facing two problems:
1. When you present a view controller, the default behavior is the controller is full screen context. That is the reason you see a black screen.
2. You are trying to change self.view.frame when the controller is presented on full screen.
Yet there is a way to achieve this kind of behavior you are looking for, in one of three ways:
A. Specify "modalPresentationStyle" to "UIModalPresentationOverCurrentContext" and set the presenting controller to "definesPresentationContext".
That way, when you present the controller, the presenting controller will be behind the presented controller.
And insted of changing self.view.frame you will set self.view background color to clear, and add a subview and use it as a background view:
//Presenting view controller:
-(void) presentPopUpViewController {
self.definesPresentationContext = YES; //self is presenting view controller
self.presentedVC.modalPresentationStyle = UIModalPresentationOverCurrentContext;
[self presentViewController:self.presentedVC animated:NO completion:nil];
}
//Presented view controller:
-(void) viewDidLoad {
[super viewDidLoad];
CGRect currFrame = self.view.frame;
CGRect newFrame = currFrame;
newFrame.origin.y -= newFrame.size.height;
self.myBackroundView = [[UIView alloc] init];
self.myBackroundView.backgroundColor = [UIColor yellowColor];
self.myBackroundView.frame = newFrame;
[self.view addSubview:self.myBackroundView];
self.view.backgroundColor = [UIColor clearColor];
}
- (void)viewWillAppear:(BOOL)animated
{
[super viewWillAppear:animated];
[UIView animateWithDuration:5.0 animations:^{
self.myBackroundView.frame = self.view.frame;
}];
}
B. Add the presented view controller as a Child view controller. that way the life cycle stays the same, but you can add I'ts view as a subview, and change I'ts frame.
//Presenting view controller:
-(void) presentChildViewController {
[self addChildViewController:self.presentedVC];
[self.view addSubview:self.presentedVC.view];
[self.presentedVC didMoveToParentViewController:self];
}
//Presented view controller:
- (void)viewWillAppear:(BOOL)animated
{
[super viewWillAppear:animated];
CGRect currFrame = self.view.frame;
CGRect newFrame = currFrame;
newFrame.origin.y -= newFrame.size.height;
self.view.frame = newFrame;
[UIView animateWithDuration:1.0 animations:^{
self.view.frame = currFrame;
}];
}
C. Don't use UIViewController, use UIView. Use a "Decorator" object, that you pass it the ViewController you would like the view to disaply on, and the "Decorator" will add the view as subview, and deal with the animation. No need for an example for this scenario.
It is wrong to start animations when your view hasn't yet appeared. Can you, please, try the same code in viewDidAppear.
Also animating main view controller's view will make underlying layers visible, so you've got to use another view on top of view controller's main view, like this:
UIView *progressView = [[UIView alloc] init];
progressView.frame = self.view.bounds;
progressView.backgroundColor = [UIColor redColor];
[self.view addSubView:progressView];
Related
I want to use Storyboards to design content for a slider, and it seems like an easy way to design offscreen content is to use a childViewController. So I've done this
myViewController = [[UIStoryboard storyboardWithName:#"ipad" bundle:NULL] instantiateViewControllerWithIdentifier:#"keyPadOffScreen"];
[self addChildViewController:myViewController];
[myViewController didMoveToParentViewController:self];
newView = myViewController.view;
[self.view addSubview:newView];
And that adds the entire view controller over top of my root view. The problem is, I only want one of the subviews to show up, not the whole view. I can handle the animation, as long as I know how to add the root view. I tried this to just add the subview (sliderView is the name of the subview I want) instead of the whole view, but that did nothing
newView = myViewController.sliderView;
[self.view addSubview:newView];
Should I be using a different strategy?
EDIT: this DOES work, but it seems silly - setting the views size to just be the size of the subview.
newView.frame = CGRectMake(newView.frame.origin.x, newView.frame.origin.y, newView.frame.size.width, **myViewController.sliderView.frame.size.height**);
It does seem a bit overkill for just a view. Once you start doing a lot of custom view/animation/transition stuff it's often easier to implement in code, or at least it is for me since I've been doing it that way for a long time.
But maybe you want to stick with Storyboards. I respect that. And if you have a few developers working on this then it's important to keep some uniformity to how you set up your UI.
Instead of keeping it in a separate view controller and adding it when you need it to animate on-screen, simply add it to your existing view controller and either set it to hidden, or set it's alpha to 0.0 in IB. Then your animation can undo that and make it visible.
you can use custom segue here, for instance:
#implementation FRPresentEnteringPopupSegue
- (void)perform
{
FirstVC *toVC = self.destinationViewController;
SecondNavigationController *fromVC = self.sourceViewController;
toVC.view.frame = CGRectMake(0.0, 0.0, 300.0, 135.0);
toVC.view.center = CGPointMake(fromVC.view.bounds.size.width/2, fromVC.view.bounds.size.height + toVC.view.bounds.size.height/2);
[fromVC.view addSubview:toVC.view];
[toVC viewWillAppear:YES];
[UIView animateWithDuration:0.5
delay:0.0
usingSpringWithDamping:0.7
initialSpringVelocity:0.5
options:UIViewAnimationOptionBeginFromCurrentState
animations:^{
toVC.view.center = CGPointMake(fromVC.view.bounds.size.width/2, fromVC.view.bounds.size.height/2);
}completion:^(BOOL finished) {
[toVC viewDidAppear:YES];
}];
}
#end
make your UIStoryboardSegue subclass
override - (void)perform method with your custom view appearance code
use segue usual way
UINavigationBars within a UINavigationController. . . they don't participate in frame animations. Useful, but exactly not what I want right now. Is there a way to turn this off?
I know I could set the showsNagitationBar property to hidden, and add my own to the view, put am looking at possible alterntives.
What I'm Trying to Achieve:
I've put my UINavigationController into a (screen-sized) container view, and want to slide it across to reveal a side menu. . last time that I did this I had custom push/pop methods on the RootVC, and my own navigation bar - worked fine, though a fair amount of boiler-plate code to set up.
This time I've got the same kind of requirement - main content is push/pop based, and some auxiliary VCs that can be revealed from the side. And so for another approach, and considering that this app's look and feel is very standard, I just included a UINavigationController within the RootVC and expected it to work the same.
However the UINavigationBar stays anchored in place, while the rest of the content within the container view moves.
I am not entirely sure what you are trying to achieve, but when using a navigation controller, the entirety of what you see on screen (your top view controller's view PLUS your navigation bar) is rooted in the Navigation Controller's view.
This means that if you do something like:
[UIView animateWithDuration:1.0 animations:^{
CGRect frame = self.navigationController.view.frame;
frame.size.width -= 30;
self.navigationController.view.frame = frame;
}];
You will get your view AND the navigation bar to shrink.
EDIT: You can add/remove the sliding-in views to the navigation controller's view where it is appropriate (for example the nav controller is your root view controller, you could do it in your appdelegate's didFinishLaunch: method). The following code would show an entirely red view sliding in from the left. In your case, this view would be the one from your side view controller.
UIView *left = [[UIView alloc] initWithFrame:[[UIScreen mainScreen] bounds]];
left.backgroundColor = [UIColor redColor];
CGRect ff = left.frame;
ff.origin.x = -ff.size.width;
left.frame = ff;
[self.navigationController.view addSubview:left];
[UIView animateWithDuration:1.0 animations:^{
CGRect frame = self.navigationController.view.frame;
frame.origin.x += 50;
self.navigationController.view.frame = frame;
}];
Is something like this what you were looking for?
I've just changed my app from being TabView driven to CollectionView driven, as there are too many sections of my app to be feasible for a TabView. When you start the app you are presented with several items in a CollectionView and selecting any of these items will take you to the relevant section of the app.
In XCode, the collection view lives in its own storyboard and each section of the app has its own storyboard.
In the CollectionView's didSelectItemAtIndexPath, I launch the relevant starboard as follows;
UIStoryboard *storyboard = [UIStoryboard storyboardWithName:#"relevant_storyboard" bundle:nil];
UIViewController* vc = [storyboard instantiateInitialViewController];
[self presentViewController:vc animated:YES completion:nil];
Now, none of the built-in transition animations really suit launching from a CollectionView, so I'd really like a custom effect, such as zoom in. However, I'm struggling to find any decent examples that work for me to create any kind of custom transition. I've tried [UIView transitionFromView], but I don't think that suits transitioning between UIViewControllers. I've tried transitionFromViewController:toViewController: but don't think I have the view hierarchy set up correctly. I've also tried using CATransition without success.
I've thought about doing it with a custom segue but, as my CollectionView is in it's own storyboard and have separate storyboards for each section of my app, I can't see how I can do this. At least not without having all sections of the app inside one storyboard, which would make the storyboard huge and difficult to manage.
So, can anyone give me any code examples or pointers on how I can solve this?
In my app I used a similar effect to zoom in from a thumbnail in a collection view cell to a child view controller that took up the entire screen. You could conceivably do the same thing for a navigation controller push as well.
In my code, I had a scoreView property on the cell subclass that I wanted to zoom up into the full screen. In your case, you may want to use a UIImageView with a screenshot of your new view. Alternatively, you could present the new view controller with a screenshot of the old view controller and then animate from there.
//Instantiate the view controller to be animated in...
//If the cell is not completely inside the collection view's frame, a dissolve animation might be more graceful.
BOOL dissolveAnimation = !CGRectContainsRect(CGRectMake(0, 0, self.collectionView.frame.size.width, self.collectionView.frame.size.height), cellRect);
//Get the frame of the cell in self.view coordinates, then the frame of the thumbnail view
CGRect cellRect = [self.collectionView layoutAttributesForItemAtIndexPath:indexPath].frame;
cellRect = CGRectOffset(cellRect, 0.0, -self.collectionView.contentOffset.y);
VSScoreCell *scoreCell = (VSScoreCell *)[self.collectionView cellForItemAtIndexPath:indexPath];
CGRect scoreRect = dissolveAnimation ? CGRectMake(0.0, 0.0, self.view.frame.size.width, self.view.frame.size.height) : CGRectMake(cellRect.origin.x + scoreCell.scoreView.frame.origin.x, cellRect.origin.y + scoreCell.scoreView.frame.origin.y, scoreCell.scoreView.frame.size.width, scoreCell.scoreView.frame.size.height);
VSScoreView *scoreView = [[VSScoreView alloc] initWithFrame:scoreRect];
//Initialize the view that will be animated up (in this case scoreView)...
if (dissolveAnimation)
scoreView.alpha = 0.0;
[self.view addSubview:scoreView];
[UIView animateWithDuration:0.3 delay:0 options:UIViewAnimationOptionBeginFromCurrentState animations:^{
if (dissolveAnimation)
scoreView.alpha = 1.0;
else
scoreView.frame = CGRectMake(0.0, 0.0, self.view.frame.size.width, self.view.frame.size.height);
} completion:^(BOOL finished) {
if (finished)
{
//Add scoreDisplayController as a child view controller or present it without animation
[scoreView removeFromSuperview];
}
}];
Of course, the new iOS might make this easier (my lips are sealed), but I hope this is somewhat helpful for your situation!
Have you tried the UIView animation block?
[UIView animationWithDuration:1.0 animation^ {
// do custom animation with the view
}completion:^(BOOL finished) {
if(finished) {
NSLog(#"Finished");
}
}];
It allows you to do custom animations when dealing with UIView(s), and even with UIViewControllers. I use it alot when dealing with custom animation actions.
EDIT:
for example, if you'd like to make the view of the current controller to move up the screen, and the second view controller to slide down in place of it, just do
[UIView animationWithDuration:1.0 animation^ {
// do custom animation with the view
// make sure CoreGraphics.framework is imported
// sliding current view to the top of the screen
self.view.transform = CGAffineTransformMakeTranslation(0,0);
// sliding 2nd view down..
// uncomment the following line, and one of the options for translation
//SecondView *sv = [[SecondView alloc] init];
// just edit the x,y in CGAffineTransformMakeTranslation to set where it will go
//sv.view.transform = CGAffineTransformMakeTranslation(320, 480) // iphone 4
//sv.view.transform = CGAffineTransformMakeTranslation(768, 1024) // ipad 1
}completion:^(BOOL finished) {
if(finished) {
NSLog(#"Finished");
}
}];
Hope this helps!
This is a strange problem. I have an iPhone view controller for creating a new user with some text fields that require animation to avoid the keyboard when it is shown, i.e. the view animates up and down to keep a minimum clearance between the top of the keyboard and the lower edge of the text field. That worked fine when it was presented modally from the login existing user screen, the initial view controller in the storyboard.
Then I decided to change the app a bit so that the login/create user views would belong to the same view controller. They transition like so:
if(accountCreateView == nil) {
UINib *nib = [UINib nibWithNibName:#"NewAccountView" bundle:nil];
NSArray *nibViews = [nib instantiateWithOwner:self options:nil];
accountCreateView = [nibViews objectAtIndex:0];
}
[UIView transitionFromView:(displayingLoginView ? loginView : accountCreateView)
toView:(displayingLoginView ? accountCreateView : loginView)
duration:0.6
options:(displayingLoginView ?
UIViewAnimationOptionTransitionFlipFromRight :
UIViewAnimationOptionTransitionFlipFromLeft)
completion:^(BOOL finished) {
if (finished) {
displayingLoginView = !displayingLoginView;
}
}
];
This works fine too. However the code that does animation of the views does not. When a text field becomes active the code to check proximity of the text field to the keyboard is called but nothing happens when the animation block executes.
So I checked:
if([UIView areAnimationsEnabled])
NSLog(#"Animations are enabled");
else
NSLog(#"Animations are disabled");
Animations are certainly enabled.
So I moved it back further to viewDidLoad, trying a simple animation to see what works, before anything else:
CGRect viewFrame = loginNameTextField.frame;
viewFrame.origin.y = 400.f;
[UIView animateWithDuration:1.f
delay:0.f
options:UIViewAnimationOptionCurveEaseInOut
animations:^ {
loginNameTextField.frame = viewFrame;
}
completion:NULL];
This works fine! Text field moves slowly down the window.
This does nothing, and that is the confusing part:
CGRect viewFrame = self.view.frame;
viewFrame.origin.y = 400.f;
[UIView animateWithDuration:1.f
delay:0.f
options:UIViewAnimationOptionCurveEaseInOut
animations:^ {
self.view.frame = viewFrame;
}
completion:NULL];
viewFrame when set from self.view.frame has origin 0,20 and size 320,460 as expected.
I tried setting the frame property directly:
CGRect viewFrame = self.view.frame;
viewFrame.origin.y = 400.f;
self.view.frame = viewFrame;
Looks like I can change the frame of subviews of self.view but not of self.view. A fix might be to leave the initial view controller as a blank view in the storyboard and create xibs for both the login view and create user view and load them in viewDidLoad - but why?
edit: Yes, switching the storyboard version of the view controller to a plain empty view and adding the view containing the actual UI from a xib as a subview does make animations work again. Still don't know why though.
If all you need to do is keep your text fields above your keyboard, why not drop the UIViewController and change to a UITableViewController and use static cells? You place one text field per cell (creating enough cells to meet your needs) and style it how you like. When you don't have a first responder and there are few enough cells to fit on the screen, it won't scroll. It there are more than what will fit, then it will scroll to reach the bottom/top cells. Yet no matter how many cells you have, if you select a text field that will be covered up by the keyboard, the device will automatically scroll the cell to a viewable position. It's all built in and I use this feature all the time. It works with text fields and text views. Just remember to comment out the number of sections, number of rows per section, and the cell for index path methods so the static cells will work correctly. It saves a lot of coding.
HTH
Rob
I have a modal view controller that I show with UIModalPresentationPageSheet. The problem is that its default size is too large for my content: so I want to resize its frame to adjust accordingly with my content.
Does anyone know of a way/trick to do this?
Thanks.
Actually you can resize the view controller that gets presented with UIModalPresentationPageSheet. To do it you need to create a custom view controller class and add the following to the class:
<!--The header file-->
#interface MyViewController: ViewController{
//Used to store the bounds of the viewController
CGRect realBounds;
}
<!--In the .m file-->
//viewDidLoad gets called before viewWillAppear, so we make our changes here
-(void)viewDidLoad{
//Here you can modify the new frame as you like. Multiply the
//values or add/subtract to change the size of the viewController.
CGRect newFrame = CGRectMake(self.view.frame.origin.x,
self.view.frame.origin.y,
self.view.frame.size.width,
self.view.frame.size.height);
[self.view setFrame:newFrame];
//Now the bounds have changed so we save them to be used later on
_realBounds = self.view.bounds;
[super viewDidLoad];
}
//viewWillAppear gets called after viewDidLoad so we use the changes
//implemented above here
-(void)viewWillAppear:(BOOL)animated{
//UIModalpresentationPageSheet is the superview and we change
//its bounds here to match the UIViewController view's bounds.
[super viewWillAppear:animated];
self.view.superview.bounds = realBounds;
}
And then you display this view controller with a UIModalPresentationPageSheet. And that's it. This does work for iOS 5.1.1 and iOS 6 as of the date of this post.
You can't resize a UIModalPresentationPageSheet because its size is not modifable.
This works for resizing a view controller that's presented as UIModalPresentationFormSheet. I would give this a try, I'm not sure if it'll work or not:
navController.modalPresentationStyle = UIModalPresentationFormSheet;
[self presentModalViewController:navController animated:YES];
//these two lines are if you want to make your view smaller than the standard modal view controller size
navController.view.superview.frame = CGRectMake(0, 0, 200, 200);
navController.view.superview.center = self.view.center;