0

I'm trying to achieve/force this safe & clean swift syntax

struct PopButton: View
{
    var Label = PopEngineLabel(label:"Hello")
    var body: some View 
    {
        Text(Label.label)
    }
}

Ojective-c

@interface PopEngineLabel : NSObject

@property (strong, nonatomic) NSString* label;

- (id)initWithLabel:(NSString*)label;

@end

@implementation PopEngineLabel 

- (id)initWithLabel:(NSString*)label
{
    if ( label )
        self.label = label;
    else
        self.label = @"<null>";
    return self;
}

@end

But in the swiftUI code, I get the error error: value of optional type 'PopEngineLabel?' must be unwrapped to refer to member 'label' of wrapped base type 'PopEngineLabel' Text(Label.label)

I can remove the errors with

  • Text(Label?.label ?? "default")
  • Text(Label!.label)

I'm assuming this is because all objective-c class/instances are implicitly optional.... BUT, the following code makes it non-optional, but crashes at runtime, as it hasn't done my objective-c initialiser (and .label is nil)

struct PopButton: View
{
    var Label = PopEngineLabel()
    var body: some View 
    {
        Text(Label.label)
    }
}

Can I force the user to use a constructor/initialiser AND be a non-optional in swift? (without making an intermediate swift class)

Soylent Graham
  • 847
  • 1
  • 12
  • 22
  • I think, to mirror your ```var Label = PopEngineLabel()``` initialiser you need an ```-init``` without any arguments on the Objective-C side. – skaak Nov 26 '20 at 12:07
  • Yeah, but really I'm trying to avoid the user being allowed to do that, as I NEED a parameter. I mentioned this one as it had a different effect to what I was expecting – Soylent Graham Nov 26 '20 at 12:31
  • 1
    Ok - I also note you did not do ```super.init``` in your initWithLabel. To hide the (default) init see https://stackoverflow.com/questions/195078/is-it-possible-to-make-the-init-method-private-in-objective-c Anyhow I see you have a good answer. – skaak Nov 26 '20 at 12:34
  • 1
    @skaak thanks, that link solved the last missing piece :) – Soylent Graham Nov 26 '20 at 12:50

1 Answers1

1

You can use nullability specifiers in Objective-C to tell Swift whether a specific property can be nil or not (and hence whether Swift should treat it as an Optional or not). By default, all reference types in Swift can be null, since they are pointers and hence by default, Swift treats all Obj-C pointer properties as implicitly unwrapped optionals.

However, this won't make the inherited initialiser from NSObject initialise label correctly (or make that initialiser unusable). So you need to assign a default value to label in the default init inherited from NSObject.

@interface PopEngineLabel : NSObject

@property (strong, nonatomic, nonnull) NSString* label;

- (instancetype)initWithLabel:(nonnull NSString*)label;
- (nonnull instancetype)init NS_UNAVAILABLE; // disable default initialiser in Swift

@end

@implementation PopEngineLabel

- (instancetype)init {
    self = [super init];
    if (self) {
        self.label = @"<null>";
    }
    return self;
}

- (instancetype)initWithLabel:(nonnull NSString*)label {
    self = [super init];
    if (self) {
        if (label) {
            self.label = label;
        } else {
            self.label = @"<null>";
        }
    }
    return self;
}

@end

Couple of other things to bear in mind in Objective-C:

  • Your init shouldn't return id, but instancetype
  • You should delegate to super init
Soylent Graham
  • 847
  • 1
  • 12
  • 22
Dávid Pásztor
  • 51,403
  • 9
  • 85
  • 116
  • 1
    This works perfectly! I'm not sure if it's bad form to modify someone's answer, but the final missing part (disable `PopEngineLabel()` in swift) came from @skaak `- (nonnull instancetype)init NS_UNAVAILABLE;` stops me from using the default constructor – Soylent Graham Nov 26 '20 at 12:52
  • @SoylentGraham feel free to edit my answer with that update – Dávid Pásztor Nov 26 '20 at 12:55
  • For any future readers (ie. me), the objc member won't trigger any swiftui changes. To do that I need an intermediatry `Observable` swift object. I write a `@State` dirty variable and then a getter/setter to access & modify the obj-c member. Then [in swift] overload a `OnChanged` objc function (called in objc field setter). That function modifies the dirty variable and swiftui's state changes. – Soylent Graham Dec 05 '20 at 12:43