How to replace the custom animation for a CALayer appearing - objective-c

I have a CALayer that I want to change the custom animation for it appearing on screen. I have created a delegate so that I can catch the method:
- (id < CAAction >)actionForLayer:(CALayer *)layer forKey:(NSString *)key
And I check for the key to be equal to kCAOnOrderIn, however, the layer hasn't been told what it's bounds will be yet (it currently reports (0, 0, 0, 0) for the bounds). So then I tried checking for the key to be equal to "bounds" but I still get reported the same rect (0, 0, 0, 0).
The animation I want to do is instead of the layer gradually "unfading" onto the screen via opacity, I want it to grow onto the screen from small and in the middle to it's full bounds. But to do that I need to know what it's full bounds will be. Is there anyway to know that so that I can replace the custom animation, or am I just simply approaching this the wrong way?
Thanks

I know this is 3 months old but I was looking for an answer for a similar problem and notice that this doesn't have an answer (an I'd like some rep ;)
I'm overwriting the actionForLayer:forKey: on a UIView (iPhone SDK) that is automatically set as the delegate to it's backing CALayer in order to inject a different action for anchorPoint. I notice I can get access to the old value via the presentationLayer of the CALayer and the new value is in the modelLayer. But what I think is happening in your case is that the system is calling for the onOrderIn action (perhaps at the time that the layer is added to the view hierarchy) before you have told the layer it's bounds. In fact this is normal.
You should be waiting for the "bounds" in addition to the "onOrderIn" key.
Are you waiting for other keys? "onOrderOut" or "hidden"?
Cheers,
Corin

Related

ios10: viewDidLoad frame width/height not initialized correctly

Since upgrading to XCode8 GM and ios10, all of my views created via Interface Builder are not being initialized correctly until much much later than expected. This means in viewDidLoad, cellForRowAtIndexPath, viewWillAppear, etc, the frame size is set to {1000,1000} for every view. At some point they seem to correct, but its far too late.
The first problem encountered is with common rounding of corners failing across the board:
view.layer.cornerRadius = view.frame.size.width/2
Further problems are showing for anything that relies on frame size to do calculations in the code.
cellForRowAtIndexPath
For cellForRowAtIndexPath, frame size fails on initial table display, but then works fine once you scroll it. willDisplayCell:forRowAtIndexPath does not have the correct frame size either.
I've hardcoded a few values but obviously this is very bad code practice, as well as quite numerous in my projects.
Is there a way or place to get correct frame sizes?
EDIT
I've discovered that using the height/width constraint instead of frame width height is more reliable. This may add the overhead of needing lot of new IBOutlets to link the height/width constraints on items though.
For now I've created a UIView category that lets me access a View's height/width constraints directly without the IBOutlets. For minimal use the small loop shouldn't be a big deal. Results not guaranteed for IB items without the width/height constraints created yet obviously. Probably returns 0 at best for the constant, or worse. Also, if you don't have a height/width constraint and your view is sized dynamically based on leading/trailing constraints, this won't work.
-viewDidLoad appears to have correct frame size, but will often result in a visual change to the UI if you do modifications here.
UIView+WidthHeightConstraints.h
#interface UIView (WidthHeightConstraints)
-(NSLayoutConstraint*)widthConstraint;
-(NSLayoutConstraint*)heightConstraint;
-(NSLayoutConstraint*)constraintForAttribute:(NSLayoutAttribute)attribute;
#end
UIView+WidthHeightConstraints.m
#import "UIView+WidthHeightConstraints.h"
#implementation UIView (WidthHeightConstraints)
-(NSLayoutConstraint*)widthConstraint{
return [self constraintForAttribute:NSLayoutAttributeWidth];
}
-(NSLayoutConstraint*)heightConstraint {
return [self constraintForAttribute:NSLayoutAttributeHeight];
}
-(NSLayoutConstraint*)constraintForAttribute:(NSLayoutAttribute)attribute {
NSLayoutConstraint *targetConstraint = nil;
for (NSLayoutConstraint *constraint in self.constraints) {
if (constraint.firstAttribute == attribute) {
targetConstraint = constraint;
break;
}
}
return targetConstraint;
}
#end
EDIT 2
The category above has proven only partially effective. Mainly because ios appears to auto add a couple extra height/width constraint duplicates, that are of type NSContentSizeLayoutConstraint, which are actually not the same size as the normal constraint. The NSContentSizeLayoutConstraint is also a private class so I can't do isKindOfClass to filter those out. I haven't found another way to effectively test for those yet. This is annoying.
The most common issues you describe are appearing in iOS 10 only and can be solved by adding this line (if necessary):
self.view.layoutIfNeeded()
just above the code, that is responsible for changing constraint, layer.cornerRadius etc.
OR
place your code related to frames / layers into viewDidLayoutSubviews() method:
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
view.layer.cornerRadius = self.myView.frame.size.width/2
view.clipsToBounds = true
... etc
}
We created a radar (28342777 (marked as duplicate for 28221021 but Open)) for the similar problem and the reply that we got was as below:
"Thank you for reporting the issue. Could we get more information about the profile image view? In Xcode 8, a fully constraint, non-misplaced view no longer saves out a frame to minimize diffs and support automatically update frames in IB. At runtime, these views get decoded with a placeholder size of 1000x1000, but are resolved after first layout. Could the image be assigned before initial layout, and would assigning the image to the image view after first layout address this case? Please send a sample to help us further analyze. thanks!"
At present we have provided them the sample project. My observations:
The problem that we had used to happen for XIBs that are converted from Xcode 7.x to Xcode 8.x
If we intentionally break the constraint in XIB then viewDidLoad will get expected height and width and not 1000x1000.
For us it was a UIImageView on which we were apply some layering for making it circular and using masksToBounds. If we set masksToBounds = NO then we everything was working fine.
Though Apple claims that it is going to be a standard from Xcode 8 that views will be set to 1000x1000, the behavior doesn't seem to be consistent.
Hope this helps.
I encountered the same issue and try to solve it without luck by referring above suggestions.
Seems it should be a bug for Apple to solve. I finally find a solution by changing to save my XIB document back to Xcode 7.x format and my UI back to normal.
Until Apple releasing a fix, I don't want to spend my time on hacking it.
What about doing this:
- (NSLayoutConstraint*)widthConstraint{
return [self constraintForAttribute:NSLayoutAttributeWidth];
}
- (NSLayoutConstraint*)heightConstraint {
return [self constraintForAttribute:NSLayoutAttributeHeight];
}
- (NSLayoutConstraint*)constraintForAttribute:(NSLayoutAttribute)attribute {
NSLayoutConstraint *targetConstraint = nil;
for (NSLayoutConstraint *constraint in self.constraints) {
//NSLog(#"constraint: %#", constraint);
if (![constraint isKindOfClass:NSClassFromString(#"NSContentSizeLayoutConstraint")]) {
if (constraint.firstAttribute == attribute) {
targetConstraint = constraint;
break;
}
}
}
return targetConstraint;
}
You should never rely on the timing of when a view is layed out. If that worked for you before, then out of pure luck. There are very little guarantees about this in UIKit. If you rely on something adopting to the size of your view, the right thing to do is override layoutSubviews in that view and adjust your stuff there.
Even after your view is fully rendered on screen, there are still so many conditions that could cause the size of the view to change. For example: Double height status bar, multitasking on iPad, device rotation, just to name a few. So it never is a good idea to do frame related layout changes at a particular point in time.
I was having the exact same problem. I had custom UITableViewCell subclasses and was using clipsToBounds = YES and self.iconView.layer.cornerRadius = self.iconView.frame.size.width/2 to give myself a circular image. Tried calling my cell configuration method from cellForRowAtIndexPath and willDisplayCell and neither worked.
Here is what works:
Move your layering code into the cell's -layoutSubviews method like this:
-(void)layoutSubviews {
[super layoutSubviews];
self.iconView.clipsToBounds = YES;
self.iconView.layer.cornerRadius = self.iconView.frame.size.width/2;
}
After this the images should load properly and your layering code should also work.
Only Update frame in your autolayout box .

NSView -alignmentRectForFrame: not respecting flipped coordinates?

HEADS-UP: I write way too much stuff. For the folks who don't want to read a bunch of whining, feel free to skip to the TL;DR at the bottom of the page. Thanks!
For the last few weeks, I've been subclassing all of the standard Aqua controls to make them look good on dark backgrounds, panels, and windows. I'm currently pretty far in, and for the most part, things are lookin' good. However, I keep running into the same problems over and over again. They're almost always math-related because I was big screwup in school. (I was that kid who would dismissively ask, "When will I ever use a stem-and-leaf plot in the real world? Ha!" In other words, I bought my TI-86 for Block Dude and Drug Wars. Anyway, I digress …
So the problem that keeps cropping up is my ineptitude when it comes to working with multiple coordinate systems. Specifically, working with Auto Layout in flipped coordinate systems. By default, NSView objects use Cartesian coordinates. However, most — if not all — of the NSControl classes flip their coordinates for drawing. Knowing that, this is what I've been doing:
- (void)drawWithFrame:(NSRect)cellFrame inView:(NSView *)controlView
{
// -isHUDPanel is just a method I added to NSWindow via category.
if ( controlView.window.isHUDPanel ) {
[self drawCustomWithFrame:cellFrame inView:controlView];
} else {
[super drawWithFrame:cellFrame inView:controlView];
}
}
If the control's window has the NSHUDWindowMask bit set, the -drawWithFrame:inView: method kicks it over to my custom method:
- (void)drawCustomWithFrame:(NSRect)cellFrame inView:(NSView *)controlView
{
NSRect drawingRect = [controlView alignmentRectForFrame:cellFrame];
// ...
// Where the magic happens
// ...
}
Auto Layout still baffles me, so I'm not even sure I'm using -alignmentRectForFrame: in the proper context, but it makes sense to me to use it to align stuff when drawing, so that's what I've been doing. Anyway, the problem I've been seeing sometimes when comparing my control side-by-side to a native control is that mine is 1–2 points "off" on one axis or another. Sometimes they align perfectly. Sometimes one axis is great and the other is whacky. It's a crapshoot. Then there are other times when the controls' frames will align just right, but the baselines of the controls' text will be a few points off. It's really maddening.
I finally decided to try some stuff, and it seems as though my alignment troubles may be stemming from the way NSEdgeInsets work. For example, let's say my control's bounds look like this:
NSRect: {{0, 0}, {150, 22}}; // (aka "cellFrame" in the drawing methods)
In my -drawWithFrame:inView: implementation, I first get the control's alignment rectangle by calling [controlView alignmentRectForFrame:cellFrame], which returns, let's say …
NSRect: {{2, 3}, {146, 19}}
At first glance, it seems as though the edges on the x-axis are inset by 2.0, and the y origin is 3.0. With that quick math out of the way, I now know everything I need to get started building NSBezierPath objects and going to work. But wait … Is that y origin in the control's coordinate system (flipped) or the default coordinate system (not flipped)? Surely it's in the control's, right? Well, I decided to test this out by manually setting the alignmentRect via the control's -alignmentRectInsets like so:
NSEdgeInsets insets = self.controlView.alignmentRectInsets;
NSRect bounds = self.controlView.bounds;
NSRect alignmentRect = bounds;
alignmentRect.origin.x += insets.left;
alignmentRect.origin.y += (self.controlView.isFlipped) ? insets.top : insets.bottom;
alignmentRect.size.width -= insets.left + insets.right;
alignmentRect.size.height -= insets.top + insets.bottom;
When manually creating the alignmentRect myself, my controls lined up with the native ones! I just don't get it though. Why aren't the controls' -alignmentRectForFrame: methods returning their alignmentRects whilst respecting flippedness of the view? I'm not even sure if they all do or not, but from the two controls I've tested this little trick on, the alignment has been on the money.
I've searched all over the web for some insight on this, but nobody seems to have the weird geometry problems I have. I always make these questions way too long, too, and I apologize. I just can't explain my problem when I don't exactly know my problem. :-(
TL;DR: How do I get the proper alignment rectangle for drawing in an NSControl subclass? Is there even a "standard" way? And finally, how does -cellSize, -cellSizeForBounds, -baselineOffsetFromBottom, and -intrinsicContentSize all fit together in the grand scheme of things? And final question: Auto Layout … WTF?

Apples ZoomingPDFViewer Example - Object creation

I'm currently working on an App which should display and allow users to zoom a PDF page.
Therefore I was looking on the Apple example ZoomingPDFViewer.
Basically I understand the sample code.
But a few lines are not obvious to me.
Link to the sample code:
http://developer.apple.com/library/ios/#samplecode/ZoomingPDFViewer/Introduction/Intro.html
in PDFView.m:
//Set the layer's class to be CATiledLayer.
+ (Class)layerClass {
return [CATiledLayer class];
}
What does the code above do?
And the second code snippet I don't understand in PDFView.m again:
self = [super initWithFrame:frame];
if (self) {
CATiledLayer *tiledLayer = (CATiledLayer *)[self layer];
...
I know it creates a CATiledLayer object. But how it will be created is not clear to me.
I hope someone could give me a short answer to my question because I don't want to use code which I don't understand.
Thank you!
The TiledPDFView.h class is a subclass of UIView, so you can see what documentation UIView has on that method. According to the docs I see, it looks like:
layerClass - Implement this method only if you want your view to use a different Core Animation layer for its backing store. For example, if you are using OpenGL ES to do your drawing, you would want to override this method and return the CAEAGLLayer class.
So it seems that it is asking the Core Animation system to use a tiled-layer. Further docs from CATiledLayer:
CATiledLayer is a subclass of CALayer providing a way to
asynchronously provide tiles of the layer's content, potentially
cached at multiple levels of detail.
As more data is required by the renderer, the layer's drawLayer:inContext: method is called on one or more background
threads to supply the drawing operations to fill in one tile of data.
The clip bounds and CTM of the drawing context can be used to
determine the bounds and resolution of the tile being requested.
Regions of the layer may be invalidated using the setNeedsDisplayInRect: method however the update will be asynchronous.
While the next display update will most likely not contain the updated
content, a future update will.

CABasicAnimation and custom types

I'm not very familiar with CoreAnimation, so I hope I've just missed something pretty simple. I want to animate a custom property (NSGradient) of a NSView in a simple manner, with [[view animator] setGradient:gradient];. I defined + (id)defaultAnimationForKey:(NSString *)key and returned a simple CABasicAnimation, however, no animation is executed. Since this works for simpler types and NSColor, I guess CABasicAnimation doesn't work with gradients. Fine, but in this particular case gradients are trivial (two stops, always), so I can easily write an interpolation functions. The question: how can I define a custom interpolation? I googled around regarding delegates on view, layer and animations, subclassing animation class etc., but I wasn't able to figure the things out. Thanks!
I thought I remembered passing by some Apple documentation when I was learning how to use Core Animation that showed how to set up animations that couldn't be handled by properticode describedes that are supplied with defined animations. Along the way I stumbled across some sample code from Apple that is described as:
A single gradient layer is displayed and continuously animated using new random colors.
That may be the answer to the specific task you already handled another way. I found it in the Documentation and API Reference within Xcode and the name of the sample code is simply Gradients. (Note that there is an original version 1.0 and an updated version 1.1 that was redone this year in April and so should be easier to use with current tools.
But, the larger question of creating a custom animation that can't be automated by Core Animation itself is to follow the example from Apple's Animation Programming Guide for Cocoa in the section Using an NSAnimation Object. It's described under the topic Subclassing NSAnimation and the recommended method is shown under the heading Smooth Animations. You override the setCurrentProgress: method so that each time it is called you first invoke Super so that NSAnimation updates the progress value, i.e., your custom animated property and then do any updating or drawing needed for the next frame of your animation. Here are the notes and example code provided by Apple in the referenced documentation:
As mentioned in “Setting and Handling Progress Marks,” you can attach a series of progress marks to an NSAnimation object and have the delegate implement the animation:didReachProgressMark: method to redraw an object at each progress mark. However, this is not the best way to animate an object. Unless you set a large number of progress marks (30 per second or more), the animation is probably going to appear jerky.
A better approach is to subclass NSAnimation and override the setCurrentProgress: method, as illustrated in Listing 4. The NSAnimation object invokes this method after each frame to change the progress value. By intercepting this message, you can perform any redrawing or updating you need for that frame. If you do override this method, be sure to invoke the implementation of super so that it can update the current progress.
Listing 4 Overriding the setCurrentProgress: method
- (void)setCurrentProgress:(NSAnimationProgress)progress
{
// Call super to update the progress value.
[super setCurrentProgress:progress];
// Update the window position.
NSRect theWinFrame = [[NSApp mainWindow] frame];
NSRect theScreenFrame = [[NSScreen mainScreen] visibleFrame];
theWinFrame.origin.x = progress *
(theScreenFrame.size.width - theWinFrame.size.width);
[[NSApp mainWindow] setFrame:theWinFrame display:YES animate:YES];
}
So basically you define a "progress value" (possibly composed of several values) that defines the state of your custom animation and write code that given the current "progress value" draws or changes what is drawn when the animation is at that particular state. Then you let NSAnimation run the animation using the normal methods of setting up an animation and it will execute your code to draw each frame of the animation at the appropriate time.
I hope that answers what you wanted to know. I doubt I could have found this easily by searching without having seen it before since I finally had to go to where I thought it might be and skim page by page through the entire topic to find it again!

Subview of NSView or CALayer?

Today I decided to try to use CALayers to show a rectangular box overlaying a NSView. The layer will contain some text and will be turned on and off depending on when it is necessary to show the variable text. The reason I wanted to use CALayer for this was the nifty rendering and animation you can easily do with CALayer. I implemented my layer and it worked like a charm. However, after using my GUI and clicking several times on various buttons turning the layer on and off, it seemed that the hierarchy of what I thought was my layer view was skewed. I think focus must have been switched to some other NSView which again was turned off. I basically got very confused as to which layer I was handling at a given time and I lost control of the view hierarchy.
My question is: should I use subviews of NSView, or CALayers to show something that may occur many times on and off in an application? It seems to me that it is easy to loose control of which layer you are working on. Is there a way to identify by name the current layer so you can reuse the layer, or is it best to work with layers, delete them and then re-create the layer the next time you need them?
Thanks for your time. Cheers, Trond
FWIW,
I have often turned CALayers "on and off" very quickly, with no problems at all. So you can do that if you want to!
Deleting them and recreating them quickly on the fly, does not seem to cause any problems. (It's not a big resource user, and I've never seen any other problems doing that.) So definitely do that if you want to!
I actually don't understand what you mean about "naming" layers - of course, as iVars they have a name! You have to name all your CALayers.
Here's a typical bit of production code (from a .h file):
CALayer *rearLayer;
CALayer *hugeBasket; // holds everything for ez-on/off
CALayer *theActualSkyline; // nb, same name as similar UIView
CALayer *someTrees; // minor stuff
CALayer *someBushes; // overs
// for the stupid help basket..
CALayer *LLDRear;
CALayer *LLDArrowLeft;
CALayer *LLDArrowRight;
CALayer *LLDPointlessUpArrow;
CALayer *LLDYetAnotherStupidShadow;
// etc etc..
And so on and on. I don't really see how you can "not" name them, you know!
Finally,
4, Don't forget layers are much "better" than NSViews, because: NSViews have shoddy/buggy relationship between overlapping siblings: they essentially don't work. Read about that here:
port an iOS (iPhone) app to mac?
Hope it helps!
PS - these may also help with CALayers...
Exactly what should happen in a CALayer's display/drawRect methods?
What's the difference and compatibility of CGLayer and CALayer?