Search code examples
swiftconcurrencyactorswift-concurrency

Observer pattern with actor


I try to implement some repository that can notify about change its state.

protocol Observer {
}

actor Repository: Actor {
    var observers: [Observer] = []
    func add(observer: Observer) {
        self.observers.append(observer)
    }
}

and add unit test for it

func test_get_update() async {
    let repository = Repository()
    let observer = MockObserver()    
    await repository.add(observer: observer)
}

and get warning

Non-sendable type 'any Observer' passed in implicitly asynchronous call to actor-isolated instance method 'add(observer:)' cannot cross actor boundary

but in main app there is no warning.

class Bar {
    func foo() async {
        let repository = Repository()
        let observer = MockObserver()
        
        await repository.add(observer: observer)
    }
}

I don't understand is this implementation of repository correct or not?


Solution

  • You could certainly make your Observer a Sendable type:

    protocol Observer: Sendable {
        func observe(something: Int)
    }
    
    struct MockObserver: Observer {
        func observe(something: Int) {
            print("I observed \(something)")
        }
    }
    
    actor Repository {
        private var observers: [any Observer] = []
    
        func add(observer: any Observer) {
            observers.append(observer)
        }
    
        private func notifyObservers(value: Int) {
            for observer in observers {
                observer.observe(something: value)
            }
        }
    
        func doSomething() {
            notifyObservers(value: 42)
        }
    }
    
    
    let myRepo = Repository()
    Task {
        let myObserver = MockObserver()
        await myRepo.add(observer: myObserver)
        await myRepo.doSomething()
    }
    

    But since Sendable imposes so many restrictions on things, that might make implementing an interesting observer a challenge.

    I would be inclined to use the Observer implementations already available in the Combine framework:

    actor AnotherRepo {
        nonisolated let interestingState: AnyPublisher<Int, Never>;
        private let actualState = PassthroughSubject<Int, Never>();
    
        init() {
            interestingState = actualState.eraseToAnyPublisher()
        }
    
        func doSomething() {
            actualState.send(42)
        }
    }
    
    let anotherRepo = AnotherRepo()
    anotherRepo.interestingState.sink { print("I also observed \($0)") }
    
    Task {
        await anotherRepo.doSomething()
    }