Extending a Swift class with Objective-C category - objective-c

Im in a situation where I need to use Objective-C category to extend a Swift class. I've done something as follows:
In "SomeClass.swift":
class SomeClass: NSObject {
}
In "SomeClass+Extension.h":
#import "Project-Swift.h"
#interface SomeClass (Extension)
-(void)someMethod();
#end
This has worked well. And if I try to use the SomeClass extension in my Objective C code, it is fine.
The problem is, if I want to use someMethod() in a another Swift class, I will need to put the SomeClass+Extension.h file into my ObjC-BridgingHeader.h file.
But doing this will cause a circular dependency, because SomeClass+Extension.h also imports Project-Swift.h.
Does anyone have a good way to get around this?
Please note that simply forward declaring the class in the category header will not work, as categories cannot use forward declarations for it's own implementation as so:
#class SomeClass without importing Project-Swift.h will give a compile error.

The Bad
i too have been fighting this issue a bunch. unfortunately the documentation pretty explicitly states that this pattern is not allowed:
To avoid cyclical references, don’t import Swift code into an
Objective-C header (.h) file. Instead, you can forward declare a Swift
class or protocol to reference it in an Objective-C interface.
Forward declarations of Swift classes and protocols can only be used
as types for method and property declarations.
also throughout the the linked page you will notice it keeps mentioning to import the generated header specifically into the .m file:
To import Swift code into Objective-C from the same target
Import the Swift code from that target into any Objective-C .m file
within that target
The Good
one solution that may work for you is to create a swift extension that redefines each method you need in the category. it is fragile and ugly, but arguably the cleanest solution.
/**
Add category methods from objc here (since circular references prohibit the ObjC extension file)
*/
extension SomeClass {
#nonobjc func someMethod() {
self.performSelector(Selector("someMethod"))
}
}
adding the #noobjc to the front allows the
same method signature to be used w/o overriding the ObjC implementation
now the import "SomeClass+Extension.h" from the bridging
header can be removed
if support for more than two input params is needed, or tighter type coupling is desired i would recommend using the runtime to call the underlying function. a great description is here.

From the Interoperability guide, we cannot directly access the subclassed / categorized / extensioned Objc-objects for the .swift [SomeClass] class.
But as a turn-around, we can do this:
For Variables , we can do this:
extension Class {
private struct AssociatedKeys {
static var DescriptiveName = "sh_DescriptiveName"
}
var descriptiveName: String? {
get {
return objc_getAssociatedObject(self, &AssociatedKeys.DescriptiveName) as? String
}
set {
if let newValue = newValue {
objc_setAssociatedObject(
self,
&AssociatedKeys.DescriptiveName,
newValue as NSString?,
.OBJC_ASSOCIATION_RETAIN_NONATOMIC
)
}
}
}
}
For Methods, we can use method_swizzling which is not recommended.

As one simple solution, you can move the extension to your Swift code. Then you won't have any dependency problems.

Related

Swift class extends Objective C delegate

I am trying to extend Objective C class in my Swift class. This is where I got so far:
SINMessageClientDelegate is Objective C class. ViewController is written in Swift. I already have Bridging Header, so I can use Objective C object in my Swift class.
This is how my Swift code class definition looks like:
class ViewController: UIViewController, SINMessageClientDelegate {
I am getting the following error:
Type "ViewController does not conform to protocol SINMessageClientDelegate"
This is how definition of SINMessageClientDelegate looks like:
#protocol SINMessageClientDelegate <NSObject>
- (void)messageClient:(id<SINMessageClient>)messageClient didReceiveIncomingMessage:(id<SINMessage>)message;
- (void)messageSent:(id<SINMessage>)message recipientId:(NSString *)recipientId;
- (void)messageDelivered:(id<SINMessageDeliveryInfo>)info;
- (void)messageFailed:(id<SINMessage>)message info:(id<SINMessageFailureInfo>)messageFailureInfo;
I tried to create these methods using Swift in ViewController:
// Tells the delegate that a message has been received.
func messageClient(id: SINMessageClient, didReceiveIncomingMessage:SINMessage)
{
}
// Tells the delegate that a message for a specific recipient has been sent by the local user.
func messageSent(id: SINMessage, recipientId: NSString)
{
}
// Tells the delegate that a message has been delivered (to a particular recipient).
func messageDelivered(id: SINMessageDeliveryInfo)
{
}
func messageFailed(id: SINMessage, info: SINMessageFailureInfo)
{
}
Can someone advice what is the proper way of doing this, since I am getting the same error after adding my code?
Thanks!
One problem in the way you pose your question is that you're using the wrong words. You are not "extending a class". You are conforming to (or adopting) a protocol.
The trouble, however, is that you are not adopting it (conforming to it), as the error message rightly tells you. The reason apparently is that you don't know how to read Objective-C. You'll need to learn to do that in order to proceed. For example, given this Objective-C declaration:
- (void)messageClient:(id<SINMessageClient>)messageClient
didReceiveIncomingMessage:(id<SINMessage>)message;
The Swift implementation will need to be:
func messageClient(SINMessageClient,
didReceiveIncomingMessage message: SINMessage) { /* ... */ }
Whereas what you have is not at all the same thing. You have this:
func messageClient(id: SINMessageClient,
didReceiveIncomingMessage:SINMessage) { /* ... */ }
That is not a match, so you are not implementing the required method, but rather some totally different method. That's legal, but it has nothing to do with the protocol you are supposed to be conforming to. And the same for the rest of your declarations.

How to change the namespace of a Swift class?

When you implement a class MyGreatClass in Swift its fully qualified name will by <MyPackageName>.MyGreatClass. This is different to Objective-C, where the fully qualified name of that same class is MyGreatClass.
Unfortunately this introduces a problem for me. When I am using NSUnarchiver and the archive was written with Objective-C objects I cannot unpack it with Swift-classes(see below for a detailed description).
This means I need to find a way to rename the namespace for my Swift classes. How do I do that?
Any help would be great!
Background: Why can't NSUnarchiver see/load my swift class?
I have implemented a small program to read a file, which was archived with NSArchive.
These are my files:
main.swift:
import Foundation
// parse command line - total path to *.trace file (from Apple's Instruments app)
var traceFilePath = Process.arguments[1]
var traceFile = NSURL(fileURLWithPath: traceFilePath)
var error:NSError?
// check if the file exists
if (traceFile?.checkResourceIsReachableAndReturnError(&error) == false){
// file does not exist or cannot be accessed
println("\(error)")
exit(1)
}
var rawData = NSData(contentsOfURL: traceFile!)
var data = NSUnarchiver(forReadingWithData: rawData!)
var decodedObject: AnyObject? = data?.decodeObject()
XRObjectAllocRun.swift:
import Foundation
class XRObjectAllocRun: NSObject {
// class to be implemented
}
When I now run my application on an Instruments-file I am getting the following error: Terminating app due to uncaught exception 'NSArchiverArchiveInconsistency', reason: '*** class error for 'XRObjectAllocRun': class not loaded'.
This is really strange because when I add the exact same class in an Objective-C file with a bridging header file I have no issues.
trace file reader-Bridging-Header.h: is empty.
XRObjectAllocRun.h:
#import <Foundation/Foundation.h>
#interface XRObjectAllocRun : NSObject
#end
XRObjectAllocRun.m:
#import "XRObjectAllocRun.h"
#implementation XRObjectAllocRun
#end
What am I missing? Why is my Objective-C class found, whereas my Swift class is not?
Swift has no issues for example with var x = XRObjectAllocRun() in main.swift, but yet the NSUnarchiver still complaints about a missing XRObjectAllocRun class when I stay purely within Swift. Is the NSUnarchiver looking in the wrong places - does it for some reason only accept Objective-C classes?
If you want to know what I am trying to do check this stackoverflow question out.
Update
This is what apple writes:
Swift classes are namespaced based on the module they are compiled in, even when used from Objective-C code. Unlike Objective-C, where all classes are part of a global namespace
Further more:
For example, when you create a document–based Mac app, you provide the name of your NSDocument subclass in your app’s Info.plist file. In Swift, you must use the full name of your document subclass, including the module name derived from the name of your app or framework.
Yikes, trying to figure out the mess now...
Try this when you declare your class:
#objc(XRObjectAllocRun) class XRObjectAllocRun: NSObject {
// class to be implemented
}
That will give this class the same name as the archived class, namely XRObjectAllocRun, instead of the namespaced Swift name trace_file_reader.XRObjectAllocRun.
This is always a concern when you're translating from Objective-C to Swift and you've got an existing archive to deal with. See Apple's documentation:
https://developer.apple.com/library/ios/documentation/Swift/Conceptual/BuildingCocoaApps/InteractingWithObjective-CAPIs.html
Note the discussion under "Exposing Swift Interfaces in Objective-C".

Swift : can not call an class method of an Objective-C class

I am willing to get a reference to an instance of an Objective-C class by calling, from Swift, one of its class initialiser functions.
The class I am willing to use is AWSMobileAnalytics, available at https://github.com/aws/aws-sdk-ios/blob/master/AWSCore/MobileAnalytics/include/AWSMobileAnalytics.h
It defines a initialiser like this
#interface AWSMobileAnalytics : NSObject
+ (instancetype)mobileAnalyticsForAppId:(NSString *)appId;
...
#end
My code is
let ma = AWSMobileAnalytics.mobileAnalytics(forAppId: appId)
(and I tried many variant of it). The compiler says
Type AWSMobileAnalytics.Type has no member 'mobileAnalytics'
Any suggestion ?
Seb
Make sure you have imported the framework in bridging header.You need to call like this
let ma = ViewController.mobileAnalyticsForAppId("abc")
There's no need to use a Bridging Header.
Just import it like this in your AppDelegate:
import AWSMobileAnalytics
then
AWSMobileAnalytics(forAppId: "yourID")

Is it possible to use Swift's Enum in Obj-C?

I'm trying to convert some of my Obj-C class to Swift. And some other Obj-C classes still using enum in that converted class. I searched In the Pre-Release Docs and couldn't find it or maybe I missed it. Is there a way to use Swift enum in Obj-C Class? Or a link to the doc of this issue?
This is how I declared my enum in my old Obj-C code and new Swift code.
my old Obj-C Code:
typedef NS_ENUM(NSInteger, SomeEnum)
{
SomeEnumA,
SomeEnumB,
SomeEnumC
};
#interface SomeClass : NSObject
...
#end
my new Swift Code:
enum SomeEnum: NSInteger
{
case A
case B
case C
};
class SomeClass: NSObject
{
...
}
Update: From the answers. It can't be done in Swift older version than 1.2. But according to this official Swift Blog. In Swift 1.2 that released along with XCode 6.3, You can use Swift Enum in Objective-C by adding #objc in front of enum
As of Swift version 1.2 (Xcode 6.3) you can. Simply prefix the enum declaration with #objc
#objc enum Bear: Int {
case Black, Grizzly, Polar
}
Shamelessly taken from the Swift Blog
Note: This would not work for String enums or enums with associated values. Your enum will need to be Int-bound
In Objective-C this would look like
Bear type = BearBlack;
switch (type) {
case BearBlack:
case BearGrizzly:
case BearPolar:
[self runLikeHell];
}
To expand on the selected answer...
It is possible to share Swift style enums between Swift and Objective-C using NS_ENUM().
They just need to be defined in an Objective-C context using NS_ENUM() and they are made available using Swift dot notation.
From the Using Swift with Cocoa and Objective-C
Swift imports as a Swift enumeration any C-style enumeration marked with the NS_ENUM macro. This means that the prefixes to enumeration value names are truncated when they are imported into Swift, whether they’re defined in system frameworks or in custom code.
Objective-C
typedef NS_ENUM(NSInteger, UITableViewCellStyle) {
UITableViewCellStyleDefault,
UITableViewCellStyleValue1,
UITableViewCellStyleValue2,
UITableViewCellStyleSubtitle
};
Swift
let cellStyle: UITableViewCellStyle = .Default
From the Using Swift with Cocoa and Objective-C guide:
A Swift class or protocol must be marked with the #objc attribute to
be accessible and usable in Objective-C. [...]
You’ll have access to anything within a class or protocol that’s
marked with the #objc attribute as long as it’s compatible with
Objective-C. This excludes Swift-only features such as those listed
here:
Generics Tuples / Enumerations defined in Swift / Structures defined in
Swift / Top-level functions defined in Swift / Global variables defined in
Swift / Typealiases defined in Swift / Swift-style variadics / Nested types /
Curried functions
So, no, you can't use a Swift enum in an Objective-C class.
Swift 4.1, Xcode 9.4.1:
1) Swift enum must be prefixed with #objc and be Int type:
// in .swift file:
#objc enum CalendarPermission: Int {
case authorized
case denied
case restricted
case undetermined
}
2) Objective-C name is enum name + case name, eg CalendarPermissionAuthorized:
// in .m file:
// point to something that returns the enum type (`CalendarPermission` here)
CalendarPermission calPermission = ...;
// use the enum values with their adjusted names
switch (calPermission) {
case CalendarPermissionAuthorized:
{
// code here
break;
}
case CalendarPermissionDenied:
case CalendarPermissionRestricted:
{
// code here
break;
}
case CalendarPermissionUndetermined:
{
// code here
break;
}
}
And, of course, remember to import your Swift bridging header as the last item in the Objective-C file's import list:
#import "MyAppViewController.h"
#import "MyApp-Swift.h"
If you prefer to keep ObjC codes as-they-are, you could add a helper header file in your project:
Swift2Objc_Helper.h
in the header file add this enum type:
typedef NS_ENUM(NSInteger, SomeEnum4ObjC)
{
SomeEnumA,
SomeEnumB
};
There may be another place in your .m file to make a change: to include the hidden header file:
#import "[YourProjectName]-Swift.h"
replace [YourProjectName] with your project name. This header file expose all Swift defined #objc classes, enums to ObjC.
You may get a warning message about implicit conversion from enumeration type... It is OK.
By the way, you could use this header helper file to keep some ObjC codes such as #define constants.
If you (like me) really want to make use of String enums, you could make a specialized interface for objective-c. For example:
enum Icon: String {
case HelpIcon
case StarIcon
...
}
// Make use of string enum when available:
public func addIcon(icon: Icon) {
...
}
// Fall back on strings when string enum not available (objective-c):
public func addIcon(iconName:String) {
addIcon(Icon(rawValue: iconName))
}
Of course, this will not give you the convenience of auto-complete (unless you define additional constants in the objective-c environment).
After researching this, I kept finding only partial answers, so I created an entire example of a Swift App bridged to Objective C that has Swift enums used by Objective C code and Objective C enums used by Swift code. It is a simple Xcode project that you can run and experiment with. It was written using Xcode 10.3 with Swift 5.0
Example Project
In case you are trying to observe an enum which looks like this:
enum EnumName: String {
case one = "One"
case two = "Two"
}
this workaround helped me.
Observable Class:
create #objc dynamic var observable: String?
create your enum instance like this:
private var _enumName: EnumName? {
didSet {
observable = _enumName!.rawValue
}
}
Observer Class:
create private var _enumName: EnumName?
create private let _instance = ObservableClass()
create
private var _enumObserver: NSKeyValueObservation = _instance.observe(\.observable, options: .new, changeHandler: { [weak self] (_, value) in
guard let newValue = value.newValue else { return }
self?._enumName = EnumName(rawValue: period)!
})
Than's it. Now each time you change the _enumName in the observable class, an appropriate instance on the observer class will be immediately updated as well.
This is of course an oversimplified implementation, but it should give you an idea of how to observe KVO-incompatible properties.
this might help a little more
Problem statement :- I have enum in swift class, which I am accessing form other swift classes, and Now I need to access it form my one of the objective C class.
Before accessing it from objective-c class :-
enum NTCType {
case RETRYNOW
case RETRYAFTER
}
var viewType: NTCType?
Changes for accessing it from objective c class
#objc enum NTCType :Int {
case RETRYNOW
case RETRYAFTER
}
and add a function to pass it on the value
#objc func setNtc(view:NTCType) {
self.viewType = view; // assign value to the variable
}

Swift to Objective-C header does not contain Swift classes

I'm attempting to use Swift classes in my Objective-C code, however my Swift classes don't seem to appear in the generated header. As a result, my build fails with "Use of undeclared identifier 'HelloWorld'".
I used the templates to create a project called TestApp.
I have the following Build Settings in my target:
Product Name : TestApp
Product Module Name : TestAppModule
Defines Module : Yes
Apple's documentation says to use #import <TestApp/TestAppModule-Swift.h> but this doesn't work.
Instead, I'm using #import "TestAppModule-Swift.h" in my ".m" file. It seems to find this.
I'm able to navigate to it, and it looks like this...
// Generated by Swift version 1.0 (swift-600.0.34.4.5)
#if defined(__has_include) && __has_include(<swift/objc-prologue.h>)
# include <swift/objc-prologue.h>
#endif
...etc...
but no classes defined in there.
I have a Swift file in the project that looks like this...
class HelloWorld {
func hello() {
println("hello world")
}
}
Why isn't this working using the standard header file location #import <TestApp/TestAppModule-Swift.h>?
How can I get my swift classes in that header file, so I won't get the "undeclared identifier" error?
Here's how I have gotten it to work. You can see a more large-scale answer here.
Change this:
class HelloWorld {
func hello() {
println("hello world")
}
}
To:
#objc class HelloWorld {
class func newInstance() -> HelloWorld {
return HelloWorld()
}
func hello() {
println("hello world")
}
}
Then, In your ObjC file:
#import "TestApp-Swift.h"
And call like this:
HelloWorld * helloWorld = [HelloWorld newInstance];
[helloWorld hello];
tl;dr Ensure you have a bridging header if you're doing any cross-calling between Objective-C and Swift.
I had the exact same problem: I could see the -Swift.h file in DerivedData but it made no mention of my Swift classes. I was importing the header file correctly, the Defines Module setting was YES, and the Product Module Name was correct. I tried deleting and re-adding the Swift files, clean buiild, quitting XCode, etc, with no luck.
Then I realised I had no -Bridging-Header.h file in my project, presumably due to the way I'd cobbled it together from a previous project. Shouldn't be a problem because I was not (yet) calling Objective-C from Swift. But when I added a bridging header, and referred to its path in the build settings (Swift Compiler - Code Generation -> Objective-C Bridging Header), it magically fixed the problem - my -Swift.h file was suddenly full of SWIFT_CLASS() goodness!
So I'm guessing the bridging header is fundamental to the process, even if you're NOT using Objective-C from Swift.
UPDATE: I finally understand this. It is related to public/internal access modifiers. Not sure if I missed this originally or if it's an addition to the Apple docs, but it now clearly states:-
By default, the generated header contains interfaces for Swift
declarations marked with the public modifier. It also contains those
marked with the internal modifier if your app target has an
Objective-C bridging header.
It is proper to use #import "TestAppModule-Swift.h" in your .m files. If you need to reference a class in a .h, use the #class forward declaration.
Further, if you want to use a Swift class from Objective-C, the Swift class must be marked with the #objc attribute. Xcode will only include classes with that attributed in the generated header. See also this documentation.
Class should be declared as #objc public class
A more convenient way would be to inherit from NSObject. Like so:
class HelloWorld: NSObject {
func hello() {
println("hello world")
}
}
In my case, by following Apple guidelines, it did not work until I ran the project. The xcode editor kept flagging the unknown swift class, until i clicked "run". The build succeeded, and the swift method worked.
In my case the class was not being compiled, because I first added it to my test target only... After adding it to my main target (Build Phases -> Compile Sources), it was actual compiled and added to the header file.
So much for TDD ;-)
Maybe you defined a Swift class with the same name as an existing Objective-C class which wouldn't be unusual if you want to refactor your Objective-C code to Swift.
As long as you have a class defined simultaneously in Swift and Objective-C the compiler quietly stops updating the bridging header altogether ("ProductModuleName-Swift.h") - which also affects subseqeuent changes in other bridged Swift files.
For general reference how to import Swift into Objective-C see:
Importing Swift into Objective-C | Apple Developer Documentation