New Build System on Xcode 10 issue with Swift Product Module

With the new build system on Xcode 10, I'm running into an issue with our Swift classes. In our project we have our main app, a Today View Extension, and a Notification Service Extension.

We previously set our Product Module Name to the same value in all targets. We also use NSKeyedArchiver / NSKeyedUnarchiver to cache Swift objects that can be accessed by all targets using app groups. Since the Product Module Name was the same in all targets, NSKeyedUnarchiver worked since all Swift classes were prefixed with the same module.

However, on the new build system I receive an error when the Product Module Name is the same for embedded targets. I get the following:
:-1: duplicate output file '/[Product_Module_Name].swiftmodule/x86_64.swiftdoc' on task: Ditto [Product_Module_Name].swiftmodule/x86_64.swiftdoc [Product Module Name].swiftdoc (in target 'Notification Service Extension')
:-1: duplicate output file '[Product Module Name].swiftmodule/x86_64.swiftmodule' on task: Ditto [Product Mdoule Name].swiftmodule/x86_64.swiftmodule [Product Module Name].build/Dev Debug Production-iphonesimulator/Notification Service Extension.build/Objects-normal/x86_64/[Product Module Name].swiftmodule (in target 'Notification Service Extension')

I am able to build by setting a distinct Product Module Name for each target, but that breaks using NSKeyedUnarchiver across app groups. I get the following crash because the Swift namespace is now different across targets and it's unable to find the class.

Terminating app due to uncaught exception 'NSInvalidUnarchiveOperationException', reason: '*** -[NSKeyedUnarchiver decodeObjectForKey:]: cannot decode object of class ([Product Module Name].SharedEventHistoryItem) for key (NS.object.0); the class may be defined in source code or a library that is not linked'

How would you recommend resolving this issue? Thanks for any help you can provide!

I would solve this by putting the common classes into a framework. This has a couple of benefits:

  • The classes in the archive will reference the framework name, and thus be the same across all targets.

  • The code for these classes will be shared, reducing your app size.

Alternatively, NSKeyedArchiver has a bunch of infrastructure for renaming classes (+setClassName:forClass:, -setClassName:forClass:) and you could use those to keep the names consistent. But a framework seems like a better option to me.

Share and Enjoy

Quinn “The Eskimo!” @ DTS @ Apple

Since NSKeyedArchiver uses the class name that is registered with the Objective-C runtime, you can simply make sure that this name is the same across all your modules. You can do this by specifying a custom symbol name in the @objc attribute.

In your code, do this:

@objc(SharedEventHistoryItem)
class SharedEventHistoryItem: NSSecureCoding {
    …
}

This way, the Objective-C runtime knows this class by just the name SharedEventHistoryItem without the module name prefix.

Thank you very much for the quick replies @eskimo @marco.masser! I agree a framework sounds like a good option but this is a pretty huge project so that task is right now non-trivial.

I was trying to work with setClassName:forClass: and objc. Is there any options to do this globally for all Swift classes? Again, this is a big project and there's potential for newer Swift classes to get serialized using NSKeyedArchiver. Ideally, I don't want something you need to manually add because there's a large chance someone forgets to do that step.

I’m not aware of a “global” option to do something like this. You have to be explicit at some point.

That said, one pretty ugly hack that comes to mind is to drop the prefix while unarchiving. This assumes that all of your codebase uses the same NSKeyedUnarchiver and its delegate. The delegate could implement this method:

func unarchiver(_ unarchiver: NSKeyedUnarchiver, cannotDecodeObjectOfClassName name: String, originalClasses classNames: [String]) -> AnyClass? {
    // Edit: There’s an off-by-one error here, but you get the idea 🙄
    let nameWithoutModulePrefix = name.drop(while: { $0 != "." })
    guard !nameWithoutModulePrefix.isEmpty else {
        return nil
    }
    return NSClassFromString(String(nameWithoutModulePrefix))
}

I do not recommend doing it this way, but this should work.

Also, a note of advice when customizing NSKeyedUnarchiver: Do not subclass it, do everything through delegates. This class seems to have performance optimizations in place that only work when it is not subclassed.

Thanks @marco.masser! However, don't I actually need to do the opposite and call NSClassFromString with the correct modulePrefix? I was thinking of something similar, but I couldn't find an easy way to grab the module prefix in the current target.

It depends on what is in the archive. Of course it would be better if the module name prefixes were never in the archive at all. That’s what the @objc attribute allows you to do.

But then you wrote that you don’t want to manually add something that can be forgotten. Working from that, I figured you want something that unarchives correctly even if the class names in the archives have the module name prefixes. And that’s what my previous message describes.

I may be misunderstanding so my apologies for a potentially stupid question. Let's say I have two targets:

  1. TestApp
  2. TestTodayView

and I have already serialized the class "TestClass" using the TestApp module. So that class name would be TestApp.TestClass. When I try to deserialize the class in TestTodayView, I thought I would need to pass "TestTodayView.TestClass" to NSClassFromString. From the code above, it looks like it would just pass "TestClass".

Ah, I think I understand why we both got confused: The code I posted before assumes that the currently running code has the @objc(SharedEventHistoryItem) attribute in it (and therefore uses just that name in the Objective-C runtime), but the archive was written by an older version that did not have the attribute in it (i.e. the class name in the archive is prefixed with the module name).

Sorry, I should have pointed this out but didn’t.

1 Like