swiftreactive-programmingimmutabilityrx-swiftrxdatasources

Find an unique identifier for an enum case to conform to Identifiable


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.


Solution

  • 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.