Swift Can't See ObjC Methods that Include Package Symbols

I am migrating code from an iOS project to a Swift package. I've hit a problem with Objective-C and Swift interoperability.

  • A Swift package has a Swift class -- Zebra -- that is marked public and @objc compatible.
  • An Xcode project depends on this Swift Package.
  • An ObjC class is in the Xcode project, AnimalFactory, includes a public method that references Zebra in the interface. This AnimalFactory class is included in the bridging header.
  • A Swift class in the Xcode project, AnimalPrinter, is a client of AnimalFactory.

The desired flow is: AnimalPrinter (Swift) asks AnimalFactory (Objc) for a Zebra (Swift).

The issue is that the AnimalPrinter in the Xcode project cannot "see" any methods in AnimalFactory that reference Zebra in the method signature. Autocomplete will not reveal this method and you will get a compile error if you use it.

The graphical diagram here should help illustrate this scenario more completely. Here is a small sample project that shows the error. Let me know if anyone understands what may be happening here or workarounds.

// Zebra.swift
// In Swift Package

@objcMembers
public class Zebra: NSObject {

    public override init() {}
    
    public func saySomething() -> String {
        return "bray"
    }
}
// AnimalFactory.h
// In Xcode Project

@class Zebra;

@interface AnimalFactory : NSObject

//Available to Objective-C and Swift Clients
-(void)printHeader;

//Only available to other Objective-C Clients
-(Zebra *)getZebra;

@end
// AnimalPrinter.swift
// In Xcode Project

import TestPackage

class AnimalPrinter {
    

    func print() {

        //Can Access Swift Package directly
        let zebra  = Zebra()

        let animalFactory = AnimalFactory()

        //Compiles ok
        animalFactory.printHeader()

        // Compile error
        animalFactory.getZebra()
    }
}
3 Likes

If you’re on Swift 5.8 or before, you likely need to #import <Zebra/Zebra-Swift.h> in the AnimalFactory header.

I'm currently using Swift 5.8.1 if that changes things. Should that import be available for Swift targets, like in my case? Here's the variations I've tried but none seem to work.

#import <TestPackage/TestPackage-Swift.h>
#import "TestPackage-Swift.h"

These do show up my derived data in folder in a few locations though. I can probably do something with Xcode header search paths but it seems I'm missing something.

./Index.noindex/Build/Intermediates.noindex/TestPackage.build/Debug/TestPackage.build/Objects-normal/arm64/TestPackage-Swift.h
./Index.noindex/Build/Intermediates.noindex/GeneratedModuleMaps/TestPackage-Swift.h
./Build/Intermediates.noindex/TestPackage.build/Debug/TestPackage.build/Objects-normal/arm64/TestPackage-Swift.h
./Build/Intermediates.noindex/GeneratedModuleMaps/TestPackage-Swift.h

@import TestPackage should work, we don't make the generated header itself available via search paths

Thanks but unfortunately I'm not able to get that to work.

In the sample project, adding @import TestPackage; to AnimalFactory.h leads to the compile error module 'TestPackage' not found. It always works in the implementation file though.

This has been my experience in the past too. Importing Objective-C packages seems ok. I believe the problem is specific to importing swift packages to ObjC headers.

Here is another thread on the issue. I don't believe there is a resolution there though.

Per this StackOverflow answer, there's a deterministic way to find the -Swift.h headers. You can directly import the package's Zebra-Swift.h header from build/GeneratedModules, by adding $(OBJROOT)/GeneratedModules to the [User] Header Search Paths. Once the linker can find it, a plain quote import, #import Zebra-Swift.h, should work.

This leaves me with some questions, though...

Why does it work this way? I'm sure there's a good reason - what's could go wrong if we use a workaround like the generated module maps?

As @gestrich said, adding @import Zebra in AnimalFactory.h causes a compiler error. (Module not found.) I don't know if it is possible or conceptually correct to include module map info in headers. (My build logs show -fmodule-map-path is passed to the .m files. Can we pass this to a header somehow?)

Summary

We have a way to expose the package's generated Swift header, but it seems... not ideal.

  1. Why is -Swift.h not exported by SPM packages? Do we break any safeguards by circumventing?
  2. How fragile is this workaround?
  3. Is there a more "correct" way to expose the module to an Obj-C header? Quote importing a framework header from another package seems like it could cause issues, but I can't put my finger on it.
  4. I have only tested this with @gestrich's local SPM package in the same Xcode project. I have no idea if this would work with remote projects, etc.

Updates:

The team I'm working with did some experiments to work around this challenge. We introduced Swift protocols that hide the Swift framework symbols. These wrappers seem really invasive to our core architecture so we are considering pivoting to using separate Xcode targets, rather than Swift Packages, to break up our code (part of modularization effort).

I did experience this same issue with the project I work on. You can overcome it by explicitly loading the modulemaps file of the Swift package's target through the OTHER_SWIFT_FLAGS build setting. Assuming you have a .xcconfig file (which's great in this use case, as you can add a comment to explain this weird logic), you can add this:

// SPM package modulemaps are not automatically available to ObjC header files while they are being parsed for the bridging
// header. In most cases this shouldn't be needed, because ObjC headers generally should forward-declare types rather than
// importing other headers. Until Swift 5.9, however, forward declared types are not imported to Swift.
//
// https://github.com/apple/swift-evolution/blob/main/proposals/0384-importing-forward-declared-objc-interfaces-and-protocols.md
//
// This overlaps several discussions of various relevancy in the Apple-sphere, but all of which help shed some light on the
// underlying issues. This is not strictly a bug. It is a mix of some missing features and "that's just how things work."
//
// https://bugs.swift.org/browse/SR-15154
// https://github.com/apple/swift-package-manager/issues/4531
// https://developer.apple.com/forums/thread/650935
// https://forums.swift.org/t/obj-c-package-with-swift-dependency-not-accessible-in-swift/63137
// https://developer.apple.com/forums/thread/120152
// https://pfandrade.me/blog/mixing-swift-objective-c-spm-and-static-frameworks/
//
OTHER_SWIFT_FLAGS = -Xcc "-fmodule-map-file=${OBJROOT}/GeneratedModuleMaps-${PLATFORM_NAME}/AnimalSwift.modulemap"

The root cause of the issue is thoroughly explained by my awesome colleague in the comment above. I did try it with your sample project and it worked. Once you load the modulemaps file, you can then uncomment your @import in the header file which will work.

1 Like

@mamouneyya I really appreciate the information. I am able to verify the same that this seems to work around the issue which is great.

My concern with any work-around is how stable it is. In other words, if we develop a lot of code with this work-around in mind (i.e. moving code our code into Swift packages), could an Xcode update break this, leaving no alternative but to refactor extensive code that no longer compiles.

That could be a major concern for a large engineering organization. This question is directed more to @NeoNacho. Is there a reason this option was not suggested already? Is it a surprise this works? I also have an Apple Feedback request where I'll pose the same question -- ideally any solution we use has some support by the swift/apple team.

Thanks again for this possible solution - I hope it is something we can leverage :+1:

@gestrich could you share your FB number? That way I could ask about it internally.

My understanding is that the forward declaration should work in Swift 5.9 because of the SE mentioned in the comment. I haven't tried it myself with Xcode 15 Beta though.

1 Like

FB12685463. Thanks.

We found this same issue occurs with one small change to the aforementioned setup. Move the Objective-C class to the Swift Package -- in an ObjC target that depends on the Swift target.

You will hit the same error in your Xcode project as before. Unfortunately, the module map solution does not seem to fix this case.

@NeoNacho We were going to go through our Apple representative but thought we should ask here first. Can you say whether this module map solution is "recommended" to work around this package issue? Our team was about to move a lot of Swift and ObjC code to Package targets. If the module map solution can't be recommended we will need to approach our modularization very differently (refactoring a lot of ObjC code to Swift).

cc @Max_Desiatov

Since I do not work on SwiftPM anymore at this point.

+1 to this issue. I also encountered it in the exact same environment as @gestrich describes.