I am trying to make multiple sections (two actually) using RxDatasources. Usually with one section, I would go like this:
Section model:
import Foundation
import RxDataSources
typealias NotificationSectionModel = AnimatableSectionModel<String, NotificationCellModel>
struct NotificationCellModel : Equatable, IdentifiableType {
static func == (lhs: NotificationCellModel, rhs: NotificationCellModel) -> Bool {
return lhs.model.id == rhs.model.id
}
var identity: String {
return model.id
}
var model: NotificationModel
var cellIdentifier = "NotificationTableViewCell"
}
then the actual model:
struct NotificationModel: Codable, Equatable {
let body: String
let title:String
let id:String
}
And I would use that like this (in view controler):
private func observeTableView(){
let dataSource = RxTableViewSectionedAnimatedDataSource<NotificationSectionModel>(
configureCell: { dataSource, tableView, indexPath, item in
if let cell = tableView.dequeueReusableCell(withIdentifier: item.cellIdentifier, for: indexPath) as? BaseTableViewCell{
cell.setup(data: item.model)
return cell
}
return UITableViewCell()
})
notificationsViewModel.notifications
.map{ notifications -> [NotificationCellModel] in
return notifications.map{ NotificationCellModel( model: $0, cellIdentifier: NotificationTableViewCell.identifier) }
}.map{ [NotificationSectionModel(model: "", items: $0)] }
.bind(to: self.tableView.rx.items(dataSource: dataSource)).disposed(by: disposeBag)
}
But how I would go with multiple sections, with different type of models/cells?
Here is a kind of worst case situation. You might be able to simplify this code depending on your use case:
// MARK: Model Code
struct ViewModel {
let sections: Observable<[SectionModel]>
}
typealias SectionModel = AnimatableSectionModel<String, CellModel>
enum CellModel: IdentifiableType, Equatable {
case typeA(TypeAInfo)
case typeB(TypeBInfo)
var identity: Int {
switch self {
case let .typeA(value):
return value.identity
case let .typeB(value):
return value.identity
}
}
var cellIdentifier: String {
switch self {
case .typeA:
return "TypeA"
case .typeB:
return "TypeB"
}
}
}
struct TypeAInfo: IdentifiableType, Equatable {
let identity: Int
}
struct TypeBInfo: IdentifiableType, Equatable {
let identity: Int
}
// MARK: View Code
class Example: UIViewController {
var tableView: UITableView!
var viewModel: ViewModel!
let disposeBag = DisposeBag()
private func observeTableView(){
let dataSource = RxTableViewSectionedAnimatedDataSource<SectionModel>(
configureCell: { _, tableView, indexPath, item in
guard let cell = tableView.dequeueReusableCell(withIdentifier: item.cellIdentifier, for: indexPath) as? BaseCell else { fatalError() }
cell.setup(model: item)
return cell
})
viewModel.sections
.bind(to: tableView.rx.items(dataSource: dataSource))
.disposed(by: disposeBag)
}
}
class BaseCell: UITableViewCell {
func setup(model: CellModel) { }
}
final class TypeACell: BaseCell { }
final class TypeBCell: BaseCell { }
Related
i am using mvvm pattern in my app here my example of code :
let userVM = UserViewModel()
userVM.getUser()
.subscribeOn(SerialDispatchQueueScheduler.init(qos: .background))
.observeOn(MainScheduler.instance)
.subscribe(onNext: { user in
self.user = user
}, onError: { error in
}, onCompleted: {
}, onDisposed: {
}).disposed(by: self.disposeBag)
i want to access the user emitted onNext outside the onSubscribe function any help with that ?
You shouldn't use any API calls on View.
Make output subscriber on your ViewModel and drive it to your view.
Example ViewModel:
import Foundation
import RxSwift
import RxCocoa
import RxDataSources
extension ModelListViewModel {
enum Sections: SectionModelType {
typealias Item = ModelDTO
case model(models: [Item])
var items: [Item] {
switch self {
case .model(let models):
return models
}
}
init(original: Sections, items: [Item]) {
self = original
}
}
}
class ModelListViewModel {
//inputs
let didLoad = PublishSubject<Void>()
let modelSelected = PublishSubject<ModelDTO>()
//outputs
let sections: Driver<[Sections]>
private let disposeBag = DisposeBag()
private static var data: [ModelDTO] = {
var array = [ModelDTO]()
guard let urlPath = Bundle.main.url(forResource: "seat_medium_quality_m_center", withExtension: "usdz") else {
return []
}
let firstModel = ModelDTO(url: urlPath, modelName: "Seat 1")
let secondModel = ModelDTO(url: urlPath, modelName: "Seat 2")
let thirdModel = ModelDTO(url: urlPath, modelName: "Seat 3")
return [firstModel, secondModel, thirdModel]
}()
init(context: ARRouter.ModelListContext) {
//TODO: Make sections due to responce from BackEnd
sections = didLoad
.mapTo([Sections.model(models: ModelListViewModel.data)])
.asDriver(onErrorDriveWith: .empty())
modelSelected.map { $0.url }
.bind(to: context.modelSelectedIn)
.disposed(by: disposeBag)
}
}
Example of ViewController:
import UIKit
import RxSwift
import RxCocoa
import RxDataSources
class ModelListViewController: BaseViewController {
private struct Cells {
static let modelListCell = ReusableCell<ModelListCell>(nibName: "ModelListCell")
}
#IBOutlet private weak var tableView: UITableView! {
didSet {
tableView.register(Cells.modelListCell)
viewModel.sections
.drive(tableView.rx.items(dataSource: dataSource))
.disposed(by: disposeBag)
tableView.rx
.modelSelected(ModelDTO.self)
.bind(to: viewModel.modelSelected)
.disposed(by: disposeBag)
}
}
private let disposeBag = DisposeBag()
private let viewModel: ModelListViewModel
private let dataSource: RxTableViewSectionedReloadDataSource<ModelListViewModel.Sections>
init(viewModel: ModelListViewModel) {
self.viewModel = viewModel
self.dataSource = .init(configureCell: { (_, tableView, indexPath, item) -> UITableViewCell in
let cell = tableView.dequeue(Cells.modelListCell, for: indexPath)
cell.setup(with: item)
return cell
})
super.init(nibName: "ModelListViewController", bundle: nil)
rx.viewDidLoad
.bind(to: viewModel.didLoad)
.disposed(by: disposeBag)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func viewDidLoad() {
super.viewDidLoad()
}
}
I'm struggle with following challenge. I created table view with custom cell that contains switch. I wanna only one switch can be on i.e, for instance after launch I switched on 3rd switched and then I switched on 7th switch and thus the 3rd one is switched off and so on. I use rx + protocols for cell and don't understand all the way how to determine which switch was toggled. Previously I was going to use filter or map to look up in dataSource array which switch is on and somehow handle this, but now I messed up with it. I'm not sure it's possible without using table view delegate methods. Thanks a lot, hope someone could explain where I am wrong.
//My cell looks like this:
// CellViewModel implementation
import Foundation
import RxSwift
protocol ViewModelProtocol {
var bag:DisposeBag {get set}
func dispose()
}
class ViewModel:ViewModelProtocol {
var bag = DisposeBag()
func dispose() {
self.bag = DisposeBag()
}
}
protocol CellViewModelProtocol:ViewModelProtocol {
var isSwitchOn:BehaviorSubject<Bool> {get set}
}
class CellVM:ViewModel, CellViewModelProtocol {
var isSwitchOn: BehaviorSubject<BooleanLiteralType> = BehaviorSubject(value: false)
let internalBag = DisposeBag()
override init() {
}
}
//My Cell implementation
import UIKit
import RxSwift
import RxCocoa
class Cell:UITableViewCell {
static let identifier = "cell"
#IBOutlet weak var stateSwitch:UISwitch!
var vm:CellViewModelProtocol? {
didSet {
oldValue?.dispose()
self.bindUI()
}
}
var currentTag:Int?
var bag = DisposeBag()
override func awakeFromNib() {
super.awakeFromNib()
self.bindUI()
}
override func prepareForReuse() {
super.prepareForReuse()
self.bag = DisposeBag()
}
private func bindUI() {
guard let vm = self.vm else { return }
self.stateSwitch.rx.controlEvent(.valueChanged).withLatestFrom(self.stateSwitch.rx.value).observeOn(MainScheduler.asyncInstance).bind(to: vm.isSwitchOn).disposed(by: vm.bag)
}
}
//TableViewController implementation
import UIKit
import RxSwift
import RxCocoa
class TableViewController: UITableViewController {
private var dataSource:[CellViewModelProtocol] = []
var vm = TableViewControllerVM()
override func viewDidLoad() {
super.viewDidLoad()
self.tableView.estimatedRowHeight = 70
self.tableView.rowHeight = UITableView.automaticDimension
self.bindUI()
}
private func bindUI() {
vm.dataSource.observeOn(MainScheduler.asyncInstance).bind { [weak self] (dataSource) in
self?.dataSource = dataSource
self?.tableView.reloadData()
}.disposed(by: vm.bag)
}
// MARK: - Table view data source
override func numberOfSections(in tableView: UITableView) -> Int {
return 1
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return self.dataSource.count
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: Cell.identifier, for: indexPath) as! Cell
if cell.vm == nil {
cell.vm = CellVM()
}
return cell
}
}
class TableViewControllerVM:ViewModel {
var dataSource:BehaviorSubject<[CellViewModelProtocol]> = BehaviorSubject(value: [])
let internalBag = DisposeBag()
override init() {
super.init()
dataSource.onNext(createDataSourceOf(size: 7))
self.handleState()
}
private func createDataSourceOf(size:Int) -> [CellViewModelProtocol] {
var arr:[CellViewModelProtocol] = []
for _ in 0..<size {
let cell = CellVM()
arr.append(cell)
}
return arr
}
private func handleState() {
}
}
Maybe this code will help you:
extension TableViewController {
// called from viewDidLoad
func bind() {
let cells = (0..<7).map { _ in UUID() } // each cell needs an ID
let active = ReplaySubject<UUID>.create(bufferSize: 1) // tracks which is the currently active cell by ID
Observable.just(cells) // wrap the array in an Observable
.bind(to: tableView.rx.items(cellIdentifier: "Cell", cellType: Cell.self)) { _, element, cell in
// this subscription causes the inactive cells to turn off
active
.map { $0 == element }
.bind(to: cell.toggleSwitch.rx.isOn)
.disposed(by: cell.disposeBag)
// this subscription watches for when a cell is set to on.
cell.toggleSwitch.rx.isOn
.filter { $0 }
.map { _ in element }
.bind(to: active)
.disposed(by: cell.disposeBag)
}
.disposed(by: disposeBag)
}
}
Have a similar UI,so tested locally and it works.But not very neat code.
ProfileCellViewModel
struct ProfileCellViewModel {
// IMPORTANT!!!
var bibindRelay: BehaviorRelay<Bool>?
}
ProfileCell
final class ProfileCell: TableViewCell {
#IBOutlet weak var topLabel: Label!
#IBOutlet weak var centerLabel: Label!
#IBOutlet weak var bottomLabel: Label!
#IBOutlet weak var onSwitch: Switch!
public var vm: ProfileCellViewModel? {
didSet {
// IMPORTANT!!!
if let behaviorRelay = vm?.bibindRelay {
(onSwitch.rx.controlProperty(editingEvents: .valueChanged,
getter: { $0.isOn }) { $0.isOn = $1 } <-> behaviorRelay)
.disposed(by: self.rx.reuseBag)
}
}
}
}
ProfileViewModel
final class ProfileViewModel: ViewModel, ViewModelType {
struct Input {
let loadUserProfileStarted: BehaviorRelay<Void>
}
struct Output {
let userItems: BehaviorRelay<[ProfileCellViewModel]>
let chatRelay: BehaviorRelay<Bool>
let callRelay: BehaviorRelay<Bool>
}
let input = Input(loadUserProfileStarted: BehaviorRelay<Void>(value: ()))
let output = Output(userItems: BehaviorRelay<[ProfileCellViewModel]>(value: []),
chatRelay: BehaviorRelay<Bool>(value: false),
callRelay: BehaviorRelay<Bool>(value:false))
override init() {
super.init()
// IMPORTANT!!!
Observable.combineLatest(output.chatRelay,output.callRelay).pairwise().map { (arg0) -> Int in
let (pre, curr) = arg0
let preFlag = [pre.0,pre.1].filter { $0 == true }.count == 1
let currFlag = [curr.0,curr.1].filter { $0 == true }.count == 2
if preFlag && currFlag {
return [pre.0,pre.1].firstIndex(of: true) ?? 0
}
return -1
}.filter {$0 >= 0}.subscribe(onNext: { (value) in
[self.output.chatRelay,self.output.callRelay][value].accept(false)
}).disposed(by: disposeBag)
}
private func createProfileCellItems(user: User) -> [ProfileCellViewModel] {
// IMPORTANT!!!
let chatCellViewModel = ProfileCellViewModel(topText: nil,
centerText: R.string.i18n.chat(),
bottomText: nil,
switchStatus: true,
bibindRelay: output.chatRelay)
// IMPORTANT!!!
let callCellViewModel = ProfileCellViewModel(topText: nil,
centerText: R.string.i18n.call(),
bottomText: nil,
switchStatus: true,
bibindRelay: output.callRelay)
return [roleCellViewModel,
teamCellViewModel,
statusCellViewModel,
sinceCellViewModel,
chatCellViewModel,
callCellViewModel]
}
}
I mark the codes you should pay attention to with // IMPORTANT!!!
I have the following code using RxSwift:
self.photos
.bind(to: collectionView.rx.items(dataSource: self.dataSource))
.disposed(by: disposeBag)
And it gives me Type of expression is ambiguous without more context
What more context does it need?
The complete code is shown below:
//
// PhotosCollectionViewController.swift
// TodoListRxSwift
//
//
import Foundation
import UIKit
import RxSwift
import RxCocoa
import RxDataSources
struct Photo {
var name :String
var imageURL :String
}
struct SectionOfPhoto {
var header: String
var items: [Photo]
}
extension SectionOfPhoto: SectionModelType {
init(original: SectionOfPhoto, items: [Photo]) {
self = original
self.items = items
}
}
class PhotosCollectionViewController :UICollectionViewController {
private let disposeBag = DisposeBag()
private (set) var photos = BehaviorRelay(value: [Photo(name: "Pic 1", imageURL: "1.png"),Photo(name: "Pic 2", imageURL: "2.png"),Photo(name: "Pic 3", imageURL: "3.png")])
let dataSource = RxCollectionViewSectionedReloadDataSource<SectionOfPhoto>(configureCell: { ds, cv, indexPath, photo in
let cell = cv.dequeueReusableCell(withReuseIdentifier: "PhotoCollectionViewCell", for: indexPath)
return cell
})
override func viewDidLoad() {
super.viewDidLoad()
self.collectionView?.delegate = nil
self.collectionView?.dataSource = nil
configureObservables()
}
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
prepareSegueForAddPhotoViewController(segue :segue)
}
override func collectionView(_ collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, at indexPath: IndexPath) -> UICollectionReusableView {
switch kind {
case UICollectionElementKindSectionHeader:
let headerView = collectionView.dequeueReusableSupplementaryView(ofKind: UICollectionElementKindSectionHeader, withReuseIdentifier: "PhotosHeaderView", for: indexPath)
return headerView
default:
return UICollectionReusableView()
}
}
private func prepareSegueForAddPhotoViewController(segue :UIStoryboardSegue) {
guard let nc = segue.destination as? UINavigationController else {
fatalError("NavigationController does not exist")
}
guard let addPhotoVC = nc.topViewController as? AddPhotoViewController else {
fatalError("AddPhotoViewController does not exist")
}
_ = addPhotoVC.selectedPhoto.subscribe(onNext: { (photo) in
self.photos.accept(self.photos.value + [photo])
})
}
private func configureObservables() {
if let collectionView = self.collectionView {
self.photos.bind(to: collectionView.rx.items(dataSource: self.dataSource))
self.photos.bind(to: collectionView.rx.items(cellIdentifier: "PhotoCollectionViewCell", cellType: PhotoCollectionViewCell.self)) { row, model, cell in
cell.photoImageView.image = UIImage(named: model.imageURL)
}.disposed(by: disposeBag)
}
}
}
The problem is that photos is the wrong type. The data source is expecting an element of [SectionOfPhoto], but photos has an element of [Photo].
However, changing/fixing the type of photos will break addPhotoVC.selectedPhoto because it's trying to add a single photo to an array of sections.
// UITableView+.Swift Extension
extension UITableView {
func cellForModel<T: CellViewModel>(at: IndexPath, model: T) -> T.CellType {
let cell: T.CellType = dequeueReusableCell()
*let t = model as! T.CellType.ModelType*
cell.setupModel(model: t)
return cell
}
func dequeueReusableCell<T: ReusableCell>() -> T {
print(T.reuseIdentifier())
let temp = self.dequeueReusableCell(withIdentifier: T.reuseIdentifier())
return temp as! T
}
}
// CellViewModel.Swift
protocol CellViewModel where Self: NSObject, CellType: ReusableCell {
associatedtype CellType
}
// ReusableCell.Swift
protocol ReusableCell where Self: UITableViewCell, ModelType: CellViewModel {
associatedtype ModelType
static func reuseIdentifier() -> String
func setupModel(model: ModelType)
var cell: UITableViewCell { get }
}
extension ReusableCell {
static func reuseIdentifier() -> String {
return String(describing: Self.self)
}
var cell: UITableViewCell {
return self as UITableViewCell
}
}
My question is, why I need to do let t = model as! T.CellType.ModelType this in UITableView Extension instead of just passing the model to setupModel(model: ModelType) function.
You need to add the constraint CellType.ModelType == Self to CellViewModel
protocol CellViewModel where Self: NSObject, CellType: ReusableCell, CellType.ModelType == Self {
associatedtype CellType
}
How can I disable auto scroll to the top of table view when I append new data to data source of it.
The problem is visible in the following gif.
Edit: Added ViewController, ViewModel and MessageEntity.
Used frameworks are: RxSwift, RxDataSources for reactive datasource of table view.
ViewController:
class RabbitMqVC: BaseViewController {
struct Cells {
static let message = ReusableCell<MessageCell>(nibName: "MessageCell")
static let messageTheir = ReusableCell<MessageCellTheir>(nibName: "MessageCellTheir")
}
#IBOutlet
weak var tableView: UITableView!{
didSet{
rabbitMqViewModel.sections
.drive(tableView.rx.items(dataSource: dataSource))
.addDisposableTo(disposeBag)
}
}
private let dataSource = RxTableViewSectionedAnimatedDataSource<RabbitMqViewModel.MessageSections>()
private let rabbitMqViewModel : rabbitMqViewModel
init(rabbitMqViewModel: rabbitMqViewModel) {
self.rabbitMqViewModel = rabbitMqViewModel
super.init(nibName: "RabbitMqVC", bundle: nil)
dataSource.configureCell = { _, tableView, indexPath, item in
let randomNumber = 1.random(to: 2)
let cell = randomNumber == 1 ? tableView.dequeue(Cells.message, for: indexPath) : tableView.dequeue(Cells.messageTheir, for: indexPath)
cell.message = item
return cell
}
}
override func viewDidLoad() {
super.viewDidLoad()
tableView.register(Cells.message)
tableView.register(Cells.messageTheir)
tableView.rowHeight = UITableViewAutomaticDimension
tableView.estimatedRowHeight = 80
}
}
ViewModel:
class RabbitMqViewModel: ViewModel {
enum MessageSections: AnimatableSectionModelType {
typealias Item = MessageEntity
typealias Identity = Int
case messages(messages: [MessageEntity])
var items: [Item] {
switch self {
case .messages(messages:let messages):
return messages
}
}
var identity: Int {
return 1
}
init(original: MessageSections, items: [Item]) {
switch original {
case .messages:
self = .messages(messages: items)
}
}
}
// input
let didLoad = PublishSubject<Void>()
//output
let sections: Driver<[MessageSections]>
init(service: RabbitMqService,){
let messages: Observable<[MessageEntity]> = didLoad
.flatMapLatest { _ -> Observable<[MessageEntity]> in
return service.listenMessages()
}
.share()
self.sections = messages
.map { (messages) -> [RabbitMqViewModel.MessageSections] in
var sections: [MessageSections] = []
sections.append(.messages(messages: messages))
return sections
}
.asDriver(onErrorJustReturn: [])
}
}
MessageEntity:
struct MessageEntity {
let id: String
let conversationId: String
let messageText: String
let sent: Date
let isSentByClient: Bool
let senderName: String
let commodityClientId : Int?
}
extension MessageEntity: IdentifiableType, Equatable {
typealias Identity = Int
public var identity: Identity {
return id.hashValue
}
public static func ==(lhs: MessageEntity, rhs: MessageEntity) -> Bool {
return lhs.id == rhs.id
}
}
estimatedRowHeight = 1
Fixed it.