Bug 30387 - Cannot customize NSLayoutConstraint.Description
Summary: Cannot customize NSLayoutConstraint.Description
Status: RESOLVED ANSWERED
Alias: None
Product: iOS
Classification: Xamarin
Component: Xamarin.iOS.dll ()
Version: XI 8.10
Hardware: PC Mac OS
: Normal normal
Target Milestone: Untriaged
Assignee: Bugzilla
URL:
Depends on:
Blocks:
 
Reported: 2015-05-24 01:29 UTC by Kent
Modified: 2015-06-02 04:15 UTC (History)
3 users (show)

Tags:
Is this bug a regression?: ---
Last known good build:


Attachments
A repro for the category recursion problem (9.43 KB, application/zip)
2015-05-26 21:16 UTC, Kent
Details


Notice (2018-05-24): bugzilla.xamarin.com is now in read-only mode.

Please join us on Visual Studio Developer Community and in the Xamarin and Mono organizations on GitHub to continue tracking issues. Bugzilla will remain available for reference in read-only mode. We will continue to work on open Bugzilla bugs, copy them to the new locations as needed for follow-up, and add the new items under Related Links.

Our sincere thanks to everyone who has contributed on this bug tracker over the years. Thanks also for your understanding as we make these adjustments and improvements for the future.


Please create a new report on Developer Community or GitHub with your current version information, steps to reproduce, and relevant error messages or log files if you are hitting an issue that looks similar to this resolved bug and you do not yet see a matching new report.

Related Links:
Status:
RESOLVED ANSWERED

Description Kent 2015-05-24 01:29:46 UTC
First of all, what I'm attempting may be a UIKit bug/limitation rather than a Xamarin one - that's something I'd like to know the answer to. The fact is, you'd use categories to achieve this in Obj-C, but that's not yet possible with Xamarin.

What I want to do is customize the Description returned by NSLayoutConstraint. My eventual goal is to replace strings like "UIKit.UILabel:0x7f739af0.bottom" with "nameLabel.bottom". This is for diagnostic/debug purposes only.

Since NSLayoutConstraint instances are created via static factory methods, it's not a simple case of subclassing it and overriding the Description property. And since Xamarin doesn't support categories, I can't simply "override" the Description property from outside the class itself.

Instead, I resorted to creating an adapter by subclassing NSLayoutConstraint, storing an inner NSLayoutConstraint instance, then overriding every single method and property (apart from Class and ClassHandle) to invoke the equivalent member of the inner instance. The only overridden member that does anything beyond that is Description, which customizes the message (currently by prefixing it with "FOO!".

I then create NSLayoutConstraint instances in the usual manner via the static factory method, then wrap them in my adapter class. The constraints I add to my views are of course the adapter instances, not the instances they wrap.

Whilst laborious, I thought this approach would work. Alas, weird things happen. For a view that works perfectly fine when using only the static factory methods, I get this output when using the adapter:

Unable to simultaneously satisfy constraints.
	Probably at least one of the constraints in the following list is one you don't want. Try this: (1) look at each constraint and try to figure out which you don't expect; (2) find the code that added the unwanted constraint or constraints and fix it. (Note: If you're seeing NSAutoresizingMaskLayoutConstraints that you don't understand, refer to the documentation for the UIView property translatesAutoresizingMaskIntoConstraints) 
(
    "FOO! <NSLayoutConstraint:0x800e1f90 UIKit.UILabel:0x7f739af0.bottom == UITableViewCellContentView:0x7f73a780.bottom - 8>"
)

Will attempt to recover by breaking constraint 
FOO! <NSLayoutConstraint:0x800e1f90 UIKit.UILabel:0x7f739af0.bottom == UITableViewCellContentView:0x7f73a780.bottom - 8

You can see the "FOO!" from my customized Description. Beyond that, nothing works. The above message makes no sense to me given that there are many constraints in my view. Also, none of the constraints have any effect. All controls are just stacked on top of each other.

So the question is: should this work? In the absence of category support, is there any means by which I can customize the Description of my NSLayoutConstraint instances?

Here follows the entire code for my adapter:

private class DebuggableLayoutConstraint : NSLayoutConstraint
{
    private readonly NSLayoutConstraint inner;

    public DebuggableLayoutConstraint(NSLayoutConstraint inner)
    {
        this.inner = inner;
    }

    protected DebuggableLayoutConstraint(NSObjectFlag t)
        : base(t)
    {
    }

    protected internal DebuggableLayoutConstraint(IntPtr handle)
        : base(handle)
    {
    }

    public override bool Active
    {
        [Export("isActive")]
        get
        { return this.inner.Active; }
        [Export("setActive:")]
        set
        { this.inner.Active = value; }
    }

    public override nfloat Constant
    {
        [Export("constant")]
        get
        { return this.inner.Constant; }
        [Export("setConstant:")]
        set
        { this.inner.Constant = value; }
    }

    public override NSLayoutAttribute FirstAttribute
    {
        [Export("firstAttribute")]
        get
        { return this.inner.FirstAttribute; }
    }

    public override NSObject FirstItem
    {
        [Export("firstItem", ArgumentSemantic.Assign)]
        get
        { return this.inner.FirstItem; }
    }

    public override nfloat Multiplier
    {
        [Export("multiplier")]
        get
        { return this.inner.Multiplier; }
    }

    public override float Priority
    {
        [Export("priority")]
        get
        { return this.inner.Priority; }
        [Export("setPriority:")]
        set
        { this.inner.Priority = value; }
    }

    public override NSLayoutRelation Relation
    {
        [Export("relation")]
        get
        { return this.inner.Relation; }
    }

    public override NSLayoutAttribute SecondAttribute
    {
        [Export("secondAttribute")]
        get
        { return this.inner.SecondAttribute; }
    }

    public override NSObject SecondItem
    {
        [Export("secondItem", ArgumentSemantic.Assign)]
        get
        { return this.inner.SecondItem; }
    }

    public override bool ShouldBeArchived
    {
        [Export("shouldBeArchived")]
        get
        { return this.inner.ShouldBeArchived; }
        [Export("setShouldBeArchived:")]
        set
        { this.inner.ShouldBeArchived = value; }
    }

    public override string Description
    {
        [Export("description")]
        get
        { return "FOO! " + this.inner.Description; }
    }

    [Export("addObserver:forKeyPath:options:context:")]
    public override void AddObserver(NSObject observer, NSString keyPath, NSKeyValueObservingOptions options, IntPtr context) =>
        this.inner.AddObserver(observer, keyPath, options, context);

    [Export("awakeFromNib")]
    public override void AwakeFromNib() => this.inner.AwakeFromNib();

    [Export("conformsToProtocol:"), Preserve]
    public override bool ConformsToProtocol(IntPtr protocol) =>
        this.inner.ConformsToProtocol(protocol);

    [Export("copy")]
    [return: Release]
    public override NSObject Copy() =>
        this.inner.Copy();

    public override string DebugDescription
    {
        [Export("debugDescription")]
        get
        { return this.inner.DebugDescription; }
    }

    [Export("didChange:valuesAtIndexes:forKey:")]
    public override void DidChange(NSKeyValueChange changeKind, NSIndexSet indexes, NSString forKey) =>
        this.inner.DidChange(changeKind, indexes, forKey);

    [Export("didChangeValueForKey:withSetMutation:usingObjects:")]
    public override void DidChange(NSString forKey, NSKeyValueSetMutationKind mutationKind, NSSet objects) =>
        this.inner.DidChange(forKey, mutationKind, objects);

    [Export("didChangeValueForKey:")]
    public override void DidChangeValue(string forKey) =>
        this.inner.DidChangeValue(forKey);

    protected override void Dispose(bool disposing)
    {
        if (disposing)
        {
            this.inner.Dispose();
        }
    }

    [Export("doesNotRecognizeSelector:")]
    public override void DoesNotRecognizeSelector(Selector sel) =>
        this.inner.DoesNotRecognizeSelector(sel);

    public override bool Equals(object obj) =>
        this.inner.Equals(obj);

    [Export("dictionaryWithValuesForKeys:")]
    public override NSDictionary GetDictionaryOfValuesFromKeys(NSString[] keys) =>
        this.inner.GetDictionaryOfValuesFromKeys(keys);

    public override int GetHashCode() =>
        this.inner.GetHashCode();

    [Export("hash")]
    public override nuint GetNativeHash() =>
        this.inner.GetNativeHash();

    public override void Invoke(Action action, double delay) =>
        this.inner.Invoke(action, delay);

    public override void Invoke(Action action, TimeSpan delay) =>
        this.inner.Invoke(action, delay);

    [Export("isEqual:")]
    public override bool IsEqual(NSObject anObject) =>
        this.inner.IsEqual(anObject);

    [Export("isKindOfClass:")]
    public override bool IsKindOfClass(Class aClass) =>
        this.inner.IsKindOfClass(aClass);

    [Export("isMemberOfClass:")]
    public override bool IsMemberOfClass(Class aClass) =>
        this.inner.IsMemberOfClass(aClass);

    public override bool IsProxy
    {
        [Export("isProxy")]
        get
        { return this.inner.IsProxy; }
    }

    [Export("mutableCopy")]
    [return: Release]
    public override NSObject MutableCopy() =>
        this.inner.MutableCopy();

    [Export("observeValueForKeyPath:ofObject:change:context:")]
    public override void ObserveValue(NSString keyPath, NSObject ofObject, NSDictionary change, IntPtr context) =>
        this.inner.ObserveValue(keyPath, ofObject, change, context);

    [Export("performSelector:")]
    public override NSObject PerformSelector(Selector aSelector) =>
        this.inner.PerformSelector(aSelector);

    [Export("performSelector:withObject:")]
    public override NSObject PerformSelector(Selector aSelector, NSObject anObject) =>
        this.inner.PerformSelector(aSelector, anObject);

    [Export("performSelector:withObject:withObject:")]
    public override NSObject PerformSelector(Selector aSelector, NSObject object1, NSObject object2) =>
        this.inner.PerformSelector(aSelector, object1, object2);

    [Export("performSelector:withObject:afterDelay:inModes:")]
    public override void PerformSelector(Selector selector, NSObject withObject, double afterDelay, NSString[] nsRunLoopModes) =>
        this.inner.PerformSelector(selector, withObject, afterDelay, nsRunLoopModes);

    [Export("performSelector:withObject:afterDelay:")]
    public override void PerformSelector(Selector selector, NSObject withObject, double delay) =>
        this.inner.PerformSelector(selector, withObject, delay);

    [Export("performSelector:onThread:withObject:waitUntilDone:")]
    public override void PerformSelector(Selector selector, NSThread onThread, NSObject withObject, bool waitUntilDone) =>
        this.inner.PerformSelector(selector, onThread, withObject, waitUntilDone);

    [Export("performSelector:onThread:withObject:waitUntilDone:modes:")]
    public override void PerformSelector(Selector selector, NSThread onThread, NSObject withObject, bool waitUntilDone, NSString[] nsRunLoopModes) =>
        this.inner.PerformSelector(selector, onThread, withObject, waitUntilDone, nsRunLoopModes);

    [Export("removeObserver:forKeyPath:")]
    public override void RemoveObserver(NSObject observer, NSString keyPath) =>
        this.inner.RemoveObserver(observer, keyPath);

    [Export("removeObserver:forKeyPath:context:")]
    public override void RemoveObserver(NSObject observer, NSString keyPath, IntPtr context) =>
        this.inner.RemoveObserver(observer, keyPath, context);

    [Export("respondsToSelector:")]
    public override bool RespondsToSelector(Selector sel) =>
        this.inner.RespondsToSelector(sel);

    public override nuint RetainCount
    {
        [Export("retainCount")]
        get
        { return this.inner.RetainCount; }
    }

    public override NSObject Self
    {
        [Export("self")]
        get
        { return this.inner.Self; }
    }

    [Export("setNilValueForKey:")]
    public override void SetNilValueForKey(NSString key) =>
        this.inner.SetNilValueForKey(key);

    [Export("setValue:forKey:")]
    public override void SetValueForKey(NSObject value, NSString key) =>
        this.inner.SetValueForKey(value, key);

    [Export("setValue:forKeyPath:")]
    public override void SetValueForKeyPath(NSObject value, NSString keyPath) =>
        this.inner.SetValueForKeyPath(value, keyPath);

    [Export("setValue:forUndefinedKey:")]
    public override void SetValueForUndefinedKey(NSObject value, NSString undefinedKey) =>
        this.inner.SetValueForUndefinedKey(value, undefinedKey);

    [Export("setValuesForKeysWithDictionary:")]
    public override void SetValuesForKeysWithDictionary(NSDictionary keyedValues) =>
        this.inner.SetValuesForKeysWithDictionary(keyedValues);

    public override Class Superclass
    {
        [Export("superclass")]
        get
        { return this.inner.Superclass; }
    }

    public override string ToString() =>
        this.inner.ToString();

    [Export("valueForKey:")]
    public override NSObject ValueForKey(NSString key) =>
        this.inner.ValueForKey(key);

    [Export("valueForKeyPath:")]
    public override NSObject ValueForKeyPath(NSString keyPath) =>
        this.inner.ValueForKeyPath(keyPath);

    [Export("valueForUndefinedKey:")]
    public override NSObject ValueForUndefinedKey(NSString key) =>
        this.inner.ValueForUndefinedKey(key);

    [Export("willChange:valuesAtIndexes:forKey:")]
    public override void WillChange(NSKeyValueChange changeKind, NSIndexSet indexes, NSString forKey) =>
        this.inner.WillChange(changeKind, indexes, forKey);

    [Export("willChangeValueForKey:withSetMutation:usingObjects:")]
    public override void WillChange(NSString forKey, NSKeyValueSetMutationKind mutationKind, NSSet objects) =>
        this.WillChange(forKey, mutationKind, objects);

    [Export("willChangeValueForKey:")]
    public override void WillChangeValue(string forKey) =>
        this.inner.WillChangeValue(forKey);

    public override NSZone Zone
    {
        [Export("zone")]
        get
        { return this.inner.Zone; }
    }
}
Comment 1 Rolf Bjarne Kvinge [MSFT] 2015-05-25 07:01:05 UTC
We've added Category support in Xamarin.iOS 8.10: http://developer.xamarin.com/guides/ios/advanced_topics/registrar/#Categories

So just add this to your project:

> [Category (typeof (NSLayoutConstraint))]
> public class DebugConstraint
>     [Export ("description")]
>     public string Description (this NSLayoutConstraint self)
>     {
>         return "description";
>     }
>
>     [Export ("debugDescription")]
>     public string DebugDescription (this NSLayoutConstraint self)
>     {
>         return "debugDescription";
>     }
> }
Comment 2 Kent 2015-05-25 19:22:26 UTC
Sounds promising, but I just get this exception when I try that:

ObjCRuntime.RuntimeException: Cannot register the method '...Utility.DebugConstraint.Description' with the selector 'description' as a category method on '..._Utility_DebugConstraint' because the Objective-C already has an implementation for this selector.

Should I open another bug?
Comment 3 Rolf Bjarne Kvinge [MSFT] 2015-05-26 03:27:14 UTC
That's by design; if you create a (real) ObjC category method for an existing implementation, at runtime it's undetermined which implementation will be called, and by default we detect this when running in the simulator (and show this error).

If you want the undefined behavior instead, you can add "--registrar:static" to the additional mtouch arguments in the project's iOS Build options (this is the default for device builds, so you only need to do it for simulator builds). The downside is that app will take a little longer to build.
Comment 4 Kent 2015-05-26 07:01:50 UTC
Thanks Rolf. Tried that and it worked, but interestingly it doesn't work if you specify the mtouch arguments under "All Platforms". One must select "iPhoneSimulator" as the platform. This seems misleading to me - is it intentional?
Comment 5 Rolf Bjarne Kvinge [MSFT] 2015-05-26 07:04:31 UTC
That's probably a bug in Xamarin Studio with project configurations.
Comment 6 Kent 2015-05-26 07:05:26 UTC
Hmm, OK. But for the record it's VS2015 I'm using.
Comment 7 Kent 2015-05-26 07:36:34 UTC
Actually, I take back what I said about this all working. Whilst my category is now called correctly, I seemingly have no way of obtaining the original description. Dereferencing the Description property on the NSLayoutConstraint passed into my category results in a NullReferenceException.
Comment 8 Rolf Bjarne Kvinge [MSFT] 2015-05-26 07:48:21 UTC
Do you have an example of an ObjC implementation with categories?
Comment 9 Kent 2015-05-26 21:15:49 UTC
There is an ObjC example here (in the section called "Unsatisfiable Constraints"): http://www.objc.io/issue-3/advanced-auto-layout-toolbox.html

As you can see, they're just dereferencing super.description, which is effectively what I'm trying to do. However, when I do it I get a StackoverflowException (the NullReferenceException I mentioned above doesn't happen in my repro for some reason). I'll attach my repro...
Comment 10 Kent 2015-05-26 21:16:50 UTC
Created attachment 11349 [details]
A repro for the category recursion problem

Search for "XAMARIN:" in Code.cs to see a couple of comments I left for you.
Comment 11 Rolf Bjarne Kvinge [MSFT] 2015-05-27 05:42:27 UTC
The StackOverflowException is the expected behavior (you replaced the 'description' implementation with your own, so when you call 'description' you end up calling yourself recursively).

The ObjC implementation instead calls the implementation in the base (super) class (which is *not* the original NSLayoutConstraint implementation), and this can also be accomplished in C# (although it's somewhat cumbersome): https://gist.github.com/rolfbjarne/ca32d9a84f9e42f4dc3d
Comment 12 Kent 2015-05-27 05:53:56 UTC
That seems like the secret sauce I would need, but it still isn't working. Sorry, this is probably annoying you by now.

If I dereference Description on an NSLayoutConstraint without my category in place, it returns something like:

"<NSLayoutConstraint:0x8252a830 V:|-(8)-[UIImageView:0x809772c0]   (Names: '|':UITableViewCellContentView:0x82575660 )>"

But with my category in place, getting the description from the super just returns:

"<NSLayoutConstraint:0x8252a830>"

So it's missing the important parts of the message. This differs from what presumably happens with an ObjC implementation (at least, based on the code in the linked article).

Any thoughts?
Comment 13 Rolf Bjarne Kvinge [MSFT] 2015-05-27 06:37:08 UTC
The category implementation in the article calls 'super.description', which (now that I'm looking at it), will only call NSObject's 'description' method (iow the ObjC code will get the same result you're getting).

If you want to call the original NSLayoutConstraint implementation, you can't use a category, you'll need to use swizzling.

Here's an example: https://gist.github.com/rolfbjarne/9a0b0c0b7e8306d3ac58 (also add a call to DebugConstraint.Swizzle as the first line in your AppDelegate.FinishedLaunching method).
Comment 14 Kent 2015-05-27 06:59:08 UTC
Sheesh, TIL swizzling. This is far from the simple little tweak I set out to accomplish on a lazy Sunday afternoon.

That totally works though! Now my constraint error messages are actually understandable. I may need to blog about this, but will give you credit of course.

Thanks for going above and beyond, Rolf!
Comment 15 Rolf Bjarne Kvinge [MSFT] 2015-05-27 07:03:35 UTC
A blog post with C#/Xamarin code would be great!
Comment 16 Kent 2015-06-01 23:55:13 UTC
For others following in my footsteps, I have blogged about this (and more) here: http://kent-boogaart.com/blog/reducing-auto-layout-friction/
Comment 17 Rolf Bjarne Kvinge [MSFT] 2015-06-02 04:15:44 UTC
Thanks, that's a wonderful blog post!