Skip to content

Instantly share code, notes, and snippets.

@lammertw
Created March 8, 2021 17:54
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save lammertw/4483b036fc532cddbf96ddb7847a808c to your computer and use it in GitHub Desktop.
Save lammertw/4483b036fc532cddbf96ddb7847a808c to your computer and use it in GitHub Desktop.
data class Restaurant(val id: Int, val name: String, val priceCategory: String)
data class Reservation(val id: Int, val date: Long, val numberOfGuests: Int)
sealed class Response<T> {
class Loading<T>: Response<T>()
class Failed<T>: Response<T>()
data class Success<T>(val data: T): Response<T>()
}
interface Api {
fun restaurant(id: Int): Flow<Response<Restaurant>>
fun restaurantReservations(restaurantId: Int): Flow<Response<List<Reservation>>>
}
func asPublisher<T>(_ flow: CFlow<T>) -> AnyPublisher<T, Never> {
return Deferred<Publishers.HandleEvents<PassthroughSubject<T, Never>>> {
let subject = PassthroughSubject<T, Never>()
let closable = flow.watch { next in
if let next = next {
subject.send(next)
}
}
return subject.handleEvents(receiveCancel: {
closable.close()
})
}.eraseToAnyPublisher()
}
private class ObservableModel<Observed>: ObservableObject {
@Published var observed: Observed?
init(publisher: AnyPublisher<Observed, Never>) {
publisher
.compactMap { $0 }
.receive(on: DispatchQueue.main)
.assign(to: &$observed)
}
}
public struct ObservingView<Observed, Content>: View where Content: View {
@ObservedObject private var model: ObservableModel<Observed>
private let content: (Observed) -> Content
public init(publisher: AnyPublisher<Observed, Never>, @ViewBuilder content: @escaping (Observed) -> Content) {
self.model = ObservableModel(publisher: publisher)
self.content = content
}
public var body: some View {
let view: AnyView
if let observed = self.model.observed {
view = AnyView(content(observed))
} else {
view = AnyView(EmptyView())
}
return view
}
}
import Combine
import SwiftUI
import shared
class ObservableRestaurantDetailViewModel: ObservableObject {
@Published var viewModelOutput: RestaurantDetailViewModelOutput?
init(viewModel: RestaurantDetailViewModel) {
asPublisher(viewModel.output)
.compactMap { $0 }
.receive(on: DispatchQueue.main)
.assign(to: &$viewModelOutput)
}
}
struct RestaurantDetailView: View {
@ObservedObject var viewModelOutput: ObservableRestaurantDetailViewModel
init(viewModel: RestaurantDetailViewModel) {
viewModelOutput = ObservableRestaurantDetailViewModel(viewModel: viewModel)
}
var body: some View {
if let output = viewModelOutput.viewModelOutput {
VStack {
Text(output.name)
Text(output.priceCategory)
// etc
}
} else {
EmptyView()
}
}
}
struct RestaurantDetailView: View {
let viewModel: RestaurantDetailViewModel
var body: some View {
ObservingView(publisher: asPublisher(viewModel.output), content: InnerView.init)
}
}
private struct InnerView: View {
let output: RestaurantDetailViewModelOutput
var body: some View {
VStack {
Text(output.name)
Text(output.priceCategory)
// etc
}
}
}
struct RestaurantDetailView_Previews: PreviewProvider {
static var previews: some View {
InnerView(
output: RestaurantDetailViewModelOutput(
name: "My restaurant",
priceCategory: "Expensive",
reservations: RestaurantDetailViewModelOutput.ReservationsLoading()
)
)
}
}
interface RestaurantDetailViewModel {
val output: CFlow<Output>
data class Output(
val name: String,
val priceCategory: String,
val reservations: Reservations
) {
sealed class Reservations {
object Loading: Reservations()
data class NoReservations(val text: String): Reservations()
data class ReservationList(val items: List<Reservation>): Reservations() {
data class Reservation(val id: Int, val date: String, val numberOfGuests: Int)
}
}
}
}
class RestaurantDetailViewModelImpl(api: Api, restaurantId: Int): RestaurantDetailViewModel {
private val _restaurant = api.restaurant(restaurantId)
private val _reservations = api.restaurantReservations(restaurantId)
private val _output = combine(_restaurant, _reservations) { restaurantResponse, reservationsResponse ->
// for simplicity we ignore error handling and exclude specific loading state for when the restaurant is loading
RestaurantDetailViewModel.Output(
name = (restaurantResponse as? Response.Success)?.data?.name ?: "Loading or error...",
priceCategory = (restaurantResponse as? Response.Success)?.data?.priceCategory ?: "Loading or error...",
reservations = when(reservationsResponse) {
is Response.Loading -> RestaurantDetailViewModel.Output.Reservations.Loading
is Response.Failed -> RestaurantDetailViewModel.Output.Reservations.NoReservations("Failed")
is Response.Success -> {
val reservations = reservationsResponse.data
if (reservations.isEmpty()) {
RestaurantDetailViewModel.Output.Reservations.NoReservations("No reservations yet")
} else {
RestaurantDetailViewModel.Output.Reservations.ReservationList(reservations.map {
RestaurantDetailViewModel.Output.Reservations.ReservationList.Reservation(
id = it.id,
date = "epoch: ${it.date}", // normally would do some formatting
numberOfGuests = it.numberOfGuests
)
})
}
}
}
)
}
override val output = _output.distinctUntilChanged().wrap()
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment