Skip to content

Instantly share code, notes, and snippets.

@sushihangover
Last active June 28, 2018 03:47
Show Gist options
  • Save sushihangover/327ba6282b2d41d8e10001b7e461bb21 to your computer and use it in GitHub Desktop.
Save sushihangover/327ba6282b2d41d8e10001b7e461bb21 to your computer and use it in GitHub Desktop.
Xamarin.iOS : Create a @dynamic property that can be used as a CoreAnimation / CALayer custom property within CAAction/CAAnimation
// https://developer.apple.com/library/content/documentation/Cocoa/Conceptual/ObjCRuntimeGuide/Articles/ocrtPropertyIntrospection.html#//apple_ref/doc/uid/TP40008048-CH101
public static bool Property(Type clsType, string propertyName, Type encodeType)
{
// far from perfect, but prevents "some" brain-farts and misuse...
// This should fixed to allow non-wrapped NSValue types (CGRect/Size, etc... consult Appendix C / Key-Value Coding Extensions)
if (!(encodeType.IsSubclassOf(typeof(NSObject)) || encodeType.IsValueType))
{
// Eric Lippert's comment: https://stackoverflow.com/questions/2742276/how-do-i-check-if-a-type-is-a-subtype-or-the-type-of-an-object#comment2771730_2742288
throw new Exception("Only NSObject subclasses and Value types supported");
}
var objClassName = clsType.GetCustomAttribute<RegisterAttribute>()?.Name;
if (string.IsNullOrEmpty(objClassName))
throw new Exception($"RegisterAttribute not found on {clsType.Name}");
var classHandle = Class.GetHandle(objClassName);
// hack!!! : This needs fixed to properly support all the CoreAnimation supported types
objc_property_attribute type;
if (encodeType.Name.StartsWith("NS", StringComparison.Ordinal))
type = new objc_property_attribute { Name = "T", Value = $"@\"{encodeType.Name}\"" };
else
{
string encodeValue;
switch (encodeType)
{
case Type nfloatType when nfloatType == typeof(nfloat):
encodeValue = "^f";
break;
default:
throw new Exception($"{encodeType.Name} is not currently supported, ping Robert/SushiHangover to add");
}
type = new objc_property_attribute { Name = "T", Value = encodeValue };
}
var retain = new objc_property_attribute { Name = "&", Value = "" };
var dynamic = new objc_property_attribute { Name = "D", Value = "" };
var attrs = new objc_property_attribute[] { type, retain, dynamic };
var result = class_addProperty(classHandle, propertyName, attrs, attrs.Length);
#if DEBUG
if (!result)
{
// failure is possible due to multilple reasons, but the main one is that this property keyPath already exists
// as we do not check that like new slick Xamarin.iOS registition process does ;-)
var props = class_copyPropertyList(classHandle);
string propStr = "";
foreach (var prop in props)
{
propStr += prop;
Console.WriteLine(prop);
}
throw new Exception($"class_addProperty Failed:{clsType.Name}/{propertyName}/{encodeType.Name}/{propStr}, yell at Robert/SushiHangover to fix");
}
#endif
return result;
}
//!!! Add @dynamic properties to your CALayer subclasses before instancing any of them or their owning UIViews
Dynamic.Property(
clsType: typeof(DigitalMixerMotorizedFaderLayer),
propertyName: DigitalMixerMotorizedFaderLayer.DynamicLevelKey,
encodeType: typeof(NSNumber)
);
@sushihangover
Copy link
Author

https://bugzilla.xamarin.com/show_bug.cgi?id=38823

RobertN 2017-10-31 16:48:15 UTC
I'm currently working on a project that has extensive use of custom CALayer properties for visual and non-visual CAActions. Other then writing all of these in ObjC, is there any work-around available to make this work (currently using Xamarin.iOS Version: 11.4.0.93).

Thanks, Robert
Comment 4 Rolf Bjarne Kvinge [MSFT] 2017-11-01 07:24:39 UTC
@robert, this is untested, but something like the following might work: https://gist.github.com/rolfbjarne/62149c28d755ef1f7c22cc76b6196b43. It might also be required to call WillChangeValue/DidChangeValue in the setter.

RobertN 2017-11-04 22:49:46 UTC

Rolf, Thank you for the reply.

I did not get the project, the client went with an ObjC/Swift solution to expedite things… :-(

But I did look at your gist, thanks, I have actually used set/get on the Ivar in Xamarin.iOS for different reasons but using a Xamarin.iOS Export/registered property will not work as CoreAnimation (and CoreData) is looking for @dynamic/NSManaged-based props that have no setter &| getter to determine what props are custom and that it should handle.

Is raining so I played around w/ ObjC and @Property and the produced ObjC property type encodings and CoreAnimiation will respond to (T@“NSNumber”,&,D) and ignore vars that have backing instance vars like (T@“NSNumber",&,N,V_foobar) or anything that has a setter or getter (G/S). It is fairly easy to create these properties in Xamarin.iOS at runtime:

https://gist.github.com/sushihangover/327ba6282b2d41d8e10001b7e461bb21

That is rough, but you get the idea ;-)

Using that, you have to register the @dynamic properties at runtime, such as:

    //!!! Add @dynamic properties to your CALayer subclasses before instancing any of them or their owning UIViews
Dynamic.Property(
	clsType: typeof(DigitalMixerMotorizedFaderLayer),
	propertyName: DigitalMixerMotorizedFaderLayer.DynamicLevelKey,
	encodeType: typeof(NSNumber)
);

As a cheap debug test, override the Layer's SetValueForUndefinedKey, and as long as is not being called with your dynamic keyPath, the property was added corrected and it is available to be "animated" (assuming that the type can be CoreAnimation handled (basically any numeric type but not things like NSDate, ...).

Since that works fine with CoreAnimiation/CALayer, I used your implementation idea and I went ahead and added it to the ObjCRuntime.DynamicRegistrar.cs and came up this with using a custom attribute (ExportDynamicAttribute), this is an example of the code the user writes as they are responsible for ValueForKey & SetValueForKey and also adding WillChangeValue & SetValueForKey if they need KVO support on these @dynamic props, etc...

public const string DynamicLevelKey = "motorizedVolumeLevel";
[ExportDynamic(DynamicLevelKey)]
public NSNumber MotorizedVolumeLevel
{
	get => (NSNumber)ValueForKey(new NSString(DynamicLevelKey));
	set
	{
		WillChangeValue(DynamicLevelKey); //!!! If you need KVO on the @dynamic property
		SetValueForKey(value, new NSString(DynamicLevelKey));
		DidChangeValue(DynamicLevelKey); //!!! If you need KVO on the @dynamic property
	}
}

I have not dived into the Xamarin.iOS static registration, so as a kludge I just add a resource bundle file that contains the classes to scan via ObjCRuntime.DynamicRegistrar to avoid scanning all classes so all the @dynamic properties are added before the app starts instancing any classes that contain them.

Works great for me, I was able to throw together an prototype that simulates a motorized mixing board by cascading @dynamic property values across the custom slider ( a UIVIew and their custom "layerClass:" CALayers that contain those @dynamic properties) to handle the channel's output audio volumes and the graphical slider positions and also link adjacent sliders to track the assigned volume curve as you override an individual channel's value (move one slider and they all ripple the changes to maintain the same assigned curve...).

Would love to see @dynamics supported out of the box in a future release ;-)

Thanks, Robert
Comment 6

Rolf Bjarne Kvinge [MSFT] 2017-11-06 14:33:24 UTC
Wow, you pretty much implemented this yourself, and already figured out solutions for the problems we would have run into too.

Great work!

Hopefully we'll be able to implement and support this soon.

Rolf, Thanks...

Learning how ObjC property type encoding works was cool but it was a really good CoreAnimation learning experience for me. I have found a lot of "misinformation" concerning usage of CALayer custom properties on non-Apple blog posts and StackOverflow concerning how these should (and can) be used. Highly recommend referring to the official docs in Apple's Core Animation Programming Guide first as I "learned" a lot of wrong assumptions by not reading that doc first (especially the appendixes) ;-)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment