Implement Swift protocol with Objective-C property getter

2.3k Views Asked by At

I want to create a common ancestor for all highlightable views in Swift. I wanted that already existing UIKit classes that implement highlighted property worked out of the box, so after reading this answer and checking that the Objective-C getter is defined as isHighlighted I changed the protocol definition to this:

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

So the protocol implementation for UILabel and UIControl is as simple as this:

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

This works great, I can access and set the highlighted property from Swift as Highlightable instances. However, when I try to implement the protocol on my Swift classes with even the simplest implementation like this:

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

I get this compilation error:

Objective-C method 'highlighted' provided by getter for 'highlighted' does not match the requirement's selector ('isHighlighted')

The only way I could get it to work is using computed properties, but it's not what I want.

class HighlightableView: UIView, Highlightable {
    var highlighted: Bool { @objc(isHighlighted) get { return true } set {} }
}

My environment is Xcode 8.0 and Swift 3. Updated to XCode 8.2 and the error persists.


My current workaround is to completely avoid any naming conflict between Objective-C and Swift. But this is far from ideal:

@objc protocol Highlightable {
    var _highlighted: Bool { get set }
}

extension UILabel: Highlightable {
    var _highlighted: Bool {
        get { return isHighlighted }
        set { isHighlighted = newValue }
    }
}
extension UIControl: Highlightable {
    var _highlighted: Bool {
        get { return isHighlighted }
        set { isHighlighted = newValue }
    }
}
class HighlightableView: UIView, Highlightable {
    var _highlighted: Bool = false
}
2

There are 2 best solutions below

5
On BEST ANSWER

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

0
On

I have a solution, but it's bit bigger than you want, however i think it will help you.

You should do swift protocol and determine default implementation for classes what you need

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

extension Highlightable where Self: UILabel {
    var highlighted: Bool {
        get {
            return isHighlighted
        }
        set {
            isHighlighted = newValue
        }
    }
}

extension Highlightable where Self: UIControl {
    var highlighted: Bool {
        get {
            return isHighlighted
        }
        set {
            isHighlighted = newValue
        }
    }
}

So you could implement it

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

class CustomView: UIView, Highlightable {
    var highlighted: Bool = false
}

I hope it helps you.