Created
March 8, 2021 17:54
-
-
Save lammertw/4483b036fc532cddbf96ddb7847a808c to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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>>> | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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() | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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() | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
struct RestaurantDetailView_Previews: PreviewProvider { | |
static var previews: some View { | |
InnerView( | |
output: RestaurantDetailViewModelOutput( | |
name: "My restaurant", | |
priceCategory: "Expensive", | |
reservations: RestaurantDetailViewModelOutput.ReservationsLoading() | |
) | |
) | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | |
} | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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