4

Sometimes my viewmodel uses a @Published property or a PassthroughSubject, but I don't want this to be writeable to the outside world. Easy enough, turn it into a public AnyPublisher and keep the writable one private, like this:

class ViewModel {
  @Published private var _models = ["hello", "world"]

  var models: AnyPublisher<[String], Never> {
    return $_models.eraseToAnyPublisher()
  }
}

let viewModel = ViewModel()
viewModel.models.sink { print($0) }

But what if you want to be able to read the value "on demand" as well? For example for this situation:

extension ViewController: UICollectionViewDelegate {
  func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
    print(viewModel.models[indexPath.row])
  }
}

Obviously, the above code doesn't work.

I thought about using a CurrentValueSubject, but its value is writable too, plus I'm having a hard time turning a Publisher into a CurrentValueSubject anyway.

My current solution is to add something like this on the viewmodel:

class ViewModel {
  @Published private var _models = ["hello", "world"]
  @Published var selectedIndex: Int?

  var models: AnyPublisher<[String], Never> {
    return $_models.eraseToAnyPublisher()
  }

  var selectedModel: AnyPublisher<String, Never> {
    return models.combineLatest($selectedIndex.compactMap { $0 }).map { value, index in
      value[index]
    }.eraseToAnyPublisher()
  }
}

let viewModel = ViewModel()
viewModel.models.sink { print($0) }

viewModel.selectedModel.sink { print($0) }
viewModel.selectedIndex = 1

But it's a bit of a chore to add the selectedIndex property, the selectedModel publisher, set the selectedIndex and subscribe to the publisher.. all because I want to be able to read the current value of viewModel.models (and not have it writable).

Any better solutions?

Kevin Renskers
  • 5,156
  • 4
  • 47
  • 95

1 Answers1

7

Why don't you simply keep the @Published property public, while making its setter private? That should suit your requirements perfectly while also keeping your code minimal and clean.

class ViewModel {
  @Published public private(set) var models = ["hello", "world"]
}

let viewModel = ViewModel()
viewModel.$models.sink { print($0) }
Dávid Pásztor
  • 51,403
  • 9
  • 85
  • 116
  • ...I never realized I could do that ‍♂️ – Kevin Renskers Apr 30 '20 at 11:45
  • 1
    @KevinRenskers I guess because Swift auto-synthesises getters and setters, unless you see it possible to add different accessibility modifiers to them, you wouldn't even think it's possible. – Dávid Pásztor Apr 30 '20 at 12:01
  • 2
    The problem here is that you can't define your `var` in a `protocol` if it is `@Published`, so you can't achieve abstraction, unless I'm misunderstanding. – bkbeachlabs Jul 16 '20 at 21:19
  • @bkbeachlabs you can't add property wrappers to protocol definitions, but you can still define `@Published` property requirements in protocols with a bit of extra code. See [this answer](https://stackoverflow.com/a/58471595/4667835) for how to do that. Btw, protocols are not the only way to achieve abstraction. – Dávid Pásztor Jul 17 '20 at 08:31