Cannot access property on Swift type from Objective-C
Asked Answered
P

4

69

I am trying to access a Swift class's Double? property from Objective-C.

class BusinessDetailViewController: UIViewController {

    var lat : Double?
    var lon : Double?

    // Other elements...
}

In another view controller, I am trying to access lat like following:

#import "i5km-Swift.h"
@interface ViewController ()

@property (strong, nonatomic) BusinessDetailViewController *businessDetailViewController;

@end

@implementation ViewController

- (void)viewDidLoad
{
    [super viewDidLoad];
    self.businessDetailViewController = [[BusinessDetailViewController alloc] initWithNibName:@"BusinessDetailViewController" bundle:nil];
    self.businessDetailViewController.lat = businessArray[1]; /* THIS GIVES ME AN ERROR */
}

and I am getting

Property 'lat' not found on object of type 'BusinessDetailViewController *'

Why can't I access this property? What am I missing?

Pomerania answered 14/10, 2014 at 16:50 Comment(2)
I don't know because in businessDetailViewController, I also have an outlet @IBOutlet var mapView: MKMapView? and I can access it from outside but it does not have publicPomerania
I think optional structs are not visible in Objective-C. Will validate.Susquehanna
I
113

Optional values of non-Objective-C types aren't bridged into Objective-C. That is, the first three properties of TestClass below would be available in Objective-C, but the fourth wouldn't:

class TestClass: NSObject {
    var nsNumberVar: NSNumber = 0      // obj-c type, ok
    var nsNumberOpt: NSNumber?         // optional obj-c type, ok
    var doubleVar: Double = 0          // bridged Swift-native type, ok
    var doubleOpt: Double?             // not bridged, inaccessible
}

In your Objective-C code, you'd access those first three properties like this:

TestClass *optTest = [[TestClass alloc] init];
optTest.nsNumberOpt = @1.0;
optTest.nsNumberVar = @2.0;
optTest.doubleVar = 3.0;

In your case, you can either convert lat and long to be non-Optional or switch them to be instances of NSNumber.


Note that you need to be careful about your Objective-C code if you take the second approach (switching lat and lon to non-optional properties of type NSNumber) -- while the Swift compiler will prevent you from assigning nil to non-optional properties, the Objective-C compiler has no qualms about allowing it, letting nil values sneak into your Swift code with no chance of catching them at runtime. Consider this method on TestClass:

extension TestClass {
    func badIdea() {
        // print the string value if it exists, or 'nil' otherwise
        println(nsNumberOpt?.stringValue ?? "nil")

        // non-optional: must have a value, right?
        println(nsNumberVar.stringValue)
    }
}

This works fine if invoked with values in both of the properties, but if nsNumberVar is set to nil from the Objective-C code, this will crash at runtime. Note that there is no way to check whether or not nsNumberVar is nil before using it!

TestClass *optTest = [[TestClass alloc] init];
optTest.nsNumberOpt = @1.0;
optTest.nsNumberVar = @2.0;
[optTest badIdea];
// prints 1, 2

optTest.nsNumberOpt = nil;
optTest.nsNumberVar = nil;
[optTest badIdea];
// prints nil, then crashes with an EXC_BAD_ACCESS exception
Indicia answered 14/10, 2014 at 17:4 Comment(5)
I couldn't track it down in the docs but the compiler will tell you it's not representable with @objc protocol FooProtocol { var lat:Double? {get} }Susquehanna
The documentation on this is patchy - for example, this page says that enumerations and structures defined in Swift aren't available in Obj-C, but doesn't mention that Optionals (which are implemented as an enum) and most of the built struct types bridge properly until you get to the page on Cocoa data types.Indicia
@NateCook I cannot prove it, but I think an optional NSNumber is bridged and an optional Int or Float isn't is because NSNumber is a reference type, and .None is mapped to nil, whereas a value type doesn't have a nil counterpart in objc. I'm not correcting you, just wondering if you agreeArvind
@Antonio: That's right - in Objective-C any reference type can be nil, so they're automatically bridged to Swift optionals. In fact, one could argue that in a bridged project, no reference types should ever be non-Optional, since you can assign nil to even a non-Optional property from Objective-C, leading to potential crashes with no recourse. See my addition...Indicia
Good to know - don't use bridging at objc side, but it's something to keep in mind when I will (it's just a matter of time for that to happen :))Arvind
M
14

If your property is a Swift protocol type, just add @objc in front of it.

Example:

class Foo: UIViewController {
   var delegate: FooDelegate?
   ...
}

@objc protocol FooDelegate {
   func bar()
}
Mahau answered 5/11, 2015 at 3:39 Comment(0)
A
3

Optionals is a swift specific feature, not available in obj-c. Optional class instances work because a nil optional can be mapped to a nil value, but value types (int, floats, etc.) are not reference types, hence variable of those types don't store a reference, but the value itself.

I don't know if there's a solution - a possible workaround is creating non optional properties mapping the nil value to an unused data type value (such as -1 when representing an index, or 999999 for a coordinate):

class Test {
    var lat : Double? {
        didSet {
            self._lat = self.lat != nil ? self.lat! : 999999
        }
    }
    var lon : Double? {
        didSet {
            self._lon = self.lon != nil ? self.lon! : 999999
        }
    }

    var _lat: Double = 99999999
    var _lon: Double = 99999999
}

That should expose the _lat and _lon properties to obj-c.

Note that I have never tried that, so please let us know if it works.

Arvind answered 14/10, 2014 at 17:7 Comment(1)
just give it a try and yes, we can access _lat and _lon from outside. Thanks.Pomerania
C
3

[UInt? Int? or Double? properties] cannot be marked @objc because its type cannot be represented in Objective-C.

It is, however, possible to "wrap" them in a NSNumber like so :

class Foo {
    var bar: Double?
}

// MARK: Objective-C Support
extension Foo {
    /// bar is `Double?` in Swift and `(NSNumber * _Nullable)` in Objective-C
    @objc(bar)
    var z_objc_bar: NSNumber? {
        get {
            return bar as NSNumber?
        }
        set(value) {
            bar = value?.doubleValue ?? nil
        }
    }
}
Casease answered 19/2, 2018 at 15:20 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.