I have a UIView without any associated UIViewController constantly animating with auto layout and layoutIfNeeded (see code bellow).
But when this view (which is contained in another view) disappears (for example when a modal view covers the view it is contained in) and after I dismiss this modal view, the animating view isn't animating anymore.
I managed to animate it back with the didMoveToWindow:animated method but i'm not sure this is the proper approach.
#interface AnimatingView()
#property (strong, nonatomic) UIView *aSubview;
#property (strong, nonatomic) NSLayoutConstraint *constraintTop;
#property (assign, nonatomic, getter = isStarted) BOOL started;
#property (assign, nonatomic) BOOL stopAnimation;
#end
#implementation AnimatingView
- (id)init
{
self = [super init];
if (self) {
self.stopAnimation = YES;
/* setting base auto layout constraints */
}
return self;
}
-(void)animate{
float aNewConstant = arc4random_uniform(self.frame.size.height);
[UIView animateWithDuration:ANIMATION_DURATION animations:^{
[self removeConstraint:self.constraintTop];
self.constraintTop = [NSLayoutConstraint constraintWithItem:self.aSubview attribute:NSLayoutAttributeTop relatedBy:NSLayoutRelationEqual toItem:self attribute:NSLayoutAttributeTop multiplier:1 constant:aNewConstant];
[self addConstraint:self.constraintTop];
[self layoutIfNeeded];
} completion:^(BOOL finished) {
if (finished && !self.stopAnimation) {
[self animate];
}
}];
}
- (void)didMoveToWindow{
[super didMoveToWindow];
if ([self isStarted]) {
[self stop];
[self start];
}
}
-(void)start{
if (![self isStarted]) {
self.stopAnimation = NO;
[self setStarted:YES];
[self animate];
}
}
-(void)stop{
if ([self isStarted]) {
self.stopAnimation = YES;
[self setStarted:NO];
}
}
#end
You can always use a behavioral pattern : KVO to control the state of the UIView or what I would do, use a notification to animate the view when you want to. Both options are valid. You can have as well a delegate associated with this class and implement the behavior you want in your UIView.
Assuming your view is contained in another view which is contained in a Controller, you can set a BOOL variable called didReturnFromModalChildView which you set it true every time you present that modal view. When you dismiss that modal view, your application will call -(void)viewWillAppear:(BOOL)animated. Here you have to check if that BOOL variable is set to Yes and then use a public method calling if you have a reference to the view you want to animate, or a delegation process or observer-notification pattern. Don't forget to set didReturnFromModalChildView to false.
I hope this will help you.
Related
I'm very new to iOS programming and I have two simple UIView's.
.h file:
#property (strong, nonatomic) IBOutlet UIView *mainPageOutlet;
#property (strong, nonatomic) IBOutlet UIView *secondPageOutlet;
.m file:
- (IBAction)secondPageToMain:(id)sender {
self.view = [self mainPageOutlet];
}
- (IBAction)mainPageToSecondPage:(id)sender {
self.view = [self secondPageOutlet];
}
How can I use animations or transitions when switching between these two views? Currently they only appear when the button is clicked. Ideally I'd like to have something like push, modal, slide in or up.
Is there a way of doing this?
Many thanks.
Use following code.
- (IBAction)secondPageToMain:(id)sender {
[UIView transitionFromView:[self secondPageOutlet]
toView:[self mainPageOutlet]
duration:2
options: UIViewAnimationOptionTransitionFlipFromRight
completion:^(BOOL finished){
[[self secondPageOutlet] sendSubviewToBack];
}];
}
- (IBAction)mainPageToSecondPage:(id)sender {
[UIView transitionFromView:[self mainPageOutlet]
toView:[self secondPageOutlet]
duration:2
options: UIViewAnimationOptionTransitionFlipFromRight
completion:^(BOOL finished){
[[self mainPageOutlet] sendSubviewToBack];
}];
}
And in your viewDidLoad method add following code,
[[self secondPageOutlet] sendSubviewToBack];
So initially your second view will be behind your main view and on performing action it will come in front with animation.
I have a parent VC with 3 children - Settings, Location and Diary.
Settings, Location & Diary are all accessed via IBActions based on their respective buttons.
When I go between Location and Diary, everything is fine. When I click Settings, it works, but when I click back to Location or Diary I get the dreaded must have a common parent view controller error. Funny thing is the exception says that the Diary and Location VC's don't share the same parent even though in that case I am clicking between Settings and either Diary or Location.
.m properties
#interface LPMainContentViewController ()
#property (weak, nonatomic) IBOutlet UIView *containerView;
#property (weak, nonatomic) IBOutlet UIButton *locationButton;
#property (weak, nonatomic) IBOutlet UIButton *diaryButton;
#property (weak, nonatomic) IBOutlet UIButton *settingsButton;
#property (strong, nonatomic) UIViewController *locationViewController;
#property (strong, nonatomic) UIViewController *diaryViewController;
#property (strong, nonatomic) UIViewController *settingsController;
- (IBAction)goToLocationView:(UIButton *)sender;
- (IBAction)goToDiaryView:(UIButton *)sender;
- (IBAction)goToSettings:(UIButton *)sender;
#end
I use if statements in the code to determine which was the previous child VC for the transition method and to set enabled on the button to YES or NO.
- (IBAction)goToLocationView:(UIButton *)sender {
if ([_diaryViewController isViewLoaded]){
[self cycleFromViewController:_diaryViewController toViewController:_locationViewController];
_locationButton.enabled = NO;
_diaryButton.enabled = YES;
}
else if ([_settingsController isViewLoaded]){
[self cycleFromViewController:_settingsController toViewController:_locationViewController];
_locationButton.enabled = NO;
_settingsButton.enabled = YES;
}
}
- (IBAction)goToDiaryView:(UIButton *)sender {
if ([_locationViewController isViewLoaded]){
[self cycleFromViewController:_locationViewController toViewController:_diaryViewController];
_locationButton.enabled = YES;
_diaryButton.enabled = NO;
}
else if ([_settingsController isViewLoaded]){
[self cycleFromViewController:_settingsController toViewController:_diaryViewController];
_diaryButton.enabled = NO;
_settingsButton.enabled = YES;
}
}
- (IBAction)goToSettings:(UIButton *)sender {
if ([_locationViewController isViewLoaded]){
[self cycleFromViewController:_locationViewController toViewController:_settingsController];
_locationButton.enabled = YES;
_settingsButton.enabled = NO;
}
else if ([_diaryViewController isViewLoaded]){
[self cycleFromViewController:_diaryViewController toViewController:_settingsController];
_diaryButton.enabled = YES;
_settingsButton.enabled = NO;
}
}
and here's the transition method pulled straight from the Apple docs
- (void) cycleFromViewController: (UIViewController*) oldVC
toViewController: (UIViewController*) newVC
{
[oldVC willMoveToParentViewController:nil];
[self addChildViewController:newVC];
newVC.view.frame = _containerView.bounds;
CGRect endFrame = _containerView.bounds;
[self transitionFromViewController: oldVC toViewController: newVC
duration: 0.0 options:0
animations:^{
newVC.view.frame = oldVC.view.frame;
oldVC.view.frame = endFrame;
}
completion:^(BOOL finished) {
[oldVC removeFromParentViewController];
[newVC didMoveToParentViewController:self];
}];
}
isViewLoaded is the wrong thing to check. The view will get loaded once and then stick around, so you're probably choosing the wrong "current" view controller after a few cycles. Have another property to hold the current view controller instead, it will simplify your code and also actually work:
#property (weak, nonatomic) UIViewController *currentViewController;
You'll need to set this to whatever the initial child view controller is when you first set up the container view controller.
Then, change the goToXX methods to be like the following:
- (IBAction)goToDiaryView:(UIButton *)sender {
self.settingsButton.enabled = YES;
self.locationButton.enabled = YES;
self.diaryButton.enabled = NO;
[self cycleFromViewController:self.currentViewController toViewController:self.diaryViewController];
}
Last of all, in your cycle method, make sure you keep this property updated in the completion block of the transition:
completion:^(BOOL finished) {
[oldVC removeFromParentViewController];
[newVC didMoveToParentViewController:self];
self.currentViewController = newVC;
}];
Note that it is safer to use your properties rather than the _instanceVariables. That's what they're there for.
I need a ViewController to be called modally to show some UIButton and other UIView on top of the current window. I want the background to be partially transparent and showing the current window below it - something similar to a UIActionSheet but with a custom design. I coded my VC to do the following: 1) during init the VC sets self.view.frame equals to [[UIApplication sharedApplication]keyWindow].frame 2) when show() is called the VC adds self.view on top of [[UIApplication sharedApplication]keyWindow] subViews 3) when an internal button calls the private method release() the VC remove self.view from its superview. Example with a single release button as follows:
#implementation ModalController
- (id)init
{
self = [super init];
if (self){
//set my view frame equal to the keyWindow frame
self.view.frame = [[UIApplication sharedApplication]keyWindow].frame;
self.view.backgroundColor = [UIColor colorWithWhite:0.3f alpha:0.5f];
//create a button to release the current VC with the size of the entire view
UIButton *releaseMyselfButton = [UIButton buttonWithType:UIButtonTypeRoundedRect];
[releaseMyselfButton setTitle:#"Release" forState:UIControlStateNormal];
releaseMyselfButton.frame = CGRectMake(0, 0, 90, 20);
[releaseMyselfButton addTarget:self action:#selector(releaseMyself) forControlEvents:UIControlEventTouchDown];
//add the button to the view
[self.view addSubview:releaseMyself];
}
return self;
}
- (void) show
{
//add self.view to the keyWindow to make sure that it will appear on top of everything else
[[[UIApplication sharedApplication]keyWindow] addSubview:self.view];
}
- (void)releaseMyself
{
[self.view removeFromSuperview];
}
#end
If I create an instance of ModalController from another VC and I call show() everything goes as expected:
#interface CurrentVC ()
#property (strong, nonatomic) ModalController *myModalController;
#end
#implementation CurrentVC
- (void)viewDidLoad
{
[super viewDidLoad];
self.myModalController = [[ModalController alloc]init];
[self.myModalController show];
}
#end
To make it work I need to retain the ModalController in a property until release () is called. However I would like to have the same freedom I have with UIActionSheet and simply keep an instance of it in a local variable:
#implementation CurrentVC
- (void)viewDidLoad
{
[super viewDidLoad];
ModalController *myModalController = [[ModalController alloc]init];
[myModalController show];
}
#end
If I do this with the current code ARC will release myModalController straight after show() is called and the release button will be pointing to nil. How can I make this work without storing the object in a property? I've identified a work around but I'm not sure it's a good design option:
#interface ModalController ()
#property (strong, nonatomic) ModalController *myselfToAutorelease;
#implementation ModalController
- (id)init
{
self = [super init];
if (self){
... ... ...
self.myselfToAutorelease = self;
}
return self;
}
- (void) show
{
... ... ...
}
- (void)releaseMyself
{
[self.view removeFromSuperview];
self.myselfToAutorelease = nil;
}
What I've done is making ModalController "self sufficient" - it stores a pointer to itself during init and set it to nil when it's ready to release himself. It works but I have the feeling that this is against the ARC design principles! Is this approach correct? If not, how can I handle this differently?
Thanks in advance for your help.
Doesn't work like that.
You don't keep a reference to self.
In the main view controller you just create your object. If you need it to be around longer keep it in a property in the main view controller , when done, set the property to nil in the main view controller.
I need to create custom container viewcontroller with label and 2 states A and B(each state presented by child viewcontroller and swap each other). Both children viewcontrollers instantiates when parent container creates and currentViewController is viewControllerA by default. So i have something like this in container viewcontroller:
#property(weak, nonatomic) id currentViewController;
#property(strong, nonatomic) id viewControllerA;
#property(strong, nonatomic) id viewControllerB;
#property(strong, nonatomic) UILabel* label;
...
// Question here
- (void)viewDidLoad
{
[super viewDidLoad];
self.viewControllerA = [ViewControllerA new];
self.viewControllerB = [ViewControllerB new];
self.currentViewController = self.viewControllerA;
[self populateCurrentViewController];
}
- (void)populateCurrentViewController
{
[self addChildViewController:self.currentViewController];
[self.view addSubview:self.currentViewController.view];
[self.currentViewController didMoveToParentViewController:self];
}
- (void)goToState: (UIViewController*)stateController
{
if(self.currentViewController != stateController)
{
[self.currentViewController willMoveToParentViewController:nil];
[self addChildViewController:state];
[self transitionFromViewController: self.currentViewController toViewController: stateController
duration: 0.25 options:UIViewAnimationOptionTransitionFlipFromLeft
animations:nil
completion:^(BOOL finished) {
[self.currentViewController removeFromParentViewController];
[stateController didMoveToParentViewController:self];
}];
}
}
- (void)goToStateA
{
[self goToState: self.viewControllerA];
self.label.text = #"A";
}
- (void)goToStateB
{
[self goToState: self.viewControllerB];
self.label.text = #"B";
}
I need to preserve state of container viewcontroller. The question is where i must instantiate and add children viewcontrollers on first creating my container viewcontroller and on restoration process? Where i must restore children viewcontrollers?
I found many candidates:
-(void)initWithCoder
-(void)awakeFromNib
-(void)viewDidLoad
-(void)viewWillAppear
and
-(void)decodeRestorableStateWithCoder
for restoration process.
A couple of thoughts on this subject. If use initWithCoder and viewDidLoad for creating children viewcontrollers and adding current viewcontroller as a child to container viewcontroller, then after restoration process i need to change current viewcontroller and swap it if necessary, what is wrong i think. If use viewWillAppear for adding current viewcontroller, then it is normal for restoration process, but this method may be invoked several times.
I think there are two ways to restore restore children viewcontrollers:
Instantiate in viewDidLoad and restore in -(void)decodeRestorableStateWithCoder:(NSCoder *)coder like [self.viewControllerA decodeRestorableStateWithCoder:coder] (i saw it here - https://stackoverflow.com/a/16647606/2492707)
Full restore in -(void)decodeRestorableStateWithCoder:(NSCoder *)coder like self.viewControllerA = [coder decodeObjectForKey: #"AKey"];
Which way is better?
Understanding of the recovery process of the UINavigationController (where visibleViewController is populated) can help me too.
Thanks in advance.
This question already has answers here:
How to perform Callbacks in Objective-C
(5 answers)
Closed 9 years ago.
I can't figure out how to make a function, that will give information to the parent object, that has done something, so I need your help.
Example what I want to make:
ViewController class instantiate class BottomView and adds it as it's subview.
Make a call on instance of BottomView class to start animate something.
After the animation ends, I want to give a sign that the animation has ended to the ViewController class, that it could release/remove an instance BottomView from itself.
I need something like a callback.
Could you help please?
You could do something like this
#interface BottomView : UIView
#property (nonatomic, copy) void (^onCompletion)(void);
- (void)start;
#end
#implementation BottomView
- (void)start
{
[UIView animateWithDuration:0.25f
animations:^{
// do some animation
} completion:^(BOOL finished){
if (self.onCompletion) {
self.onCompletion();
}
}];
}
#end
Which you would use like
BottomView *bottomView = [[BottomView alloc] initWithFrame:...];
bottomView.onCompletion = ^{
[bottomView removeFromSuperview];
NSLog(#"So something when animation is finished and view is remove");
};
[self.view addSubview:bottomView];
[bottomView start];
1.You can do it with blocks!
You can pass some block to BottomView.
2. Or you can do it with target-action.
you can pass to BottomView selector #selector(myMethod:) as action,
and pointer to the view controller as target. And after animation ends
use performeSelector: method.
3. Or you can define delegate #protocol and implement methods in your viewController,
and add delegate property in BottomView.
#property (assign) id delegate;
If you make some animations in your BottomView, you can use
UIView method
animateWithDuration:delay:options:animations:completion:
that uses blocks as callbacks
[UIView animateWithDuration:1.0f animations:^{
}];
update:
in ButtomView.h
#class BottomView;
#protocol BottomViewDelegate <NSObject>
- (void)bottomViewAnimationDone:(BottomView *) bottomView;
#end
#interface BottomView : UIView
#property (nonatomic, assign) id <BottomViewDelegate> delegate;
.....
#end
in ButtomView.m
- (void)notifyDelegateAboutAnimationDone {
if ([self.delegate respondsToSelector:#selector(bottomViewAnimationDone:)]) {
[self.delegate bottomViewAnimationDone:self];
}
}
and after animation complit you should call [self notifyDelegateAboutAnimationDone];
you should set you ViewController class to confirm to protocol BottomViewDelegate
in MyViewController.h
#interface MyViewController : UIViewController <BottomViewDelegate>
...
#end
and f.e. in viewDidLoad you should set bottomView.delegate = self;
if you specifically want to just run a function after an animation has occurred you could get away with just using this:
[UIView animateWithDuration:0.5 animations:^{
//do animations
} completion:^(BOOL finished){
//do something once completed
}];
Since you are using animations, a more specific way to handle it is to use animation delegates. You can set the delegate of your animation to the viewcontroller so that after the animation ends below method will be called in your viewcontroller.
-(void) animationDidStop:(CAAnimation *)anim finished:(BOOL)flag
To do that in the 2nd step you mentioned you should pass viewcontroller as an extra parameter so there in you can set the animation delegate.
Many thanks for all of you. I have tried two ways, by selector and by a delegate.
It's cool and I suppose, that I understand those mechanisms.
I have found one odd thing:
ViewController.h
#import <UIKit/UIKit.h>
#import "BottomPanelView.h"
#interface ViewController : UIViewController <BottomPanelViewDelegate>
#property (nonatomic, assign) BottomPanelView* bottomPanelView;
#end
ViewController.m
#import "ViewController.h"
#interface ViewController ()
#end
#implementation ViewController
#synthesize bottomPanelView;
- (void)viewDidLoad
{
[super viewDidLoad];
// Do any additional setup after loading the view, typically from a nib.
bottomPanelView = [[BottomPanelView alloc] initWithFrame:CGRectMake(0, 480, 320, 125)];
[bottomPanelView setDelegate:self];
[self.view addSubview: bottomPanelView];
[bottomPanelView start];
}
//delegate function
- (void)bottomViewAnimationDone:(BottomPanelView *)_bottomPanelView
{
NSLog(#"It Works!");
[bottomPanelView removeFromSuperview]; //1) Does it clears memory?
[bottomPanelView release]; //2) Strange is that if I have released it and set pointer to nil and use it below in touchesBegan it doesn't crashes. Why?
// but if I release it and don't set to nil, it will crash. Why reading from nil is okay and gives back 0 (myValue was set to 15)?
bottomPanelView = nil;
}
-(void) touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
NSLog(#"Touched!");
NSLog(#"Pointer: %p", bottomPanelView);
NSUInteger val = [bottomPanelView myValue];
NSLog(#"myValue: %d", val);
}
My questions are in comments in code above.
Please, tell me especially why reading from nil pointer doesn't crashes the application?