I am loading ViewControllerA inside ViewController B. It is a small animated character, inside a larger scene.
Inside ViewControllerA, there is a rotation animation like:
CAKeyframeAnimation *bobble = [CAKeyframeAnimation animationWithKeyPath:#"transform.rotation"];
NSArray *times = [NSArray arrayWithObjects:[NSNumber numberWithFloat:0.0],
[NSNumber numberWithFloat:0.25],
[NSNumber numberWithFloat:0.5],
[NSNumber numberWithFloat:0.75],
[NSNumber numberWithFloat:1.0],
nil];
[bobble setKeyTimes:times];
NSArray *values = [NSArray arrayWithObjects:
[NSNumber numberWithFloat:degreesToRadian(0)],
[NSNumber numberWithFloat:degreesToRadian(5)],
[NSNumber numberWithFloat:degreesToRadian(0)],
[NSNumber numberWithFloat:degreesToRadian(-5)],
[NSNumber numberWithFloat:degreesToRadian(0)],
nil];
[bobble setValues:values];
bobble.repeatCount = HUGE_VALF;
bobble.autoreverses = YES;
bobble.duration = 5.0;
bobble.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionLinear];
[self.view.layer addAnimation:bobble forKey:#"transform.rotation"];
It's own viewDidLoad and viewDidAppear look like:
- (void)viewDidLoad
{
[super viewDidLoad];
// Do any additional setup after loading the view from its nib.
self.view.center = CGPointMake(640, 201);
[self.view setAnchorPointAndReposition:CGPointMake(.7, .7)];
}
- (void)viewDidAppear:(BOOL)animated
{
[super viewDidAppear:animated];
[self bobble];
[NSTimer scheduledTimerWithTimeInterval:3. target:self selector:#selector(blinkFromTimer:) userInfo:nil repeats:YES];
}
In iOS5+ it loads and animates just fine, in iOS 4.3, it loads but no animation ca be seen.
Any insight?
You aren't supposed to host one view controller inside another unless you use the new parent view controller support in iOS 5. Even in iOS 5, using a view controller to manage a small animated character is serious overkill, and probably not a good fit at all. Better to create a custom subclass of UIView and use that.
It is POSSIBLE to host one view controller inside another pre iOS 5, but the burden is on you to make everything work, and you wind up fighting against the OS design every step of the way. I fought that battle early in the days of the iPhone SDK (as it was known at first) and gave up. It's a nightmare from start to finish. I strongly advise against it.
I know of a major software developer (Apple partner level) who's app Apple threatened to take down from the store for doing that.
Related
I'm using an NSTimer to write text to a CATextLayer but the text only displays for a split second.
I set the first timer like this:
-(void) startSentanceAnimation{
float firstTime = [[sentanceArray objectAtIndex:0][#"time"]floatValue];
[NSTimer scheduledTimerWithTimeInterval:firstTime target:self selector:#selector(timedSentances:) userInfo:sentanceArray repeats:NO];
}
It calls this method that changes the text and then creates a new timer.
-(void)timedSentances:(NSTimer *)timer{
NSArray *userInfo = timer.userInfo;
timer = nil;
NSString *sentance = [userInfo objectAtIndex:sentanceCount][#"sentance"];
[self nextLine:sentance];
//textLayer.string = sentance;
float intervalLength = [[userInfo objectAtIndex:sentanceCount][#"time"]floatValue];
[NSTimer scheduledTimerWithTimeInterval:intervalLength target:self selector:#selector(timedSentances:) userInfo:userInfo repeats:NO];
sentanceCount++;
}
The code runs without error, and the text is displayed at the appropriate times, but the text only flashes to the screen for a split second and doesn't persist in between method calls. Is it because I'm not keeping a reference to the timer?
The text should be set using the method nextLine as follows:
-(void)nextLine:(NSString *)st{
textLayer.string = st;
}
The TextLayer is a CATextLayer and is declared as a variable in the interface like this:
#interface ICIRecordDetail(){
CATextLayer *textLayer;
}
And it is instantiated in the viewDidLoad method.
I've also tried setting the text like this:
[textLayer setText:sentance];
But the same thing happens. Sentance only appears when the timer fires, and then disappears again. I'm wondering is it because it is in a CATextLayer? Very new to IOS so am at a loss. Any help would be great. thanks
I'm using the root viewcontroller to display a couple of labels to keep track of scores etc, which is working fine. However, the root viewcontroller is not responding to messages from the scene right away when the scene is loaded.
The situation is this: UIViewController presents SKView where I initialize MyScene on top of that (normal SpriteKit stuff). Immediately in MyScene I load the level from a json-file, which contains the number of possible points on that specific level. When the level is done loaded, I try to send the number of possible points back to my root viewcontroller, so it can display it in a label.
I've tried setting a property myScene.mainViewController = self from the viewWillLayoutSubviews on ViewController, and then just [self.mainViewController setInitialScore:_bgLayer.maxScore];, but the method setInitialScore never fires. I've also tried using the NSNotificationCenter, but this does not work either.
However, I call a method in self.mainViewController whenever the player collects a point (which happens a little bit later), and that works fine. So the problems seems to be that the ViewController is not done loading or something, so it doesn't respond right away.
How could I solve this?
EDIT 1:
Here's some code by request:
ViewController.m
- (void)viewWillLayoutSubviews {
[super viewWillLayoutSubviews];
SKView *skView = (SKView *)self.view;
if (!skView.scene) {
skView.showsFPS = YES;
skView.showsNodeCount = YES;
// Create and configure the scene.
MyScene* scene = [MyScene sceneWithSize:skView.bounds.size];
scene.scaleMode = SKSceneScaleModeAspectFill;
scene.mainViewController = self;
// Present the scene.
[skView presentScene:scene];
self.numLives = bStartNumLives;
self.score = 0;
[self resetLabels];
[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(setInitialRings:) name:#"setInitialRings" object:nil];
}
}
- (void)setInitialRings:(NSNotification *)notification {
NSLog(#"Initial rings set");
NSDictionary *userInfo = [notification userInfo];
NSInteger rings = [(NSNumber *)[userInfo objectForKey:#"rings"] integerValue];
self.numRings = rings;
[self resetLabels];
}
MyScene.m
- (void)createWorld {
_bgLayer = [self createScenery];
NSLog(#"world created setting initial rings");
NSDictionary *userInfo = [NSDictionary dictionaryWithObject:[NSNumber numberWithInteger:_bgLayer.numRings] forKey:#"rings"];
[[NSNotificationCenter defaultCenter] postNotificationName:#"setInitialRings" object:userInfo];
_worldNode = [SKNode node];
[_worldNode addChild:_bgLayer];
[self addChild:_worldNode];
self.anchorPoint = CGPointMake(0.5, 0.5);
_worldNode.position = CGPointMake(-_bgLayer.layerSize.width / 2, -_bgLayer.layerSize.height / 2);
}
As you can see from this snippet of code, I'm trying to use the NSNotificationCenter. I've also tried calling [self.mainViewController setInitialRings:_bgLayer.numRings]; manually (but changing the setInitialRings-method to take an integer instead of a notification).
When my app was in development, iAds worked great. Every 30 seconds I would either get a call to "bannerViewDidLoadAd" or to "didFailToReceiveAdWithError" and I prepared the app to handle either callback. I get the green checkmark "You're connected to iAd's App Network" and the other test ads.
Now that the app is live, it only gets "didFailToReceiveAdWithError" and never loads an ad.
I'm running the released version of the app on my phone plugged in to the Xcode Organizer Console, and I see the NSLog that prints within "didFailToReceiveAdWithError"
The iAd Portal doesn't show any requests though, it lists 0 requests.
I've built it to my phone again from XCode with the development profile and again it works as it should. I've deleted the app, shut down my phone, signed out of my iTunes Apple ID, and redownloaded the app from the App Store and still the ad fails every time.
Here's how I've got the ad coded:
In my rootViewController, the user chooses to start a new game, and I animate the new view:
UIViewController *nextController = [[GamePlayViewController alloc] initWithNibName:#"GamePlayView" bundle:nil];
[nextController performSelector:#selector(setDelegate:) withObject:self];
nextController.view.frame = CGRectMake(0, 570, 320, 568);
[self.view addSubview:nextController.view];
[UIView beginAnimations:nil context:nil];
[UIView setAnimationCurve:UIViewAnimationCurveEaseInOut];
[UIView setAnimationDuration:0.23];
[UIView setAnimationDelegate:self];
[UIView setAnimationDidStopSelector:#selector(animationDidStop:finished:context:)];
nextController.view.frame = CGRectMake(0, 0, 320, 568);
[UIView commitAnimations];
temporaryController = nextController;
GamePlayViewController.h includes:
- #import <iAd/iAd.h>
- #interface GamePlayViewController : UIViewController <ADBannerViewDelegate, UIDocumentInteractionControllerDelegate> {
GamePlayViewController.m includes:
- ADBannerView *_bannerView;
Once the user is in GamePlayViewController.m, there is an animation triggered in viewDidLoad, and once that animation completes, an ad is called:
if ([ADBannerView instancesRespondToSelector:#selector(initWithAdType:)]) {
_bannerView = [[ADBannerView alloc] initWithAdType:ADAdTypeBanner];
} else {
_bannerView = [[ADBannerView alloc] init];
}
_bannerView.delegate = self;
[self.view bringSubviewToFront:_bannerView];
}
That's really all there is to it other than the callback methods for iAds.
- (void)bannerViewDidLoadAd:(ADBannerView *)banner
{
NSLog(#"ad loaded!");
_bannerView.hidden = NO;
[self layoutAnimated:YES];
}
- (void)layoutAnimated:(BOOL)animated
{
// As of iOS 6.0, the banner will automatically resize itself based on its width.
// To support iOS 5.0 however, we continue to set the currentContentSizeIdentifier appropriately.
CGRect contentFrame = self.view.bounds;
if (contentFrame.size.width < contentFrame.size.height) {
_bannerView.currentContentSizeIdentifier = ADBannerContentSizeIdentifierPortrait;
} else {
_bannerView.currentContentSizeIdentifier = ADBannerContentSizeIdentifierLandscape;
}
CGRect bannerFrame = _bannerView.frame;
if (_bannerView.bannerLoaded) {
bannerFrame.origin.y = 0;
} else {
bannerFrame.origin.y = contentFrame.size.height;
}
[UIView animateWithDuration:animated ? 0.25 : 0.0 animations:^{
_contentView.frame = contentFrame;
[_contentView layoutIfNeeded];
_bannerView.frame = bannerFrame;
[self.view addSubview:_bannerView];
}];
}
- (void)bannerView:(ADBannerView *)banner didFailToReceiveAdWithError:(NSError *)error
{
NSLog(#"ad failed!");
_bannerView.hidden = YES;
}
Maybe something I am doing is wrong or I should have the ad on the rootViewController itself, but this code works great with test iAds, so I'm not sure why it's not working with the App Store version of the app.
Thanks for any help!
I've not looked at your code but I've observed this also. Sometimes Apple just hasn't sold any ads so there aren't any to display for you. Consider using an ad aggregator. I use https://github.com/larsacus/LARSAdController but modified it to display ads the way I want to.
I cover that in my blog entry: http://www.notthepainter.com/iad-admob-integration-with-a-dynamic-uiview/
Here is the text of the blog entry so it is preserved here, just in case my blog goes away someday.
I’ve released 2 apps both with iAds. I used Apple’s BannerView sample code to implement this. Basically, in your delegate you don’t set root to your expected root UIViewController but rather you set root to a BannerView which contains your real root. When an iAd is available, your main view shrinks and the iAd is displayed at the bottom. When an ad isn’t available, your view expands to its “normal” size.
This worked very well in testing so I released both apps to the app store. They’re Done Yet? and Wheelin. However, when I first downloaded the versions from the store I was quite surprised to see no ads ever. It turns out that at least right now, iAd had a pretty horrible fill rate. So I wanted to show another ad when an iAd wasn’t available.
I found LARSAdController, an open source project by larsacus on GitHub. He makes ad integration very easy except for one thing. When you go down his quick development route you get the ads covering your view, it doesn’t shrink to accommodate the ad. This is a completely reasonable design decision, just not one I wanted.
So I decided to modify Apple’s BannerView to use LARSAdController. It was pretty easy.
The first thing you do is remove iAd from BannerView’s .h file and ad in the LARS TOLAdViewController class.
#import "TOLAdViewController.h"
#define KVO_AD_VISIBLE #"KVO_AD_VISIBLE"
#interface BannerViewController : TOLAdViewController
(Just ignore the KVO_AD_VISIBLE define for now, I’ll cover that later.) In the .m file also remove iAd and make these changes:
#implementation BannerViewController {
UIView *_bannerView;
UIViewController *_contentController;
BOOL isLoaded;
}
We changed _bannerView from an ADBannerView into a plain old UIVIew. ADBannerView also has a bannerLoaded property which we’ll have to replace with our isLoaded boolean. initWithContentViewController is also easy to modify.
// IAPHelper *sharedInstance = [//IAPHelper sharedInstance];
//if ([sharedInstance showBannerAds]) {
if (YES) {
_bannerView = [[UIView alloc] initWithFrame:CGRectZero];
} else {
_bannerView = nil; // not showing ads since the user has upgraded
}
Notice the commented out section. If you are using in-app purchases to transform an ad supported version into an ad free version you can do that right there.
At the end of the method well use Key-Value-Observing (KVO) to watch LARS and see when an ad is served or removed. (I’ll probably cover KVO in a future blog entry.)
[[LARSAdController sharedManager] addObserver:self
forKeyPath:kLARSAdObserverKeyPathIsAdVisible
options:0
context:KVO_AD_VISIBLE];
And the observing code:
- (void)observeValueForKeyPath:(NSString *)keyPath
ofObject:(id)object
change:(NSDictionary *)change
context:(void *)context;
{
if(context == KVO_AD_VISIBLE) {
NSNumber *isVisible = [object valueForKey:kLARSAdObserverKeyPathIsAdVisible];
if ([isVisible boolValue]) {
_bannerView.frame = [[LARSAdController sharedManager] containerView].frame;
isLoaded = YES;
} else {
isLoaded = NO;
}
}
[self.view setNeedsLayout];
[self.view layoutIfNeeded];
}
We save the frame of the new ad and also update the isLoaded variable. (Originally thought I would need to call setNeedsLayout and layoutIfNeeded but in practice I didn’t.) The mods to viewDidLayoutSubviews were also pretty straightforward. The only odd part was removing the references to ADBannerContentSizeIdentifierPortrait and ADBannerContentSizeIdentifierLandscape and just replacing that all with a single line:
bannerFrame.size = [_bannerView sizeThatFits:contentFrame.size];
And a few lines later you use the new isLoaded variable
if (isLoaded) {
Don’t forget to clean up your observer in dealloc:
-(void) dealloc;
{
[[LARSAdController sharedManager] removeObserver:self forKeyPath:kLARSAdObserverKeyPathIsAdVisible];
}
All that remains is to tell LARS about your ads in your app delegate:
[[LARSAdController sharedManager] registerAdClass:[TOLAdAdapterGoogleAds class] withPublisherId:#"a14e55c99c24b43"];
[[LARSAdController sharedManager] registerAdClass:[TOLAdAdapteriAds class]];
LARSBannerViewController *root = [[LARSBannerViewController alloc] initWithNibName:#"LARSBannerViewController" bundle:nil];
_bannerViewController = [[BannerViewController alloc] initWithContentViewController:root];
[self.window setRootViewController:_bannerViewController];
And that’s it. Now your app should show iAD or AdMob ads and your view will shrink to accommodate them.
Of course there’s a bug, I don’t know if this is in AdMob server or in LARS but when you are in Landscape mode on an iPhone the ad’s size and the reported size are different leaving a black bar at the bottom of the screen. I’ve pinged larsacus about it and will update this post when I know more.
I’ve forked LARSAdController and submitted the above sample code in a full project on my github.
After a few days, I was getting ready to call for support, and then the iAds started working. So I guess the answer is that there is potential for a lag to occur between an app going live and ads populating for that app.
Initially when the ads started coming in, the fill rate wasn't much above 50% and I was only seeing the one ad for iTunes Radio, but at least the ads had started working. Now fill rate is up to 67% and I'm seeing a bit more of a variety with the ads.
I'm working on code for an expandable tray view that uses UIDynamicAnimator to achieve a nice expand/contract animation.
To achieve a realistic acceleration I use UIGravityBehavior to make my tray fall, until the "tab" of the tray hits the bottom of the screen.
This works well, but even though all items in the scene have stopped moving, UIDynamicAnimatorDelegate dynamicAnimatorDidPause: is never called. This means that the animator continues using CPU cycles to animate the scene ( the delegate is set, and fires for UIDynamicAnimatorDelegate dynamicAnimatorDidPause: ).
I tried removing the UIGravityBehavior from the scene, which did indeed cause the animator to stop in the end. I can't time the removal of the gravity behavior right though, since I need to remove it from the scene once everything has stopped moving.
I understand that gravity is a constant force, but I still assumed it would stop the animator once everything has 0 velocity and 0 acceleration.
Is this last assumption false?
Anyone having similar problems?
You are correct that the animator should pause once everything comes to rest.
Check what items are attached to your gravity behavior, and make sure that there aren't other items still falling. For example, it is easy to accidentally create the following bug:
Add a view to gravity and collision
Remove view from superview and from collision
Fail to remove view from gravity
In this situation, the "ghost item" will fall forever.
Another possible problem (though less likely given your description) is if your items are attached to other behaviors that are causing infinite but small "bounce." I would check the full list of behaviors on your animator (remember to check child behaviors, too). In particular I'd be interested in any UIDynamicItemBehavior that adds elasticity.
EDIT:
You may also want to go the other way. Start with a very basic dynamics system and add components from yours until you can reproduce the problem. For instance, the following does converge quite quickly (logging "pause"):
#interface PTLViewController () <UIDynamicAnimatorDelegate>
#property (nonatomic, strong) UIDynamicAnimator *animator;
#end
#implementation PTLViewController
- (void)viewDidLoad {
[super viewDidLoad];
UIView *view = [[UIView alloc] initWithFrame:CGRectMake(100,100,100,100)];
view.backgroundColor = [UIColor lightGrayColor];
[self.view addSubview:view];
self.animator = [[UIDynamicAnimator alloc] initWithReferenceView:self.view];
self.animator.delegate = self;
UICollisionBehavior *collisionBehavior = [[UICollisionBehavior alloc] initWithItems:#[view]];
collisionBehavior.translatesReferenceBoundsIntoBoundary = YES;
[self.animator addBehavior:collisionBehavior];
UIGravityBehavior *gravityBehavior = [[UIGravityBehavior alloc] initWithItems:#[view]];
[self.animator addBehavior:gravityBehavior];
}
- (void)dynamicAnimatorDidPause:(UIDynamicAnimator *)animator {
NSLog(#"pause");
}
#end
To your question about getting all item velocities, I don't know of an easy way to do that. Unfortunately, UIDynamicAnimator doesn't directly know all of its items. This is indirectly because UIDyanamicBehavior doesn't include an items property. If this bothers you as much as it does me, consider duping radar://15054405.
But there is a solution if you just want to know the current linear velocity of specific items. Just add a UIDynamicItemBehavior with a custom action to log it:
UIDynamicItemBehavior *dynamicItemBehavior = [[UIDynamicItemBehavior alloc] initWithItems:#[view]];
__weak UIDynamicItemBehavior *weakBehavior = dynamicItemBehavior;
dynamicItemBehavior.action = ^{
NSLog(#"Velocity: %#", NSStringFromCGPoint([weakBehavior linearVelocityForItem:view]));
};
[self.animator addBehavior:dynamicItemBehavior];
I had a similar issue recently. In the end I used a UICollisionBehavior with boundaries instead of items (because otherwise the moving items were bumping the others...) and implement the delegate method collisionBehavior:beganContactForItem:withBoundaryIdentifier:atPoint: to know when I should remove the gravity
UICollisionBehavior *collide = [[[UICollisionBehavior alloc] initWithItems:borders] retain];
[collide addItem:movingItem];
[collide setCollisionMode:UICollisionBehaviorModeBoundaries];
[collide setTranslatesReferenceBoundsIntoBoundary:YES];
If you find a better solution, let me know :) !
My problem is the same. My animator never comes to rest so once started, my app consumes 3 to 4% CPU forever. My views all appear to stop moving within 1/2 second. So rather than figure out why I'm not reaching equilibrium, I just hit it with a hammer and kill the animator with a timer. I give it 2 seconds.
- (void)createAnimator {
if (_timer) {
[_timer invalidate];
}
if (_animator) {
[_animator removeAllBehaviors];
}
_animator = [[UIDynamicAnimator alloc] initWithReferenceView:self.view];
// create all behaviors
// kill the animator in 2 seconds
_timer = [NSTimer scheduledTimerWithTimeInterval:2.0 target:self selector:#selector(killAnimator:) userInfo:nil repeats:NO];
}
- (void)killAnimator:(NSTimer *)timer {
[_animator removeAllBehaviors];
_animator = nil;
_timer = nil;
}
I've encountered a very interesting example, that I am so curios to reproduce.
There is a great app that I love a lot using:
https://itunes.apple.com/en/app/oldbooth/id298007500?mt=8
I an not advertising as I am not the author.
Now they have a interesting point, when from a table view, by selecting a thumb, a ViewController with a video session is loaded.
What I've noticed:
1) The loading of the video session screen is really fast, almost no waiting.
2) No UIImagePicker shutter animation present
3) The VC appearance animations looks like "pushVC", I mean it is horizontal
4)the view with preview is firstly white, then with an alpha animation it goes to 0 alpha.
I have some experience using AVFoundation video capturing, but it is not as quick! It is incredible for me, and I see that this level of speed is possible, ofcourse I want the same.. Please could you please advice how it is possible to achieve such level of performance?
I am trying the following:
In parent VC, in didLoad:
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
UIStoryboard *storyboard = [UIStoryboard storyboardWithName:#"MainStoryboard" bundle:[NSBundle mainBundle]];
self.photoVC = [storyboard instantiateViewControllerWithIdentifier:#"PhotoCreationVC"];
dispatch_async(queue, ^{
[self.photoVC configureFoundation];
});
The method does the following:
-(void) configureFoundation{
NSError * error;
_captureManager = [[AVCamDemoCaptureManager alloc] init];
if ([_captureManager setupSessionWithPreset:AVCaptureSessionPresetHigh error:&error]) {
[self setCaptureManager:_captureManager];
_sessionEstablished = YES;
} else {
[Utils showMessage:[error localizedDescription] withTitle:#"Input Device Init Failed"];
}
}
and when the VC runs didLoad:
- (void)viewDidLoad
{
[super viewDidLoad];
if (_sessionEstablished) {
AVCaptureVideoPreviewLayer *captureVideoPreviewLayer = [[AVCaptureVideoPreviewLayer alloc] initWithSession:[_captureManager session]];
UIView *view = self.previewView;
CALayer *viewLayer = view.layer;
CGRect bounds = view.bounds;
[captureVideoPreviewLayer setFrame:bounds];
if ([captureVideoPreviewLayer isOrientationSupported]) {
[captureVideoPreviewLayer setOrientation:AVCaptureVideoOrientationPortrait];
}
[captureVideoPreviewLayer setVideoGravity:AVLayerVideoGravityResizeAspectFill];
self.captureVideoPreviewLayer = captureVideoPreviewLayer;
if ([[_captureManager session] isRunning]) {
[_captureManager setDelegate:self];
NSUInteger cameraCount = [_captureManager cameraCount];
if (cameraCount < 1) {
} else if (cameraCount < 2) {
}
if (cameraCount < 1) {
}
[viewLayer insertSublayer:captureVideoPreviewLayer below:[[viewLayer sublayers] objectAtIndex:0]];
} else {
[Utils showMessage:#"Failed to start session." withTitle:#"Error"];
}
}
}
So what happens:
in the iTunes app it firstly loads the screen, then releases the alpha of the white view (i guess it is just cover view, which starts to lose alfa after a delay).
In my case, it still thinks a while before loading the VC. The time (1.5 seconds) is the same, but the waiting is not so friendly.
If i put all the loading code in didAppear, it loads immediately, but the loads, so the total time between tap and ready-video increases to 2.5 secs, which is not good.
If i put the loading code in willAppear, it works the same as in the first situation, when it first thinks, the loads (total 1.5 secs)...
PS I use AVCamDemoCaptureManager sample code from Apple, simplified, without any audio and movie output. just video input device and still image capturing.
Guys, please, can you give me some performance advice? how it is possible to achieve some high performance in loading a camera session?
Any help will be highly appreciated!