Version 96 breaks Objective C plugins


We have a legacy CocoaScript plugin (we are really early integrators) and have been able to manage this over time by looking at the headers as Sketch progressed, and by reverse engineering what we needed to change. Whenever that was not enough, we’ve had a lot of excellent communication and help from Sketch (huge kudos to Ale Munoz!) as there was no official documentation after version 84.

Our plugin has stopped working since version 96 came out, and after some debugging we traced it to the removal of the class MSSliceTrimming. We were relying on a couple of functions from this class, namely trimSlice and trimmedRectForLayerAncestry to get the position and size of the trimmed slice from the (potentially bigger) layer.

Would someone from Sketch be able to point us to the right direction (which class/functions have replaced this functionality), as it seems that other communication channels might no longer be available? We need to get the position and size (CGRect) of the trimmed image that results from exporting a layer. Thanks in advance!

Hey, it’s nice to meet a fellow CocoaScript/Objective-C plugin developer here!

So MSSliceTrimming class is actually still available in Sketch 96, but it’s been renamed to SketchRendering.MSSliceTrimming (it’s a Swift class now I presume, thus the namespace):

// Dynamic class lookup trying both old and new names in case you
// need to supportolder Sketch versions as well
const SliceTrimmingClass = NSClassFromString("SketchRendering.MSSliceTrimming") ?? NSClassFromString("MSSliceTrimming")
let trimmed = SliceTrimmingClass.trimmedRectForLayerAncestry_(layer.ancestry())

As for +[MSSliceTrimming trimSlice:], I believe it would look like the following when implemented in CocoaScript:

function trimSlice(layer) {
  const SliceTrimmingClass = NSClassFromString("SketchRendering.MSSliceTrimming") ?? NSClassFromString("MSSliceTrimming")
  let trimmed = SliceTrimmingClass.trimmedRectForLayerAncestry_(layer.ancestry())
  return trimmed

Hope it helps!

Wow, I didn’t really hope to find anyone else still using CocoaScript :grinning:

I’ve tried the namespaced version of the class and you’re absolutely right, trimmedRectForLayerAncestry can be called like this!!

The downside is that the rect is still the full layer size. For reference, the example I use is a text layer that’s sized to be greater than the text itself, so the bottom margin gets cropped, and the text is center aligned, so it also crops a bit on each side on the exported image.

In the original code we first called trimSlice on the layer (returning void) which probably set the absolute bounding box internally, and then trimmedRectForLayerAncestry returned the new rect.

Thanks for the help though, at least we located one of the 2 functions!!

Right, it seems that something about whitespace trimming have changed in Sketch 96 and MSSliceTrimming class no longer provides accurate results that match the size of layer renditions exported by Sketch.

I’ve done a quick research and I think I found an alternative method that returns an expected fully trimmed rect for all kind of layers:

  1. First we start with an inexact rect from MSSliceTrimming API:
    let dirty = MSSliceTrimming.trimmedRectForLayerAncestry_(layer.ancestry())
  2. Next let’s pretend we’re going to export this layer via MSExporter with empty pixels trimmed:
    // This assumes that "layer" is already marked as exportable
    let request = MSExportRequest.exportRequestsFromLayerAncestry_(layer.ancestry()).firstObject()
    request.setShouldTrim_(true) // 👈 this is key
    let exporter = MSExporter.exporterForRequest_colorSpace_(request, nil)
  3. Now MSExported has this little useful property trimmedBounds which is basically a combination of 1) a real trimmed layer size and 2) a relative offset of this real trimmed frame within the layer’s untrimmed frame.
    let trimmedSize = exporter.trimmedBounds().size
    let trimmedOriginOffset = exporter.trimmedBounds().origin
  4. To get the final real trimmed layer frame we’d need a bit of math to combine the offset we just got with a rect we got earlier from MSSliceTrimming:
    let unalignedResult = NSMakeRect(dirty.origin.x + trimmedOriginOffset.x,
                                     dirty.origin.y + trimmedOriginOffset.y,
                                     trimmedSize.width, trimmedSize.height)
    // (Optional step) The calculated rectangle might not be pixel-aligned by default
    // (i.e. there could be non-integral coordinates or size values like 0.5 or 1.89,
    // and bitmap renditions can only start/end on a full pixel)
    let result = NSIntegralRectWithOptions(unalignedResult, NSAlignAllEdgesOutward)

I’ve tested this approach on a bunch of “trimmable” text layers and layer groups – and it seems to be returning the exact same frames Sketch 96 uses when rendering previews for those layers when they’re marked as exportable in the Inspector.

I still don’t really understand why the resulting offset of -[MSExporter trimmedBounds] should be added to the trimmed rect we got from MSSliceTrimming instead of -[MSLayer frame] directly tho, so there might be flaws in my understanding of this whole whitespace trimming machinery :man_shrugging: Please feel free to test it yourself and let me know what I’m missing!

1 Like

This is the same approach we used, but we didn’t think to explicitly call setShouldTrim because we outputted shouldTrim and it was already set to true :exploding_head: Just adding that one line made all the difference and it works.

Thanks so much for the input and help, it was a real life saver!

1 Like

On version 96, there is an issue where if a layer’s parent group has a drop shadow, the position and size are incorrect

If the projection is on the layer itself, the position and size are correct

I can’t quite reproduce this one, can you share a sample design with a problematic layer embedded into a group with a drop shadow?

I’ve tested the following case:

and the text layer’s absolute influence rect calculated with the code above matches the one used by Sketch itself when exporting this layer.