Ay!
So it took me a while to find a minimal reproducible scenario that triggers this error, but I think I got it:
- Create a âMyLibrary-light.sketchâ library with a single symbol named âItem/lightâ.
- Duplicate this library (via File > Duplicate in Sketch) to âMyLibrary-dark.sketchâ, rename the symbol to âItem/darkâ, make a minor change (color) so it looks different from the light version.
- Register both libraries with Sketch.
- Create a new âMyDocument.sketchâ document, insert an âItem/darkâ symbol there.
- Close the document and re-open it again.
- Run the following script:
const Document = require('sketch/dom').Document
const Library = require('sketch/dom').Library
let document = Document.getSelectedDocument()
let library = Library.getLibraries().find(library => {
return library.name == "MyLibrary-light"
})
document.getSymbols().forEach(symbol => {
symbol.sketchObject.foreignObject().setLibraryID(library.id)
symbol.sketchObject.foreignObject().setSourceLibraryName(library.name)
});
Note that the 5th step with re-opening the document is absolutely necessary here, otherwise you wonât be able to reproduce the problem.
Iâve also uploaded my test libraries and a sample document here for those following along at home:
Sketch101_foreignObject_setLibraryID.zip - Google Drive
My theory of whatâs happening
(Everything below is a pure speculation as I can only reverse-engineer Sketch so far with my limited skills and time constraints).
What might be happening in this case is an interesting combination of the following Sketch architecture aspects:
-
For performance and other reasons, Sketch maintains not one but two virtual layer trees per document:
[âŠ] the double tree structure we use in our model â a mutable tree that lets the UI layer easily make changes, backed by a lockless immutable model that we can safely pass between threads for doing things such as rendering the canvas across multiple threads.
Speeding up Symbols: how we improved performance in Sketch 67 · Sketch
In order to reduce memory footprint and to avoid extra work required to keep these two trees in sync, certain model objects in Sketch exist in a read-only/immutable form by default â while their mutable counterpart is just a lightweight proxy object. Such lightweight objects donât store any data themselves, but simply proxy certain calls to the read-only model.
This is until you actually try to modify one of these lightweight objects of course. As soon as you do that, Sketch converts this lightweight model to a full one by copying all of the data from its read-only counterpart and performing all other kinds of heavy object initialization procedures (I guess you can call this approach âcopy-on-writeâ?).
-
When a library (aka foreign) symbol is inserted into a document for the first time, Sketch creates a hidden copy of this symbol, and starts creating instances of that copy from now on. This, among other things, enables you to decouple your designs from your libraries by having âfrozenâ versions of your library symbols that are not updated automatically as the libraries evolve.
This internal frozen symbol copy is hidden from users and only exists deep in Sketch data structures. Sketch is free to update, delete and re-create this copy whenever it pleases: which is not only when e.g. you explicitly apply library changes via UI, but in other cases too â and one of these other cases is converting a lightweight foreign symbol model (the âforeignObjectâ in your code) to a full-weight one as described above.
Now with all this background theory in place, this is what I believe happens:
The first time you call âsetLibraryID()â on a âforeignObjectâ (which is actually a âforeign symbolâ) in your code, Sketch performs some heavy-lifting for it to become a full mutable object instead of a lightweight-one. As part of this initialization it generates a new hidden symbol copy of said library symbol.
As a result, the âsymbolâ object youâre currently holding a reference to is now obsolete and no longer referenced anywhere in Sketch: itâs been completely replaced by a newly generated copy.
This is why the next time you try to access its âforeignObjectâ property, it returns ânullâ, leading to the aforementioned error â it simply doesnât belong to any library symbol anymore. In other words, this âsymbolâ object is now stale and should be re-queried from the Sketch API.
Workaround
The only workaround I have is to make Sketch initialize all library symbols and their hidden local copies before doing any thing with those symbols:
// [...]
// WORKAROUND: let Sketch fully initialize all foreign model objects beforehand,
// (re)creating local symbols as needed
document.getSymbols().forEach(symbol => {
let foreignObject = symbol.sketchObject.foreignObject()
foreignObject.fireFaultIfNeeded()
// Alternative: any other no-op assignment like
// foreignObject.setLibraryID(foreignObject.libraryID());
// will also do. `fireFaultIfNeeded()` is just asking Sketch directly
})
document.getSymbols().forEach(symbol => {
if (/* need to switch source library */) {
symbol.sketchObject.foreignObject().setLibraryID(library.id);
symbol.sketchObject.foreignObject().setSourceLibraryName(library.name);
symbol.syncWithLibrary();
}
});
Note that I donât store the result of document.getSymbols()
in a variable because it will become stale after the first forEach loop (since the corresponding Sketch models will be gone) and youâll have to re-query it.
Hope it helps!