skip to Main Content

I’m exposing my Swift structs as wrapped classes so I don’t pollute my Swift API by converting structs to @objc classes.

Since I can’t use generics in @objc classes, I’m trying to eliminate some boilerplate code within a protocol to provide features like equality as shown below. However, the function in the protocol extension is not getting called.

Can you think of other tricks to accomplish what I’m trying to do?

It’d be nice to get rid of the initializer to and move it to a default implementation of the extension too, but I couldn’t figure that out either.

public protocol InitializableConverter: NSObject {
    associatedtype SwiftStructType: Equatable
    var _data: SwiftStructType { get set }
    init(_ data: SwiftStructType)
}

extension InitializableConverter {
    /// Returns object equality if underlying structs are equal
    func isEqual(_ object: Any?) -> Bool {
        if let other = object as? Self {
            return self._data == other._data
        }
        return false
    }
}

@objc public class UserSettingsObjc: NSObject, InitializableConverter {
    public var _data: UserSettings
    required public init(_ data: UserSettings) { _data = data }

    @objc public var locale: String { _data.locale }
}

public struct UserSettings: Equatable {
    public private(set) var locale: String

    public init(locale: String) {
        self.locale = locale
    }
}

UPDATE:

Here are additional methods for context on how it’s used:

// Original swift method that uses Result<> syntax
func call(username: String?, password: String?, completion: @escaping (Result<UserSettings, MyError>) -> Void) {
    completion(.success(UserSettings(locale: "hello world")))
}

// Objc method using non-Result<> syntax (@objc commented out because of playground)
@objc
func wrappedCall(username: String?, password: String?, completion: @escaping (UserSettingsObjc?, MyErrorObjc?) -> Void) {
    call(username: username, password: password) { result in
        callCompletion(completion, with: result)
    }
}

// Converting result to non-result via InitializableConverter protocol
func callCompletion<T, TOrig, E, Eorig>(
    _ completion: (T?, E?) -> Void,
    with result: Result<TOrig, Eorig>
) where T: InitializableConverter, E: InitializableConverter {
    switch result {
        case let .success(torig):
            return completion(T(torig as! T.SwiftStructType), nil)

        case let .failure(eorig):
            return completion(nil, E(eorig as! E.SwiftStructType))
    }
}

2

Answers


  1. You cannot override a method in an extension. If you want to change the default implementation of isEqual, you’ll have to subclass NSObject. (If the compiler doesn’t force you to include the word override, you’re not overriding.)

    I doubt what you’re trying to do is possible in an ObjC bridge, but it definitely cannot be done with an extension.

    Login or Signup to reply.
  2. Protocol extensions are a Swift-only feature, meaning everything you declare in a protocol extension won’t be visible/callable from Objective-C code.

    As others have said, if you want to provide a custom isEqual, then the only viable approach is to subclass NSObject. However in that case you run into the generics problem you described.

    However, not all is lost, you could push the generic constraint to the initializer, with a class crafted like this:

    @objc class Bridge: NSObject {
        private let _isEqual: (Any?) -> Bool
    
        init<T: Equatable>(_ data: T) {
            _isEqual = { object in
                guard let other = object as? Bridge, let otherData = other._data as? T else {
                    return false
    
                }
                return data == otherData
            }
        }
    
        override func isEqual(to object: Any?) -> Bool {
            _isEqual(object)
        }
    }
    

    Pushing the generic constraint to the initializer allows Objective-C to consume the class, however, as you might’ve already noticed, this increases the amount of boilerplate code you need to write in the class definition. Basically, every method you want to support will need to be backed up by a closure created in the initializer

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search