Xcode 8 sets frame size differently. How do I find more details? - uikit

In several places in my application I get zero frame sizes where I used to get a frame. This impacted me especially when creating round objects by doing a cornerRadius of size/2.
For example, in Xcode 7 this worked fine:
class AvatarUIButton: UIButton {
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
layer.masksToBounds = true
layer.cornerRadius = bounds.size.width / 2
}
}
But now in Xcode 8 I have to do this:
class AvatarUIButton: UIButton {
override var bounds: CGRect { didSet {
layer.cornerRadius = bounds.size.width / 2
}}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
self.layer.masksToBounds = true
}
}
In this example, the change is arguably better/more obvious. I have another situation that is much less isolated involving a 0 frame on TableHeaderView that is only for xcode 8.
I'm looking for release notes, mailing list discussion, or similar that discusses a change in ordering of frame size determination so I can figure out what changed and how I can fix it.

In Xcode 8, Apple say :
Xcode 8 removes the ability to configure views without constraints
with frame varying per size class. Views without constraints may look
different after compiling with Xcode 8 when switching size classes at
runtime. Going forward, use constraints and autoresizing masks with
the device bar to achieve the desired layout. (25894544)
In previous versions of Xcode, we used to define the design on the ViewDidLoad. But here, the different elements were not created (and constraints too), so the view was created with the size in the Storyboard.
Now, in Xcode 8, we cannot do it anymore. We need to have constraints because the frame is not initialize (that's why some of you have these values for the frame size : 0, 0, 1000, 1000). For example, make it in the ViewDidAppear and it will work fine. But, you will see at first your button without corner radius few times.
So, what you can do is to use this function (here in Objective C) (where you want, even in the loading function) :
[self performSelector:#selector(setCornerToMyButton) withObject:NULL afterDelay:0];
You can pu the delay you want, but even with 0 it works.
And change your radius in this function like this for example :
- (void) setCornerToMyButton {
//Do what you want...
layer.masksToBounds = true
layer.cornerRadius = bounds.size.width / 2
}
Hope this will help you.

in your case just call :
override func draw(_ rect: CGRect) {
self.layoutIfNeeded()
layer.masksToBounds = true
layer.cornerRadius = bounds.size.width / 2
}
for a UIViewController just call view.layoutIfNeed() right after super.viewDidLoad(),it ll work like a charm;
i dont know what s the cause of this problem (if it s a problem) either way it seems that loading the view doesnt "layout" the view anymore neither using autoresize or autolayout for that matter , so like i said for the time being you can use this solution till we get a better understanding about what changed exactly
also for custom classes (custom button for exemple) you can override layoutSubviews() or layoutIfNeeded() , there you ll find the frame/layout you are looking for and apply whatever changes you want

I got the answer just now
[super viewDidLoad];
[self.myNeedFrameView layoutIfNeeded];

Related

iOS 13 - UIPopoverPresentationController sourceview content visible in the arrow

When I am displaying some view in UIPopoverPresentationController and presenting it as popover
popoverCon?.modalPresentationStyle = UIModalPresentationStyle.popover
the content have moved upward toward and a some part is being display in the arrow.
Further I had border around the popover
popoverCon?.view.layer.borderColor = .orange
popoverCon?.view.layer.borderWidth = 1.0;
popoverCon?.view.layer.cornerRadius = 10.0;
popoverCon?.view.layer.masksToBounds = false;
it is not showing toward the part where arrow is but it displays a little of the border line in the tip of the arrow.
This was working fine until iOS 12 but in iOS 13 this issue is coming.
Any suggestions on how I can solve this?
The top of my tableView content was cut off by the arrow. This is how I fixed it in my case (code inserted in my tableViewController Swift file):
override func viewSafeAreaInsetsDidChange() {
if #available(iOS 11.0, *) {
super.viewSafeAreaInsetsDidChange()
self.tableView.contentInset = UIEdgeInsets(top: self.tableView.safeAreaInsets.top, left: 0, bottom: 0, right: 0)
}
}
My solution in Obj-C, for those who need an obj-c solution.
I had previously only popovercontroller, that was creating the error like shown in the question. I renamed it to childController for clarity and created a containing popoverController to make the solution given by #SaintMSent work in my situation of only one view originally. Also used https://stackoverflow.com/a/47076040/2148757 solution and https://useyourloaf.com/blog/self-sizing-child-views/ to resize appropriately since all of my childControllers set the preferred content size frequently.
//Create container popover controller and add child to it
UIViewController* popoverController = [[MyParentPopoverController alloc] init];
[popoverController.view addSubview:childController.view];
[popoverController addChildViewController:childController];
[popoverController setPreferredContentSize:childController.preferredContentSize];
//set popover settings on container
popoverController.modalPresentationStyle = UIModalPresentationPopover;
popoverController.popoverPresentationController.sourceRect = sourceRect;
popoverController.popoverPresentationController.sourceView = buttonView;
popoverController.popoverPresentationController.permittedArrowDirections = direction;
//Fix ios13 'bug' that Apple claims is a feature
UILayoutGuide* guide = popoverController.view.safeAreaLayoutGuide;
childController.view.translatesAutoresizingMaskIntoConstraints = NO;
[childController.view.leadingAnchor constraintEqualToAnchor:guide.leadingAnchor].active = YES;
[childController.view.trailingAnchor constraintEqualToAnchor:guide.trailingAnchor].active = YES;
[childController.view.topAnchor constraintEqualToAnchor:guide.topAnchor].active = YES;
[childController.view.bottomAnchor constraintEqualToAnchor:guide.bottomAnchor].active = YES;
[popoverController.view layoutIfNeeded];
//Show the popover
...
#interface MyParentPopoverController : UIViewController
#end
#implementation MyParentPopoverController
-(void)preferredContentSizeDidChangeForChildContentContainer:(id <UIContentContainer>)container {
[super preferredContentSizeDidChangeForChildContentContainer:container];
[self setPreferredContentSize:container.preferredContentSize];
}
#end
Note: I didn't check for ios11 compatibility because my user base is restricted to not use it.
It is definitely a feature, they want you to use safe area since iOS 11, actually, but it seems now they want to force you to use it
Had the same problem as you, this worked for me
https://useyourloaf.com/blog/safe-area-layout-guide/
Definitely a bug. When you have a situation where you use UIPopoverArrowDirectionAny you will see that the problem only exists when the arrow is at the top or left of the popover and not when the arrow appears at the right or the bottom of the popover. If you make adjustments in your code to compensate it will work if you use UIPopoverArrowDirectionUp or UIPopoverArrowDirectionLeft but will not display correctly using that adjustment when using UIPopoverArrowDirectionAny and the popup appears above or to the right of the target rectangle.
I don't have an 'answer' yet, but I have identified what's going on and why it's so hard to fix.
ios13 UIPopoverViewController showing UITableViewController - Safe Area problems / Missing parts of table
Basically, any UITableView that has headers or footers is going to be broken in iOS 13 unless there's some way to alter the _UITableViewHeaderFooterViewBackground
That is notoriously problematic and doesn't play nicely with Auto-Layout - it's been known about for years, but Apple have never fixed it or made it easier to deal with and more publicly known.
https://www.google.com/url?sa=t&rct=j&q=&esrc=s&source=video&cd=1&cad=rja&uact=8&ved=0ahUKEwibouuozfvkAhVCXRUIHVGsBegQtwIIKjAA&url=https%3A%2F%2Fwww.youtube.com%2Fwatch%3Fv%3DajsCY8SjJ1Y&usg=AOvVaw3_U_jy9EWH2dJrM8p-XhDQ
https://spin.atomicobject.com/2016/10/12/auto-layout-uitableview/
I'm unable to push my app to the App Store until I get this sorted.. I hope someone can identify how to manipulate this view so that it stops pushing the boundaries of the table out of whack with AutoLayout which causes this safe area intrusion.
Searching on the internet I got help from following link
Twitter
so I had to add safe area and manage my views accordingly
CGFloat topPadding = 0.0;
if (#available(iOS 11.0, *)) {
topPadding = self.view.safeAreaLayoutGuide.layoutFrame.origin.y;
}
Swift:
var topPadding: CGFloat = 0.0
if #available(iOS 11.0, *) {
topPadding = self.view.safeAreaLayoutGuide.layoutFrame.origin.y
}
but I haven't got solution to the border problem of mine yet.
Edit:
Temporarily I did solved the border problem by creating an invisible view on popover and giving it same frame as safe area and drawing its border.
You should use constraints. And also pay attention to topAnchor. It must be safeAreaLayoutGuide.topAnchor. In my case, it works correctly. For example:
[NSLayoutConstraint activateConstraints:#[
[toolbar.leftAnchor constraintEqualToAnchor:self.view.leftAnchor],
[toolbar.rightAnchor constraintEqualToAnchor:self.view.rightAnchor],
[toolbar.topAnchor constraintEqualToAnchor:self.view.safeAreaLayoutGuide.topAnchor],
[toolbar.heightAnchor constraintEqualToConstant:50]
]];
Embed the contents of the popover in another view with "safe area relative margins" on. This should have -21,-21 as the origin. Turn off vertical and horizontal auto resizing. Seems to work, although you lose auto-stretching.
Setup your popover's content UIViewController like such:
NSLayoutConstraint.activate([
myContentView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
myContentView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor),
myContentView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor),
myContentView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor)
])

Handling AutoLayout constraint animation differences in iOS 10?

I've noticed that in iOS 10 Beta 5 (about to try Beta 6), AutoLayout constraint animation behaves a bit differently.
For example, this approach does not work the same as it did in previous iOS releases:
[view addConstraints:#[constraints...]];
[view setNeedsUpdateConstraints];
[view layoutIfNeeded];
[UIView animateWithDuration:...
{
/* adjust constraint here... */
[view layoutIfNeeded]; // Only the adjusted constraints since previous layoutIfNeeded() call should animate change with duration.
} completion:{ ... }];
... In my testing, the constraints initially added with addConstraints() will also animate in iOS 10 with the UIView animateWithDuration() block... which is causing some funky/undesirable behavior so far.
For example, setting the left/right constraints in the array (but the vertical constraints in the block) causes the entire view to animate onto the screen diagonally with this approach... which is totally incorrect.
Does anyone know how to do this correctly for both iOS 9 (and below), as well as 10?
Try calling layoutIfNeeded on the view's superview, instead of the view itself.
--
I had a similar problem, my view was supposed to animate from the top at a 0 height, downward to a > 0 height (i.e. (0, 0, 320, 0) to (0, 0, 320, 44)).
Using Xcode 8 with iOS 10, the animation behavior was much different. Instead of animating downward from the top, the view animates up & down from the vertical center of the destination frame.
I fixed this by, instead of calling layoutIfNeeded on the view itself, calling it on the view's superview. Somehow, the behavior was restored.
Hope it helps!
Helpful comment by #Ramiro:
According to the documentation, layoutIfNeeded lays out the subviews (but doesn't contemplate the current view itself). So, in order to update the current view lay out, you need to call layoutIfNeeded from the super view.
In below method :
[super viewDidLoad];
Write this line:
[self.view layoutIfNeeded];
I made a small test changing H and V of a small red view:
self.red_POS_H = NSLayoutConstraint.constraints(withVisualFormat: "H:|-90-[redView]", options: defaultOptions, metrics: nil, views: viewsDictionary)
self.red_POS_V = NSLayoutConstraint.constraints(withVisualFormat: "V:|-30-[redView]", options: defaultOptions, metrics: nil, views: viewsDictionary)
self.view.addConstraints(self.red_POS_V!)
self.view.addConstraints(self.red_POS_H!)
and animated it:
// we hope is only one:
let currV = self.red_POS_V![0]
let currH = self.red_POS_H![0]
UIView.animate(withDuration: 3) {
// Make all constraint changes here
currV.constant = 100
currH.constant = 300
self.view.layoutIfNeeded() // Forces the layout of the subtree animation block and then captures all of the frame changes
}
red view moved correctly, if You comment out single line in animation, it does work, horizontally or vertically.
Small project avalable, if You need it.

Why is my split view behaving like this and how can I fix it?

I created a split view controller that displays two views, like this :
When I compile it, it gives me this result :
Unfortunately, the first view isn't visible and I must drag from the left hand side of the window to see the two views :
First, why is the split view behaving like this ? Why isn't it already at the right size from the beginning ?
If I add this line to the viewDidLoad() function of my SplitViewController :
splitView.adjustSubviews()
Then the two view appears, with equal size, but I don't understand what the adjustSubviews() function does exactly, and I can't control the position of either.
How to fix it programmatically ? How to adjust the size of each view ? Is there a way to adjust it in interface builder ?
Thank you.
EDIT : There is now a bounty of 50 points for this question
Since NSSplitViewController is auto-layout based, you can use auto-layout either if you want a fixed or a dynamic behavior on your views.
For a fixed width what you can do is add a custom view inside your 2 views with a 0 constraint to all the edges and a fixed width. For example:
Then the window will get expanded as follows:
You should be able to set the position of the divider in the split view using (assuming 2 views in the split view):
self.splitView.setPosition(200.0, ofDividerAtIndex: 0)
That would set the first divider at position 200.0
We need to set sizes of NSSplitView's sub-views directly.
Setting ones through splitViewItems seems to have no effect.
// NSSplitViewController
- (void)viewDidLoad {
[super viewDidLoad];
NSView *view1 = self.splitView.subviews[0];
NSRect rect = view1.frame;
rect.size.width = 150;
view1.frame = rect;
NSView *view2 = self.splitView.subviews[1];
rect = view2.frame;
rect.size.width = 300;
view2.frame = rect;
}

Automatically wrap NSTextField using Auto Layout

How does one go about having auto-layout automatically wrap an NSTextField to multiple lines as the width of the NSTextField changes?
I have numerous NSTextFields displaying static text (i.e.: labels) in an inspector pane. As the inspector pane is resized by the user, I would like the right hand side labels to reflow to multiple lines if need be.
(The Finder's Get Info panel does this.)
But I haven't been able to figure out the proper combination of auto layout constraints to allow this behavior. In all case, the NSTextFields on the right refuse to wrap. (Unless I explicitly add a height constraint that would allow it to.)
The view hierarchy is such that each gray band is a view containing two NSTextFields, the property name on the left and the property value on the right. As the user resizes the inspector pane, I would like the property value label to auto-resize it's height as need-be.
Current situation:
What I would like to have happen:
(Note that this behavior is different than most Stack Overflow questions I came across regarding NSTextFields and auto layout. Those questions wanted the text field to grow while the user is typing. In this situation, the text is static and the NSTextField is configured to look like a label.)
Update 1.0
Taking #hamstergene's suggestion, I subclassed NSTextField and made a little sample application. For the most part, it now works, but there's now a small layout issue that I suspect is a result of the NSTextField's frame not being entirely in sync with what auto-layout expects it to be. In the screenshot below, the right-hand side labels are all vertically spaced with a top constraint. As the window is resized, the Where field is getting properly resized and wrapped. However, the Kind text field does not get pushed down until I resize the window "one more pixel".
Example: If I resize the window to just the right width that the Where textfield does it's first wrap, then I get the results in the middle image. If I resize the window one more pixel, then the Kind field's vertical location is properly set.
I suspect that's because auto-layout is doing it's pass and then the frames are getting explicitly set. I imagine auto-layout doesn't see that on that pass but does it it on the next pass, and updates the positions accordingly.
Assuming that's the issue, how do I inform auto-layout of these changes I'm doing in setFrameSize so that it can run the layout again. (And, most importantly, not get caught in recursive state of layout-setFrameSize-layout-etc...)
Solution
I've come up with a solution that appears to work exactly how I was hoping. Instead of subclassing NSTextField, I just override layout in the superview of the NSTextField in question. Within layout, I set the preferredMaxLayoutWidth on the text field and then trigger a layout pass. That appears to be enough to get it mostly working, but it leaves the annoying issue of the layout being briefly "wrong". (See note above).
The solution to that appears to be to call setNeedsDisplay and then everything Just Works.
- (void)layout {
NSTextField *textField = ...;
NSRect oldTextFieldFrame = textField.frame;
[textField setPreferredMaxLayoutWidth:NSWidth(self.bounds) - NSMinX(textField.frame) - 12.0];
[super layout];
NSRect newTextFieldFrame = textField.frame;
if (oldTextFieldFrame.size.height != newTextFieldFrame.size.height) {
[self setNeedsDisplay:YES];
}
}
The simplest way to get this working, assuming you're using an NSViewController-based solution is this:
- (void)viewDidLayout {
[super viewDidLayout];
self.aTextField.preferredMaxLayoutWidth = self.aTextField.frame.size.width;
[self.view layoutSubtreeIfNeeded];
}
This simply lets the constraint system solve for the width (height will be unsolvable on this run so will be what ever you initially set it to), then you apply that width as the max layout width and do another constraint based layout pass.
No subclassing, no mucking with a view's layout methods, no notifications. If you aren't using NSViewController you can tweak this solution so that it works in most cases (subclassing textfield, in a custom view, etc.).
Most of this came from the swell http://www.objc.io/issue-3/advanced-auto-layout-toolbox.html (look at the Intrinsic Content Size of Multi-Line Text section).
If inspector pane width will never change, just check "First Runtime Layout Width" in IB (note it's 10.8+ feature).
But allowing inspector to have variable width at the same time is not possible to achieve with constraints alone. There is a weak point somewhere in AutoLayout regarding this.
I was able to achieve reliable behaviour by subclassing the text field like this:
- (NSSize) intrinsicContentSize;
{
const CGFloat magic = -4;
NSSize rv;
if ([[self cell] wraps] && self.frame.size.height > 1)
rv = [[self cell] cellSizeForBounds:NSMakeRect(0, 0, self.bounds.size.width + magic, 20000)];
else
rv = [super intrinsicContentSize];
return rv;
}
- (void) layout;
{
[super layout];
[self invalidateWordWrappedContentSizeIfNeeded];
}
- (void) setFrameSize:(NSSize)newSize;
{
[super setFrameSize:newSize];
[self invalidateWordWrappedContentSizeIfNeeded];
}
- (void) invalidateWordWrappedContentSizeIfNeeded;
{
NSSize a = m_previousIntrinsicContentSize;
NSSize b = self.intrinsicContentSize;
if (!NSEqualSizes(a, b))
{
[self invalidateIntrinsicContentSize];
}
m_previousIntrinsicContentSize = b;
}
In either case, the constraints must be set the obvious way (you have probably already tried it): high vertical hugging priority, low horizontal, pin all four edges to superview and/or sibling views.
Set in the size inspector tab in section Text Field Preferred Width to "First Runtime layout Width"
This works for me and is a bit more elegant. Additionally i've made a little sample project on Github
public class DynamicTextField: NSTextField {
public override var intrinsicContentSize: NSSize {
if cell!.wraps {
let fictionalBounds = NSRect(x: bounds.minX, y: bounds.minY, width: bounds.width, height: CGFloat.greatestFiniteMagnitude)
return cell!.cellSize(forBounds: fictionalBounds)
} else {
return super.intrinsicContentSize
}
}
public override func textDidChange(_ notification: Notification) {
super.textDidChange(notification)
if cell!.wraps {
validatingEditing()
invalidateIntrinsicContentSize()
}
}
}

Correct Way To Move UIButton Between Two Points?

Trying to make a small tapping game, where you have a grid of squares and need to tap them in different orders. Occasionally these squares will swap places with each other.
I figure that I will need to put the locations of each square into an array. Then when it is time to move, go from the current location to a one selected at random from the array, and then deleting that location in the array so I don't get multiple buttons in the same place.
Unfortunately I can't even get the UIButtons to move, let alone interpolate between two positions.
Currently this will move a button:
-(IBAction)button:(id)sender
{
button.center = CGPointMake(200, 200);
}
But I don't want it to work like that, I want it to work something like this and it doesn't work:
if (allButtonsPressed == YES)
{
button.center = CGPointMake(200, 200);
}
It won't even move if placed like this:
-(void)viewDidLoad
{
[super viewDidLoad];
button.center = CGPointMake(200, 200);
}
For clarity, the button is added through Interface Builder, and all these situations work when doing other things, so I'm guessing UIButtons need to moved/animated in specific ways?
viewDidLoad occurs before the view is visible so it will move just not animated. You could try putting it in the viewDidAppear method.
You can wrap things in UIView animation blocks like so if you want them to animate instead of blink into position.
[UIView animateWithDuration:0.5 animations:^{
button.center = CGPointMake(200.0, 200.0);
}];
Since you're using interface builder I'm assuming your using properties to keep track of the buttons. This can work if you're only using a few set buttons but I'd expect in a game those will change so you may want to move to code or atleast use IBOutletCollections to keep track of the buttons.
Swift 3.0
UIView.animate(withDuration: 0.5) { [weak self] in
self?.button.center = CGPoint(x: 200, y: 200)
}

Resources