How did Apple bridge this struct to Objective-C

I'm currently working on an AppKit port of this function (and many more).

https://developer.apple.com/documentation/uikit/uicollectionviewcell/3600950-updateconfiguration

The function is open, so it can be subclassed despite that it takes a struct. My function for the AppKit port should also be open.

When I try to recreate the function, it throws: "Method cannot be marked @objc because the type of the parameter cannot be represented in Objective-C".

Apples implementation includes @objc(_bridgedUpdateConfigurationUsingState:). What is it doing?

Swift struct types cannot generally be represented in Objective-C. The UICellConfigurationState was defined in C, so it is available in both Swift and Objective-C.

3 Likes

UICellConfigurationState – a class in Obj-C is redefined as a struct in Swift. We have this with NSString / String, NSURL / URL, the curiosity of this case is that the name is left unchanged.

2 Likes

Most C struct and ObjC class types are brought into Swift with their given names. The Foundation class types are bridged to Swift value types as a special case. There is no supported way to implement this bridging for other types.

1 Like

how can I bridge a objective-c class to a struct? I only know how to bridge a ObjC class to a swift class.

Also the struct implements a subscript. This can't be done in ObjC, or?

I think more important is the difference of how, say, UITraitCollection is brought to Swift (it is left a class) vs how a type like UICellConfigurationState is brought to Swift (redefined as a struct). The former is more like a rule and the latter is more like an exception, no?

You can't. This ability is reserved to the Swift implementation for specific types.

Vanilla ObjC doesn't provide a facility for subscript members on structs, no. If you're willing to adopt Objective-C++, you could give the struct an operator[].

1 Like

UICellConfigurationState isn't one of the specially bridged types (like String -> NSString), so I believe it is manually replaced by UIKit's Swift overlay rather than properly bridged. If an ObjC declaration is marked as NS_REFINED_FOR_SWIFT, then it'll get imported into Swift with a double-underscored name (like UICellConfigurationState --> __UICellConfigurationState), allowing the Swift interface to provide its own definition. The APIs that take UICellConfigurationState are then explicitly redefined in Swift to manually convert the Swift struct to the underlying class and invoke the ObjC implementation.

4 Likes

Aha, good to know. This might be what OP is looking for, albeit being double underscored it's probably non-recommended:

class MyCollectionViewCell: UICollectionViewCell {
    override func __updateConfiguration(using state: __UICellConfigurationState) {
        print("here you are")
        super.__updateConfiguration(using: state)
    }
}
1 Like

ah i understand. thanks.

Sounds like a lot of work just to make the function open/subclassable.

Apples function includes @objc(_bridgedUpdateConfigurationUsingState:). What is it doing? I couldn't find any documentation about @objc followed by brackets.

From the docs:


Although this doesn't answer the question fully:

@objc(_bridgedUpdateConfigurationUsingState:) dynamic open
func updateConfiguration(using state: UIKit.UICellConfigurationState)

if UIKit.UICellConfigurationState here is a "struct" Swift type how could that be used in Obj-C?

It's quite a challenge to replicate that setup. Complications:

  • For those who's unfamiliar: to use UICollectionViewCell you typically make a subclass of it and override its methods (one of which is updateConfiguration).
  • UICollectionViewCell has to be declared in Obj-C otherwise Obj-C code won't be able subclassing it (I assume that using it from Obj-C is important).
  • updateConfiguration(using: Struct_version_of_UICellConfigurationState) can't be declared in Obj-C as the "Struct_version_of_UICellConfigurationState" is only available in Swift.
  • you could add updateConfiguration(using: Struct_version_of_UICellConfigurationState) in a Swift's extension of UICollectionViewCell but instance methods declared in extensions are not overridable, unless they are marked with @objc.
  • that method can't be marked @objc as it's using a Swift-only type that's not available in Objc-C.
Details
// .h
#import <Foundation/Foundation.h>

NS_SWIFT_NAME(__UICellConfigurationState)
@interface UICellConfigurationState: NSObject // UIViewConfigurationState but let's keep it simpler
@end

@interface UICollectionViewCell: NSObject
- (void)updateConfigurationUsingState:(UICellConfigurationState*)state NS_SWIFT_NAME(__updateConfiguration(using:));
@end

// .m
@implementation UICellConfigurationState
@end

@implementation UICollectionViewCell
- (void)updateConfigurationUsingState:(UICellConfigurationState*)state {}
@end

// .swift
public struct UICellConfigurationState {}

extension UICollectionViewCell {
    open func updateConfiguration(using state: UICellConfigurationState) {}
    // 🔶 Warning: Non-'@objc' instance method in extensions cannot be overridden; use 'public' instead
}

class MyCell: UICollectionViewCell {
    override func updateConfiguration(using state: UICellConfigurationState) {
        // 🛑 Error: instance method 'updateConfiguration(using:)' is declared in extension of 'UICollectionViewCell' and cannot be overridden
    }
}
1 Like

Something is not exactly right here. If I declare my cell subclass in Obj-C:

@interface MyObjcCell: UICollectionViewCell
@end

@implementation MyObjcCell
- (void)updateConfigurationUsingState:(UICellConfigurationState *)state {
    NSLog(@"MyObjcCell. updateConfigurationUsingState: %@, b4 calling super", state);
    [super updateConfigurationUsingState:state];
    NSLog(@"after calling super");
}
@end

and call its update from Swift:

    let cell = MyObjcCell()
    let state = UICellConfigurationState(traitCollection: .current)
    cell.updateConfiguration(using: UICellConfigurationState(traitCollection: .current))

that will not call my Obj-C overridden method.

I can call the hidden (from Swift) method:

    cell.__updateConfiguration(using: __UICellConfigurationState(traitCollection: .current))

in which case the Obj-C's the "updateConfigurationUsingState" override is called, but I guess I am not supposed to, as it is prefixed with "__".

Ditto would happen if I make a Swift's subclass of Obj's "MyObjcCell".

I would expect a more transparent interop story here, with Swift's updateConfiguration(using:) converting the passed Swift struct into obj-c class and calling through Obj-C updateConfigurationUsingState:.


Regardless of this I still don't understand how could I add an overridable method like "updateConfiguration(using: Swift_only_type)" to an Obj-c defined class like UICollectionViewCell.


Until a better solution is found you may try this approximation:

// api.h
#import <Foundation/Foundation.h>

NS_REFINED_FOR_SWIFT
@interface UICellConfigurationState: NSObject // UIViewConfigurationState but let's keep it simpler
@property BOOL isDisabled;
// other fields here
@end

NS_REFINED_FOR_SWIFT
@interface UICollectionViewCell: NSObject
- (void)updateConfigurationUsingState:(UICellConfigurationState*)state NS_REFINED_FOR_SWIFT;
@end

Here NS_REFINED_FOR_SWIFT is used to hide (double underscore) the corresponding names from Swift.

// api.m
@implementation UICellConfigurationState
@end

@implementation UICollectionViewCell
- (void)updateConfigurationUsingState:(UICellConfigurationState*)state {
    printf("objc updateConfigurationUsingState");
}
@end

Nothing special in objc implementation, business as usual.

// api.swift
public struct UICellConfigurationState {
    var isDisabled: Bool
    // other fields here
}

Completely redefine what UICellConfigurationState is in Swift

class UICollectionViewCell: __UICollectionViewCell {
    open func updateConfiguration(using state: UICellConfigurationState) {
        let objState = __UICellConfigurationState()
        objState.isDisabled = state.isDisabled
        // other fields here
        super.__updateConfiguration(using: objState)
    }
}

Here use the underscored name as a base class and underscored method to reach out for obj-c implementation.

// test.swift
class MyCell: UICollectionViewCell {
    override func updateConfiguration(using state: UICellConfigurationState) {
    }
}

In your subclass in the app everything should work as expected.

1 Like

I know it's an underscored protocol, but wouldn't the bridging be possible with the adoption of the _ObjectiveCBridgeable protocol? The API would still be imported as the original class, but other than that, it should still be automatically convertible to the respective struct and back, no?

2 Likes

I just tried _ObjectiveCBridgeable and it seems to work.

NSCellConfigurationState.swift

public struct NSCellConfigurationState {
    public var isSelected: Bool = false
    public var isEditing: Bool = false
}

extension NSCellConfigurationState: _ObjectiveCBridgeable {

    public func _bridgeToObjectiveC() -> NSCellConfigurationStateObjc {
        return NSCellConfigurationStateObjc(isSelected: self.isSelected, isEditing: self.isEditing)
    }

    public static func _forceBridgeFromObjectiveC(_ source: NSCellConfigurationStateObjc, result: inout NSCellConfigurationState?) {
        result = NSCellConfigurationState(isSelected: source.isSelected, isEditing: source.isEditing)
    }
    
    public static func _conditionallyBridgeFromObjectiveC(_ source: NSCellConfigurationStateObjc, result: inout NSCellConfigurationState?) -> Bool {
        _forceBridgeFromObjectiveC(source, result: &result)
        return true
    }
    
    public static func _unconditionallyBridgeFromObjectiveC(_ source: NSCellConfigurationStateObjc?) -> NSCellConfigurationState {
        if let source = source {
            var result: NSCellConfigurationState?
            _forceBridgeFromObjectiveC(source, result: &result)
            return result!
        }
        return NSCellConfigurationState()
    }
}

NSCellConfigurationStateObjc.h

@interface NSCellConfigurationStateObjc : NSObject

- (instancetype)initWithIsSelected:(BOOL)isSelected isEditing:(BOOL)isEditing;

@property (nonatomic, assign) BOOL isSelected;
@property (nonatomic, assign) BOOL isEditing;

@end

NSCellConfigurationStateObjc.m

@implementation NSCellConfigurationStateObjc

- (instancetype)initWithIsSelected:(BOOL)isSelected isEditing:(BOOL)isEditing {
    self = [super init];
    if (self != nil) {
        self.isSelected = isSelected;
        self.isEditing = isEditing;
    }
    return self;
}

@end

NSTableCellView+CellConfiguration.swift

extension NSTableCellView {
    @objc open func updateConfiguration(uding state: NSCellConfigurationState) {
        
    }
}

2 Likes

Good to know!

What are the "_isBridgedToObjectiveC()" and "_getObjectiveCType()"? They are not _ObjectiveCBridgeable requirements, are they still needed?

Could you show your "UICollectionViewCell.updateConfiguration(using:)" emulation?

No they are not needed. I was reading some older documentation. I updated the example.

1 Like

I checked UICellConfigurationState and it indeed conforms to _ObjectiveCBridgeable:

func check1(_ v: any _ObjectiveCBridgeable) {}
func check2<T: _ObjectiveCBridgeable>(_ v: T) {}

let cell = UICellConfigurationState(traitCollection: .current)
check1(cell) // ✅ compiles
check2(cell) // ✅ compiles

Found this on the forum:

@Joe_Groff please clarify. Do you mean "it shouldn't" (e.g. because it is private) or that "it mustn't" (e.g. because something will definitely break, or is that recommendation no longer applies or what. The symbol is underscored to tell us something, but what? And it is already used outside of the standard library e.g. in the UICellConfigurationState above.


Digging further, looks like this feature was "deferred" in 2016 (SE-0058). Interestingly it is exposed now and seems to be working fine for clients outside of the standard library, so it is in a kind of a limbo state but it feels more alive than dead.

4 Likes

I integrated NSCellConfigurationState with _ObjectiveCBridgeable in my library AdvancedCollectionTableView which ports most of the newer UIKit APIs to AppKit. Like table CellRegistration, table cell contentConfiguration, collection view items with SwiftUI views, etc.

2 Likes