Problems inheriting a Universal Framework interface

Hi everyone.

I am a beginner at Swift. I am trying to extend Google's MLKit StrokePoint to make it Codable:

import Foundation
import MLKit

class CodableStrokePoint: StrokePoint, Codable {
    
    public func encode(to encoder: Encoder) throws {
        var container = encoder.container(keyedBy: CodingKeys.self)
        try container.encode(self.x, forKey: .x)
        try container.encode(self.y, forKey: .y)
        try container.encode(self.t!.intValue, forKey: .t)
    }
    
    public required convenience init(from decoder: Decoder) throws {
        let values = try decoder.container(keyedBy: CodingKeys.self)
        let _x = try values.decode(Float.self, forKey: .x)
        let _y = try values.decode(Float.self, forKey: .y)
        let _t = try values.decode(Int.self, forKey: .t)
        self.init(x: _x, y: _y, t: _t)
    }

    enum CodingKeys: String, CodingKey {
        case x
        case y
        case t
    }
}

However Xcode gives me a linker error:

Undefined symbol: _OBJC_METACLASS_$_MLKStrokePoint

The StrokePoint is defined like that:

/** A single touch point from the user. */
NS_SWIFT_NAME(StrokePoint)
@interface MLKStrokePoint : NSObject

/** Horizontal coordinate. Increases to the right. */
@property(nonatomic, readonly) float x;

/** Vertical coordinate. Increases downward. */
@property(nonatomic, readonly) float y;

/** Time when the point was recorded, in milliseconds. */
@property(nonatomic, readonly, nullable) NSNumber *t;

/** Unavailable. Use `init(x:y:t:)` instead. */
- (instancetype)init NS_UNAVAILABLE;

/**
 * Creates a `StrokePoint` object using the coordinates provided as argument.
 *
 * Scales on both dimensions are arbitrary but be must be identical: a displacement of 1
 * horizontally or vertically must represent the same distance, as seen by the user.
 *
 * Spatial and temporal origins can be arbitrary as long as they are consistent for a given ink.
 *
 * @param x Horizontal coordinate. Increases to the right.
 * @param y Vertical coordinate. Increases going downward.
 * @param t Time when the point was recorded, in milliseconds.
 */
- (instancetype)initWithX:(float)x y:(float)y t:(long)t;

/**
 * Creates an `MLKStrokePoint` object using the coordinates provided as
 * argument, without specifying a timestamp. This method should only be used
 * when it is not feasible to include the timestamp information, as the
 * recognition accuracy might degrade.
 *
 * Scales on both dimensions are arbitrary but be must be identical: a
 * displacement of 1 horizontally or vertically must represent the same
 * distance, as seen by the user.
 *
 * Spatial origin can be arbitrary as long as it is consistent for a given ink.
 *
 * @param x horizontal coordinate. Increases to the right.
 * @param y vertical coordinate. Increases going downward.
 */
- (instancetype)initWithX:(float)x y:(float)y;

@end

Could you please suggest what I am doing wrong and how to fix it?

that's a linker error, same would be with a shorter sample:

let x = StrokePoint(x: 0, y: 0)

you haven't included the library into the project/target, or it's of a different architecture (and thus ignored), or something to that effect.

I used it in the code without any problems like following:

point = StrokePoint.init(x: Float(lastPoint.x),
                             y: Float(lastPoint.y),
                             t: Int(t * kMillisecondsPerTimeInterval))

It's defined in the pod 'GoogleMLKit/DigitalInkRecognition', '2.4.0' which is used in the application. The linker started complaining only when I inherited the class.

strange. to troublershoot try to subclass that class from obj-c, will that work? also will you be able making an extension of that class in swift? the answers to those might lead you closer to the solution.

thanks! I know almost nothing about Obj C, so I will need to do some reading first.

After playing with config a little more my error message changed and now it looks following:

ld: warning: Could not find or use auto-linked library 'stdc++'
Undefined symbols for architecture x86_64:
  "_OBJC_METACLASS_$_MLKStrokePoint", referenced from:
      _OBJC_METACLASS_$__TtC7Minutes18CodableStrokePoint in CodableStrokePoint.o
  "_OBJC_METACLASS_$_MLKStroke", referenced from:
      _OBJC_METACLASS_$__TtC7Minutes13CodableStroke in CodableStroke.o
ld: symbol(s) not found for architecture x86_64
clang: error: linker command failed with exit code 1 (use -v to see invocation)

The framework I am using is a Cocoapod and I guess I am failing to link it properly.
Here is my Pod file:

platform :ios, '14.0'
use_frameworks!

target 'MyApp' do
  use_frameworks!

  pod 'GoogleMLKit/DigitalInkRecognition'
  pod 'Firebase/Firestore'

  target 'MyAppTests' do
    inherit! :search_paths
  end

  target 'MyAppUITests' do
  end

end

I have played with search path configuration for headers, frameworks and libraries, but it did not solve the problem.

is this on M1 or Intel mac? double check the "build active architecture only" setting (by default it is on in release and off in debug configuration).

It's Intel Mac. Build Active Architectures Only is set to "Yes" and the framework is added to Link Binary With Libraries.

After a lot of struggles I have found out that Objective C class cannot be made Codable: Can I make my Objective-C class Codable?

As a result, I decided to implement NSCoding instead and then decode it. The example can be found here:

But I was disappointed once again, since I've got the same problem:

ld: warning: Could not find or use auto-linked library 'stdc++'
Undefined symbols for architecture x86_64:
" OBJC_METACLASS $_MLKStrokePoint", referenced from:
OBJC_METACLASS $_SerializableStrokePoint in SerializableStrokePoint.o
" OBJC_METACLASS $_MLKStroke", referenced from:
OBJC_METACLASS $_SerializableStroke in SerializableStroke.o
" OBJC_METACLASS $_MLKInk", referenced from:
OBJC_METACLASS $_SerializableInk in SerializableInk.o
ld: symbol(s) not found for architecture x86_64
clang: error: linker command failed with exit code 1 (use -v to see invocation)

My friend recommended me to play with nm and I have found out that the framework doesn't export the Meta-class. It is defined as 'local':

nm ./MLKitDigitalInkRecognition | grep MLKInk
000000000000ae44 t -[MLKInk .cxx_destruct]
000000000000adcf t -[MLKInk initWithStrokes:]
000000000000ae3a t -[MLKInk strokes]
0000000001121f30 S _OBJC_CLASS_$_MLKInk
0000000001121a18 s _OBJC_IVAR_$_MLKInk._strokes
0000000001121f58 s _OBJC_METACLASS_$_MLKInk
000000000111fb50 s __OBJC_$_INSTANCE_METHODS_MLKInk
000000000111fba0 s __OBJC_$_INSTANCE_VARIABLES_MLKInk
000000000111fbc8 s __OBJC_$_PROP_LIST_MLKInk
000000000111fbe0 s __OBJC_CLASS_RO_$_MLKInk
000000000111fb08 s __OBJC_METACLASS_RO_$_MLKInk

The same for MLKStroke :

nm ./MLKitDigitalInkRecognition | grep MLKStroke
000000000000adbf t -[MLKStroke .cxx_destruct]
000000000000ad4a t -[MLKStroke initWithPoints:]
000000000000adb5 t -[MLKStroke points]
000000000000ad3a t -[MLKStrokePoint .cxx_destruct]
000000000000ad06 t -[MLKStrokePoint initWithX:y:]
000000000000ac92 t -[MLKStrokePoint initWithX:y:t:]
000000000000ac09 t -[MLKStrokePoint initWithX:y:timestamp:]
000000000000ad30 t -[MLKStrokePoint t]
000000000000ad1a t -[MLKStrokePoint x]
000000000000ad25 t -[MLKStrokePoint y]
0000000001121ee0 S _OBJC_CLASS_$_MLKStroke
0000000001121e90 S _OBJC_CLASS_$_MLKStrokePoint
0000000001121a10 s _OBJC_IVAR_$_MLKStroke._points
0000000001121a08 s _OBJC_IVAR_$_MLKStrokePoint._t
00000000011219f8 s _OBJC_IVAR_$_MLKStrokePoint._x
0000000001121a00 s _OBJC_IVAR_$_MLKStrokePoint._y
0000000001121f08 s _OBJC_METACLASS_$_MLKStroke
0000000001121eb8 s _OBJC_METACLASS_$_MLKStrokePoint
000000000111fa30 s __OBJC_$_INSTANCE_METHODS_MLKStroke
000000000111f850 s __OBJC_$_INSTANCE_METHODS_MLKStrokePoint
000000000111fa80 s __OBJC_$_INSTANCE_VARIABLES_MLKStroke
000000000111f900 s __OBJC_$_INSTANCE_VARIABLES_MLKStrokePoint
000000000111faa8 s __OBJC_$_PROP_LIST_MLKStroke
000000000111f968 s __OBJC_$_PROP_LIST_MLKStrokePoint
000000000111fac0 s __OBJC_CLASS_RO_$_MLKStroke
000000000111f9a0 s __OBJC_CLASS_RO_$_MLKStrokePoint
000000000111f9e8 s __OBJC_METACLASS_RO_$_MLKStroke
000000000111f808 s __OBJC_METACLASS_RO_$_MLKStrokePoint

Could you please suggest if there is a way to overcome this thing or I have to implement my own classes and do the expensive copy whenever I need to save data to the database?
Thank you in advance!

perhaps you can do just this:

import Foundation

/*
@objc class StrokePoint: NSObject { // or whatever MLKit has in Obj-C
    var x: Float = 0
    var y: Float = 0
    var t: Int = 0
    
    init(x: Float, y: Float, t: Int) {
        self.x = x
        self.y = y
        self.t = t
    }
}
 */

extension StrokePoint {
    struct CodableStrokePoint: Codable {
        let x: Float
        let y: Float
        let t: Int
    }
    var toCodable: CodableStrokePoint {
        .init(x: x, y: y, t: t)
    }
    convenience init(fromCodable pt: CodableStrokePoint) {
        self.init(x: pt.x, y: pt.y, t: pt.t)
    }
}

it's not too much boilerplate and i hope not too expensive.

1 Like

It's not too efficient since I have nested arrays of them, but it worked! Thank you very much!

You are welcome. It's not just for Obj-C interop... I had to do this trick to swift pure SwiftUI.Font... which was quite a fair amount of boilerplate and for sure will be a nightmare to support going forward, but seems the only way, unless someone has a better idea how to make SwiftUI.Font, Color & friends Codable...

SwiftUI structs are overly opaque...

... to the point it's counter productive - I have to wrap them all over again (!), have my wrapper providing the same API as the wrapped type just to "record" what's been "set" on them to be able to "get" the set values later on for making the thing serialisable, and having the app use the wrappers instead of real types, ouch :disappointed: