I have a very heavyweight scene that takes like 5 seconds to load.
I want some loading screen with a spinner to appear while it is waiting to load - what is elegant solution to that?
Should I make intermediate scene and show spinner on it while my scene loads?
Can I alloc and initWithSize scene in background thread?
Here is how I do it now:
MyScene *newScene = [[MyScene alloc] initWithSize:self.size];
SKTransition *transition = [SKTransition flipHorizontalWithDuration:1.0];
[self.view presentScene:newScene transition:transition];
But as soon as I press the button - game freezes for these 5 seconds and provides no feedback for player what is going on.
Tagging this as coco2d since it has very similar API and problems.
Don't do things like this on a background thread. What you can do is show your spinner first, then dispatch_async the actual transition:
// Write code for showing your spinner
dispatch_async(dispatch_get_main_queue(), ^{
MyScene *newScene = [[MyScene alloc] initWithSize:self.size];
SKTransition *transition = [SKTransition flipHorizontalWithDuration:1.0];
[self.view presentScene:newScene transition:transition];
});
This will show your spinner and on the next run loop execute the transition.
Here is my approach:
In Interface Builder add SKView, Loading image, Large White Activity Indicator to GameSceneViewController:
Add outlets for added controls to GameSceneViewController:
#interface GameSceneViewController : UIViewController
#property (nonatomic, weak) IBOutlet SKView *skView;
#property (weak, nonatomic) IBOutlet UIImageView *loadingScreen;
#property (weak, nonatomic) IBOutlet UIActivityIndicatorView *activityIndicator;
#end
In your SKScene add class method, which loads all assets for the scene:
+(void)loadSceneAssetsWithCompletionHandler:(AGAssetLoadCompletionHandler)handler {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
// Load the shared assets in the background.
[self loadSceneAssets];
if (!handler) {
return;
}
dispatch_async(dispatch_get_main_queue(), ^{
// Call the completion handler back on the main queue.
handler();
});
});
}
where AGAssetLoadCompletionHandler is defined in GameScene.h as
/* Completion handler for callback after loading assets asynchronously. */
typedef void (^AGAssetLoadCompletionHandler)(void);
Same approach uses Apple in their Sprite Kit Adventure Game.
Finally, GameSceneViewController's viewWillAppear method:
- (void)viewWillAppear:(BOOL)animated
{
[super viewWillAppear:animated];
[self.activityIndicator startAnimating];
if (!self.skView.window) {
[self.view addSubview:self.skView];
}
[GameScene loadSceneAssetsWithCompletionHandler:^{
GameScene *scene = [[GameScene alloc] initWithSize:self.skView.bounds.size];
scene.scaleMode = SKSceneScaleModeAspectFit;
[self.skView presentScene:scene transition:[SKTransition crossFadeWithDuration:1]];
[self.activityIndicator stopAnimating];
[UIView animateWithDuration:1.0 delay:0.0 options:UIViewAnimationOptionCurveEaseInOut animations:^{
self.loadingScreen.alpha = 0.0f;
} completion:NULL];
}];
}
Related
How would one present a UIViewController (from Storyboard say) that is modal, and slides up from the bottom of the presenting view controller. Requirements would be:
slides up from bottom, with widths aligning with the width of the presenting view controller
does NOT take up whole screen or whole parent presenting view controller (rather only is as high as required to show itself)
can be shown within the context of a view controller which doesn't take the whole screen
I do not use storyboards so I wrote it all out. You can copy paste this into a brand new project and run it to see it working.
Your PresentingController needs to conform to two things. The first protocol is: UIViewControllerTransitioningDelegate which allows the controller to provide a custom presenter (namely itself in our case below). Whatever you return here (be it self, or some other object) needs to conform to UIViewControllerAnimatedTransitioning and provide the custom animations. For this self-contained example, I chose the current viewController to be the presenter and animator.
Next, it needs to conform to protocol: UIViewControllerAnimatedTransitioning which provides the custom animation for any presenting or dismissing controllers.
In other words, when we present or dismiss a viewController, animateTransition from the UIViewControllerAnimatedTransitioning protocol will be called to determine how the child controller should animate into perspective or dismiss from the view-port.
Example (With Transition Animation):
//
// ViewController.m
// SO
//
// Created by Brandon T on 2017-01-23.
// Copyright © 2017 XIO. All rights reserved.
//
#import "ViewController.h"
//Some view controller that will be presented modally.
//I have coloured it red.
#interface ModalController : UIViewController
#end
#implementation ModalController
- (void)viewDidLoad {
[super viewDidLoad];
self.view.backgroundColor = [UIColor redColor];
}
#end
//The view controller that will present or dismiss some other view controller modally.
//I have coloured it white.
#interface ViewController () <UIViewControllerTransitioningDelegate, UIViewControllerAnimatedTransitioning>
#property (nonatomic, assign) bool presentingModalController;
#property (nonnull, nonatomic, strong) ModalController *modalController;
#property (nonnull, nonatomic, strong) UIButton *button;
#end
#implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.view.backgroundColor = [UIColor whiteColor];
//For this example, I add a button to present and dismiss the redViewController.
self.button = [[UIButton alloc] initWithFrame:CGRectMake(15, self.view.center.y - 100, self.view.frame.size.width - 30, 45)];
[self.button setTitle:#"Present" forState:UIControlStateNormal];
[self.button setTitleColor:[UIColor blackColor] forState:UIControlStateNormal];
[self.button setBackgroundColor:[UIColor lightGrayColor]];
[self.button.layer setCornerRadius:5.0];
[self.button addTarget:self action:#selector(onButtonClicked:) forControlEvents:UIControlEventTouchUpInside];
[self.view addSubview:self.button];
//Create the redViewController and set its transitioning delegate to self (this controller will be providing the animation and presenter).
//We also set the style to OverFullScreen because we don't want this controller to disappear.
//When a view controller is presented, the one that presented it usually disappears or gets removed from the hierarchy until the child is dismissed. In the case of alerts, or controllers that need to display OVER the current one, we need to set the modalPresentationStyle.
self.modalController = [[ModalController alloc] init];
self.modalController.transitioningDelegate = self;
self.modalController.modalPresentationStyle = UIModalPresentationOverFullScreen;
}
- (void)didReceiveMemoryWarning {
[super didReceiveMemoryWarning];
}
- (void)onButtonClicked:(UIButton *)button {
if (self.modalController.view.window == nil) {
[self presentViewController:self.modalController animated:YES completion:nil];
[self.button setTitle:#"Dismiss" forState:UIControlStateNormal];
//not a good idea but meh.. I need to keep this example short.
[self.view.window addSubview:self.button];
}
else {
[self.modalController dismissViewControllerAnimated:YES completion:nil];
[self.button setTitle:#"Present" forState:UIControlStateNormal];
[self.view addSubview:self.button];
}
}
//Custom Animations and Presenters.
- (nullable id <UIViewControllerAnimatedTransitioning>)animationControllerForPresentedController:(UIViewController *)presented presentingController:(UIViewController *)presenting sourceController:(UIViewController *)source {
self.presentingModalController = true; //We are presenting the controller.
return self; //Who is animating it? We are.
}
- (nullable id <UIViewControllerAnimatedTransitioning>)animationControllerForDismissedController:(UIViewController *)dismissed {
self.presentingModalController = false; //We are dismissing the view controller.
return self; //Who animated it? We did.
}
//How fast should it present? I chose 0.5 seconds.
- (NSTimeInterval)transitionDuration:(nullable id <UIViewControllerContextTransitioning>)transitionContext {
return 0.5;
}
//The actual animation code.
- (void)animateTransition:(id <UIViewControllerContextTransitioning>)transitionContext {
if (self.presentingModalController) {
//If we are presenting, we need to add the new controller's view as a sub-view.
UIViewController *toViewController = [transitionContext viewControllerForKey:UITransitionContextToViewControllerKey];
//We need a starting frame for the animation.
CGRect startingFrame = transitionContext.containerView.bounds;
startingFrame.origin.y = startingFrame.size.height; //Starts from the bottom of the parent.
startingFrame.size.height = 100; //Has a height of 100.
//We need an end frame for the animation.
CGRect finalFrame = transitionContext.containerView.bounds;
finalFrame.origin.y = finalFrame.size.height - 100; //100 from the bottom of the parent.
finalFrame.size.height = 100; //Present with a size of 100 height.
//Add the controller's view as a subview of the context.
[transitionContext.containerView addSubview:toViewController.view];
[toViewController.view setFrame:startingFrame];
//Start animating from "startFrame" --> "endFrame" with 0.5 seconds duration and no delay. I chose easeIn style.
[UIView animateWithDuration:[self transitionDuration:transitionContext] delay:0.0 options:UIViewAnimationOptionCurveEaseIn animations:^{
[toViewController.view setFrame:finalFrame];
} completion:^(BOOL finished) {
//We are finished animating, complete the transition!
[transitionContext completeTransition:YES];
}];
}
else {
//If we are dismissing the view controller, we need to animate it down the screen and then remove its view from the context.
UIViewController *fromViewController = [transitionContext viewControllerForKey:UITransitionContextFromViewControllerKey];
//We only need one frame. This is the first frame. We are animating from "endFrame" --> "startingFrame" (backwards/reverse animation).
CGRect startingFrame = transitionContext.containerView.bounds;
startingFrame.origin.y = startingFrame.size.height; //Starts from the bottom of the parent.
startingFrame.size.height = 100; //Has a height of 100.
//Start the animation with 0.5 seconds duration and I chose easeOut style.
[UIView animateWithDuration:[self transitionDuration:transitionContext] delay:0.0 options:UIViewAnimationOptionCurveEaseOut animations:^{
[fromViewController.view setFrame:startingFrame];
} completion:^(BOOL finished) {
//Remove the view controller's view from the context and complete the transition!
[fromViewController.view removeFromSuperview];
[transitionContext completeTransition:YES];
}];
}
}
#end
Example (Without Transition Animation):
//
// ViewController.m
// SO2
//
// Created by Brandon Thomas on 2017-01-23.
// Copyright © 2017 XIO. All rights reserved.
//
#import "ViewController.h"
#interface ModalController : UIViewController
#end
#implementation ModalController
- (void)viewDidLoad {
[super viewDidLoad];
self.view.backgroundColor = [UIColor redColor];
}
#end
#interface ViewController ()
#property (nonatomic, assign) bool presentingModalController;
#property (nonnull, nonatomic, strong) ModalController *modalController;
#property (nonnull, nonatomic, strong) UIButton *button;
#end
#implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.view.backgroundColor = [UIColor whiteColor];
self.button = [[UIButton alloc] initWithFrame:CGRectMake(15, self.view.center.y - 100, self.view.frame.size.width - 30, 45)];
[self.button setTitle:#"Present" forState:UIControlStateNormal];
[self.button setTitleColor:[UIColor blackColor] forState:UIControlStateNormal];
[self.button setBackgroundColor:[UIColor lightGrayColor]];
[self.button.layer setCornerRadius:5.0];
[self.button addTarget:self action:#selector(onButtonClicked:) forControlEvents:UIControlEventTouchUpInside];
[self.view addSubview:self.button];
self.modalController = [[ModalController alloc] init];
}
- (void)didReceiveMemoryWarning {
[super didReceiveMemoryWarning];
}
- (void)onButtonClicked:(UIButton *)button {
if (self.modalController.view.window == nil) {
//Present
CGRect startingFrame = self.view.bounds;
startingFrame.origin.y = startingFrame.size.height; //Starts from the bottom of the parent.
startingFrame.size.height = 100; //Has a height of 100.
CGRect finalFrame = self.view.bounds;
finalFrame.origin.y = finalFrame.size.height - 100; //100 from the bottom of the parent.
finalFrame.size.height = 100; //Present with a size of 100 height.
[self.modalController.view setFrame:startingFrame];
[self.modalController willMoveToParentViewController:self];
[self addChildViewController:self.modalController];
[self.view addSubview:self.modalController.view];
[self.modalController didMoveToParentViewController:self];
[UIView animateWithDuration:0.5 animations:^{
[self.modalController.view setFrame:finalFrame];
} completion:^(BOOL finished) {
}];
}
else {
//Dismiss
CGRect startingFrame = self.view.bounds;
startingFrame.origin.y = startingFrame.size.height; //Starts from the bottom of the parent.
startingFrame.size.height = 100; //Has a height of 100.
[UIView animateWithDuration:0.5 animations:^{
[self.modalController.view setFrame:startingFrame];
} completion:^(BOOL finished) {
[self.modalController.view removeFromSuperview];
[self.modalController willMoveToParentViewController:nil];
[self.modalController removeFromParentViewController];
[self.modalController didMoveToParentViewController:nil];
}];
}
}
#end
Check out the Apple documentation for this:
Presenting a View Controller Using Custom Animations
To present a view controller using custom animations, do the following
in an action method of your existing view controllers:
Create the view controller that you want to present. Create your
custom transitioning delegate object and assign it to the view
controller’s transitioningDelegate property. The methods of your
transitioning delegate should create and return your custom animator
objects when asked. Call the
presentViewController:animated:completion: method to present the view
controller. When you call the
presentViewController:animated:completion: method, UIKit initiates the
presentation process. Presentations start during the next run loop
iteration and continue until your custom animator calls the
completeTransition: method. Interactive transitions allow you to
process touch events while the transition is ongoing, but
noninteractive transitions run for the duration specified by the
animator object.
EDIT:
Your alternative option is to create a container with your custom sizes and animate your UIViewController added as a child view of your UIViewController:
[self addChildViewController:content];
content.view.frame = [self frameForContentController];
[self.view addSubview:self.currentClientView];
[content didMoveToParentViewController:self];
Taken from this Thread
How can I record a video for a certain period of time from a UIView just like we are recording a portion of the phone's screen?
use Glimpse
Glimpse allows you to create videos from UIViews. More documentation is here, but basically it records animations and actions as they happen by taking screen shots of a UIView in a series and then creating a quicktime video and saving it to your app’s document folder.
Here is a sample usage:
#import <Glimpse/Glimpse.h>
#implementation myViewController
- (void)viewDidAppear
{
[super viewDidAppear:animated];
// Create a new Glimpse object.
Glimpse *glimpse = [[Glimpse alloc] init];
// Start recording and tell Glimpse what to do when you are finished
[glimpse startRecordingView:self.view onCompletion:^(NSURL *fileOuputURL) {
NSLog(#"DONE WITH OUTPUT: %#", fileOuputURL.absoluteString);
}];
// Create a subview for this example
UIView *view = [[UIView alloc] initWithFrame:CGRectInset(self.view.bounds, 40.0f 40.0f)];
view.backgroundColor = [UIColor greenColor];
view.alpha = 0.0f;
[self.view addSubview:view];
// We are going to record the view fading in.
[UIView animateWithDuration:5.0 animations:^{
view.alpha = 1.0f;
} completion:^(BOOL finished) {
// Since our animation is complete, lets tell Glimpse to stop recording.
[glimpse stop];
}];
}
#end
As the title says, whenever I want to set the text of a UILabel after the animation of the movement of the label, it returns the UILabel back to the origin before the animation. What's interesting is that this only happens when the animation is triggered by a UIButton action.
I've replicated this in a new project. Copy the below code and see what happens.
ViewController.h
#import <UIKit/UIKit.h>
#interface ViewController : UIViewController
#property (strong, nonatomic) IBOutlet UILabel *hello;
- (IBAction)move:(id)sender;
#end
ViewController.m
#import "ViewController.h"
#interface ViewController ()
#end
#implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view, typically from a nib.
self.hello.text = #"Hello";
}
- (void)didReceiveMemoryWarning {
[super didReceiveMemoryWarning];
// Dispose of any resources that can be recreated.
}
- (IBAction)move:(id)sender {
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 1.5 * NSEC_PER_SEC), dispatch_get_main_queue(), ^{
self.hello.text = #"Howdy";
});
[UIView animateWithDuration:1.0f animations:^{
self.hello.center = CGPointMake(self.hello.center.x + 50, self.hello.center.y + 50);
}];
}
#end
Where self.hello and move are linked to a UILabel and UIButton in the storyboard.
Playing around with this I can see that this error does not occur when instead of using the self.hello.center, you use self.hello.transform = CGAffineTransformMakeTranslation(50, 50). Why is this? Is there any way to continue using .center or do I have to change all of my animations to .transform?
Thanks for your help.
I've had to give into the ways of .transform
Please use constraints rather then modifying centre of label. Such issue happens when we mix auto layout and coordinate system handling. Which I feel you should do something similar to this :
[UIView animateWithDuration:1.0f animations:^{
self.xPos.constant = self.hello.center.x + 50;
self.yPos.constant = self.hello.center.y + 50;
[self.hello layoutIfNeeded];
} completion:^(BOOL finished) {
self.hello.text = #"Howdy";
}];
where xPos and yPos are x and y NSLayoutConstraint position of UILabel
In my viewController, there are two separate "custom views". And the first view has some quit animations, while the second view has some enter animations. The question is how could I change my view in my viewController exactly after the first view's quit animation is done? I have tried the view.hidden property, but it lost both the quit and the enter animations. I also tried to add some animateWithDuration thing and put the switchView method in the completion block, which turned out to fail, too.
Here is the code of my viewController:
#interface PopingViewController ()
#property (nonatomic, retain) HomepageView *homepage;
#property (nonatomic, retain) AboutView *about;
#end
#implementation PopingViewController
- (void)switchView
{
self.homepage = nil;
self.about = [[AboutView alloc] initWithFrame:self.view.frame];
self.about.backgroundColor = [UIColor whiteColor];
[self.view addSubview:self.about];
}
- (IBAction)tap:(UITapGestureRecognizer *)sender
{
// the onTouch method basically does some quit animations which take about 1 sec
[self.homepage onTouch:[sender locationInView:self.homepage]];
[self switchView];// it will execute before the animations are finished!
}
- (void)viewDidLoad
{
[super viewDidLoad];
self.homepage = [[HomepageView alloc] initWithFrame:self.view.frame];
self.homepage.backgroundColor = [UIColor whiteColor];
[self.view addSubview:self.homepage];
}
Thanks in advance!
If they are just plain UIView subclasses then you could implement the UIView transition with class method transitionFromView:(UIView *)fromView toView:(UIView *)toView duration:(NSTimeInterval)duration options:(UIViewAnimationOptions)options completion:(void (^)(BOOL finished))completion on UIView.
So, for your case, you would do it like this,
- (void)switchView
{
self.about = [[AboutView alloc] initWithFrame:self.view.frame];
[self.view insertSubView:about belowSubView:self.homepage];
self.about.backgroundColor = [UIColor whiteColor];
[UIView transitionFromView:self.homepage toView:self.about duration:0.6 options:UIViewAnimationOptionCurveEaseInOut | UIViewAnimationOptionShowHideTransitionViews completion:^(BOOL completed){ [self.homePage removeFromSuperView]; }];
}
Otherwise, if you are adding the UIViewController's view to some view then, you would use the UIViewController containment methods. Look at this for further detail http://developer.apple.com/library/ios/#featuredarticles/ViewControllerPGforiPhoneOS/CreatingCustomContainerViewControllers/CreatingCustomContainerViewControllers.html#//apple_ref/doc/uid/TP40007457-CH18-SW6
I'm am trying to delete an image while it is moving on the screen using UITapGestureRecognizers and UIViewAnimation. I have used the UIViewAnimationOptionAllowUserInteraction, but I still get the SIGBART error. I have been looking through the internet and couldn't find a solution.
This is my .m file
#import "AbcViewController.h"
#interface AbcViewController ()
#end
#implementation AbcViewController
//single tap gesture action /deletes person
-(void) tapped:(UITapGestureRecognizer *)recognizer{
[recognizer.view removeFromSuperview];
}
//double tap gesture action
-(void) tapped2:(UITapGestureRecognizer *)recognizer{
[recognizer.view removeFromSuperview];
}
- (void)viewDidLoad
{
[super viewDidLoad];
//this makes a zombie/person
UIImageView *zombieView =[[UIImageView alloc] initWithFrame:CGRectMake(135 , 300, 50, 75)];
UIImage *zombie=[UIImage imageNamed:#"free-vector-stick-figure-clip-art_105575_Stick_Figure_clip_art_hight.png"];
[zombieView setImage:zombie];
[self.view addSubview:zombieView];
[self.view insertSubview:zombieView belowSubview:_Railing];
[zombieView setUserInteractionEnabled:TRUE];
// double tap gesture recognizer
UITapGestureRecognizer *touch2 = [[UITapGestureRecognizer alloc] initWithTarget:self action:#selector(tapped2:)];
[touch2 setDelegate: self];
[touch2 setNumberOfTapsRequired: 2];
[zombieView addGestureRecognizer:touch2];
//tap gesture recognizer
UITapGestureRecognizer *touch = [[UITapGestureRecognizer alloc] initWithTarget:self action:#selector(tapped:)];
[touch setDelegate:self];
[touch setNumberOfTapsRequired: 1];
[zombieView addGestureRecognizer:touch];
//This makes the Person move down until he is behind the railing
[UIView animateWithDuration:8.5
delay:0.0
options:UIViewAnimationOptionAllowUserInteraction | UIViewAnimationOptionCurveLinear
animations:^{
CGAffineTransform transform= CGAffineTransformMakeTranslation(0,-200);
zombieView.transform = transform;
}
completion:nil];
};
-(void)didReceiveMemoryWarning
{
[super didReceiveMemoryWarning];
// Dispose of any resources that can be recreated.
}
#end
This is my .h file:
#import <UIKit/UIKit.h>
#import <QuartzCore/QuartzCore.h>
#interface AbcViewController : UIViewController <UIGestureRecognizerDelegate>
#property (strong, nonatomic) IBOutlet UIImageView *Railing;
//#property (strong, nonatomic) IBOutlet UIImageView *Person;
#property (strong, nonatomic) IBOutlet UIImageView *zombieView;
#property (strong, nonatomic) IBOutlet UIImage *zombie;
-(void) tapped:(UITapGestureRecognizer *)recognizer;
-(void) tapped2:(UITapGestureRecognizer *)recognizer;
#end
Every time I click it in mid-animation I get an error but when it's complete it works fine. I am fairly new to this so I'm sure it's simple, it always is. But, thank you for your patience and answers!
The object during the animation is not where you see it. It has already moved to its final position. Animation is an illusion: the "presentation layer" is animated, but the underlying "model layer" (the real object) stands still and has already moved when the animation starts. The model layer is the one inside a view so it is the only one that can be tapped.
Tapping an object while it is being animated is therefore a very tricky problem. I discuss it here:
http://www.apeth.com/iOSBook/ch18.html#_hit_testing_during_animation
Perhaps that discussion will give you some ideas. But no matter what, this is not going to be easy.
(Consider that perhaps you should not be using view animation for this kind of game. Sprite Kit was invented for just this sort of thing.)