Auto Layout manipulating object created in IB through code - objective-c

I asked a more general question in a previous thread about converting to Auto Layout, but as I'm doing the conversion I've run into something very specific.
I have a base class VC that handles getting text fields and views out of the way when the keyboard pops up. It also does the same with date pickers and picker views. So if the VC has a date picker, then in IB a date picker is created, and it's Y value in the frame is set to be just off the bounds of the main view. There is an IBOutlet that it is connected to in the base class, and then in the code [self showDatePicker] is called, and the base class does everything else. Works great.
Enter Auto Layout. Even though constraints are generated at runtime for the date picker, since I don't create any, it still seems to work by manipulating the frame, i.e. the picker still animates up and down the screen when it is called or dismissed.
In trying to convert that to Auto Layout, I added a new constraint in code which binds the top of the view to the Top Layout Guide. This works fine, except for the error messages in the debugger that a constraint has to be broken, since one already exists, (created at runtime) for this.
I'd like to avoid having to manually create the constraints on the picker, and then bind one to a property, but it seems the only way around that is to go through the list of constraints on the main view, find the specific one for the top binding of the picker, and then manipulate that. But I don't see a convenient way to find specific constraint, especially one generated at runtime.
So, any thoughts on this? Should I just add the constraints on the picker and attach the top one to a property? Leaving it as it is doesn't seem like a good option, as this is the first time I've seen adjusting the frame work with constraints...

Related

ArrayController's CoreData selection binding not refreshed across multiple NIB files

I've a hard time getting my Cocoa application to work as expected. It consists of a toolbar in the main.nib and a custom view in a details.nib file. Now I want the user to select an entry in a NSPopupButton in the toolbar and the content of the custom view should be changed accordingly.
To achive this I've added an ArrayController to my main.nib, showing the following configuration:
Furthermore the Managed Object Context is bound to the Model Key Path delegate.managedObjectContext (it is no document based application).
With this configuration the NSPopupButton works just fine and if I add a Label to the toolbar (also in the main.nib) and bind it's value to the selection (Controller Key), name (Key Value Path) the content is refreshed whenever I change the selection.
The Bindings of the NSPopupButton look like shown in the following screenshot:
So in my details.nib I tried the following to achieve the same effect. I've added an ArrayController, whichs Managed Object Context is also bound to the Model Key Path delegate.managedObjectContext. Also the configuration is exactly the same as shown in the above pictures. I've then added a label and bound it's value to the selection (Controller Key), name (Key Value Path) of this ArrayController.
The problem is that the Label only displays the the initial selection after the application did launched correctly. Afterwards, when I change the selection of my NSPopupButton, the label does not change accordingly.
What are my options to get the ArrayController working accross multiple NIB files?
BTW: I've tried to follow this blog post to get it working but it seems I'm missing something here.
Update:
If I replace the Label in the details.nib by a NSTextField and change the text of it, the changes are reflected in the related NSPopupButton entry. So I guess I made something right, but the main problem remains: I can only edit the entry which was loaded during application startup. Switching to another NSPopupButton entry does not change the text in the NSTextField.
Update 2:
I've created a small sample project with exactly the same configuration and uploaded it on GitHub. So feel free to check it out or create a pull request with a solution approach.
It seems you're missing the fact that, when you create the second array controller on the Details.xib it has no relation to the array controller on the MainMenu.xib. They are two separate instances.
When you change the selection on the PopUp the only array controller affected is the one on MainMenu.xib.
You have several options here:
When you create your DetailViewController pass a reference to the array controller on the Controller and bind to that (don't create a new one on the details.xib)
Just use simple KVO to observe the selection on Controller, and programatically change your label value.
Just use simple KVO to observe the selection on Controller and update the array controller on the DetailsViewController to keep them in sync.
your solution here...
As long as you understand what's going on I'm sure you'll find the best solution to your original problem.

Handling NSLayoutConstraints from code properly

I started to use AutoLayout in my latest project, and I am stuck at some point. I use a content view controller, which adds other view controllers as sub view controllers, and their views as subviews aswell. Everything is fine, except that my sub controllers' view have wrong view sizes.
One of my sub view controllers sets up something like a slider, which has a knob. The initial position of this know depends from the actual size of this view, which is not fix, because I reuse it in many forms. Everything is set up with AutoLayout. Of course, because the value of this slider is coming from another part of code, the position of the know can be described only something like this:
self.knobVerticalPositionConstraint.constant = topPos + height * percentage;
where height is depending on the size of the slider view itself.
The problem is, that when the whole thing gets displayed, parts of my UI have wrong sizes. I even have a wrong view size in the sub controller's -viewDidAppear.
I think this is a problem of how I wrote my code. Maybe not everything is in it's place. I have read this article, which promises a clean and elegant way to solve this type of issues, but it is never revealed:
http://doing-it-wrong.mikeweller.com/2013/06/ios-app-architecture-and-tdd-1.html
I have read the whole Internet, but I didn't found anything like this. It is full of stuff for beginners and first step guides about "How to use AutoLayout", but I never found any advanced tips.
So my specific question is: do you have any best practices, how to assemble a View Controller–View pair which uses AutoLayout from code? Where do you set up your constraints? Where do you update constraint constants like mine, which depends on the calculated values of the constraint equalation system? What code goes to the View and what to the controller? I'd be glad if you could share me some tips to solve this issue.
I have recently completed the migration of my first (and so far only) iOS project to Auto Layout. The stuff below is roughly what I have taken away from my efforts. It's not much, but since I wanted to write it down for myself anyway, here you go, maybe it helps.
Static constraints that never change go into a view controller's loadView override, or in a view's initializer (or some sub-method thereof)
Dynamic constraints which depend on other values calculated by the layout system go into a view controller's viewDidLayoutSubviews override, or in a view's layoutSubviews override (in the latter case it makes sense to call [super layoutSubviews] first to get updated values that the dynamic constraint can be based on).
After a change to a dynamic constraint, it may be necessary to tell the layout system that something changed. For instance, in a view's layoutSubviews override I call [super layoutSubviews] a second time after the constraint change so that the update is effective immediately
A view controller can also override updateViewConstraints, and a view can override updateConstraints, but I have not used these so far.
There are many other events that might trigger an update of dynamic constraints (e.g. keyboard events), but this is beyond the scope of best practices.
Some general guidelines:
Constraints are installed by the view controller or view who sets up the view hierarchy and therefore knows about and is responsible for the layout of the view hierarchy. Although this rule is very basic, it sometimes pays to keep it in mind.
The translatesAutoresizingMaskIntoConstraints property of a UIView is set by that view controller which installs constraints for that UIView
A view controller therefore does not set the translatesAutoresizingMaskIntoConstraints property of its own root view. Instead, this is the responsibility of the parent view controller.
View controller properties related to bar handling in iOS 7:
edgesForExtendedLayout, usually set to UIRectEdgeNone
topLayoutGuide and bottomLayoutGuide
make all bars opaque (by default all bars are translucent in iOS 7), which would then allow the property extendedLayoutIncludesOpaqueBars to kick in
And some traps:
Don't install constraints in UIWindow. When I tried this I had trouble when dismissing a VC after it was presented modally.
The view controller properties topLayoutGuide and bottomLayoutGuide should only be used inside a view controller's override of viewDidLayoutSubviews, and after they are used to install constraints one must invoke [self.view layoutSubviews]. This advice is actually from Apple's documentation of those two properties. Not following the advice may lead to weird layout issues (it happened to me).
If a view controller's view hierarchy contains one or more scroll views (e.g. table views), and there are unexpected layout results, then the view controller property automaticallyAdjustsScrollViewInsets is probably playing a role. Setting it to NO might help.
My personal favourite ressource, by the way, for understanding Auto Layout was and still is this section from the book "Programming iOS 6".

Xcode's auto layout is only effective in viewDidAppear and this is very problematic

After upgrading my project to iOS 6, I realized that auto layout is only effective in viewDidAppear and most of my code expects the view's frame to be available in viewDidLoad. This limitation renders the really nice auto layout feature almost useless for me. Is there any suggestions to help me use auto layout?
For example, sometimes the developer needs to adjust information about a subview based on where auto layout chooses to place that particular subview. The subview's final location cannot be ascertained by the developer until AFTER the user has already seen it. The user should not see these information adjustments but be presented the final results all at once.
More specifically: What if I want to change an image in a view based on where auto-layout places that view? I cannot query that location and then change the image without the user seeing that happen.
As a general rule, the views frame/bounds should never be relied on in viewDidLoad.
The viewDidLoad method only gets called once the view has been created either programmatically or via a .nib/.xib file. At this point, the view has not been setup, only loaded into memory.
You should always do your view layout in either viewWillAppear or viewDidAppear as these methods are called once the view has been prepared for presentation.
As a test, if you simply NSLog(#"frame: %#", NSStringFromCGRect(self.view.frame)); in both your viewDidLoad and viewWillAppear methods, you will see that only the latter method returns the actual view size in relation to any other elements wrapped around your view (such as UINavigationBar and UITabBar).
As told by #charshep in a comment, calling view.layoutIfNeeded() in viewWillAppear can do the trick.
Quote of his original comment
I had trouble getting a table view to appear at the correct scroll position when pushing it [...] because layout wasn't occurring until after viewWillAppear. That meant the scroll calculation was occurring before the correct size was set so the result was off. What worked for me was calling layoutIfNeeded followed by the code to set the scroll position in viewWillAppear.

Embedding a field editor in a NSScrollView

Has anyone ever had experience embedding a field editor (for a NSTextField) inside a scroll view? I'm trying to make the NSTextField scrollable while editing.
Things I've tried:
Dynamically embed it when the custom field editor's -becomeFirstResponder gets called. This semi works; the problem is that when the NSTextField gets resized during editing the custom field editor no longer gets resized with it (and I need this - making an accordion
style application)
Create a "masquerading" field editor out of a NSScrollView, and using NSInvocation forward the methods to the actual surrogate field editor. This is the method I really hope would work; I've implemented all the methods as listed here; but I get an EXC_BAD_ACCESS whenever the field editor is actually loaded (e.g. when I call [customTextField selectText:nil]). I can't seem to pry any information out of the debugger even with Zombies enabled, and looking at the logs of NSObjCMessageLoggingEnabled yields nothing either. It seems like these guys got it working but that was seven years ago.
The last resort would be to drop NSTextFields completely and use NSTextViews (or instead of relying on the field editor mechanism, write one myself), but since I have many rows of data of which only one will be edited at a time, I don't want to instantiate a NSTextView for every single one of them... but then, perhaps it won't be so bad.
I ended up using option 1, and getting it to work without much difficulty. Option 2 was a complete dead end because EXC_BAD_ACCESS popped up everywhere I went.
My custom field editor now keeps a reference to a (custom) scroll view to embed itself in (vvScrollView), and inserts it into the view hierarchy. My code inside my custom field editor (NSTextView) for embedding it inside a scroll view, which is called as soon as the field editor becomes first responder and is automatically inserted into the view hierarchy:
- (void)embedSelfInScrollView {
NSView *realSuperview = [[self superview] superview];
// [self superview] is some kind of private NSClipView class
if ([realSuperview isKindOfClass:[NSTextField class]]) { // the expected behavior: this may change? TODO make less prone to chance
[realSuperview addSubview:[self vvScrollView]]; // insert into view
[[self vvScrollView] setFrameSize:[realSuperview frame].size]; // se the initial size equivalent to control size so it can autoresize the same way
// add the scrollview into the view hierarchy
[[self vvScrollView] setDocumentView:self]; // removes self from previous superview
}
}
The initial problem I had was that I was trying to insert the scrollview into the superview immediately above the field editor's (the private class of NSClipView) which broke almost every automatic sizing option (because I want to be able to resize the NSTextField while editing). Going a step further and bypassing the private class seems to work, but almost seems arbitrary.

NSOutlineView elements remain hopelessly undraggable

I have a program with a NSOutlineView (that supports single selection only) from which I'd like to be able to drag elements. These elements should either be received as text or files: for instance, dropping the item on a TextEdit window should put text, but dropping the item on the Finder should create a file. I don't want anything to be dropped over my outline view, even it it comes from itself. This seems easy enough, but for some reason, I can't get it to work.
I checked the NSOutlineView drag and drop example from Apple, and I came to implement the following methods (plus a few definitely unrelated ones):
-(BOOL)outlineView:shouldSelectItem: // I don't expect to drag unselectable items
-(NSArray*)outlineView:namesOfPromisedFilesDroppedAtDestination:forDraggedItems:
-(BOOL)outlineView:writeItems:toPasteboard:
However, when I try to drag an item from my outline view, nothing happens. Instead, it just changes the selection following the cursor.
I've put breakpoints in the two last methods, and they never get called, so their implementation is not the immediate issue.
I must be missing something really obvious here.
Also, this is not (yet) a problem, but how am I supposed to provide contents to my promised files?
I was being stupid and I implemented the methods in the delegate instead of the data source (the two are distinct in my app). Problem solved!
Are you using a custom table view cell? The result of NSCell's hitTestForEvent:inRect:ofView: determines whether a dragging operation can be initiated. It also determines whether your outlineView:writeItems:toPasteboard: should be called.
This method should return NSCellHitContentArea to initiate a drag, or NSCellHitTrackableArea to extend or change the selection.
A standard text cell returns NSCellHitContentArea when you click on the actual text of the cell, and NSCellHitTrackableArea when you click outside of the text. This produces the drag behavior you see in Finder's table view.
You can override this method and always return NSCellHitContentArea if you want all areas of the cell to initiate a drag operation.
See Hit Testing for more information.