I have recently started using UICollectionView, and am a bit confused about the UICollectionViewFlowLayout. It would seem that the frames for each cell in the collection view are calculated with equal space between each item. This causes the frames of some of the cells to have fractional positions, which will cause blurry labels and misaligned image pixels and so on.
I am surprised to find that there are no questions about this on stack overflow though, which makes me think I am doing something wrong. I have created a test project that demonstrates the problem quite simply:
https://github.com/rmaz/BlurryCollectionView
Is this really the standard behaviour? It seems to me that this makes the flow layout basically unusable without subclassing. Or am I missing something?
Workaround: subclass UICollectionViewFlowLayout, override UICollectionViewLayout's -layoutAttributesForElementsInRect: and for every layout attributes make the frame integral:
- (NSArray *)layoutAttributesForElementsInRect:(CGRect)rect
{
NSArray *allLayoutAttributes = [super layoutAttributesForElementsInRect:rect];
for (UICollectionViewLayoutAttributes *layoutAttributes in allLayoutAttributes) {
layoutAttributes.frame = CGRectIntegral(layoutAttributes.frame);
}
return allLayoutAttributes;
}
Note: iOS 7 UICollectionViewFlowLayout has been fixed to always use integral frames for its cells' frames. I recommend keeping the fix for iOS 6.x but conditionally deprecate it for iOS 7 and newer.
Best,
Raphael
Related
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 .
In ios 7 and before, I was updating the bounds of presentedViewController.view.superview to custom the size of presented view controller, but it seems this would not be the case in ios 8 any more. Since there is no superview can be set on the view controller(return nil when you try to call it in debugger).
Any suggestions how to update the presented view controller's size? This would be used for the custom presentation transition.
I guess the following is easier and it works in iOS 8:
self.myViewController.modalPresentationStyle = UIModalPresentationFormSheet;
self.myViewController.modalTransitionStyle = UIModalTransitionStyleCrossDissolve;
//This will be the size you want
self.myViewController.preferredContentSize = CGSizeMake(822, 549);
[self presentViewController:self.myViewController animated:YES completion:nil];
In case anyone runs into this later, here is how I solve it.
Subclass the UIPresentationController and return the frame in frameOfPresentedViewInContainerView. Feed this into the transitioningDelegate that you create for the presentedViewController.
Or, you may set the final frame for the presentedView in the animateTransition:, which belongs to the animator object you created for transitioningDelegate. However, this is the old iOS 7 way of doing it. Since Apple introduce UIPresentationController, any size/frame changes should be done there instead, which is the previous method I mentioned.
Here are some extra information that may not be directly related to solving the problem.
For those of you who never got your hands on the apple view controller transition api, just like me before, here are the steps.
Create YourTransitioningDelegate, which conforms UIViewControllerTransitioningDelegate. In here, generally three things need to be set, PresentationController, PresentedAnimationController, DismissedAnimationController.
Create YourTransitionAnimator, which conforms UIViewControllerAnimatedTransitioning. Here, two functions need to be override, transitionDuration and animateTransition(This is where all the animation happens, adding/removing and animating the presentedView. Make you call completeTransition on transitionContext to end the animation).
Subclass UIPresentationController. Depends on each individual needs, you may do a ton of things here. I just added a dimmingView and changed the frame of presentedViewController.
Finally, hook things up before presenting the view controller, which is changing the modalPresentationStyle to be custom and setting the transitioning delegate.
Things I found really helpful, two 2014 WWDC videos("View controllers advancements" and "A look inside presentation controllers") and the sample project from Apple(LookInside-photoEditingApp).
Instead of subclassing you can use the preferredContentSize property
- (void)viewDidLoad {
[super viewDidLoad];
self.preferredContentSize = CGSizeMake((self.view.frame.size.width / 100) * 65, (self.view.frame.size.height / 100) * 65);
}
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?
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.
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!