{"id":6833,"date":"2013-08-02T17:09:12","date_gmt":"2013-08-03T00:09:12","guid":{"rendered":"http:\/\/blog.xamarin.com\/?p=6833"},"modified":"2013-08-02T17:09:12","modified_gmt":"2013-08-03T00:09:12","slug":"autolayout-with-xamarin-mac","status":"publish","type":"post","link":"https:\/\/devblogs.microsoft.com\/xamarin\/autolayout-with-xamarin-mac\/","title":{"rendered":"AutoLayout with Xamarin.Mac"},"content":{"rendered":"<p>\t\t\t\tAs part of our (almost) weekly <a href=\"http:\/\/docs.xamarin.com\/releases\/studio\/xamarin.studio_4.1\/xamarin.studio_4.1\">preview releases<\/a> of the iOS designer, we recently revamped our property panel using native Cocoa widgets.<\/p>\n<p>This proved to be an interesting challenge on the layout side of things since, historically, Cocoa didn\u2019t offer any layout primitives like Gtk+ or WPF. And the property panel, being fairly dynamic in nature, needs to adapt to a lot of situations.<\/p>\n<p>This changed with Mac OS X Lion where AutoLayout (otherwise known as layout constraints) was introduced to provide a better tool to describe a user interface in terms of relationships between individual views.<\/p>\n<h2>An Introduction to Cocoa constraints<\/h2>\n<p>First, let\u2019s see exactly what a constraint is. A constraint is used to describe a characteristic (position or size) of a view with respect to a set of constants and, optionally, another view in the same hierarchy.<\/p>\n<p>For those of you who have done any mechanical engineering and CAD in their life, constraints should come naturally to you if you think of the Cocoa version as a simple rigid joint between elements.<\/p>\n<p>The root of the constraint system is the simple linear equation:<\/p>\n<p align=\"center\"><code>view1.attr = m \u22c5 view2.attr + c<\/code><\/p>\n<p>If a constraint doesn&#8217;t depend on another view, the equation will simply reduce to:<\/p>\n<p align=\"center\"><code>view1.attr = c<\/code><\/p>\n<p>The job of the constraint system is to gather all those equation in a system and solve it (i.e. set each view Frame property) so that they are all respected.<\/p>\n<p>Of course, living in the real world, it may happen that resolution is impossible because a set of constraints collides with others. In that case, the layout is declared ambiguous and the constraint system will repeatedly retry the process after removing one of the offending constraints until it reaches a stable system.<\/p>\n<p>Another simpler, and more visual, way to understand Cocoa constraint is that it\u2019s a way to say \u201cI want this view\u2019s left edge to be 13 pixels from the right side of this other view.&#8221;<\/p>\n<p>If you want to further &#8220;see&#8221; constraints, AppKit provides a way to programmatically visualize constraints in any of your window by calling the NSWindow\u2019s method <a href=\"http:\/\/macapi.xamarin.com\/?link=M%3aMonoMac.AppKit.NSWindow.VisualizeConstraints\">VisualizeConstraints<\/a> with an array of constraints.<\/p>\n<p>This is, for instance, what you would see if you enabled this with the new designer property panel:<\/p>\n<p><a href=\"\/wp-content\/uploads\/sites\/44\/2019\/04\/Capture-d\u2019\u00e9cran-2013-08-02-\u00e0-15.07.19.png\"><img decoding=\"async\" class=\"aligncenter size-medium wp-image-6841\" alt=\"Capture d\u2019\u00e9cran 2013-08-02 \u00e0 15.07.19\" src=\"\/wp-content\/uploads\/sites\/44\/2019\/04\/Capture-d\u2019\u00e9cran-2013-08-02-\u00e0-15.07.19-154x300.png\" width=\"154\" height=\"300\" \/><\/a><\/p>\n<p>Further Apple documentation links:<\/p>\n<ul>\n<li><a href=\"https:\/\/developer.apple.com\/library\/mac\/#documentation\/UserExperience\/Conceptual\/AutolayoutPG\/Articles\/Introduction.html\">About AutoLayout<\/a><\/li>\n<li><a href=\"https:\/\/developer.apple.com\/library\/mac\/#documentation\/UserExperience\/Conceptual\/AutolayoutPG\/Articles\/constraintFundamentals.html#\/\/apple_ref\/doc\/uid\/TP40010853-CH2-SW1\">What are constraints<\/a><\/li>\n<\/ul>\n<p><!--more--><\/p>\n<h2>Writing constraints<\/h2>\n<h3>The Basics<\/h3>\n<p>Constraints are created using the <code>NSLayoutConstraint<\/code> class. To instantiate it you have either the option to use the <code>Create<\/code> method (where you pass every parameter of the constraint) or an ASCII-based DSL.<\/p>\n<p>I tend to prefer the first option just because our .NET wrapping makes it more readable than the other version (and we will see that we can do better anyway).<\/p>\n<p>When you have created your set of constraints, you can use the <code>AddConstraint<\/code> (1 parameter constraint) or <code>AddConstraints<\/code> (constraint array parameter) from <code>NSView<\/code> to commit the constraint to the layout system. If the constraint involves the view container, you would usually add them to the latter.<\/p>\n<p>If you want to remove constraints, you can use the equivalent Remove* calls or simply remove the <code>NSView<\/code> from its parent which will automatically dispose the constraints that were attached to it.<\/p>\n<p>Following is an example of adding two views to a superview and constraining them to display one below the other inside their container.<\/p>\n<pre class=\"lang:csharp decode:true\">\nvar mainView = ...;\nvar view1 = new CustomView ();\nvar view2 = new CustomView ();\n\n\/\/ View-level constraints to set constant size values\nview1.AddConstraints (new[] {\n    NSLayoutConstraint.Create (view1, NSLayoutAttribute.Height, NSLayoutRelation.Equal, null, NSLayoutAttribute.NoAttribute, 1, 14),\n    NSLayoutConstraint.Create (view1, NSLayoutAttribute.Width, NSLayoutRelation.Equal, null, NSLayoutAttribute.NoAttribute, 1, 80),\n});\nview2.AddConstraints (new[] {\n    NSLayoutConstraint.Create (view2, NSLayoutAttribute.Height, NSLayoutRelation.Equal, null, NSLayoutAttribute.NoAttribute, 1, 11),\n    NSLayoutConstraint.Create (view2, NSLayoutAttribute.Width, NSLayoutRelation.Equal, null, NSLayoutAttribute.NoAttribute, 1, 80),\n});\n\nmainView.AddSubview (view1);\nmainView.AddSubview (view2);\n\n\/\/ Container view-level constraints to set the position of each subview\nmainView.AddConstraints (new[] {\n    NSLayoutConstraint.Create (view1, NSLayoutAttribute.Left, NSLayoutRelation.Equal, mainView, NSLayoutAttribute.Left, 1, 10),\n    NSLayoutConstraint.Create (view1, NSLayoutAttribute.Top, NSLayoutRelation.Equal, mainView, NSLayoutAttribute.Top, 1, 10),\n\n    NSLayoutConstraint.Create (view2, NSLayoutAttribute.Left, NSLayoutRelation.Equal, mainView, NSLayoutAttribute.Left, 1, 10),\n    NSLayoutConstraint.Create (view2, NSLayoutAttribute.Top, NSLayoutRelation.Equal, view1, NSLayoutAttribute.Bottom, 1, 5),\n});\n<\/pre>\n<h3>Adding some Sugar<\/h3>\n<p>Using constraints like this is manageable, but you will soon realize that you end up repeating the same constraints over and over again\u2014either in the form of vertically\/horizontally aligned rows of elements or to make a subview fill its container.<\/p>\n<p>As such, I wrote a little set of extensions methods that lets you do these common tasks with a lighter syntax:<\/p>\n<pre class=\"lang:csharp decode:true\">\n\/\/ Constraining a button inside a container with 30 pixels padding\ncontainer.AddSubview (button);\n\n\/\/ Using the flexibility of Linq expressions\ncontainer.DoConstraints (\n       button.ConstraintTo (container, (b, c) =&gt; b.Left == c.Left + 30),\n       button.ConstraintTo (container, (b, c) =&gt; b.Right == c.Right - 30),\n       button.ConstraintTo (container, (b, c) =&gt; b.Top == c.Top - 30),\n       button.ConstraintTo (container, (b, c) =&gt; b.Bottom == c.Bottom + 30)\n);\n\n\/\/ Or even simpler\ncontainer.AddConstraints (button.ConstraintFill (container, 30));\n<\/pre>\n<p>You can see the underlying code <a href=\"https:\/\/gist.github.com\/garuma\/3de3bbeb954ad5679e87\">in this Gist<\/a>. Feel free to take inspiration from it to write your own helper.<\/p>\n<p>Finally, know that with Maverick, Apple is finally addressing some of those common constraint patterns by introducing the <code>NSStackView<\/code> class, although it will be several releases until this is generally available to users.<\/p>\n<h3>Constraints resolution<\/h3>\n<p>Constraints are resolved in a special layout phase called just before the view hierarchy is drawn to the screen. After that phase, each view involved will have its <code>Frame<\/code> and <code>Bounds<\/code> properties set to the right values (which means you shouldn\u2019t set those manually anymore).<\/p>\n<p>In some cases, however, you want to access these values earlier for animation purposes or to apply some height-for-width strategy\u2014like wrapping a text field with long content depending on the final width of its container.<\/p>\n<p>In that scenario, you can \u201cforce\u201d evaluation of a subset of your <code>NSView<\/code> hierarchy constraints by calling the view\u2019s <a href=\"http:\/\/macapi.xamarin.com\/?link=M%3aMonoMac.AppKit.NSView.LayoutSubtreeIfNeeded\">LayoutSubtreeIfNeeded<\/a> method or globally by calling <code>NSWindow<\/code> <a href=\"http:\/\/macapi.xamarin.com\/?link=M%3aMonoMac.AppKit.NSWindow.LayoutIfNeeded\">LayoutIfNeeded<\/a> method.<\/p>\n<h2>An example, expander using constraints<\/h2>\n<p>We are going to see how the expander used in the property panel was implemented with constraints.<\/p>\n<p align=\"center\"><video width=\"349\" height=\"674\" preload=\"none\" controls=\"controls\" poster=\"\/wp-content\/uploads\/sites\/44\/2019\/04\/blog_post_constraint_poster.jpg\"><\/video><\/p>\n<p>Here is the source code of the expander:<\/p>\n<pre class=\"lang:csharp decode:true\">\nusing System;\nusing System.Drawing;\nusing MonoMac.AppKit;\nusing MonoMac.ObjCRuntime;\nusing MonoMac.Foundation;\nusing MonoMac.CoreAnimation;\n\nnamespace Xamarin.Mac.Examples\n{\n    public class Expander : NSView\n    {\n        const int HeaderHeight = 22;\n\n        NSGradient backgroundGradient;\n        NSColor strokeColor;\n\n        NSTextField label;\n        NSView childView;\n\n        NSLayoutConstraint heightConstraint;\n        NSTrackingArea trackingArea;\n\n        bool expanded = true;\n        float height;\n        int heightFluctuating;\n\n        \/\/ This ctor is used by CoreAnimation\n        public ReactiveExpander (IntPtr handle) : base (handle)\n        {\n        }\n\n        public ReactiveExpander ()\n        {\n            label = new NSTextField {\n                Bordered = false,\n                Editable = false,\n                DrawsBackground = false,\n                Bezeled = false,\n                TextColor = NSColor.FromDeviceRgba (.21f, .21f, .21f, 1),\n                TranslatesAutoresizingMaskIntoConstraints = false\n            };\n            label.Cell.Font = NSFont.BoldSystemFontOfSize (10);\n\n            AddSubview (label);\n\n            AddConstraints (new NSLayoutConstraint[] {\n                NSLayoutConstraint.Create (label, NSLayoutAttribute.Left, NSLayoutRelation.Equal, this, NSLayoutAttribute.Left, 1, 10),\n                NSLayoutConstraint.Create (label, NSLayoutAttribute.CenterY, NSLayoutRelation.Equal, this, NSLayoutAttribute.Top, 1, HeaderHeight \/ 2),\n            });\n\n            backgroundGradient = new NSGradient (NSColor.FromCalibratedRgba (0.80f, 0.81f, 0.855f, 1.0f),\n                                                 NSColor.FromCalibratedRgba (0.94f, 0.94f, 0.95f, 1.0f));\n            strokeColor = NSColor.FromCalibratedRgba (0.70f, 0.70f, 0.70f, 1.0f);\n\n            UpdateTrackingAreas ();\n        }\n\n        public override void UpdateTrackingAreas ()\n        {\n            if (trackingArea != null)\n                RemoveTrackingArea (trackingArea);\n            var area = Bounds;\n            area.Height = HeaderHeight;\n            trackingArea = new NSTrackingArea (area, NSTrackingAreaOptions.ActiveInKeyWindow | NSTrackingAreaOptions.MouseEnteredAndExited, this, null);\n            AddTrackingArea (trackingArea);\n        }\n\n        public override void MouseUp (NSEvent theEvent)\n        {\n            Expanded = !Expanded;\n        }\n\n        public override void DrawRect (RectangleF dirtyRect)\n        {\n            var headerFrame = new RectangleF (PointF.Empty, new SizeF (Bounds.Width, HeaderHeight));\n            backgroundGradient.DrawInRect (headerFrame, -90);\n            strokeColor.SetFill ();\n            NSGraphics.RectFill (new RectangleF (0, 0, Bounds.Width, 1));\n            NSGraphics.RectFill (new RectangleF (0, HeaderHeight - 1, Bounds.Width, 1));\n        }\n\n        public override SizeF IntrinsicContentSize {\n            get {\n                return new SizeF (-1, HeaderHeight);\n            }\n        }\n\n        public bool Expanded {\n            get {\n                return expanded;\n            }\n            set {\n                expanded = value;\n                if (childView != null) {\n                    if (!expanded &amp;&amp; heightFluctuating == 0)\n                        height = Bounds.Height;\n                    heightFluctuating++;\n                    NSAnimationContext.RunAnimation (ctx =&gt; {\n                        ctx.TimingFunction = CAMediaTimingFunction.FromName (CAMediaTimingFunction.EaseOut);\n                        ((NSView)childView.Animator).AlphaValue = !expanded ? 0 : 1;\n                        ((NSLayoutConstraint)heightConstraint.Animator).Constant = expanded ? HeaderHeight : -height + HeaderHeight;\n                    }, () =&gt; heightFluctuating--);\n                }\n            }\n        }\n\n        public string Label {\n            get {\n                return label.StringValue;\n            }\n            set {\n                label.StringValue = value;\n            }\n        }\n\n        public override bool IsFlipped {\n            get {\n                return true;\n            }\n        }\n\n        public NSView ChildView {\n            get {\n                return childView;\n            }\n            set {\n                if (childView != null &amp;&amp; value != null)\n                    ReplaceSubviewWith (childView, value);\n                else if (childView == null &amp;&amp; value != null)\n                    AddSubview (value);\n                else if (childView != null &amp;&amp; value == null)\n                    childView.RemoveFromSuperview ();\n\n                childView = value;\n                childView.TranslatesAutoresizingMaskIntoConstraints = false;\n                if (value != null) {\n                    AddConstraints (new NSLayoutConstraint[] {\n                        NSLayoutConstraint.Create (childView, NSLayoutAttribute.Top, NSLayoutRelation.Equal, this, NSLayoutAttribute.Top, 1, HeaderHeight),\n                        NSLayoutConstraint.Create (childView, NSLayoutAttribute.Left, NSLayoutRelation.Equal, this, NSLayoutAttribute.Left, 1, 0),\n                        NSLayoutConstraint.Create (childView, NSLayoutAttribute.Width, NSLayoutRelation.Equal, this, NSLayoutAttribute.Width, 1, 0),\n                        (heightConstraint = NSLayoutConstraint.Create (this, NSLayoutAttribute.Height, NSLayoutRelation.GreaterThanOrEqual, childView, NSLayoutAttribute.Height, 1, HeaderHeight))\n                    });\n                }\n            }\n        }\n    }\n}\n<\/pre>\n<p>The idea of this code is to host two views, a text field as a header label and a child view with the content. The header is painted as a subset of our bounds with a gradient and border in DrawRect.<\/p>\n<p>These subviews are constrained to fit inside the container view, one below the other. The only part where the container uses its child view is for the total height of the widget, which is deemed equal to the height of our child view and the additional padding necessary to draw the header.<\/p>\n<p>After <code>NSLayoutConstraint<\/code> objects are created, the only aspect that can be changed without having to remove the constraint and create another one is its <code>Constant<\/code> parameter.<\/p>\n<p>Thus, the idea is to use this with our vertical constraint by changing the constant to minus the height of our child view (after constraints are resolved and the <code>Bounds<\/code> property returns a valid value) so that, thanks to normal NSView clipping, the child view effectively \u201ccollapses\u201d.<\/p>\n<p>There are several more points to highlight in this code:<\/p>\n<h3>TranslatesAutoresizingMaskIntoConstraints<\/h3>\n<p>When you start using constraints, you should make a habit of setting that property to false on any views that are involved with constraints.<\/p>\n<p>The goal of this property is to provide backward compatibility with the legacy autoresizing mask facility by automatically generating a pair of constraints mimicking the autoresizing mask set on a view.<\/p>\n<p>However, as soon as you start adding more constraints, these auto-generated ones will usually provoke conflicts in the resolution phase. The safest thing is to always disable them.<\/p>\n<h3>IntrinsicContentSize<\/h3>\n<p>This new property is used by the constraint system to advertise the intrinsic size of your views. The idea is that the value returned here corresponds to the minimum size required for your views to display its content properly.<\/p>\n<p>If you think of a button hosting an icon image and a label, for instance, its intrinsic content size is the minimum width\/height required to display both of those items on a single line with no padding applied.<\/p>\n<p>When returning an intrinsic size, you may also set one of the members to -1 to tell the system that the view has no specific intrinsic value in that direction.<\/p>\n<p>In our case, for instance, the expander must at least display its header (hence its intrinsic height) but doesn\u2019t care about its width (the text field we use internally will report its eventual intrinsic width itself).<\/p>\n<h3>Animations<\/h3>\n<p>As we saw earlier, the only component that can be changed later in a constraint is its <code>Constant<\/code> property.<\/p>\n<p>Since <code>NSLayoutConstraint<\/code> implements the <a href=\"http:\/\/developer.apple.com\/library\/mac\/#documentation\/Cocoa\/Reference\/NSAnimatablePropertyContainer_protocol\/Introduction\/Introduction.html#\/\/apple_ref\/occ\/intf\/NSAnimatablePropertyContainer\">NSAnimatablePropertyContainer protocol<\/a>, it has an <code>Animator<\/code> property that can be used to animate value changes to <code>Constant<\/code>.<\/p>\n<p>We use that facility in the <code>Expander<\/code> property setter to provide a smooth transition between the opening and closed state of our expander (while also changing the alpha value at the same time).\t\t<\/p>\n","protected":false},"excerpt":{"rendered":"<p>As part of our (almost) weekly preview releases of the iOS designer, we recently revamped our property panel using native Cocoa widgets. This proved to be an interesting challenge on the layout side of things since, historically, Cocoa didn\u2019t offer any layout primitives like Gtk+ or WPF. And the property panel, being fairly dynamic in [&hellip;]<\/p>\n","protected":false},"author":1925,"featured_media":39167,"comment_status":"closed","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"_acf_changed":false,"footnotes":""},"categories":[2],"tags":[6,4],"class_list":["post-6833","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-developers","tag-ios","tag-xamarin-platform"],"acf":[],"blog_post_summary":"<p>As part of our (almost) weekly preview releases of the iOS designer, we recently revamped our property panel using native Cocoa widgets. This proved to be an interesting challenge on the layout side of things since, historically, Cocoa didn\u2019t offer any layout primitives like Gtk+ or WPF. And the property panel, being fairly dynamic in [&hellip;]<\/p>\n","_links":{"self":[{"href":"https:\/\/devblogs.microsoft.com\/xamarin\/wp-json\/wp\/v2\/posts\/6833","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/devblogs.microsoft.com\/xamarin\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/devblogs.microsoft.com\/xamarin\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/xamarin\/wp-json\/wp\/v2\/users\/1925"}],"replies":[{"embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/xamarin\/wp-json\/wp\/v2\/comments?post=6833"}],"version-history":[{"count":0,"href":"https:\/\/devblogs.microsoft.com\/xamarin\/wp-json\/wp\/v2\/posts\/6833\/revisions"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/xamarin\/wp-json\/wp\/v2\/media\/39167"}],"wp:attachment":[{"href":"https:\/\/devblogs.microsoft.com\/xamarin\/wp-json\/wp\/v2\/media?parent=6833"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/xamarin\/wp-json\/wp\/v2\/categories?post=6833"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/xamarin\/wp-json\/wp\/v2\/tags?post=6833"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}