Is it possible to set up a `@Published` in an `ObservableObject` even more type-safe?

51 Views Asked by At

So I have an ObservableObject which has an input-binding named query and a @Published output of search results named output. It looks something like this:

class SearchViewModel: ObservableObject {
    typealias Output = Result<[SearchResult], ApiError>

    // `@Input` is similar to `@Published` except
    // that it doesn't cause SwiftUI to re-render.
    @Input var query = ""

    @Published private(set) var output = Output.success([])
    
    private let loader = SearchResultsLoader()

    init() {
        configureDataPipeline()
    }

    func configureDataPipeline() {
        $query
            .debounce(for: .seconds(0.5), scheduler: DispatchQueue.main)
            .removeDuplicates()
            // ... map to results from self.loader ...
            .receive(on: DispatchQueue.main)
            .assign(to: &$output)
    }
}

Now I guess this is all fine, but it's not like perfectly type-safe. I'm coming from javascript with RxJS and there we would do something like:

const output$ = query$.pipe(/* transformations here */)

And the beautiful thing about that, is two things basically:

  1. Everything you need to know about output$ is right there in its declaration. You don't need to look for other mentions of output$ to determine what output$ is and how it behaves.
  2. Because output$ is a const you can rest assured that output$ will never be changed into something other than what it was declared as.

I would really like it if I can get these same things in my Swift code. Is that possible? Maybe by writing my own property wrapper that slightly differs from @Published in that it accepts an AnyPublisher and then internally assigns it? Or would that interfere with using the property in SwiftUI?

Or is this whole thing a fool's errand?

1

There are 1 best solutions below

0
Scott Thompson On

What seems closest to the RxJS you posted is something along these lines:

class SearchViewModel {
  typealias Output = Result<[SearchResult], APIError>

  let output: AnyPublisher<Output, Never>
  private let results = CurrentValueSubject<Output, Never>(Output.success([]))

  init() {
    output = results./* pipeline building here */.eraseToAnyPublisher()
  }
}

You loose the ability to handle changes to "output" using Swift UI's syntax sugaring but you could use an onReceive based on output to respond to changes.