Search code examples
swiftrealmcombine

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


I've never come across a witness table error before but this is my first venture into testing custom Publishers and, if I was to guess, I suspect there is something weird and wonderful going on with threading based on how mangled the witness name is. Completely out at sea here so a pointer (or pointers!) would be much appreciated.

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?


Solution

  • This was a red herring, but at all relating to Combine but rather some Swift build issue, try switching to Carthage instead of using SPM to see if the problem goes away.

    For other this link might be relevant