swiftprotocolscombinepublisher

Swift extension for Publisher.Output that conforms to protocol AND might be Optional


I am trying to implement an extension for Publisher with a function sinkWithActions(). This function should perform sink with some custom actions inside and returns an AnyCancellable.

sinkWithActions() expects values of a type that conforms to a custom NavigationRoute protocol and this values should be Optional.

I have managed to get this working with non-optional values, but I am having trouble handling optional values as well. How can I achieve this?

Here's what I've tried so far. I expected that I will be able to use sinkWithActions() here.

protocol NavigationRoute: Identifiable, Equatable { }

struct DetailsRoute: NavigationRoute {
    var id: Int
}

final class ViewModel: ObservableObject {
    @Published var fullscreen: DetailsRoute?
    private var cancellables = Set<AnyCancellable>()

    init() {
        $fullscreen
            .sinkWithActions()    // here
            .store(in: &cancellables)
    }
}

extension Publisher where Output: NavigationRoute, Failure == Never {
    func sinkWithActions() -> AnyCancellable {
        self.sink { _ in
            // actions...
        }
    }
}

Compiler is saying:

Referencing instance method 'sinkWithActions()' on 'Publisher' requires that 'Published<DetailsRoute?>.Publisher.Output' (aka 'Optional<DetailsRoute>') conform to 'NavigationRoute'


Solution

  • You can simply declare another sinkWithActions overload for optional navigation routes.

    extension Publisher where Failure == Never{
        func sinkWithActions<T: NavigationRoute>() -> AnyCancellable where Output == T? {
            self.sink { _ in
                // ...
            }
        }
    }
    

    Alternatively, you can also make Optional conform to NavigationRoute if its wrapped value conforms to NavigationRoute. This is similar to how an optional of some Equatable thing is also Equatable.

    // the protocol should be public for this to work
    public protocol NavigationRoute: Identifiable, Equatable { }
    
    extension Optional: Identifiable where Wrapped: NavigationRoute {
        public var id: Wrapped.ID? { self?.id }
    }
    
    extension Optional: NavigationRoute where Wrapped: NavigationRoute {
        
    }
    

    Note: depending on what exactly you are in sink, and how you are using NavigationRoute elsewhere, this conformance might not make sense.