How to Expose Existing Property on Obj-C Class Using an Extension Protocol in Swift

How to expose existing property on Obj-C class using an extension protocol in Swift

The compiler error message


note: Objective-C method 'isEnabled' provided by getter for 'enabled' does not match the requirement's selector ('enabled')

gives a hint about the problem. The enabled property of UIButton is inherited from UIControl and in Objective-C declared as

@property(nonatomic, getter=isEnabled) BOOL enabled

Therefore the protocol method has to be

@objc protocol Enableable: class {
var enabled: Bool { @objc(isEnabled) get set }
}

and the implementation (similarly as in Swift 1.2 error on Objective-C protocol using getter):

extension UIImageView: Enableable {
var enabled: Bool {
@objc(isEnabled) get {
return alpha > DisabledAlpha
}
set(enabled) {
alpha = enabled ? EnabledAlpha : DisabledAlpha
}
}
}

Swift protocol extension in Objective-C class

Objective-C protocols cannot have default implementations.

Objective-C protocols can have real optional methods/properties, unlike Swift protocols, which only have required methods/properties. The workaround for this in Swift is the use of a default implementation, however, sadly those cannot be seen in Objective-C.

I would suggest creating a pure Swift protocol and for all Objective-C classes that want to extend this, write the conformance in Swift, then create @objc wrapper functions in Swift that call the default protocol implementations - if it needs to be called, if it doesn't need to be called, simply ignore it.

Something along the lines of:

protocol SwiftProtocol {
func requiredFunc()
func optionalFunc()
}

extension SwiftProtocol {
func optionalFunc() {}
}

@objc extension ObjcClass: SwiftProtocol {
@objc func requiredFunc() {
print("do something")
}

// This will call the default implementation - can be omitted if you don't need to call the default implementation from Objective-C
@objc func objc_optionalFunc() {
optionalFunc()
}
}

Implement Swift protocol with Objective-C property getter

Your UILabel and UIControl extensions satisfy the way you created your protocol because they already have a property called highlighted whose getter accessor method is isHighlighted.

Your HighlightableView does not satisfy the adoption of your Highlightable protocol because you have the @objc(isHighlighted) requirement on your getter.

You have to use computed properties to satisfy this. However, this means that you also need a backing store for the highlighted property. Something like private var _highlighted = false.

In your case, since this is undesirable, you could remove the @objc attribute on your protocol.

protocol Highlightable: class {
var highlighted: Bool { get set }
}

extension UILabel: Highlightable { }
extension UIControl: Highlightable { }

class HighlightableView: UIView, Highlightable {
var highlighted = false
}

let label = UILabel()
label.isHighlighted // Prior to iOS 10, this property is called "highlighted"

let view = HighlightableView()
view.highlighted

let highlightables: [Highlightable] = [ label, view ]

for highlightable in highlightables {
print(highlightable.highlighted)
}

// Prints:
// false
// false

However the property names would not be consistent across the concrete types.

Here is an alternative approach:

@objc protocol Highlightable: class {
var isHighlighted: Bool { @objc(isHighlighted)get @objc(setHighlighted:)set }
}

extension UILabel: Highlightable { }
extension UIControl: Highlightable { }

class HighlightableView: UIView, Highlightable {
private var _isHighlighted = false
var isHighlighted: Bool {
@objc(isHighlighted) get {
return _isHighlighted
}
@objc(setHighlighted:) set {
_isHighlighted = newValue
}
}
}

let label = UILabel()
label.isHighlighted = true

let view = HighlightableView()
view.isHighlighted

let highlightables: [Highlightable] = [ label, view ]

for highlightable in highlightables {
print(highlightable.isHighlighted)
}

// Prints:
// false
// false

This exposes a consistent isHighlighted property across all of the concrete types while still conforming to Highlightable. The drawback here is the @objc attribute is more pervasive in a context where is should not be necessary. That is, the @objc attribute is not being used to expose a Swift protocol to Objective-C code.

EDIT:

Looking at the iOS 10 API diffs for Swift (and doing some testing in Xcode 7.2), UILabel and UIControl's isHighlighted property was previously named highlighted. Using the code above while linking against iOS SDK 9.3 or lower will result in compile time errors.

In the first example, these errors can be fixed by renaming the label.isHighlighted line to label.highlighted.

In the second example, these errors can be fixed by renaming all instances of isHighlighted to highlighted (except for those within the brackets of @objc attributes).

9.3 to iOS 10.0 API Differences: https://developer.apple.com/library/content/releasenotes/General/iOS10APIDiffs/index.html

Accessing Obj-C properties in Swift extension file

I think that you can't access private properties from extension. Your scrollView property is in .m file, not .h - which means it's private and it's not visible from extension file.

Solution: move

@property (strong, nonatomic) UIScrollView *scrollView;

to your header file.

Swift extension storage for protocols

Any protocol object can be converted into a type-erased class. Build an AnySomeProtocol and store that.

private var sourceKey: UInt8 = 0

final class AnySomeProtocol: SomeProtocol {
func getData() -> String { return _getData() }
init(_ someProtocol: SomeProtocol) { _getData = someProtocol.getData }
private let _getData: () -> String
}

extension UIViewController: SomeProtocolInjectable {
var source: SomeProtocol! {
get {
return objc_getAssociatedObject(self, &sourceKey) as? SomeProtocol
}
set(newValue) {
objc_setAssociatedObject(self, &sourceKey, AnySomeProtocol(newValue), .OBJC_ASSOCIATION_RETAIN)
}
}
}

class MyViewController : UIViewController {
override func viewDidLoad() {
self.title = source.getData()
}
}

The caller can only use this to access the protocol methods. You can't force it back into its original type with as, but you should avoid that anyway.

As a side note, I'd really recommend making source return SomeProtocol? rather than SomeProtocol!. There's nothing here that promises that source will be set. You don't even set it until viewDidLoad.



Related Topics



Leave a reply



Submit