0

I am trying to access an Objective C singleton from Swift, however I only seem to get the initial value created in the init function of the singleton. The flightControllerState object exposed is updated in a delegate function and I can see that the value is properly updated on the Objective C side.

I have followed a few different posts here on SO and also this article on how to call the shared object from Swift. (I should also mention this is running inside a react native project if that may have any impact?)

EDIT updated swift code - I added the wrong line to the init method to grab shared instance - issue is still the same

Objective-C Singleton

@import DJISDK;

@interface RCTBridgeDJIFlightController : RCTEventEmitter<DJIFlightControllerDelegate> {
    DJIFlightControllerState *flightControllerState;
}


@property(nonatomic, readonly) DJIFlightControllerState *flightControllerState;

+ (id)sharedFlightController;

@end


@implementation RCTBridgeDJIFlightController

DJIFlightControllerState *flightControllerState;

@synthesize flightControllerState;

    + (id)sharedFlightController {
        static RCTBridgeDJIFlightController *sharedFlightControllerInstance = nil;
        static dispatch_once_t onceToken;
        dispatch_once(&onceToken, ^{
            sharedFlightControllerInstance = [[self alloc] init];
        });
        return sharedFlightControllerInstance;
    }

    - (id)init {
    // I also tried this to make sure the shared instance was returned but no luck
    //if (sharedFlightControllerInstance != nil) {
    //    return sharedFlightControllerInstance;
    //}
    if (self = [super init]) {
        flightControllerState = nil;
    }
    return self;
    }


    -(void)flightController:(DJIFlightController *)fc didUpdateState:(DJIFlightControllerState *)state {
    flightControllerState = state;
    }
@end

Swift class calling singleton and accessing values

class VirtualStickController {
  var flightControllerSharedInstance: RCTBridgeDJIFlightController

  override init() {
      self.flightControllerSharedInstance = RCTBridgeDJIFlightController.sharedFlightController()
  }

  func getFlightControllerState() {
      if let state = flightControllerSharedInstance.flightControllerState {
      print("FLIGHT CONTROLLER STATE: \(state)") // always null
    } else {
      print ("NULL")
    }
  }
Brien Crean
  • 2,599
  • 5
  • 21
  • 46
  • if you have an init in your class and alloc it in swift override init you are not using the singleton in your code but a normal instancetype. – Ol Sen Aug 20 '20 at 21:14

2 Answers2

2
DJIFlightControllerState *flightControllerState;

@synthesize flightControllerState;

There is no need to use @synthesize for properties in (modern) Objective-C except in special circumstance.

The property flightControllerState is an instance property and will be synthesised (with or without the @synthesize) using a hidden instance variable for its storage.

The variable flightControllerState is a global variable, it happens to have the same name as the property but has no connection whatsoever with it.

At a guess you are changing the global variable in Objective-C and expecting to see the result in Swift via the property, you won't.

Remove the global variable and then check the rest of your code.

Apart from that your code produces a valid shared instance which can be shared between Objective-C and Swift and changes made in one language will be visible in the other.

HTH

CRD
  • 52,522
  • 5
  • 70
  • 86
  • Thank you for that detailed explanation! Yes that's exactly what I was expecting to see and I definitely confused the global var and property. I had fixed the issue by adjusting my init method to always return the sharedFlightControllerInstance unless it does not exist. Not sure if this is good practice? – Brien Crean Aug 21 '20 at 22:00
  • - (id)init { if (sharedFlightControllerInstance) { return sharedFlightControllerInstance; } //ensure only one instance exists @synchronized(self) { self = [super init]; if (self) { sharedFlightControllerInstance = self; } return self; } } – Brien Crean Aug 21 '20 at 22:01
  • For one way to write a true singleton see [this answer](https://stackoverflow.com/questions/30799696/singleton-in-ios-objective-c-doesnt-prevent-more-than-one-instance/30828622#30828622) which is based on the same model Apple documented in a bygone era ;-) – CRD Aug 21 '20 at 22:23
1

Regarding the titular question about how to access an Objective C singleton from Swift, I would recommend an alternative. Modern convention is to declare your sharedFlightController as a class property and declare init as NS_UNAVAILABLE:

@interface RCTBridgeDJIFlightController : NSObject
...
@property (nonatomic, readonly, class) RCTBridgeDJIFlightController *sharedFlightController;
- (instancetype)init NS_UNAVAILABLE;
@end

The implementation would implement a getter for this class property:

@implementation RCTBridgeDJIFlightController

+ (instancetype)sharedFlightController {
    static RCTBridgeDJIFlightController *sharedFlightControllerInstance = nil;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        sharedFlightControllerInstance = [[self alloc] init];
    });
    return sharedFlightControllerInstance;
}

...

@end

Now, your Swift code can reference RCTBridgeDJIFlightController.shared, as is the convention with Swift singletons.


Regarding why you are receiving a nil for the status, there are one of two possible problems:

  • You Objective-C code has confusing combination of explicitly defined ivars, manual synthesis, and global variables. (See below.)

  • I would also suggest that you confirm whether flightController:didUpdateState: is ever getting called at all. (I don't see you ever setting the delegate of the flight controller.) Add a breakpoint or NSLog statement in that method and confirm.

On the first issue, above, I would suggest:

  1. You should not use those commented lines in your init method. If you want to make sure that your singleton object is used, then declare init as NS_UNAVAILABLE.

  2. Given that all your init method is doing is updating flightControllerState to nil, you can remove it entirely. In ARC, properties are initialized to nil for you.

  3. You should not declare explicit ivar in your @interface. Let the compiler synthesize this automatically for you.

  4. You should not @synthesize the ivar in your @implementation. The compiler will now automatically synthesize for you (and will use an appropriate name for the ivar, adding an underscore to the property name.

  5. You should not declare that global in your @implementation.

  6. If you want to use this sharedFlightController from Swift, you should define it to be a class property, not a class method. I know that that article suggested using a class method, but that really is not best practice.

Thus:

// RCTBridgeDJIFlightController.h

#import <Foundation/Foundation.h>

// dji imports here

NS_ASSUME_NONNULL_BEGIN

@interface RCTBridgeDJIFlightController : NSObject
@property (nonatomic, readonly, nullable) DJIFlightControllerState *flightControllerState;
@property (nonatomic, readonly, class) RCTBridgeDJIFlightController *sharedFlightController;
- (instancetype)init NS_UNAVAILABLE;
@end

NS_ASSUME_NONNULL_END

And

// RCTBridgeDJIFlightController.m

#import "RCTBridgeDJIFlightController.h"

@interface RCTBridgeDJIFlightController ()
@property (nonatomic, nullable) DJIFlightControllerState *flightControllerState;
@end

@implementation RCTBridgeDJIFlightController

+ (instancetype)sharedFlightController {
    static RCTBridgeDJIFlightController *sharedFlightControllerInstance = nil;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        sharedFlightControllerInstance = [[self alloc] init];
    });
    return sharedFlightControllerInstance;
}

- (void)flightController:(DJIFlightController *)fc didUpdateState:(DJIFlightControllerState *)state {
    NSLog(@"State updated");
    self.flightControllerState = state;
}
@end

The end result is that you can now use it like so:

class VirtualStickController {
    func getFlightControllerState() {
        if let state = RCTBridgeDJIFlightController.shared.flightControllerState {
            print("FLIGHT CONTROLLER STATE: \(state)")
        } else {
            print("NULL")
        }
    }
}

Note, because the sharedFlightController is now a class property, Swift/ObjC interoperability is smart enough so the Swift code can just reference shared, as shown above.

Rob
  • 415,655
  • 72
  • 787
  • 1,044