Skip to content

Instantly share code, notes, and snippets.

@gbasile
Last active April 30, 2021 08:25
Show Gist options
  • Save gbasile/13901ae0e186f7a142435ba4e6bda278 to your computer and use it in GitHub Desktop.
Save gbasile/13901ae0e186f7a142435ba4e6bda278 to your computer and use it in GitHub Desktop.
ViewModel ViewState generation
import Combine
public extension Publisher where Output: Equatable {
@discardableResult func sink() -> AnyCancellable {
sink(
receiveCompletion: { _ in /* No handling needed */},
receiveValue: { _ in /* No handling needed */ })
}
}
struct Device {
let name: String
}
struct User {
let name: String
}
protocol DeviceRepositoryType {
func query() -> AnyPublisher<[Device], Never>
}
protocol UserRepositoryType {
func query() -> AnyPublisher<User, Never>
func update(name: String) -> AnyPublisher<Never, Error>
}
enum ViewState: Equatable {
struct Content: Equatable {
let title: String
let message: String
}
case loading
case error
case content(Content)
}
class ViewModel {
fileprivate enum ActionState {
case loading
case idle
case error
}
private let userRepository: UserRepositoryType
private let deviceRepository: DeviceRepositoryType
private let actionState = CurrentValueSubject<ActionState, Never>(.idle)
private var disposables = Set<AnyCancellable>()
init(userRepository: UserRepositoryType, deviceRepository: DeviceRepositoryType) {
self.userRepository = userRepository
self.deviceRepository = deviceRepository
}
func viewState() -> AnyPublisher<ViewState, Never> {
Publishers.CombineLatest3(
userRepository.query(),
deviceRepository.query(),
actionState
)
.map(toViewState)
.eraseToAnyPublisher()
}
func update(userName: String) {
userRepository.update(name: userName)
.handleEvents(
receiveSubscription: { _ in
self.actionState.send(.loading)
},
receiveCompletion: { completion in
switch completion {
case .finished:
self.actionState.send(.idle)
case .failure:
self.actionState.send(.error)
}
})
.sink()
.store(in: &disposables)
}
}
private func toViewState(user: User, devices: [Device], actionState: ViewModel.ActionState) -> ViewState {
if actionState == .loading {
return .loading
}
if actionState == .error {
return .error
}
let content = ViewState.Content(
title: "Hello \(user.name)",
message: "You have \(devices.count) on your profile"
)
return .content(content)
}
@hisavali
Copy link

💪

@hisavali
Copy link

It seems above snippet needs to tie lifecycle of effect with lifecycle of VM.

VM needs to add

var disposables = Set<AnyCancellable>()

and at the call side

userRepository.update(name: userName)
            .handleEvents(....)
            .sink()
            .store(in: &disposables)

and when VM gets deallocated, the handle to effect gets purged too.

@gbasile
Copy link
Author

gbasile commented Apr 30, 2021

Yes, I wanted to remove at maximum everything that is was not really relevant for the ViewState creation, but as we want to use this code as a reference for our documentation it might be better to be explicit

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment