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?
Related
I am fairly new to creating Xcode projects using Objective-C and i'm trying to make a simple 2d graphics program to show me how well my graphing code works before I implement it elsewhere.
I have gotten the drawing of everything right but the problem comes when I want to clear the strokes and stroke new lines. From what I see from when I run the program the view will only display what I have executed once it hits that #end at the end of the implementation. The issue with this is that by the time it hits the #end the code has already been run. and I can't figure out if I need to recall the class in a loop to update the view each time (or where or how to do this, perhaps something in main.m?) or if I need to call a function to update the view before it exits the implementation because right now all the lines are just overwriting each other before the user can see anything.
Here is the interface in my header file (connected to a UIView):
#interface GraphView : NSView
- (void)drawRect:(NSRect)dirtyRect;
#end
Implementation file:
Here is how I am creating my rectangle:
- (void)drawRect:(NSRect)dirtyRect {
[self clearGrid:dirtyRect];
[self drawLines];
}
- (void)clearGrid:(NSRect)theRect {
//Wiping the slate clean
[super drawRect:theRect];
[self.window setBackgroundColor:[NSColor whiteColor]];
...
Here is what I am using to draw my lines:
NSBezierPath* eqLine = [NSBezierPath bezierPath];
[[NSColor greenColor] setStroke];
[eqLine setLineWidth:5.0];
[eqLine moveToPoint:NSMakePoint([self convertToPixels:previousX], [self convertToPixels:previousY])];
[eqLine lineToPoint:NSMakePoint([self convertToPixels:finalX], [self convertToPixels:finalY])];
[eqLine stroke];
I have been searching for the past few days now on how I could solve this but so far it hasn't turned up anything, perhaps i'm just not searching for the right thing. Any information is helpful even if it's just a point to a resource that I can look at. Let me know if any additional information is needed. Thanks!
It's clear that you're a noobie to this, which is not a crime. So there's a lot to address here. Let's take things one at a time.
To answer your basic question, the -drawRect: method is called whenever a view needs to draw its graphic representation, whatever that is. As long as nothing changes that would alter its graphical representation, the -drawRect: method will only be received once.
To inform Cocoa (or CocoaTouch) that the graphic representation has changed, you invalidate all, or a portion of, your view. This can be accomplished in many ways, but the simplest is by setting the needsDisplay property to YES. Once you do that, Cocoa will (at some point in the future) call the -drawRect: method again to modify the graphic representation of your view. Wash, rinse, repeat.
So here's the basic flow:
You create your class and add it to a view. Cocoa draws your view the first time when it appears by sending your class a -drawRect: message.
If you want your view to change, you invalidate a portion of the view (i.e. view.needsDisplay = YES). Then you just sit back and wait. Very soon, Cocoa will send -drawRect: again, your class draws the new and improved image, and it appears on the screen.
Many changes will cause your view to be invalidated, and subsequently redrawn, automatically—say, if it's resized. But unless you make a change that Cocoa knows will require your view to redraw itself, you'll have to invalidate the view yourself.
Now for the nit-picking, which I hope you understand is all an effort to help you understand what's going on and improve your code...
Your subject line says UIView but your code example subclasses NSView so I'm actually not sure if you're writing a macOS or an iOS app. It doesn't matter too much, because at this level the differences are minimal.
Your -drawRect: calls [self clearGrid:..., when then calls [super drawRect:... Don't do this. As a rule, never use super except from within the overloaded method of the same name. In other words, -drawRect: can use [super drawRect:, but no other methods should. It's not "illegal", but it will save you grief in the long run.
Your -clearGrid: method sets the backgroundColor of the window. Don't do this. The window's background color is a property, and your -drawRect: method should only be drawing the graphical representation of your view—nothing more, nothing less.
You're calling [super drawRect: from within a direct subclass of NSView (or UIView). While that's OK, it's unnecessary. Both of these base classes clearly document that their -drawRect: method does nothing, so there's nothing to be gained by calling it. Again, it won't cause any harm, it's just pointless. When your -drawRect: method begins execution, a graphics context has already been set up, cleared, and is ready to draw into. Just start drawing.
#end is not a statement. It does not get "executed". It's just a word that tell the compiler that the source code for your class has come to an end. The stuff that gets executed are the methods, like -drawRect:.
In your #interface section you declared a -drawRect: method. This is superfluous, because the -drawRect: method is already declared in the superclass.
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 .
I'm using CGRect's for hitboxes, and my collisions seem to be a bit off. I want to quickly see where my hitboxes actually are.
I tried a bunch of different approaches but most of them seem to be outdated, or just didn't work for me.
I tried this already and a bunch of similar approaches.
What is the simplest way to show the borders of a CGRect?
With cocos2d 2.0, in ccConfig.h there is a CC_SPRITE_DEBUG_DRAW symbol. If you set that to 1, the box will be drawn during the visit cycle.
If CC_SPRITE_DEBUG_DRAW, as YvesLeBorg suggested, doesn't suit you, you can override draw method in you layer or nodes and draw in that method using helper functions from CCDrawingPrimitives.h. Don't forget to call [super draw].
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!
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?