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 .
Related
With iOS 8 and Xcode 6, in storyboards we now have the screen size grid letting us select a size class. Where you can select layout formatting for the different screen sizes.
I have found this brilliantly helpful, as it allows me to set the base constraints and then unique ones for each screen size.
My question is, can you do this programmatically? I create my NSLayoutConstraint as normal but I need to be able to specify different constraints for different screen sizes.
iOS 8 introduces the active property on NSLayoutConstraint. It allows you to activate or deactivate a constraint. There are also methods to activate/deactivate multiple constraints.
+ (void)activateConstraints:(NSArray *)constraints
+ (void)deactivateConstraints:(NSArray *)constraints
Keep your constraints in arrays when creating them programmatically.
Create an array for each of the layouts you need.
Activate/Deactivate whatever set of constraints you need from within willTransitionToTraitCollection
To answer your question, you can set the size class programmatically, however, it's a bit of a pain. You must call "setOverrideTraitCollection" but from the parent view controller, not the one you actually wished to make a trait change to.
In my situation, I wanted to change the Master VC of a split view controller on iPad to look differently than the one on the iPhone, however, they are both set to Compact width / Regular height by default. So I subclassed the Master's nav controller and added code to set the Master's traits to Regular width when it's not an iPhone.
Swift code:
class MasterNavigationController: UINavigationController {
override func viewDidLoad() {
super.viewDidLoad()
if (self.traitCollection.userInterfaceIdiom != .Phone) {
let newTraitCollection = UITraitCollection(horizontalSizeClass: .Regular)
self.setOverrideTraitCollection(newTraitCollection, forChildViewController: self.topViewController)
}
}
}
I hope this helps someone looking for a similar solution.
It's a bit confusing & hard to find in the documentation because a "size class" isn't actually a "Class" like NSObject. They're really defined in an enum/typedef called: UIUserInterfaceSizeClass
The way to get the horizontal & vertical size class for a view is with a UITraitCollection
Class/Type methods for UITraitCollection allow you to create one based on a particular display scale (e.g. retina or not), from an array of other trait collections, with a UI idiom (iPad/iPhone), or specific horizontal & vertical options (compact, regular), but to be honest I'm not sure yet how you'd use this...
This question discusses updating constraints when the traitCollection changes, using willTransitionToTraitCollection(newCollection: UITraitCollection!,
withTransitionCoordinator coordinator: UIViewControllerTransitionCoordinator!)
You're right that both the UITraitCollection and its properties are readonly, but clearly you can create a new collection for some reason, and handle layout changes when the traitCollection changes.
This previous question is pretty similar & links to an Apple article about using Adaptive Layout. Also check the WWDC video "Building Adaptive Apps with UIKit."
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 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
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.