I have an Xcode project with an NSWindowController whose contentViewController was set to a subclass of NSViewController. I recently removed the NSViewController subclass from the storyboard and replaced the contentViewController with an NSTabViewController subclass.
Now, when I run the application, the NSWindow opens with a size of 500x500 instead of the size of the first tab. What's more, there is no view I can see in the storyboard that has a size of 500x500, and that size isn't being programmatically, either. The window itself is set to a different size, as is the view in the NSTabViewController's first NSViewController.
I'm assuming that there is some sort of constraint I have to set somewhere, but if there is, I don't know where/how to find it. Using Xcode 9.2 and High Sierra.
Programmatically setting the window's size to the correct size in windowDidLoad() works, but if I ever change the size of the view, I'll have to change that, as well, which will get old, quick.
Sorry if this is vague; I genuinely have no clue what kind of screenshot or code snippet would be helpful.
I recently ran into this frustrating problem as well.
There are a couple options to workaround this problem:
As you mentioned, set preferredContentSize in each of your custom view controllers that hold the tab's content to your desired size. This is inflexible but it does work.
// Swift
class FooViewController: ViewController {
override func viewWillAppear() {
super.viewWillAppear()
preferredContentSize = NSSize(width: 400, height: 280)
}
}
I found a hint to a better solution in this SO answer. You can add a subview (stackview, nsview, etc...) to the main view of the view controller that handles the tab's content (phew!) and then add constraints that pin it to each edge and add constraints that set the size.
Here's a screenshot of what it looks like in Interface Builder. I added a Stack View and then added 6 constraints.
Hope this helps.
Joshua's answer with setting the preferredContentSize did the trick, all kudos to him! One remark worth making is that since this is done exclusively for the parent tab view controller it's a good idea to subclass it and move this handling into tabView(_ tabView: NSTabView, didSelect tabViewItem: NSTabViewItem?) delegate method, which gets invoked when the tab is selected:
override func tabView(_ tabView: NSTabView, didSelect tabViewItem: NSTabViewItem?) {
tabViewItem?.viewController?.preferredContentSize = tabViewItem?.view?.frame.size
// Alternatively: tabViewItem?.viewController?.preferredContentSize = tabViewItem?.view?.fittingSize
super.tabView(tabView, didSelect: tabViewItem)
}
This way the preferred content size is always up to date and you can worry not about manually refreshing it, assuming the view provides the correct frame size or fitting size, which is easily achieved with constraints.
This method also get's invoked after the window controller finishes loading and where the 500×500 gets initially set.
Setting the preferred content size in every tabbed view controller itself is not ideal: the same code is duplicated across multiple controllers and adds unnecessary noise if these controllers are reused else where.
I had a similar issue. I added a view controller with a container view as the window content and pointed the container view content to the tab view controller.
Related
I'm trying to implement an NSSplitView similar to Xcode where you can collapse a view by dragging its handle to under half its width and it will collapse automatically. I have that part working, but I need to update the state of a button in the toolbar when this happens. I tried listening to splitViewDidResizeSubviews and checking if the splitView's view is collapsed, but that method fires 16 times with collapsed == true, so I don't think I want to update the button's state 16 times. Is there a cleaner way to do this? I'm new to Cocoa, but from what I've seen, I would expect there to be some way to just say bind this button's state to the isCollapsed property and be done with it. Does such a thing exist?
If you subclass your NSSplitViewController you can add a listener for the SplitViewItem's isCollapsed property:
class MySplitViewController: NSSplitViewController {
var observer: NSKeyValueObservation?
override func viewDidLoad() {
super.viewDidLoad()
// Do view setup here.
let sideViewSplitViewItem = splitViewItems[0]
observer = sideViewSplitViewItem.observe(\.isCollapsed, options: [.initial, .new]) {splitViewItem, _ in
print("Sidebar collapsed state changed to: \(splitViewItem.isCollapsed)")
}
}
}
The best way to see what bindings are available is to check the docs, specifically the Cocoa Bindings Reference (look in the sidebar for the view you're after).
NSSplitView doesn't have the binding you describe, so I think you're on the right track with your current approach. Of course, you don't need to update the button's state sixteen times, just check it's value each time, and update it if needs be. It might seem a bit wasteful, but checking the value of a bool is a very cheap operation, and you won't notice any kind of performance hit.
While NSSplitView has no event or observable property for when one of it's subviews is "collapsed", the immediate subview itself will have its hidden property set to YES.
So you can either observe the subview's hidden property yourself, or if you're creating your own custom subview of NSView, you can override the -(void) setHidden:(BOOL)hidden to catch the event.
I do the latter, and it works correctly every time.
-(void)setHidden:(BOOL)hidden
{
[super setHidden:hidden];
// Insert code to react to the "collapsed" event here.
// (You're likely going to tell your parent view,
// self.window.windowController, or something.)
}
Of course the setHidden method / hidden property can in theory be called for purposes other than from NSSplitView "collapsing" your view, but this is very unlikely to ever happen in practice.
Xcode 7.0.1 (7A1001)
iOS 9.0
I made a custom #IBDesignable UIView, only implementing the drawRect function. Like so:
#IBDesignable
class CustomView: UIView {
override func drawRect(rect: CGRect) {
let context = UIGraphicsGetCurrentContext()
// Lots more drawing code after this
// ...
}
}
While I was writing this drawRect, I had a storyboard with several different instances of this rendering on a view controller. When I made a change in drawRect and saved, these changes would be reflected in the storyboard in close to real time. There was even an IBInspectable String which allowed me to change an enum type and draw differently depending on that. The views could be resized in the storyboard, and the new width and height would be accounted for in my drawing code, and the drawing would be scaled and translated, exactly as I intended.
This was all working fine, and I had not made any changes to that UIView subclass, but I did delete my test views in the storyboard while building out newer views which would actually end up using this custom view.
After doing a lot of dancing around with constraints, I was ready to place my custom view where it belonged. But it does not work. While the identity inspector acknowledges my custom class as an IBDesignable, it hangs on "Updating" status.
To prove I wasn't going crazy, I made entirely new #IBDesignable UIView subclasses which implemented simple drawRects or even completely empty drawRects. These are now having the same issue and are not rendering on the storyboard.
Note that I can run this in the simulator and the drawing appears as I expect at runtime. So what happened to the storyboard's ability to give that nice feedback I expect from an IBDesignable?
Things I have tried:
Clean / clean build folder / clearing XCode Derived Data directory
Restarting XCode
Deleting / commenting out drawRect guts
Making the most simple #IBDesignables with HelloWorld-like tutorials
Using existing known working #IBDesignables
Creating fresh UIViewController in storyboard to place custom view
Toggled: Editor > Automatically Refresh Views
Editor > Refresh All Views
Editor > Debug Selected Views
All these attempts continue to show "Updating" in Custom Class section of the identity inspector.
Clearly something beyond my implementation is causing a problem here. Anybody else having this issue? Can anybody point me in the right direction? Not sure what else to try.
In the Identity inspector where it says "Module" add your AppName
I'm fairly new to Mac development and am slightly confused by the new "storyboard" feature in Xcode 6. What I'm trying to do is segue from one view controller to another in the same window. As of right now, all the different NSViewControllerSegues present the view controller in a new window, be it a modal or just another window. What I'd like to do is just segue within the same window, much in the same way one would on iOS (though an animated transition is not crucial). How would this be achieved?
If you provide a custom segue (subclass of NSStoryboardSegue) you can get the result you are after. There are a few gotchas with this approach though:
the custom segue will use presentViewController:animator so you will need to provide an animator object
because the presented view is not backed by a separate Window object, you may need to provide it with a custom NSView just to catch out mouse events that you don't want to propagate to the underlying NSViewController's view
there's also a Swift-only glitch regarding the custom segue's identifier property you need to watch out for.
As there doesn't seem to be much documentation about this I have made a small demo project with custom segue examples in Swift and Objective-C.
I also have provided some more detail in answer to this question.
(Reviving this as it comes up as first relevant result on Google and I had the same problem but decided against a custom segue)
While custom segues work (at least, the code given in foundry's answer worked under Swift 3; it needs updating for Swift 4), the sheer amount of work involved in writing a custom animator suggests to me that their main use case is custom animations.
The simple solution to changing the content of a window is to create an NSWindowController for your window, and to set its contentViewController to the desired viewController. This is particularly useful if you are following the typical pattern of storyboards and instantiate a new ViewController instance every time you switch.
However.
The NSStoryboard documentation says, quite clearly in macOS, containment (rather than transition) is the more common notion for storyboards which led me to look again at the available tools.
You could use a container view for this task, which adds a NWViewController layer instead of the NSWindowController outlined above. The solution I've gone with is to use an NSTabViewController. In the attributes inspector, set the style to 'unspecified', then select the TabView and set its style to 'tabless'.
To change tabs programatically, you set the selectedTabViewItemIndexof your TabViewController.
This solution reuses the same instance of the ViewControllers for the tab content, so that any data entered in text fields is preserved when the user switches to the other 'tab'.
Simple way with no segues involved to replace the current view controller in the same window:
if let myViewController = self.storyboard?.instantiateController(withIdentifier: "MyViewController") as? MyViewController {
self.view.window?.contentViewController = myViewController
}
I have a view that needs to have its frame manipulated programmatically - it's a kind of document view that wraps to its content which is then scrolled and zoomed around a superview by manipulating the frame origin. Autolayout fights with this at runtime.
Disabling autolayout completely seems a bit harsh because it could reasonably be used to handle layout for the other views. It seems like what I might want is some kind of "null constraint".
I had the same problem. But I have resolved it.
Yes, you can disable auto layout at runtime for a specific UIView, instead of disabling it for the whole xib or storyboard which is set by default in Xcode 4.3 and later.
Set translatesAutoresizingMaskIntoConstraints to YES, before you set the frame of your subview:
self.exampleView.translatesAutoresizingMaskIntoConstraints = YES;
self.exampleView.frame = CGRectMake(20, 20, 50, 50);
I had a similar issue where Autolayout was overriding some of my frame-setting at run time (I had a dynamic view that in some cases pushed a new view controller...pushing and then pressing Back would reset the initial view).
I got around this by putting my manipulation code in viewDidLayoutSubviews of my View Controller. This seems to get called after whatever constraint mojo gets called, but before viewDidAppear, so the user is none the wiser.
Perhaps just setting translatesAutoresizingMaskIntoConstraints to YES (and not adding additional constraints affecting that view) will let you set the frame without fighting the auto layout system.
In iOS 8 you can set an NSLayoutConstraint to be active or not. So if I'm using interface builder, I add all my constraints to an OutletCollection and then activate or deactivate using:
NSLayoutConstraint.deactivateConstraints(self.landscapeConstraintsPad)
NSLayoutConstraint.activateConstraints(self.portraitConstraintsPad)
The particular application I'm using it for here is having different constraints in portrait and landscape mode and I activate/deactivate based on the rotation of the device. It means I can create some complex layout changes all in interface builder for both orientations, and still use auto layout without the verbose auto layout code.
Or you can activate / deactivate using removeConstraints and addConstraints.
I don't know if this will help anyone else, but I wrote a category to make this convenient because I find myself doing this a lot.
UIView+DisableAutolayoutTemporarily.h
#import <UIKit/UIKit.h>
#interface UIView (DisableAutolayoutTemporarily)
// the view as a parameter is a convenience so we don't have to always
// guard against strong-reference cycles
- (void)resizeWithBlock:(void (^)(UIView *view))block;
#end
UIView+DisableAutolayoutTemporarily.m
#import "UIView+DisableAutoResizeTemporarily.h"
#implementation UIView (DisableAutoResizeTemporarily)
- (void)resizeWithBlock:(void (^)(UIView * view))block
{
UIView *superview = self.superview;
[self removeFromSuperview];
[self setTranslatesAutoresizingMaskIntoConstraints:YES];
__weak UIView *weakSelf = self;
block(weakSelf);
[superview addSubview:self];
}
#end
I use it like this:
[cell.argumentLabel resizeWithBlock:^(UIView *view) {
[view setFrame:frame];
}];
Hope it helps.
You can set the translatesAutoresizingMaskIntoConstraints type Boolean, Value Yes in the User Defined Runtime Attributes of the UIView you want in the xib/storyboard.
In my view I had a Label and a Text. The label had pan gesture. The label moves around fine during drag. But when I use the text box keyboard, the label resets its position to the original location defined in auto layout. The issue got resolved when I added the following in swift for the label. I added this in viewWillAppear but it can be added pretty much anywhere you have access to the target field.
self.captionUILabel.translatesAutoresizingMaskIntoConstraints = true
Open project in 4.5
Select storyboard
Open the file inspector
Under Interface Builder Document uncheck 'Use Autolayout'
You can split across multiple storyboards if you want to use autolayout for some views.
For me it worked to create the subview programmatically, in my case the auto layout was messing with a view that I needed to rotate around its center but once I created this view programmatically it worked.
I've encountered a similar scenario, where I joined a project that was initiated with auto-layout, but I needed to make dynamic adjustments to several views. Here is what has worked for me:
Do NOT have views or components laid out in interface builder.
Add your views purely programmatically starting with alloc/init and setting their frames appropriately.
Done.
This happened to me in a project without storyboards or xib files. All 100% code. I had an ad banner at the bottom and wanted the view bounds to stop at the ad banner. The view would resize itself automatically after loading. I tried every resolution on this page but none of them worked.
I ended up just creating a sub view with the shortened height and placed that in into the main view of the controller. Then all my content went inside the sub view. That solved the problem very easily without doing anything that felt like it was going against the grain.
I am thinking if you want a view that is not the normal size that fills the window then you should use a sub view for that.
Instead of disabling autolayout, I would just calculate the new constraint with the frame you are replacing. That appears to me to be the appropriate way. If you are adjusting components that rely on constraints, adjust them accordingly.
For example, if you have a vertical constraint of 0 between two views (myView and otherView), and you have a pan gesture or something that adjusts the height of myView then you can recalculate the constraint with the adjusted values.
self.verticalConstraint.constant = newMyViewYOriginValue - (self.otherView.frame.origin.y + self.otherView.frame.size.height);
[self.myView needsUpdateConstraints];
For those of you who are using auto layout, please check out my solution here. You should be making #IBOutlet's of the constraints you want to adjust and then change their constants.
if it's xib file:
select the .xib file
select the "File's Owner"
show the Utilities
click on: "File Inspector"
Under "Interface Builder Document" disable: "Use Autolayout"
I have a puzzling problem. Working on a cocoa app in mac os x 10.7.
My app main window contains a split view. In a certain use context in one of the subviews of the split view is loaded a custom view with some labels (nstextfield) and a split view (instantiating a view controller that loads a nib and getting view from that controller). Frame of the custom view is set to split view subview bounds and everything works fine.
Problem is that one of the subviews of the second split view should be loaded (same method: view controller-nib-view-frame/bounds) with a custom view containing a table view and a button, but in this case nothing shows. Everything is done the same way but last custom view is not visible. Any idea?
Thanks
(edit)
this is the code I use to instantiate controller for the view to be added, get the view, and add it as subview to a subview of the split view
- (void)loadSubview {
self.subviewToAddController = [[viewController alloc] initWithNibName:nil bundle:nil];
//nib name is coded in the controller class definition
[[self.subviewToAddController view] setFrame:[self.splitViewContainerSubView bounds]];
//container subView is an outlet
[self.splitViewContainerSubView addSubview:[self.subviewToAddController view]];
}
However I don't think the problem is in this code because if I ask the container subview for its own subviews I can see the new subview is present in the list. It just doesn't show. If I add it as a subview of the split view (a test a just made) or as subview of the subview of the most external split view it is correctly showed too (sorry for the confused explanation, I would need a diagram but in this moment I can't make it)
To elaborate more my doubt (I didn't want to misled so I didn't mention before) can't it be a problem of coordinates, so view is correctly loaded and added as subview but is not visible because hidden by something or showed out of visible area?
(update)
Sorry it took so long to post an update.
After more testing I found out the problem is related to autolayout. No idea what the exact problem is and how to solve it. I ended up turning it off for the nibs the were in troubles and use the old way to set interface objects position and size/resize. Not the best way but for now I can go on.
My best guess is that you didn't set the autoresizing masks of the view properly.