I am testing RxDataSources which requires the models to conform to the Equatable
and IdentifiableType
(similar to Identifiable
protocols.
I am using a unidirectional data flow to drive the UI and my model is the following enum:
enum ViewModelType: Equatable, IdentifiableType {
case loading
case post(Post)
var identity: String {
switch self {
case .loading: return "???" // <-- What to choose here?
case let .post(post): return String(post.id)
}
}
static func == (lhs: ViewModel.ViewModelType, rhs: ViewModel.ViewModelType) -> Bool {
switch (lhs, rhs) {
case (.loading, .loading): return true
case let (.post(l), .post(r)): return l == r
default: return false
}
}
}
}
Concerning the .loading
ViewModelType, I have to find a possible stable unique identifier if I want to use multiple ViewModelType of this type at once like [.loading, .loading, .loading]
or the diffing breaks from identical identifier
.
I prepared a sample project to reproduce the issue: https://github.com/florianldt/RxDataSourcesIdentifiableType
Any ideas of possible stable unique identifiers I can find for this use case.
CodePudding user response:
There are several changes you will need to make, all stemming from the fact that you are completely replacing your view model without regard to its previous state. When building a state machine, remember that the update is based on the input and the previous state. Here are the minimal changes you should make:
There is no need to build your own AnimatableSectionModelType. Simply use the one that the library provides. Replace your SingleSectionModel
struct with the below:
typealias SingleSectionModel<T> = AnimatableSectionModel<String, T> where T: IdentifiableType & Equatable
Turn your ViewModelType
into a struct instead of an enum:
struct ViewModelType: Equatable, IdentifiableType {
let identity: UUID
let post: Post?
}
By doing this, you assure that every value has an identity. You can tell that it is "loading" by the fact that post
is nil.
Instead of completely replacing your view model every time it changes state, use its previous state to help determine the new state. This means removing your init(state:)
function and adding these two functions:
func loading() -> ViewModel {
ViewModel(
state: .loading,
viewModels: [
ViewModelType(identity: UUID(), post: nil),
ViewModelType(identity: UUID(), post: nil),
ViewModelType(identity: UUID(), post: nil)
]
)
}
func loaded(posts: [Post]) -> ViewModel {
ViewModel(
state: .loaded,
viewModels: zip(viewModels, posts).map { ViewModelType(identity: $0.0.identity, post: $0.1) }
(posts.count > viewModels.count ? posts[viewModels.count...].map { ViewModelType(identity: UUID(), post: $0) } : [])
)
}
Notice how now, all three view model types have different identities and when the posts come in, they are not changed. That is the key.
When you fetch your posts, you will need to use the previous state of the view model to make the updates:
private func fetchPosts() {
viewModel.accept(viewModel.value.loading())
Post.list()
.subscribe(onSuccess: { [viewModel] posts in
viewModel.accept(viewModel.value.loaded(posts: posts))
})
.disposed(by: disposeBag)
}
The rest of the changes should be pretty obvious.
CodePudding user response:
While I believe you shouldn't base your Data Sources on the state of whatever it is you're loading (if these are unique posts, try that post's ID instead), you can always use UUID
(or, specifically, UUID().uuidString
for a unique identifier string.