Casey
Casey

Reputation: 6701

make init() private for NSObject subclass

the class FooClass should only allow interaction via its sharedInstance. i am trying to prevent misuse by not allowing anyone to access init() of FooClass.

i have tried a few different approaches, but none work:

Using private keyword:

class FooClass: NSObject {
    // singleton
    static let sharedInstance = FooClass()

    let value: String

    private override init() {
        self.value = "asdf"
    }
}

// this should be a compile error, but it is not
let foo = FooClass()

Using @available:

class FooClass: NSObject {
    // singleton
    // COMPILE ERROR - "init is unavailable. use sharedInstance"
    static let sharedInstance = FooClass()

    let value: String

    @available(*, unavailable, message="use sharedInstance")
    override init() {
        self.value = "asdf"
    }
}

// COMPILE ERROR - as it should be
let foo = FooClass()

i have tried using internal as well, but still no luck.

UPDATE

first version works if you move it to its own file, however the ObjC version of the class still allows for init to be called. Any ideas?

SWIFT_CLASS("_TtC11SwiftToObjC8FooClass")
@interface FooClass : NSObject
+ (FooClass * __nonnull)sharedInstance;
@property (nonatomic, readonly, copy) NSString * __nonnull value;
- (nonnull instancetype)init OBJC_DESIGNATED_INITIALIZER;
@end

Upvotes: 4

Views: 9680

Answers (2)

Ludovic Landry
Ludovic Landry

Reputation: 11784

You can use the @available() annotation:

@available(*, unavailable, message: "Use __your_init__ instead")
override init() {
    fatalError("init() has not been implemented")
}

Upvotes: 8

user887210
user887210

Reputation:

This answer addresses Swift 2. In Swift 3 it looks like the access level of a method is correctly imported from Swift to Objective-C and does not need to be marked as NS_UNAVAILABLE in order to disallow it from being available. Of course, there is no new method in Swift so that will still need to be marked as NS_UNAVAILABLE to maintain the singleton properly.


Your first approach will work as long as the class is put in its own file. The access control keyword private means that the defined feature will only be available within the containing file.

However, as you said, using the Swift class in Objective-C will remove the protection that private gives you. I believe that's because anything marked private will not have an entry in the imported header file generated by the compiler. Thus the init function inherited from NSObject is available because it isn't overridden.

The solution I found is to create another header file that explicitly declares an init function that can't be called.

Swift class:

@objc(FooClass)
class FooClass: NSObject {
  // singleton
  static let sharedInstance = FooClass()

  let value: String

  private override init() {
    self.value = "asdf"
  }
}

Objective-C header:

@interface FooClass (SharedInstance)
+ (instancetype) new  NS_UNAVAILABLE;
- (instancetype) init NS_UNAVAILABLE;
@end

You have to also block new because if you don't then an instance of the class could be created through it.

Testing:

FooClass* foo  = [FooClass sharedInstance]; // All good here
FooClass* foo2 = [[FooClass alloc] init];   // 'init' is unavailable
FooClass* foo3 = [FooClass new];            // 'new' is unavailable

I have a rudimentary example project here: SharedInstance project git

Upvotes: 5

Related Questions