Group Group Group Group Group Group Group Group Group

Failed to demangle witness error when using Realm in a custom Publisher

This question actually crosses over between the RW Combine and Realm books but, as they share some authors I’m hoping this won’t cause a problem.

I’m attempting to test a custom Publisher that emits Realm object arrays associated with a given query. The code works as expected in a playground and inside the app. The problem arises when I attempt to be good and write a test (as described in the Combine book)… the test case crashes with an error I’ve never seen before relating to mangled witness table names. This is far further into the weeds than I’ve gone before and I’m uncertain if it’s a bug in my code, a Realm/Combine bug or something to do with threading. Any thoughts or guidance on how to resolve this would be very much appreciated as I’ve no idea what to do next!

Code base as follows (the Patient Object is a very simple class but I can provide the code if needed):

Custom publisher

// MARK: Custom publisher - produces a stream of Object arrays in response to change notifcations on a given Realm collection
extension Publishers {
    struct Realm<Collection: RealmCollection>: Publisher {
        typealias Output = Array<Collection.Element>
        typealias Failure = Never // TODO: Not true but deal with this later

        let collection: Collection

        init(collection: Collection) {
            self.collection = collection
        }

        func receive<S>(subscriber: S) where S : Subscriber, Failure == S.Failure, Output == S.Input {
            let subscription = RealmSubscription(subscriber: subscriber, collection: collection)
            subscriber.receive(subscription: subscription)
        }
    }
}

// MARK: Convenience accessor function to the custom publisher
extension Publishers {
    static func realm<Collection: RealmCollection>(collection: Collection) -> Publishers.Realm<Collection> {
        return Publishers.Realm(collection: collection)
    }
}

// MARK: Custom subscription
private final class RealmSubscription<S: Subscriber, Collection: RealmCollection>: Subscription where S.Input == Array<Collection.Element> {
    private var subscriber: S?
    private let collection: Collection
    private var notificationToken: NotificationToken?

    init(subscriber: S, collection: Collection) {
        self.subscriber = subscriber
        self.collection = collection

        self.notificationToken = collection.observe { (changes: RealmCollectionChange) in
            switch changes {
            case .initial:
                // Results are now populated and can be accessed without blocking the UI
                print("Initial")
                let _ = subscriber.receive(Array(collection.elements)) // ERROR THROWN HERE 
            //            case .update(_, let deletions, let insertions, let modifications):
            case .update(_, _, _, _):
                print("Updated")
                let _ = subscriber.receive(Array(collection.elements))
            case .error(let error):
                fatalError("\(error)")
                #warning("Impl error handling - do we want to fail or log and recover?")
            }
        }
    }

    func request(_ demand: Subscribers.Demand) {
        // no impl as RealmSubscriber is effectively just a sink
    }

    func cancel() {
        print("Cancel called on RealnSubscription")
        subscriber = nil
        notificationToken = nil
    }
}

Service class

protocol RealmServiceType {
    func all<Element>(_ type: Element.Type, within realm: Realm) -> AnyPublisher<Array<Element>, Never> where Element: Object

    @discardableResult
    func addPatient(_ name: String, to realm: Realm) throws -> AnyPublisher<Patient, Never>

    func deletePatient(_ patient: Patient, from realm: Realm)
}

extension RealmServiceType {
    func all<Element>(_ type: Element.Type) -> AnyPublisher<Array<Element>, Never> where Element: Object {
        print("Called \(#function)")
        return all(type, within: try! Realm())
    }
}

final class TestRealmService: RealmServiceType {
    private let patients = [
        Patient(name: "Tiddles"), Patient(name: "Fang"), Patient(name: "Phoebe"), Patient(name: "Snowy")
    ]

    init() {
        let realm = try! Realm()
        guard realm.isEmpty else { return }
        try! realm.write {
            for p in patients {
                realm.add(p)
            }
        }
    }

    func all<Element>(_ type: Element.Type, within realm: Realm) -> AnyPublisher<Array<Element>, Never> where Element: Object {
        return Publishers.realm(collection: realm.objects(type).sorted(byKeyPath: "name")).eraseToAnyPublisher()
    }


    func addPatient(_ name: String, to realm: Realm) throws -> AnyPublisher<Patient, Never> {
        let patient = Patient(name: name)
        try! realm.write {
            realm.add(patient)
        }
        return Just(patient).eraseToAnyPublisher()
    }

    func deletePatient(_ patient: Patient, from realm: Realm) {
        try! realm.write {
            realm.delete(patient)
        }
    }

}

Test case

class AthenaVSTests: XCTestCase {
    private var cancellables = Set<AnyCancellable>()
    private var service: RealmServiceType?

    override func setUp() {
        service = TestRealmService()
    }

    override func tearDown() {
        // Put teardown code here. This method is called after the invocation of each test method in the class.
        service = nil
        cancellables.removeAll()
    }

    func testRealmPublisher() {
        var outcome = [""]
        let expectation = self.expectation(description: #function)
        let expected = ["Tiddles", "Fang", "Phoebe", "Snowy"]

        let _ = service?.all(Patient.self)
            .sink(receiveCompletion: { _ in
                expectation.fulfill() },
                  receiveValue: { value in
                    outcome += value.map { $0.name }
            })
            .store(in: &cancellables)

        waitForExpectations(timeout: 2, handler: nil)

        XCTAssert(outcome == expected, "Expected \(expected) Objects but got \(outcome)")
    }
}

Error message

failed to demangle witness for associated type ‘Iterator’ in conformance ‘RealmSwift.Results: Sequence’ from mangled name ‘10RealmSwift11RLMIteratorVyxG’ 2020-01-13 22:46:07.159964+0000 AthenaVS[3423:171342] failed to demangle witness for associated type ‘Iterator’ in conformance ‘RealmSwift.Results: Sequence’ from mangled name ‘10RealmSwift11RLMIteratorVyxG’

The error is thrown when attempting to execute code in the Realm notification observer within RealmSubscription (I’ve flagged it in the code above), specifically:

let _ = subscriber.receive(Array(collection.elements))

Ideas?

Update on this one…

I was using SwiftPM to handle dependencies (the only one in this case was Realm) and the issue has been isolated to this. I’ve dropped back to using Carthage and the runtime error has disappeared. I’m not sure if this is a SwiftPM bug or down to Realm’s implementation. For now however it’s working OK :slight_smile:

@rustprooffish Glad you fixed this! Cheers! :]