Autolayout constraints and child view controller - objective-c

I have two view controllers, parent and child.
So in the viewDidLoad method I do the following:
ChildViewController* childViewController = [[ChildViewController alloc] init];
[self addChildViewController:childViewController];
// ChildViewController sets his own constraints in viewDidLoad
[self.view addSubview:childViewController.view];
[childViewController didMoveToParentViewController:self];
//
// setup constraints to expand childViewController.view to
// fill the size of parent view controller
//
So basically what happens is that updateViewConstraints is called on ChildViewController before parent controller constraints apply, so in fact self.view.frame == CGRectZero, exactly the same as I specified in custom loadView method in ChildViewController.
translatesAutoresizingMaskIntoConstraints all set to NO for all views.
What's the proper way to setup constraints in this case so ChildViewController updates his constraints after parent?
Current log from both controllers is pretty frustrating, I do not understand how updateViewConstraints can be called before viewWillLayoutSubviews:
App[47933:c07] ChildViewController::updateViewConstraints. RECT: {{0, 0}, {0, 0}}
App[47933:c07] ParentViewController::updateViewConstraints
App[47933:c07] ChildViewController:viewWillLayoutSubviews. RECT: {{0, 0}, {984, 454}}
App[47933:c07] ChildViewController:viewDidLayoutSubviews. RECT: {{0, 0}, {984, 454}}

You can add constraints right after invoke of addSubview: , don't forget to set translatesAutoresizingMaskIntoConstraints to false
Here is a code snippet of adding and hiding child view controller with constraints (inspired by apple guide)
Display
private func display(contentController content : UIViewController)
{
self.addChildViewController(content)
content.view.translatesAutoresizingMaskIntoConstraints = false
self.containerView.addSubview(content.view)
content.didMove(toParentViewController: self)
containerView.addConstraints([
NSLayoutConstraint(item: content.view, attribute: .top, relatedBy: .equal, toItem: containerView, attribute: .top, multiplier: 1, constant: 0),
NSLayoutConstraint(item: content.view, attribute: .bottom, relatedBy: .equal, toItem: containerView, attribute: .bottom, multiplier: 1, constant: 0),
NSLayoutConstraint(item: content.view, attribute: .leading, relatedBy: .equal, toItem: containerView, attribute: .leading, multiplier: 1, constant: 0),
NSLayoutConstraint(item: content.view, attribute: .trailing, relatedBy: .equal, toItem: containerView, attribute: .trailing, multiplier: 1, constant: 0)
])
}
Hide
private func hide(contentController content : UIViewController)
{
content.willMove(toParentViewController: nil)
content.view.removeFromSuperview()
content.removeFromParentViewController()
}

I have a similar situation where I add a child controller to a visible controller (a popup).
I define the child view controller in interface builder. It's viewDidLoad method just calls setTranslatesAutoresizingMaskIntoConstraints:NO
Then I define this method on the child controller which takes a UIViewController parameter, which is the parent.
This method adds itself to the given parent view controller and defines its own constraints:
- (void) addPopupToController:(UIViewController *)parent {
UIView *view = [self view];
[self willMoveToParentViewController:parent];
[parent addChildViewController:self];
[parent.view addSubview:view];
[self didMoveToParentViewController:parent];
NSArray *horizontalConstraints = [NSLayoutConstraint constraintsWithVisualFormat:#"H:|-0-[view]-0-|"
options:0
metrics:nil
views:NSDictionaryOfVariableBindings(view)];
[parent.view addConstraints:horizontalConstraints];
NSArray *verticalConstraints = [NSLayoutConstraint constraintsWithVisualFormat:#"V:|-0-[view]-0-|"
options:0
metrics:nil
views:NSDictionaryOfVariableBindings(view)];
[parent.view addConstraints:verticalConstraints];
}
then inside the parent UIViewController (the one which is already displayed), when I want to display my child popup view controller it calls:
PopUpNotificationViewController *popup = [[self storyboard] instantiateViewControllerWithIdentifier:#"NotificationPopup"];
[popup addPopupToController:self];
You can define whatever constraints you want on the child controller's view when adding it to the parent controller's view.

I think the layoutIfNeeded method will only work if you have previously called setNeedsLayout. If you use setNeedsLayout instead, the system will know to update at an appropriate time. Try changing that in your code.
When you add constraints to a view, that view should automatically layout its subviews again to account for the new constraints. There should be no need to call setNeedsLayout unless something has changed since you have added the constraints. Are you adding the constraints to a view and if so: are you adding them to the right view?
One thing you can try is to subclass UIView so that you see a log message whenever the ChildViewController.view or ParentViewController.view performs a fresh layout:
-(void) layoutSubviews {
[super layoutSubviews];
NSLog(#"layoutSubviews was called on the following view: %#", [view description]);
}
Maybe that will reveal something about when your views are (or aren't) layout out their subviews.

According to the following link, I moved constraints creation to viewWillLayoutSubviews, this is the place where view bounds are set properly. I feel this answer misses explanation on why Child view controller's updateViewConstraints called before parent view controller, or maybe it's just some bug in my code, but this workaround solves the problem...

It is convenient to put the code for adding and removing a child controller into an extension. Usage example:
override func viewDidLoad() {
super.viewDidLoad()
let childController = YourCustomController()
add(childController)
}
The code itself:
extension UIViewController {
func add(_ controller: UIViewController) {
addChild(controller)
view.addSubview(controller.view)
controller.view.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
controller.view.leadingAnchor.constraint(equalTo: view.leadingAnchor),
controller.view.topAnchor.constraint(equalTo: view.topAnchor),
controller.view.trailingAnchor.constraint(equalTo: view.trailingAnchor),
controller.view.bottomAnchor.constraint(equalTo: view.bottomAnchor),
])
controller.didMove(toParent: self)
}
func remove() {
guard parent != nil else {
return
}
willMove(toParent: nil)
view.removeFromSuperview()
removeFromParent()
}
}

Related

Animating CALayer shadow simultaneously as UITableviewCell height animates

I have a UITableView that I am attempting to expand and collapse using its beginUpdates and endUpdates methods and have a drop shadow displayed as that's happening. In my custom UITableViewCell, I have a layer which I create a shadow for in layoutSubviews:
self.shadowLayer.shadowColor = self.shadowColor.CGColor;
self.shadowLayer.shadowOffset = CGSizeMake(self.shadowOffsetWidth, self.shadowOffsetHeight);
self.shadowLayer.shadowOpacity = self.shadowOpacity;
self.shadowLayer.masksToBounds = NO;
self.shadowLayer.frame = self.layer.bounds;
// this is extremely important for performance when drawing shadows
UIBezierPath *shadowPath = [UIBezierPath bezierPathWithRoundedRect:self.shadowLayer.frame cornerRadius:self.cornerRadius];
self.shadowLayer.shadowPath = shadowPath.CGPath;
I add this layer to the UITableViewCell in viewDidLoad:
self.shadowLayer = [CALayer layer];
self.shadowLayer.backgroundColor = [UIColor whiteColor].CGColor;
[self.layer insertSublayer:self.shadowLayer below:self.contentView.layer];
As I understand it, when I call beginUpdates, an implicit CALayerTransaction is made for the current run loop if none exists. Additionally, layoutSubviews also gets called. The problem here is that the resulting shadow is drawn immediately based on the new size of the UITableViewCell. I really need to shadow to continue to cast in the expected way as the actual layer is animating.
Since my created layer is not a backing CALayer it animates without explicitly specifying a CATransaction, which is expected. But, as I understand it, I really need some way to grab hold of beginUpdates/endUpdates CATransaction and perform the animation in that. How do I do that, if at all?
So I guess you have something like this:
(I turned on “Debug > Slow Animations” in the simulator.) And you don't like the way the shadow jumps to its new size. You want this instead:
You can find my test project in this github repository.
See #horseshoe7's answer for a Swift translation.
It is tricky but not impossible to pick up the animation parameters and add an animation in the table view's animation block. The trickiest part is that you need to update the shadowPath in the layoutSubviews method of the shadowed view itself, or of the shadowed view's immediate superview. In my demo video, that means that the shadowPath needs to be updated by the layoutSubviews method of the green box view or the green box's immediate superview.
I chose to create a ShadowingView class whose only job is to draw and animate the shadow of one of its subviews. Here's the interface:
#interface ShadowingView : UIView
#property (nonatomic, strong) IBOutlet UIView *shadowedView;
#end
To use ShadowingView, I added it to my cell view in my storyboard. Actually it's nested inside a stack view inside the cell. Then I added the green box as a subview of the ShadowingView and connected the shadowedView outlet to the green box.
The ShadowingView implementation has three parts. One is its layoutSubviews method, which sets up the layer shadow properties on its own layer to draw a shadow around its shadowedView subview:
#implementation ShadowingView
- (void)layoutSubviews {
[super layoutSubviews];
CALayer *layer = self.layer;
layer.backgroundColor = nil;
CALayer *shadowedLayer = self.shadowedView.layer;
if (shadowedLayer == nil) {
layer.shadowColor = nil;
return;
}
NSAssert(shadowedLayer.superlayer == layer, #"shadowedView must be my direct subview");
layer.shadowColor = UIColor.blackColor.CGColor;
layer.shadowOffset = CGSizeMake(0, 1);
layer.shadowOpacity = 0.5;
layer.shadowRadius = 3;
layer.masksToBounds = NO;
CGFloat radius = shadowedLayer.cornerRadius;
layer.shadowPath = CGPathCreateWithRoundedRect(shadowedLayer.frame, radius, radius, nil);
}
When this method is run inside an animation block (as is the case when the table view animates a change in the size of a cell), and the method sets shadowPath, Core Animation looks for an “action” to run after updating shadowPath. One of the ways it looks is by sending actionForLayer:forKey: to the layer's delegate, and the delegate is the ShadowingView. So we override actionForLayer:forKey: to provide an action if possible and appropriate. If we can't, we just call super.
It is important to understand that Core Animation asks for the action from inside the shadowPath setter, before actually changing the value of shadowPath.
To provide the action, we make sure the key is #"shadowPath", that there is an existing value for shadowPath, and that there is already an animation on the layer for bounds.size. Why do we look for an existing bounds.size animation? Because that existing animation has the duration and timing function we should use to animate shadowPath. If everything is in order, we grab the existing shadowPath, make a copy of the animation, store them in an action, and return the action:
- (id<CAAction>)actionForLayer:(CALayer *)layer forKey:(NSString *)event {
if (![event isEqualToString:#"shadowPath"]) { return [super actionForLayer:layer forKey:event]; }
CGPathRef priorPath = layer.shadowPath;
if (priorPath == NULL) { return [super actionForLayer:layer forKey:event]; }
CAAnimation *sizeAnimation = [layer animationForKey:#"bounds.size"];
if (![sizeAnimation isKindOfClass:[CABasicAnimation class]]) { return [super actionForLayer:layer forKey:event]; }
CABasicAnimation *animation = [sizeAnimation copy];
animation.keyPath = #"shadowPath";
ShadowingViewAction *action = [[ShadowingViewAction alloc] init];
action.priorPath = priorPath;
action.pendingAnimation = animation;
return action;
}
#end
What does the action look like? Here's the interface:
#interface ShadowingViewAction : NSObject <CAAction>
#property (nonatomic, strong) CABasicAnimation *pendingAnimation;
#property (nonatomic) CGPathRef priorPath;
#end
The implementation requires a runActionForKey:object:arguments: method. In this method, we update the animation that we created in actionForLayer:forKey: using the saved-away old shadowPath and the new shadowPath, and then we add the animation to the layer.
We also need to manage the retain count of the saved path, because ARC doesn't manage CGPath objects.
#implementation ShadowingViewAction
- (void)runActionForKey:(NSString *)event object:(id)anObject arguments:(NSDictionary *)dict {
if (![anObject isKindOfClass:[CALayer class]] || _pendingAnimation == nil) { return; }
CALayer *layer = anObject;
_pendingAnimation.fromValue = (__bridge id)_priorPath;
_pendingAnimation.toValue = (__bridge id)layer.shadowPath;
[layer addAnimation:_pendingAnimation forKey:#"shadowPath"];
}
- (void)setPriorPath:(CGPathRef)priorPath {
CGPathRetain(priorPath);
CGPathRelease(_priorPath);
_priorPath = priorPath;
}
- (void)dealloc {
CGPathRelease(_priorPath);
}
#end
This is Rob Mayoff's answer written in Swift. Could save someone some time.
Please don't upvote this. Upvote Rob Mayoff's solution. It is awesome, and correct. (Note from mayoff: why not upvote both? 😉)
import UIKit
class AnimatingShadowView: UIView {
struct DropShadowParameters {
var shadowOpacity: Float = 0
var shadowColor: UIColor? = .black
var shadowRadius: CGFloat = 0
var shadowOffset: CGSize = .zero
static let defaultParameters = DropShadowParameters(shadowOpacity: 0.15,
shadowColor: .black,
shadowRadius: 5,
shadowOffset: CGSize(width: 0, height: 1))
}
#IBOutlet weak var contentView: UIView! // no sense in have a shadowView without content!
var shadowParameters: DropShadowParameters = DropShadowParameters.defaultParameters
private func apply(dropShadow: DropShadowParameters) {
let layer = self.layer
layer.shadowColor = dropShadow.shadowColor?.cgColor
layer.shadowOffset = dropShadow.shadowOffset
layer.shadowOpacity = dropShadow.shadowOpacity
layer.shadowRadius = dropShadow.shadowRadius
layer.masksToBounds = false
}
override func layoutSubviews() {
super.layoutSubviews()
let layer = self.layer
layer.backgroundColor = nil
let contentLayer = self.contentView.layer
assert(contentLayer.superlayer == layer, "contentView must be a direct subview of AnimatingShadowView!")
self.apply(dropShadow: self.shadowParameters)
let radius = contentLayer.cornerRadius
layer.shadowPath = UIBezierPath(roundedRect: contentLayer.frame, cornerRadius: radius).cgPath
}
override func action(for layer: CALayer, forKey event: String) -> CAAction? {
guard event == "shadowPath" else {
return super.action(for: layer, forKey: event)
}
guard let priorPath = layer.shadowPath else {
return super.action(for: layer, forKey: event)
}
guard let sizeAnimation = layer.animation(forKey: "bounds.size") as? CABasicAnimation else {
return super.action(for: layer, forKey: event)
}
let animation = sizeAnimation.copy() as! CABasicAnimation
animation.keyPath = "shadowPath"
let action = ShadowingViewAction()
action.priorPath = priorPath
action.pendingAnimation = animation
return action
}
}
private class ShadowingViewAction: NSObject, CAAction {
var pendingAnimation: CABasicAnimation? = nil
var priorPath: CGPath? = nil
// CAAction Protocol
func run(forKey event: String, object anObject: Any, arguments dict: [AnyHashable : Any]?) {
guard let layer = anObject as? CALayer, let animation = self.pendingAnimation else {
return
}
animation.fromValue = self.priorPath
animation.toValue = layer.shadowPath
layer.add(animation, forKey: "shadowPath")
}
}
Assuming that you're manually setting your shadowPath, here's a solution inspired by the others here that accomplishes the same thing using less code.
Note that I'm intentionally constructing my own CABasicAnimation rather than copying the bounds.size animation exactly, as in my own tests I found that toggling the copied animation while it was still in progress could cause the animation to snap to it's toValue, rather than transitioning smoothly from its current value.
class ViewWithAutosizedShadowPath: UIView {
override func layoutSubviews() {
super.layoutSubviews()
let oldShadowPath = layer.shadowPath
let newShadowPath = CGPath(rect: bounds, transform: nil)
if let boundsAnimation = layer.animation(forKey: "bounds.size") as? CABasicAnimation {
let shadowPathAnimation = CABasicAnimation(keyPath: "shadowPath")
shadowPathAnimation.duration = boundsAnimation.duration
shadowPathAnimation.timingFunction = boundsAnimation.timingFunction
shadowPathAnimation.fromValue = oldShadowPath
shadowPathAnimation.toValue = newShadowPath
layer.add(shadowPathAnimation, forKey: "shadowPath")
}
layer.shadowPath = newShadowPath
}
}
UITableView is likely not creating a CATransaction, or if it is, it's waiting until after you end the updates. My understanding is that table views just coalesce all changes between those functions and then creates the animations as necessary. You don't have a way to get a handle on the actual animation parameters it's committing, because we don't know when that actually happens. The same thing happens when you animate a content offset change in UIScrollView: the system provides no context about the animation itself, which is frustrating. There is also no way to query the system for current CATransactions.
Probably the best you can do is inspect the animation that UITableView is creating and just mimic the same timing parameters in your own animation. Swizzling add(_:forKey:) on CALayer can allow you to inspect all animations being added. You certainly don't want to actually ship with this, but I often use this technique in debugging to figure out what animations are being added and what their properties are.
I suspect that you're going to have to commit your own shadow layer animations in tableView(_:willDisplayCell:for:row:) for the appropriate cells.

IOS8 how to move an active popover

I have developed an app for iOS7 and now trying to update it for iOS8.
Issue i have is the following:
The app screen orientation can be rotated and a few buttons in some cases move drastically. I have a few popovers that point to these buttons, so if a popover is open when screen rotates, button moves, i need the popover to also.
In iOS7 i did this by the following:
When screen rotated i updated the constraints
- (void) updateViewConstraints
{
if (UIInterfaceOrientationIsLandscape(self.interfaceOrientation))
{
self.Button.constant = (CGFloat)10;
}
else
{
self.Button.constant = (CGFloat)5;
}
[super updateViewConstraints];
}
I also move the popover
- (void) didRotateFromInterfaceOrientation:(UIInterfaceOrientation)fromInterfaceOrientation{
if(TempDisplayPopoverController == examplePopoverController)
{
[examplePopoverController presentPopoverFromRect:[self ExamplePopoverPosition] inView:self.view permittedArrowDirections:UIPopoverArrowDirectionAny animated:YES];
}
}
I initially load the popover
- (void) LoadPopover{
examplePopover = [[examplep alloc] initWithNibName:#"exampleP" bundle:nil];
[examplePopover setDelegate:self];
examplePopoverController = [[UIPopoverController alloc] initWithContentViewController: examplePopover];
[examplePopoverController setDelegate:self];
examplePopoverController.popoverContentSize = examplePopover.view.frame.size;
TempDisplayPopoverController = examplePopoverController;
if ([examplePopoverController isPopoverVisible])
{
[examplePopoverController dismissPopoverAnimated:YES];
}
else
{
[examplePopoverController presentPopoverFromRect:[self ExamplePopoverPosition] inView:self.view permittedArrowDirections:UIPopoverArrowDirectionAny animated:YES];
}
}
[self ExamplePopoverPosition] just returns button position.
This all worked fine, i was happy, iPad was happy and all behaved.
Now due to iOS8 i have to change a few bits.
self.interfaceOrientation is depreciated
[examplePopoverController presentPopoverFromRect:[self ExamplePopoverPosition] inView:self.view permittedArrowDirections:UIPopoverArrowDirectionAny animated:YES];
in didRotateFromInterfaceOrientation throws an error
"Application tried to represent an active popover presentation: <UIPopoverPresentationController: 0x7bf59280>"
I've managed to rectify self.interfaceOrientation by
- (void)willAnimateRotationToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation duration:(NSTimeInterval)duration
{
[self SetUpScreen:toInterfaceOrientation];
}
- (void) SetUpScreen:(UIInterfaceOrientation)toInterfaceOrientation{
if (toInterfaceOrientation == UIInterfaceOrientationLandscapeLeft ||
toInterfaceOrientation == UIInterfaceOrientationLandscapeRight)
{
self.Button.constant = (CGFloat)10;
}
else
{
self.Button.constant = (CGFloat)5;
}
[super updateViewConstraints];
}
but have no idea how to resolve the popover issue. I have tried
popoverController: willRepositionPopoverToRect: inView:
but just can't to seem to get it to work.
Can anyone advice
Thanks
In iOS 8 you can use -viewWillTransitionToSize:withTransitionCoordinator: to handle screen size (and orientation) changes:
- (void)viewWillTransitionToSize:(CGSize)size
withTransitionCoordinator:(id<UIViewControllerTransitionCoordinator>)coordinator
{
[_popover dismissPopoverAnimated:NO];
[coordinator animateAlongsideTransition:^(id<UIViewControllerTransitionCoordinatorContext> context) {
// Update your layout for the new size, if necessary.
// Compare size.width and size.height to see if you're in landscape or portrait.
} completion:^(id<UIViewControllerTransitionCoordinatorContext> context) {
[_popover presentPopoverFromRect:[self popoverFrame]
inView:self.view
permittedArrowDirections:UIPopoverArrowDirectionAny
animated:NO];
}];
}
When you implement this method, the deprecated rotation methods like willAnimateRotationToInterfaceOrientation: will not be called when running on iOS 8.
When using popoverController:willRepositionPopoverToRect:inView:, when reassigning to the rect parameter, try using:
*rect = myNewRect;
and not:
rect = &presentingRect;
Also, make sure you have properly assigned the popover controller's delegate.
First, you don't need to dismiss and present the popover on rotation. UIPopoverPresentationController does that for you. You don't even need to update sourceView/sourceRect once they are set on creating the popover.
Now, the trick with animate(alongsideTransition: ((UIViewControllerTransitionCoordinatorContext) -> Void)?, completion: ((UIViewControllerTransitionCoordinatorContext) -> Void)? = nil) is that you should update your constraints in alongsideTransition closure, not in completion. This way you ensure that UIPopoverPresentationController has the updated sourceRect when restoring the popover at the end of rotation.
What might seem counter-intuitive is that inside alongsideTransition closure you already have your new layout that you derive your constraints calculation from.
Here's an example in Swift:
override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
super.viewWillTransition(to: size, with: coordinator)
coordinator.animate(alongsideTransition: { _ in
if self.popover != nil {
// optionally scroll to popover source rect, if inside scroll view
let rect = ...
self.scrollView.scrollRectToVisible(rect, animated: false)
// update source rect constraints
myConstraint.constant = ...
myConstrainedView.setNeedsLayout()
myConstrainedView.layoutIfNeeded()
}
}, completion: nil)
}
Very interesting - I got this to work without updating the position manually. I don't know why this works though.
let buttonContainer = UIView(frame: CGRectMake(0, 0, 44, 44))
let button = UIButton(frame: CGRectMake(0, 0, 44, 44))
buttonContainer.addSubview(button)
view.addSubview(buttonContainer)
popover!.presentPopoverFromRect(button, inView: button.superview!, permittedArrowDirections: .Any, animated: true)
Put the button that the popover is presenting from inside a "container view". Then the popover will automatically adjust location upon orientation change.

Content padding in custom view with Cocoa auto layout

I have a custom NSView subclass which has a border around itself. The border is drawn inside this view. Is it possible to respect this borders with auto layout?
For example, when I place the subview to my custom view and set constraints like this:
#"H:|-(myViewSubView)-|" (not #"H:|-(myViewBorderWidth)-(myViewSubView)-(myViewBorderWidth)-|")
#"V:|-(myViewSubView)-|"
the layout must be:
Horizontal: |-(myViewBorderWidth)-|myViewSubview|-(myViewBorderWidth)-|
Vertical: |-(myViewBorderWidth)-|myViewSubview|-(myViewBorderWidth)-|
I've tried to overwrite -bounds method in my view to return the bounds rect without the borders, but it doesn't help.
UPDATE
I just noticed that your question is talking about NSView (OS X), not UIView (iOS). Well, this idea should still be applicable, but you won't be able to drop my code into your project unchanged. Sorry.
ORIGINAL
Consider changing your view hierarchy. Let's say your custom bordered view is called BorderView. Right now you're adding subviews directly to BorderView and creating constraints between the BorderView and its subviews.
Instead, give the BorderView a single subview, which it exposes in its contentView property. Add your subviews to the contentView instead of directly to the BorderView. Then the BorderView can lay out its contentView however it needs to. This is how UITableViewCell works.
Here's an example:
#interface BorderView : UIView
#property (nonatomic, strong) IBOutlet UIView *contentView;
#property (nonatomic) UIEdgeInsets borderSize;
#end
If we're using a xib, then we have the problem that IB doesn't know that it should add subviews to the contentView instead of directly to the BorderView. (It does know this for UITableViewCell.) To work around that, I've made contentView an outlet. That way, we can create a separate, top-level view to use as the content view, and connect it to the BorderView's contentView outlet.
To implement BorderView this way, we'll need an instance variable for each of the four constraints between the BorderView and its contentView:
#implementation BorderView {
NSLayoutConstraint *topConstraint;
NSLayoutConstraint *leftConstraint;
NSLayoutConstraint *bottomConstraint;
NSLayoutConstraint *rightConstraint;
UIView *_contentView;
}
The contentView accessor can create the content view on demand:
#pragma mark - Public API
- (UIView *)contentView {
if (!_contentView) {
[self createContentView];
}
return _contentView;
}
And the setter can replace an existing content view, if there is one:
- (void)setContentView:(UIView *)contentView {
if (_contentView) {
[self destroyContentView];
}
_contentView = contentView;
[self addSubview:contentView];
}
The borderSize setter needs to arrange for the constraints to be updated and for the border to be redrawn:
- (void)setBorderSize:(UIEdgeInsets)borderSize {
if (!UIEdgeInsetsEqualToEdgeInsets(borderSize, _borderSize)) {
_borderSize = borderSize;
[self setNeedsUpdateConstraints];
[self setNeedsDisplay];
}
}
We'll need to draw the border in drawRect:. I'll just fill it with red:
- (void)drawRect:(CGRect)rect {
CGRect bounds = self.bounds;
UIBezierPath *path = [UIBezierPath bezierPathWithRect:bounds];
[path appendPath:[UIBezierPath bezierPathWithRect:UIEdgeInsetsInsetRect(bounds, self.borderSize)]];
path.usesEvenOddFillRule = YES;
[path addClip];
[[UIColor redColor] setFill];
UIRectFill(bounds);
}
Creating the content view is trivial:
- (void)createContentView {
_contentView = [[UIView alloc] init];
[self addSubview:_contentView];
}
Destroying it is slightly more involved:
- (void)destroyContentView {
[_contentView removeFromSuperview];
_contentView = nil;
[self removeConstraint:topConstraint];
topConstraint = nil;
[self removeConstraint:leftConstraint];
leftConstraint = nil;
[self removeConstraint:bottomConstraint];
bottomConstraint = nil;
[self removeConstraint:rightConstraint];
rightConstraint = nil;
}
The system will automatically call updateConstraints before doing layout and drawing if somebody has called setNeedsUpdateConstraints, which we did in setBorderSize:. In updateConstraints, we'll create the constraints if necessary, and update their constants based on borderSize. We also tell the system not to translate the autoresizing masks into constraints, because that tends to create unsatisfiable constraints.
- (void)updateConstraints {
self.translatesAutoresizingMaskIntoConstraints = NO;
self.contentView.translatesAutoresizingMaskIntoConstraints = NO;
[super updateConstraints];
if (!topConstraint) {
[self createContentViewConstraints];
}
topConstraint.constant = _borderSize.top;
leftConstraint.constant = _borderSize.left;
bottomConstraint.constant = -_borderSize.bottom;
rightConstraint.constant = -_borderSize.right;
}
All four constraints are created the same way, so we'll use a helper method:
- (void)createContentViewConstraints {
topConstraint = [self constrainContentViewAttribute:NSLayoutAttributeTop];
leftConstraint = [self constrainContentViewAttribute:NSLayoutAttributeLeft];
bottomConstraint = [self constrainContentViewAttribute:NSLayoutAttributeBottom];
rightConstraint = [self constrainContentViewAttribute:NSLayoutAttributeRight];
}
- (NSLayoutConstraint *)constrainContentViewAttribute:(NSLayoutAttribute)attribute {
NSLayoutConstraint *constraint = [NSLayoutConstraint constraintWithItem:_contentView attribute:attribute relatedBy:NSLayoutRelationEqual toItem:self attribute:attribute multiplier:1 constant:0];
[self addConstraint:constraint];
return constraint;
}
#end
I have put a complete working example in this git repository.
For future reference, you can override NSView.alignmentRectInsets to affect the position of the layout guides:
Custom views that draw ornamentation around their content can override
this property and return insets that align with the edges of the
content, excluding the ornamentation. This allows the constraint-based
layout system to align views based on their content, rather than just
their frame.
Link to documentation:
https://developer.apple.com/documentation/appkit/nsview/1526870-alignmentrectinsets
Have you tried setting the intrinsic size to include the border size?
- (NSSize)intrinsicContentSize
{
return NSMakeSize(width+bordersize, height+bordersize);
}
then you would set the content compression resistance priorities in both directions to be required:
[self setContentCompressionResistancePriority:NSLayoutPriorityRequired forOrientation:NSLayoutConstraintHorizontal];
[self setContentCompressionResistancePriority:NSLayoutPriorityRequired forOrientation:NSLayoutConstraintVertical];
The one solution I found is to overload the addConstraint: method and modify constraints before they'll be added:
- (void)addConstraint:(NSLayoutConstraint *)constraint
{
if(constraint.firstItem == self || constraint.secondItem == self) {
if(constraint.firstAttribute == NSLayoutAttributeLeading) {
constraint.constant += self.leftBorderWidth;
} else if (constraint.firstAttribute == NSLayoutAttributeTrailing) {
constraint.constant += self.rightBorderWidth;
} else if (constraint.firstAttribute == NSLayoutAttributeTop) {
constraint.constant += self.topBorderWidth;
} else if (constraint.firstAttribute == NSLayoutAttributeBottom) {
constraint.constant += self.bottomBorderWidth;
}
}
[super addConstraint:constraint];
}
And then also handle this constraints in xxxBorderWidth setters.

Create NSScrollView Programmatically in an NSView - Cocoa

I have an NSView class which takes care of a Custom View created in the nib file.
Now I want to add an NSScrollView to the custom view, but I need to do it programmatically and not using Interface Builder (Embed Into Scroll View).
I have found this code:
NSView *windowContentView = [mainWindow contentView];
NSRect windowContentBounds = [windowContentView bounds];
scrollView = [[NSScrollView alloc] init];
[scrollView setBorderType:NSNoBorder];
[scrollView setHasVerticalScroller:YES];
[scrollView setBounds: windowContentBounds];
[windowContentView addSubview:scrollView];
Assuming I declare as IBOutlets the variables 'mainWindow' and 'scrollView' above, how would I go about connecting them to the proper components in Interface Builder? Does it make any sense to do it this way?
Or is there a better way to add a scroll view programmatically?
P.S. I cannot connect them in the usual way because I cannot create an NSObject Object from Interface Builder, or use the File Owner..
I had difficulty creating NSScrollView with AutoLayout programmatically but finally got it to work. This is a Swift version.
// Initial scrollview
let scrollView = NSScrollView()
scrollView.translatesAutoresizingMaskIntoConstraints = false
scrollView.borderType = .noBorder
scrollView.backgroundColor = NSColor.gray
scrollView.hasVerticalScroller = true
window.contentView?.addSubview(scrollView)
window.contentView?.addConstraints(NSLayoutConstraint.constraints(withVisualFormat: "H:|[scrollView]|", options: [], metrics: nil, views: ["scrollView": scrollView]))
window.contentView?.addConstraints(NSLayoutConstraint.constraints(withVisualFormat: "V:|[scrollView]|", options: [], metrics: nil, views: ["scrollView": scrollView]))
// Initial clip view
let clipView = NSClipView()
clipView.translatesAutoresizingMaskIntoConstraints = false
scrollView.contentView = clipView
scrollView.addConstraint(NSLayoutConstraint(item: clipView, attribute: .left, relatedBy: .equal, toItem: scrollView, attribute: .left, multiplier: 1.0, constant: 0))
scrollView.addConstraint(NSLayoutConstraint(item: clipView, attribute: .top, relatedBy: .equal, toItem: scrollView, attribute: .top, multiplier: 1.0, constant: 0))
scrollView.addConstraint(NSLayoutConstraint(item: clipView, attribute: .right, relatedBy: .equal, toItem: scrollView, attribute: .right, multiplier: 1.0, constant: 0))
scrollView.addConstraint(NSLayoutConstraint(item: clipView, attribute: .bottom, relatedBy: .equal, toItem: scrollView, attribute: .bottom, multiplier: 1.0, constant: 0))
// Initial document view
let documentView = NSView()
documentView.translatesAutoresizingMaskIntoConstraints = false
scrollView.documentView = documentView
clipView.addConstraint(NSLayoutConstraint(item: clipView, attribute: .left, relatedBy: .equal, toItem: documentView, attribute: .left, multiplier: 1.0, constant: 0))
clipView.addConstraint(NSLayoutConstraint(item: clipView, attribute: .top, relatedBy: .equal, toItem: documentView, attribute: .top, multiplier: 1.0, constant: 0))
clipView.addConstraint(NSLayoutConstraint(item: clipView, attribute: .right, relatedBy: .equal, toItem: documentView, attribute: .right, multiplier: 1.0, constant: 0))
// Subview1
let view1 = NSView()
view1.translatesAutoresizingMaskIntoConstraints = false
view1.wantsLayer = true
view1.layer?.backgroundColor = NSColor.red.cgColor
documentView.addSubview(view1)
documentView.addConstraints(NSLayoutConstraint.constraints(withVisualFormat: "H:|[view1]|", options: [], metrics: nil, views: ["view1": view1]))
// Subview2
let view2 = NSView()
view2.translatesAutoresizingMaskIntoConstraints = false
view2.wantsLayer = true
view2.layer?.backgroundColor = NSColor.green.cgColor
documentView.addSubview(view2)
documentView.addConstraints(NSLayoutConstraint.constraints(withVisualFormat: "H:|[view2]|", options: [], metrics: nil, views: ["view2": view2]))
// Subview3
let view3 = NSView()
view3.translatesAutoresizingMaskIntoConstraints = false
view3.wantsLayer = true
view3.layer?.backgroundColor = NSColor.blue.cgColor
documentView.addSubview(view3)
documentView.addConstraints(NSLayoutConstraint.constraints(withVisualFormat: "H:|[view3]|", options: [], metrics: nil, views: ["view3": view3]))
// Vertical autolayout
documentView.addConstraints(NSLayoutConstraint.constraints(withVisualFormat: "V:|[view1(==100)][view2(==200)][view3(==300)]", options: [], metrics: nil, views: ["view1": view1, "view2": view2, "view3": view3]))
documentView.addConstraint(NSLayoutConstraint(item: documentView, attribute: .bottom, relatedBy: .equal, toItem: view3, attribute: .bottom, multiplier: 1.0, constant: 0))
This code fragment should demonstrate how to create an NSScrollView programmatically and use it to display any view, whether from a nib or from code. In the case of a nib generated view, you simply need to load the nib file to your custom view prior, and have an outlet to your custom view (outletToCustomViewLoadedFromNib) made to File's Owner.
NSScrollView *scrollView = [[NSScrollView alloc] initWithFrame:[[mainWindow contentView] frame]];
// configure the scroll view
[scrollView setBorderType:NSNoBorder];
[scrollView setHasVerticalScroller:YES];
// embed your custom view in the scroll view
[scrollView setDocumentView:outletToCustomViewLoadedFromNib];
// set the scroll view as the content view of your window
[mainWindow setContentView:scrollView];
Apple has a guide on the subject, which I won't link to as it requires Apple Developer Connection access and their links frequently break. It is titled "Creating and Configuring a Scroll View" and can currently be found by searching for its title using Google.
Brian's answer is correct, here 's how to create NSStackView inside NSScrollView in Swift 4.2
See https://github.com/onmyway133/blog/issues/173
You might need to flip NSClipView
final class FlippedClipView: NSClipView {
override var isFlipped: Bool {
return true
}
}
private func setup() {
setupScrollView()
setupStackView()
}
private func setupScrollView() {
view.addSubview(scrollView)
scrollView.translatesAutoresizingMaskIntoConstraints = false
scrollView.hasVerticalScroller = true
scrollView.drawsBackground = false
NSLayoutConstraint.activate([
scrollView.leftAnchor.constraint(equalTo: view.leftAnchor),
scrollView.rightAnchor.constraint(equalTo: view.rightAnchor),
scrollView.topAnchor.constraint(equalTo: view.topAnchor),
scrollView.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: -30),
scrollView.heightAnchor.constraint(equalToConstant: 400)
])
let clipView = FlippedClipView()
clipView.drawsBackground = false
scrollView.contentView = clipView
clipView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
clipView.leftAnchor.constraint(equalTo: scrollView.leftAnchor),
clipView.rightAnchor.constraint(equalTo: scrollView.rightAnchor),
clipView.topAnchor.constraint(equalTo: scrollView.topAnchor),
clipView.bottomAnchor.constraint(equalTo: scrollView.bottomAnchor)
]
scrollView.documentView = stackView
stackView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
stackView.leftAnchor.constraint(equalTo: clipView.leftAnchor),
stackView.topAnchor.constraint(equalTo: clipView.topAnchor),
stackView.rightAnchor.constraint(equalTo: clipView.rightAnchor),
// NOTE: No need for bottomAnchor
])
}
private func setupStackView() {
stackView.orientation = .vertical
stackView.edgeInsets = NSEdgeInsets(top: 16, left: 16, bottom: 16, right: 16)
NSLayoutConstraint.activate([
myRowView.heightAnchor.constraint(equalToConstant: 40)
])
myRowView.onPress = { [weak self] in
self?.doSomething()
}
stackView.addArrangedSubview(myRowView)
}
Here is an example of NSStackView progrmatically added to NSScrollView. Any view can be added using the following solution but I am taking NSStackView as an example
I am using SnapKit to keep the auto layouting code concise, however, you can use anchors to add constraints without any 3rd party dependency
// Create NSScrollView and add it as a subview in the desired location
let scrollView = NSScrollView()
scrollView.borderType = .noBorder
scrollView.verticalScrollElasticity = .none
addSubview(scrollView)
scrollView.snp.makeConstraints { $0.edges.equalToSuperview() } //match edges to superview
// Assign an instance of NSClipView to `contentView` property of `NSScrollView`
let clipView = NSClipView()
scrollView.contentView = clipView
clipView.snp.makeConstraints { $0.edges.equalTo(scrollView) }
// Assign whatever view you want to put inside scroll view to the `documentView` property.
// Also note I have added just 3 constraints; top bottom and left. That way stackview can freely expand on the right
scrollView.documentView = stackView
stackView.snp.makeConstraints { $0.top.bottom.left.equalTo(clipView) }
Note that I am not using translatesAutoresizingMaskIntoConstraints after adding a subview because SnapKit handles it internally but if you are adding constraints using anchors then ensure translatesAutoresizingMaskIntoConstraints is set to false for all the subviews that are added programatically.

Highlighting a NSMenuItem with a custom view?

I have created a simple NSStatusBar with a NSMenu set as the menu. I have also added a few NSMenuItems to this menu, which work fine (including selectors and highlighting) but as soon as I add a custom view (setView:) no highlighting occurs.
CustomMenuItem *menuItem = [[CustomMenuItem alloc] initWithTitle:#"" action:#selector(openPreferences:) keyEquivalent:#""];
[menuItem foo];
[menuItem setTarget:self];
[statusMenu insertItem:menuItem atIndex:0];
[menuItem release];
And my foo method is:
- (void)foo {
NSView *view = [[NSView alloc] initWithFrame:CGRectMake(5, 10, 100, 20)];
[self setView:view];
}
If I remove the setView method, it will highlight.
I have searched and searched and cannot find a way of implementing/enabling this.
Edit
I implemented highlight by following the code in this question in my NSView SubClass:
An NSMenuItem's view (instance of an NSView subclass) isn't highlighting on hover
#define menuItem ([self enclosingMenuItem])
- (void) drawRect: (NSRect) rect {
BOOL isHighlighted = [menuItem isHighlighted];
if (isHighlighted) {
[[NSColor selectedMenuItemColor] set];
[NSBezierPath fillRect:rect];
} else {
[super drawRect: rect];
}
}
Here's a rather less long-winded version of the above. It's worked well for me. (backgroundColour is an ivar.)
- (void)drawRect:(NSRect)rect
{
if ([[self enclosingMenuItem] isHighlighted]) {
[[NSColor selectedMenuItemColor] set];
} else if (backgroundColour) {
[backgroundColour set];
}
NSRectFill(rect);
}
Update for 2019:
class CustomMenuItemView: NSView {
private var effectView: NSVisualEffectView
override init(frame: NSRect) {
effectView = NSVisualEffectView()
effectView.state = .active
effectView.material = .selection
effectView.isEmphasized = true
effectView.blendingMode = .behindWindow
super.init(frame: frame)
addSubview(effectView)
effectView.frame = bounds
}
required init?(coder decoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func draw(_ dirtyRect: NSRect) {
effectView.isHidden = !(enclosingMenuItem?.isHighlighted ?? false)
}
}
Set one of those to your menuItem.view.
(Credit belongs to Sam Soffes who helped me figure this out and sent me almost that code verbatim.)
If you're adding a view to a menu item, that view has to draw the highlight itself. You don't get that for free, I'm afraid. From the Menu Programming Topics:
A menu item with a view does not draw its title, state, font, or other standard drawing attributes, and assigns drawing responsibility entirely to the view.
Yes, as mentioned earlier you must draw it yourself. I use AppKit's NSDrawThreePartImage(…) to draw, and also include checks to use the user's control appearance (blue or graphite.) To get the images, I just took them from a screenshot (if anyone knows a better way, please add a comment.) Here's a piece of my MenuItemView's drawRect:
// draw the highlight gradient
if ([[self menuItem] isHighlighted]) {
NSInteger tint = [[NSUserDefaults standardUserDefaults] integerForKey:#"AppleAquaColorVariant"];
NSImage *image = (AppleAquaColorGraphite == tint) ? menuItemFillGray : menuItemFillBlue;
NSDrawThreePartImage(dirtyRect, nil, image, nil, NO,
NSCompositeSourceOver, 1.0, [self isFlipped]);
}
else if ([self backgroundColor]) {
[[self backgroundColor] set];
NSRectFill(dirtyRect);
}
EDIT
Should have defined these:
enum AppleAquaColorVariant {
AppleAquaColorBlue = 1,
AppleAquaColorGraphite = 6,
};
These correspond to the two appearance options in System Preferences. Also, menuItemFillGray & menuItemFillBlue are just NSImages of the standard menu item fill gradients.