RxMVVM-Texture best practice
RxSwift MVVM pattern best practice built on Texture(AsyncDisplayKit) and written in Swift
[ Model ]
class Repository : Decodable {
var id: Int
var user: User?
var repositoryName: String ?
var desc: String ?
var isPrivate: Bool = false
var isForked: Bool = false
enum CodingKeys : String , CodingKey {
case id = " id"
case user = " owner"
case repositoryName = " full_name"
case desc = " description"
case isPrivate = " private"
case isForked = " fork"
}
func merge (_ repo : Repository? ) {
guard let repo = repo else { return }
user? .merge (repo.user )
repositoryName = repo.repositoryName
desc = repo.desc
isPrivate = repo.isPrivate
isForked = repo.isForked
}
[ ViewModel ]
class RepositoryViewModel {
// @INPUT
let didTapUserProfile = PublishRelay< Void > ()
let updateRepository = PublishRelay< Repository> ()
let updateUsername = PublishRelay< String ?> ()
let updateDescription = PublishRelay< String ?> ()
// @OUTPUT
var openUserProfile: Observable<Void >
var username: Driver<String ? >
var profileURL: Driver<URL? >
var desc: Driver<String ? >
var status: Driver<String ? >
let id: Int
private let disposeBag = DisposeBag ()
deinit {
// release Model from DataProvider
RepoProvider.release (id : id)
}
init (repository : Repository) {
self .id = repository.id
// retain Model to DataProvider
RepoProvider.addAndUpdate (repository)
// load Model Observer from ModelProvider
let repoObserver = RepoProvider.observable (id : id)
.asObservable ()
.share (replay : 1 , scope : .whileConnected )
self .username = repoObserver
.map { $0 ? .user ? .username }
.asDriver (onErrorJustReturn : nil )
self .profileURL = repoObserver
.map { $0 ? .user ? .profileURL }
.asDriver (onErrorJustReturn : nil )
self .desc = repoObserver
.map { $0 ? .desc }
.asDriver (onErrorJustReturn : nil )
[ View ]
class RepositoryListCellNode : ASCellNode {
init (viewModel : RepositoryViewModel) {
...
// ViewModel Binding
userProfileNode.rx
.tap (to : viewModel.didTapUserProfile )
.disposed (by : disposeBag)
viewModel.profileURL .asObservable ()
.bind (to : userProfileNode.rx .url )
.disposed (by : disposeBag)
viewModel.username .asObservable ()
.bind (to : usernameNode.rx .text (Node.usernameAttributes ),
setNeedsLayout : self )
.disposed (by : disposeBag)
viewModel.desc .asObservable ()
.bind (to : descriptionNode.rx .text (Node.descAttributes ),
setNeedsLayout : self )
.disposed (by : disposeBag)
viewModel.status .asObservable ()
.bind (to : statusNode.rx .text (Node.statusAttributes ),
setNeedsLayout : self )
.disposed (by : disposeBag)
}
Open Profile
class RepositoryListCellNode : ASCellNode {
...
init (viewModel : RepositoryViewModel) {
...
// HERE!
userProfileNode.rx
.tap (to : viewModel.didTapUserProfile )
.disposed (by : disposeBag)
}
class RepositoryViewModel {
// @INPUT
let didTapUserProfile = PublishRelay< Void > ()
// @OUTPUT
var openUserProfile: Observable<Void >
...
init (repository : Repository) {
...
// HERE!
self .openUserProfile = self .didTapUserProfile .asObservable ()
}
}
class RepositoryViewController : ASViewController<ASTableNode> {
...
func tableNode (_ tableNode : ASTableNode, nodeBlockForRowAt indexPath : IndexPath)
-> ASCellNodeBlock {
return {
guard self .items .count > indexPath.row else { return ASCellNode () }
let viewModel = self .items [indexPath.row ]
let cellNode = RepositoryListCellNode (viewModel : viewModel)
// HERE!
viewModel.openUserProfile
.observeOn (MainScheduler.asyncInstance )
.subscribe (onNext : { [weak self ] _ in
self ? .openUserProfile (indexPath : indexPath)
}).disposed (by : self .disposeBag )
return cellNode
}
}
}
Update description
class UserProfileViewController : ASViewController<ASDisplayNode> {
...
init (viewModel : ... ) {
...
// HERE!
self .descriptionNode .textView .rx .text
.bind (to : self .viewModel .updateDescription ,
setNeedsLayout : self .node )
.disposed (by : self .disposeBag )
}
}
class RepositoryViewModel {
// @INPUT
let updateDescription = PublishRelay< String ?> ()
// @OUTPUT
var desc: Driver<String ? >
init ( ... ) {
...
let repoObserver = RepoProvider.observable (id : id)
.asObservable ()
.share (replay : 1 , scope : .whileConnected )
self .desc = repoObserver
.map { $0 ? .desc }
.asDriver (onErrorJustReturn : nil )
updateDescription.withLatestFrom (repoObserver) { ($0 , $1 ) }
.subscribe (onNext : { text, repo in
guard let repo = repo else { return }
repo.desc = text
RepoProvider.update (repo)
}).disposed (by : disposeBag)
}
}
Example Video
Example Video Link