CALayer frame value causing animation even after removal from superlayer - ios

Into the CALayer world:
I am creating a layer that needs to remain in middle of view regardless of device orientation. Can someone tell me why does my layer animates after rotation from the old position even though I removed it from superlayer? I understand that the frame and borderWidth properties are animatable but are they animatable even after removal from superLayer?
And if removal from superLayer does not reset the layer properties because the layer object has not been released (ok I can understand that), how do I mimic the behavior of a newly displayed layer so that the border does not shows like it is moving from an old position after rotation.
I created this sample project - cut and paste if you wish. You will just need to link the quartz core library.
#import "ViewController.h"
#import <QuartzCore/QuartzCore.h>
#interface ViewController ()
#property (nonatomic,strong) CALayer *layerThatKeepAnimating;
#end
#implementation ViewController
-(CALayer*) layerThatKeepAnimating
{
if(!_layerThatKeepAnimating)
{
_layerThatKeepAnimating=[CALayer layer];
_layerThatKeepAnimating.borderWidth=2;
}
return _layerThatKeepAnimating;
}
-(void) viewDidAppear:(BOOL)animate
{
self.layerThatKeepAnimating.frame=CGRectMake(self.view.bounds.size.width/2-50,self.view.bounds.size.height/2-50, 100, 100);
[self.view.layer addSublayer:self.layerThatKeepAnimating];
}
-(void) willRotateToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation duration:(NSTimeInterval)duration
{
[self.layerThatKeepAnimating removeFromSuperlayer];
}
-(void) didRotateFromInterfaceOrientation:(UIInterfaceOrientation)fromInterfaceOrientation
{
self.layerThatKeepAnimating.frame=CGRectMake(self.view.bounds.size.width/2-50,self.view.bounds.size.height/2-50, 100, 100);
[self.view.layer addSublayer:self.layerThatKeepAnimating];
}
#end

As odd as this sounds, the answer is to move code in
willRotateToInterfaceOrientation
to
viewWillLayoutSubviews
-(void) viewWillLayoutSubviews
{
self.layerThatKeepAnimating.frame=CGRectMake(self.view.bounds.size.width/2-50,self.view.bounds.size.height/2-50, 100, 100);
[self.view.layer addSublayer:self.layerThatKeepAnimating];
}
It looks like any layer "redrawing" here happens without animation, even if layer properties are animatable.

Well the problem is not what you think; when you remove that layer from the superview it is not actually nillified because you are retaining a strong reference to it. Your code doesn't enter the if statement in the getter for creating a fresh layer, because it is never nil after the first time:
if(!_layerThatKeepAnimating)
{
_layerThatKeepAnimating=[CALayer layer];
_layerThatKeepAnimating.borderWidth=2;
}
So either change your reference to layer in vc to weak:
#property (nonatomic, weak) CALayer * layerThatKeepAnimating;
Or delete it explicitly by:
-(void) willRotateToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation duration:(NSTimeInterval)duration
{
[self.layerThatKeepAnimating removeFromSuperlayer];
self.layerThatKeepAnimating = nil;
}
I will suggest you to use the first option because as you add layers (or subviews) to views you already earn one strong reference. That's why it is always recommended do it like this:
#property (weak, nonatomic) IBOutlet UIView *view;
but not (strong, nonatomic).

[self.sublayerToRemove removeFromSuperlayer];
self.sublayerToRemove = nil;

Related

Connecting a UIScrollView to a UIView class

I'm currently creating a "slide-show" of pictures that the user can scroll through. Following a guide, I made it so that the UIScrollView I am using shows the edges of the previous and next pictures as the user scrolls along. This comes with a side-effect of the user not being able to scroll if he touches on one of the edges of the pictures, because these edges are technically not within the border of the UIScrollView itself. In order to compensate for this, I am going to create a UIView in which I will embed the UIScrollView. The UIVew will be extend the entire width of the page so that the user can scroll when touching the edges of the pictures. To do this, I need to connect the UIScrollView to the code of the UIView through an IBOutlet. Usually this is simply accomplished by Ctrl-clicking on the UIScrollView and dragging to the code. However, this does not seem to work for me and I am stumped as to why.
Here is a screenshot of the environment I am dealing with when I try to ctrl-click on the UIScrollView and drag to the code to create an IBOutlet (it simply doesn't give the option to create anything).
Here is a screenshot of what running the simulator produces. If i try to click and drag where my mouse currently is, it doesn't scroll, which is the problem I am trying to correct.
It is because you should link your storyboard view to UIView class. You can choose your ScrollViewController class in custom class settings. I added the sample jpg
what you are looking for is not that 'easy' by just connecting the two.
There are two options:
1) In the future you should use a UICollectionView and implement the paging behaviour yourself, that is the cleanest code I think.
NEVER MIND THE SECOND OPTION - I DIDN'T SEE YOU ACTUALLY ALREADY USED THIS
2) To directly answer your question:
You have to subclass the UIView that contains the UIScrollView like this:
header:
#import <UIKit/UIKit.h>
#interface SCTouchForwardView : UIView
#property (nonatomic, weak) IBOutlet UIView* receiver;
#property (nonatomic, assign) BOOL force;
#end
Implementation:
#import "SCTouchForwardView.h"
#implementation SCTouchForwardView
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
{
UIView* result = [super hitTest:point withEvent:event];
if (![result isKindOfClass:[SCTouchForwardView class]]) {
return result;
}
if (result==self) {
UIView *result = [self.receiver hitTest:[self convertPoint:point toView:self.receiver] withEvent:event];
if (result) {
return result;
}else{
if (self.force) {
return self.receiver;
}
return nil;
}
}
return nil;
}
#end
When you want to use this, you just have to Right-Click-Drag from the container to the UIScrollview and select 'receiver' and what you tried to do will work!

CALayer - strange drawInContext bug

I'm currently having a very strange bug with regard to a CALayer subclass. This CALayer subclass is responsible for drawing a UI element, and is contained inside of a custom UIView class. When the UIView receives touch events, it changes the UIElementLayer's properties, causing the layer to be redrawn.
However, the weird problem that I'm having is that occasionally the layer will freeze up. When I say freeze up, I mean that drawInContext() does not get called until the first second of the next minute. Literally every time it freezes up, I can count on it going back to normal at the :00 component of the next minute. I have tried debugging all of my functions, and I just can't find a reason why this is happening. Any ideas on what could be causing this?
The details of that class are:
#interface UIElementLayer : CALayer
#property (nonatomic) CGFloat beginningValue;
#property (nonatomic) CGFloat endingValue;
#property (nonatomic) BOOL shouldReset;
#end
#implementation UIElementLayer
#dynamic beginningValue, endingValue, shouldReset;
- (id)initWithLayer:(id)layer
{
if (self = [super initWithLayer:layer])
{
if ([layer isKindOfClass:[UIElement class]])
{
UIElement *element = (UIElement *)layer;
self.shouldReset = element.shouldReset;
self.startingValue = element.startingValue;
self.endingValue = element.endingValue;
}
}
return self;
}
+ (BOOL)needsDisplayForKey:(NSString *)key
{
if ([key isEqualToString:#"startingValue"] ||
[key isEqualToString:#"endingValue"]
|| [key isEqualToString:#"shouldReset"])
{
return YES;
}
return [super needsDisplayForKey:key];
}

How to make iOS callback functions? [duplicate]

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?

How to pass data to a custom class(subview) drawRect function?

Edit:
I'm drawing a line using drawRect: function which resides in a custom class in a view which I created in the MainStoryboard.storyboard file. The view does point to the custom class (draw2D) in the interface builder.
All code i have regarding draw in my main file(viewController) is now:
(header file)
#class draw2D;
#property (weak, nonatomic) IBOutlet draw2D *draw;
(m file)
#import "draw2D.h"
[draw.listOfFreqToDraw addObject: closestChar];
[draw setNeedsDisplay];
The problem I seem to have now is that it only runs the drawRect method in draw2D class once and never calls it again (therefor listOfFreqToDraw isn't even called after the first run)
You can always change a variable in draw2D
Set this up in the draw2D.h
#property (nonatomic, strong) NSMutableArray * listOfFreqToDraw;
in draw2D.m
- (void)drawRect:(CGRect)rect {
for (UIBezierPath *path in self. listOfFreqToDraw) {
[path stroke];
}
}
and in your main class
[draw.listOfFreqToDraw addObject: closestChar];
[draw setNeedsDisplay];

Release hidden objects?

I write app on iOS. I've got main class and UIView subclass with some UILabel fields.
I want to release memory when objects of subclass are out of the screen (I hide view by animation). How can I do this?
ViewController.h
#import "Histogram.h"
#import "HistogramDelegate.h"
{
UIScrollView *filtersScrollView;
UITapGestureRecognizer *tapGesture;
UISwipeGestureRecognizer *swipeGesture;
...some UILabels and other components.
Histogram *_Histogram;
}
#property (nonatomic, retain) Histogram *_Histogram;
... other properties
... some functions
#end
ViewController.m
-(void)viewDidLoad {
_Histogram = [[Histogram alloc] initWithFrame:...];
}
-(void)viewDidUnload // here i add nil value to objects, for ex. UIScrollView = nil.
-(void)someFunc {
[_Histogram hideHistogram];
}
Histogram.h
//some objects/fields like UILabels, UISliders, UIViews
Histogram.m
some functions.
-(void)hideHistogram {
}
How and where can I release _Histogram and his objects from memory when are out of the screen? When I alloc and init _Histogram and when I hide _Histogram, my app is slower.
The question is not very clear. Anyways if you mean to ask how to free your memory once your histogram is hidden: then what you have to do is after calling [_histogram _hidden] call [_histogram release]; _histogram=nil;
Also in the dealloc function of your histogram.m file you should release all the elements you have initialized in that class. Otherwise releasing _histogram object wouldn't be of much help

Resources