How to access an internal Swift class in Objective-C within the same framework? - ios

Working on a mixed framework. imported inside the Obj-C file but the internal classes are not visible, only the public ones.
The documentation clearly states the internal clasees should be available between Swift and Obj-C:
Importing Swift into Objective-C To import a set of Swift files in the same framework target as your Objective-C code, you don’t
need to import anything into the umbrella header for the framework.
Instead, import the Xcode-generated header file for your Swift code
into any Objective-C .m file you want to use your Swift code from.
Because the generated header for a framework target is part of the
framework’s public interface, only declarations marked with the public
modifier appear in the generated header for a framework target. You
can still use Swift methods and properties that are marked with the
internal modifier from within the Objective-C part of your framework,
as long they are declared within a class that inherits from an
Objective-C class. For more information on access-level modifiers, see
Access Control in The Swift Programming Language (Swift 2).
Code Sample (Create a new project with a framework)
// SwiftObject.swift
public class SwiftObject: NSObject {
public class func doSomething() {}
}
internal class YetAnotherSwiftObject: NSObject {
internal class func doSomething() {}
}
// SomeObject.m file
#implementation SomeObject
- (void)someMethod {
[SwiftObject doSomething];
}
- (void)someOtherMethod {
[YetAnotherSwiftObject doSomething]; // Use of undeclared identifier
}
#end

As indicated in the docs, declarations marked with internal modifier don't appear in the generated header, so the compiler does not know about them and thus complaints. Of course, you could send messages using performSelector approach, but that's not convenient and bug-prone. We just need to help the compiler know that those declarations are there.
First, we need to use #objc attribute variant that allows you to specify name for your symbol in Objective-C:
// SwiftObject.swift
#objc(SWIFTYetAnotherSwiftObject)
internal class YetAnotherSwiftObject: NSObject {
internal class func doSomething() {}
}
And then you just need to create #interface declaration with the methods you want to use in your code - so the compiler will be happy, and also apply SWIFT_CLASS macro with the symbol name you've specified earlier - so the linker would pick the actual implementation:
// SomeObject.m file
SWIFT_CLASS("SWIFTYetAnotherSwiftObject")
#interface YetAnotherSwiftObject : NSObject
+ (void)doSomething;
#end
#implementation SomeObject
- (void)someOtherMethod {
[YetAnotherSwiftObject doSomething]; // Should work now !!!
}
#end
I've used the interface declaration in .m file just for clarity, the better option would be to combine such declarations in .h file, and include it.
By declaring methods in that interface we're making a promise to compiler, and it won't complain if you'll put there a method that does not exist (or with wrong signature, etc.) Obviously, you'll crash in runtime in that case - so be cautious.

For me it just worked by checking: "Allow app extension API only". You find it by going to the project setting, select your target and then it is in the General tab under Deployment Info.
Can someone explain to me, why this does solve the problem?

While the above solution works (https://stackoverflow.com/a/33159964/5945317), it seems overly complicated and unintuitive:
Complicated, because it seems to add more things than necessary – I will provide a smoother solution below.
Unintuitive, because the objc macro SWIFT_CLASS resolves to SWIFT_RUNTIME_NAME, and the provided value is not actually the runtime name – nor is the objc class name in the header matching the Swift attribute param in #objc. Still, surprisingly, the solution works – but to me it is not clear why.
Here is what we have tested in our own project, and believe to be the better solution (using the example above):
// YetAnotherSwiftObject.swift
#objc(OBJCPREFIXYetAnotherSwiftObject)
internal class YetAnotherSwiftObject: NSObject {
#objc internal class func doSomething() {}
}
// OBJCPREFIXYetAnotherSwiftObject.h
#interface OBJCPREFIXYetAnotherSwiftObject : NSObject
+ (void)doSomething;
#end
That's it. The interface looks like a regular objc interface. This gives the added benefit that you can include it in other header files (which you cannot do if you use the SWIFT_CLASS macro, as it comes from the autogenerated Swift header file, which in turn you cannot include in an objc header, due to circular dependency).
On the Swift side, the only thing relevant is that you provide the class with the proper objc name. Mind that I only used the name prefix for language consistency – you can even just use YetAnotherSwiftObject everywhere (i.e., in the objc header and in the #objc attribute in Swift – but you need to keep this attribute with explicit naming in any case, and need to keep it consistent with the class name in the header).
This also makes your life easier if you're in the process of converting your objc framework step by step to Swift. You just keep the objc header as before, and now provide the implementation in Swift.

Methods and properties that are marked with the internal modifier and declared within a class that inherits from an Objective-C class are accessible to the Objective-C runtime.
so let's make use of that:
class MyInternalClass: NSObject {
#objc var internalProperty = 42
}
#interface MyPublicClass()
#end
#implementation MyPublicClass
+ (void) printValue {
Class myInternalClass = NSClassFromString(#"MyPackageNameIfAny.MyInternalClass");
id myInternalClassInstance = [myInternalClass new];
int value = [myInternalClassInstance performSelector:#selector(internalProperty)];
NSLog(#"Value is %d ", value); // "value is 42"
}
#end
Using the SWIFT_CLASS macro and #objc class attribute could easily lead to errors when archiving. This approach is safer.

Related

Unable to access SPM classes in Swift classes referencing Obj-C classes

I'm currently working on moving one of our apps dependencies from Cocoapods to SPM. The dependency is written purely in Swift, but our codebase using it is both ObjC and Swift.
The problem I'm running into now is that anytime a class from the SPM library is defined in an ObjC class, it works, but if Swift tries to reference the property (from the ObjC class) it throws the error message: Value of type SwiftClass has no member libraryClass.
Before moving this package into SPM, Swift files had no issue referencing ObjC definitions of the librarys' classes. Now, anywhere in the app we try to do that the compiler doesn't have access to them.
The code structure looks something along the lines of this, stripped down to keep things minimalistic.
#objc
public class LibrarySwiftClass: NSObject {
etc etc
}
#interface ObjectiveCClass
#class LibrarySwiftClass;
#property (nonatomic, strong) LocalSwiftClass *localClass;
#property (nonatomic, strong) LibrarySwiftClass *libraryClass;
#end
extension ObjectiveCClass {
#objc
func checkPropertyValue() {
let testValue1 = self.localClass <----- This works correctly with no issue
let testValue2 = self.libraryClass <----- The above error happens here
}
}
NOTE: For this example the Swift class is an extension of the Objective C class, but there are other spots in the app where the Swift class isn't related to the ObjC class and they're still failing.
I put in breakpoints to check, and upon using self.value(forKey: "objCProperty") in the swift class, it does show it correctly. Also, there are other non-SPM classes in the ObjC file that are able to properly be referenced with no issue.
I've tried including the ObjC files in the bridging header, and a couple other solutions to get the Swift file access to this property, with no luck. Seems like a very specific error case when setting up SPM. Anyone have any ideas?

Add Swift protocol conformance to Objective-C header and make it public

I've read along here and here about conforming to Swift protocols in Objective-C headers, and I'm not quite getting the behaviour I want - I'm also looking for a better understanding of how this all works. Here's my scenario.
I have a protocol in Swift:
PersonalDetailsProtocol.swift
#objc
protocol PersonalDetailsProtocol {
func doSomeWork()
}
I then have an Objective-C class with a header and implementation file
RegistrationPersonalDetails.h
#protocol PersonalDetailsProtocol; // Forward declare my Swift protocol
#interface RegistrationPersonalDetails : NSObject <PersonalDetailsProtocol>
#end
RegistrationPersonalDetails.m
#import "RegistrationPersonalDetails.h"
#implementation RegistrationPersonalDetails
- (void)doSomeWork {
NSLog(#"Working...");
}
#end
At this point everything compiles, although there is a warning in the RegistrationPersonalDetails.h file stating Cannot find protocol definition for 'PersonalDetailsProtocol'. Other than that warning, the issue I'm facing is I can't publicly call the doSomeWork method on an instance of RegistrationPersonalDetails.
The call site in Swift would look something like:
let personalDetails = RegistrationPersonalDetails()
personalDetails.doSomeWork()
but I get an error stating:
Value of type 'RegistrationPersonalDetails' has no member 'doSomeWork'
I get that the method isn't public, because it's not declared in the header file. But I didn't think it should have to be as long as the protocol conformance is public i.e. declared in the header file.
Can anyone point me on the right path here and offer an explanation? Or is this even possible? I can obviously rewrite the protocol in ObjC, but I just always try to add new code as Swift.
In pure objective-c, you cannot make a class conform to a protocol without importing it in the header. To make the use of a protocol private, the conformance shouldn't be declared in the header. It remains possible to call such a method using a cast however, but it should be done with caution because it's a little bit unsafe :
if ([anInstance respondToSelector:#selector(myProtocolMethod)])
[(id<MyProtocol>)anInstance myProtocolMethod];
I'm not familiar with Swift, but I think you can do the same this way (or something close to it) :
if let conformingInstance = anInstance as? MyProtocol
conformingInstance.myProtocolMethod
EDIT : To complete my first assertion, forward declarations can still be used in the header when you need to declare a method receiving or returning an instance conforming to that protocol :
#SomeProtocol;
// This is not possible
#interface MyClass : NSObject <SomeProtocol>
// But this is possible
#property (nonatomic) id<SomeProtocol> someProperty;
-(void) someMethod:(id<SomeProtocol>)object;
#end
In this document Apple clearly said that :
Forward declarations of Swift classes and protocols can be used only
as types for method and property declarations.
So it seems that the rule is the same whatever the protocol is an Objective-c protocol or a Swift protocol.

Objective C to Swift interoperability issues

I have a curious case where I have the following Objective C protocol:
NS_ASSUME_NONNULL_BEGIN
#protocol AccountCriteria <NSObject>
- (BOOL)criteriaIsApplicableForIdentifier:(NSString *)identifier;
- (nullable id <ModularFactory>)criteriaMetForAccount:(Account *)account error:(NSError **)error NS_SWIFT_NOTHROW;
#end
NS_ASSUME_NONNULL_END
To which I have decided to have a Swift class implement the protocol, like so:
import Foundation
#objc(PaymentCriteria)
public class PaymentCriteria: NSObject, AccountCriteria {
public func criteriaIsApplicable(forIdentifier identifier: String) -> Bool {
//Lots of code here
}
public func criteriaMet(for account: Account, error: NSErrorPointer) -> ModularFactory? {
//Lots of code here
}
}
The intention is to use the PaymentCriteria class in other Objective C code. However with this particular code I keep getting a Lexical or Preprocessor Issue. Now if I remove the public from the class everything builds fine. However, I can't use PaymentCriteria in any Objective C code.
If I keep the class as public but remove AccountCriteria from the class, everything still builds fine. However, I have to have this class abide by the protocol. So this route doesn't help me very much as well.
So my question is, why can't I have a Swift class abide by an Objective C protocol and then try and use that Swift class in Objective C? Or is there something pernicious that I'm doing wrong in the Swift class?
As I understood, the issue here with let's call it, cyclic importing.
So, You've created Objc protocol, then add this Objc file to Bridging header. It is being imported to all swift files, including your PaymentCriteria.swift file.
Then you try using PaymentCriteria back in objc, which leads to adding it to umbrella header. And generally because of such a path I experienced similar errors.
The simplest workaround would be creation AccountCriteria as a protocol using Swift with #objc.

{Module_name}-Swift.h file not working well only in Swift 4 projects unlike Swift 3.2

Anyone faced problems in {Module_name}-Swift.h file for Swift 4 projects? I've noticed -Swift.h autogenerated file not working well with Swift 4 syntax unlike Swift 3.2!.
For example, -Swift.h file doesn't contain all variables and methods which implemented in the custom Swift classes which inherited from NSObject class!
I've used #objc and #classkeywords but no way.
I don't get any errors! the problem is if I've created a class like this:
import Foundation
class Utils: NSObject {
let abc: String?
func xyz() {
print("")
}
}
and navigate to {Module_name}-Swift.h I see something like that:
SWIFT_CLASS("_TtC3{Module_name}5Utils")
#interface Utils : NSObject
- (nonnull instancetype)init OBJC_DESIGNATED_INITIALIZER;
#end
Problem
Both let abc: String? and func xyz() have been never included in {Modue_name}-Swift.hfile!
I think in Swift 4 you have to mark a lot more things #objc (nothing implicit anymore) but other than that it should just be in there.
You can all check it to confirm class name in .h file like:
#class filename;
The generated file {Module}-Swift.h does not contain your variables and methods, the file is generated to give you access to the Module namespace.
The actual interface for the generated module lives in Module.swiftmodule/arm64.swiftmodule (depending on built architecture).
More information on its contents:
https://bugs.swift.org/browse/SR-2502
https://lists.swift.org/pipermail/swift-users/Week-of-Mon-20160111/000827.html
https://stackoverflow.com/a/24396175/1755720
however... the format is not documented anywhere and is subject to change. A good starting point would be to look in include/swift/Serialization/ModuleFormat.h
As to why it's not working - Swift 4 has a migration process, please ensure you have followed it: https://swift.org/migration-guide-swift4/
Xcode will pick up most things ... but it won't get everything!
And why do you need header files for Swift classes? You just can mark swift class as #objc and you will be able to reach all its properties.

Objective-C compiler is leaving out a protocol definition

I'm writing a couple classes that make use of the Objective-C runtime library. This includes retrieving Protocol definitions at runtime based on their name. However, it appears that Protocols that are not explicitly adopted by a class or referenced in code using #protocol(ProtocolName) are excluded from compilation and not available at runtime.
Example:
#protocol MyProtocol <NSObject>
-(void)doSomething;
#end
//Somewhere else in code
Protocol *protocol = NSProtocolFromString(#"MyProtocol");
// ^ value of "protocol" will be nil when I run the application!
//However, if I use do the following:
Protocol *whyDoIHaveToDoThis = #protocol(MyProtocol);
Protocol *protocol = NSProtocolFromString(#"MyProtocol");
// ^ value of "protocol" will now be a pointer as expected when I run the application!
Does anyone know why this is, and even better, how to force the compiler to include Protocol definitions that are not in use at compile time, but which I will later want available at runtime?
You can force the compiler to include the protocol by making a dummy method that's not called that uses it. I've done this before:
void preserveProtocolFromBeingTrimmed()
{
(void)#protocol(BrightnessProtocol);
}
I see that Apple uses this in their FxBrightness plug-in from the FxPlug SDK.

Resources