I'm trying to follow https://github.com/sergdort/CleanArchitectureRxSwift building an app with RxSwift and MVVM.
I can't wrap my head around how to communicate with a child view controller in this scenario.
I have one trigger from the ParentViewController that is needed in the ChildViewModel and a second one from the ChildViewModel that should trigger some UI changes in the ParentViewController:
trigger1: ParentViewController (input)> ... (no idea)> ChildViewModel
trigger2: ChildViewModel (output)> ... (no idea)> ParentViewController
My code currently looks something like this:
final class ParentViewController: UIViewController {
var viewModel: ParentViewModel?
private let disposeBag = DisposeBag()
override func viewDidLoad() {
super.viewDidLoad()
bindViewModel()
}
private func bindViewModel() {
guard let viewModel = self.viewModel else {
fatalError("View Model not set!")
}
let triggerToChild = rx.someTrigger
let input = ParentViewModel.Input(triggerToChild: triggerToChild)
let output = viewModel.transform(input: input)
output.triggerFromChild
.drive(rx.someProperty)
.disposed(by: disposeBag)
}
}
final class ParentViewModel: ViewModelType {
struct Input {
let triggerToChild: Driver<Void>
}
struct Output {
let triggerFromChild: Driver<Void>
}
func transform(input: Input) -> Output {
let triggerFromChild = ??? <===================
return Output(triggerFromChild: triggerFromChild)
}
}
final class ChildViewController: UIViewController {
var viewModel: ChildViewModel?
private let disposeBag = DisposeBag()
override func viewDidLoad() {
super.viewDidLoad()
bindViewModel()
}
private func bindViewModel() {
guard let viewModel = self.viewModel else {
fatalError("View Model not set!")
}
let triggerFromParent = ??? <===================
let input = ChildViewModel.Input(triggerFromParent: triggerFromParent)
let output = viewModel.transform(input: input)
output.triggerFromParent
.drive(rx.someProperty)
.disposed(by: disposeBag)
}
}
final class ChildViewModel: ViewModelType {
struct Input {
let triggerFromParent: Driver<Void>
}
struct Output {
let triggerToParent: Driver<Void>
}
func transform(input: Input) -> Output {
let triggerToParent = rx.someTrigger
return Output(triggerToParent: triggerToParent)
}
}
Maybe someone could point me in the right direction? Thank You!
Related
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'm trying to create an app to get some news from an API and i'm using Moya, RxSwift and MVVM.
This is my ViewModel:
import Foundation
import RxSwift
import RxCocoa
public enum NewsListError {
case internetError(String)
case serverMessage(String)
}
enum ViewModelState {
case success
case failure
}
protocol NewsListViewModelInput {
func viewDidLoad()
func didLoadNextPage()
}
protocol MoviesListViewModelOutput {
var newsList: PublishSubject<NewsList> { get }
var error: PublishSubject<String> { get }
var loading: PublishSubject<Bool> { get }
var isEmpty: PublishSubject<Bool> { get }
}
protocol NewsListViewModel: NewsListViewModelInput, MoviesListViewModelOutput {}
class DefaultNewsListViewModel: NewsListViewModel{
func viewDidLoad() {
}
func didLoadNextPage() {
}
private(set) var currentPage: Int = 0
private var totalPageCount: Int = 1
var hasMorePages: Bool {
return currentPage < totalPageCount
}
var nextPage: Int {
guard hasMorePages else { return currentPage }
return currentPage + 1
}
private var newsLoadTask: Cancellable? { willSet { newsLoadTask?.cancel() } }
private let disposable = DisposeBag()
// MARK: - OUTPUT
let newsList: PublishSubject<NewsList> = PublishSubject()
let error: PublishSubject<String> = PublishSubject()
let loading: PublishSubject<Bool> = PublishSubject()
let isEmpty: PublishSubject<Bool> = PublishSubject()
func getNewsList() -> Void{
print("sono dentro il viewModel!")
NewsDataService.shared.getNewsList()
.subscribe { event in
switch event {
case .next(let progressResponse):
if progressResponse.response != nil {
do{
let json = try progressResponse.response?.map(NewsList.self)
print(json!)
self.newsList.onNext(json!)
}
catch _ {
print("error try")
}
} else {
print("Progress: \(progressResponse.progress)")
}
case .error( _): break
// handle the error
default:
break
}
}
}
}
This is my ViewController, where xCode give me the following error when i try to bind to tableNews:
Expression type 'Reactive<_>' is ambiguous without more context
import UIKit
import RxSwift
import RxCocoa
class ViewController: UIViewController {
#IBOutlet weak var tableNews: UITableView!
let viewModel = DefaultNewsListViewModel()
var disposeBag = DisposeBag()
override func viewDidLoad() {
super.viewDidLoad()
}
private func setupBindings() {
viewModel.newsList.bind(to: tableNews.rx.items(cellIdentifier: "Cell")) {
(index, repository: NewsList, cell) in
cell.textLabel?.text = repository.name
cell.detailTextLabel?.text = repository.url
}
.disposed(by: disposeBag)
}
}
This is the service that get data from API:
import Moya
import RxSwift
struct NewsDataService {
static let shared = NewsDataService()
private let disposable = DisposeBag()
private init() {}
fileprivate let newsListProvider = MoyaProvider<NewsService>()
func getNewsList() -> Observable<ProgressResponse> {
self.newsListProvider.rx.requestWithProgress(.readNewsList)
}
}
I'm new at rxSwift, I followed some documentation but i'd like to know if i'm approaching in the right way. Another point i'd like to know is how correctly bind my tableView to viewModel.
Thanks for the support.
As #FabioFelici mentioned in the comments, UITableView.rx.items(cellIdentifier:) is expecting to be bound to an Observable that contains an array of objects but your NewsListViewModel.newsList is an Observable<NewsList>.
This means you either have to extract the array out of NewsList (assuming it has one) through a map. As in newsList.map { $0.items }.bind(to:...
Also, your MoviesListViewModelOutput should not be full of Subjects, rather it should contain Observables. And I wouldn't bother with the protocols, struts are fine.
Also, your view model is still very imperative, not really in an Rx style. A well constructed Rx view model doesn't contain functions that are repeatedly called. It just has a constructor (or is itself just a single function.) You create it, bind to it and then you are done.
I am using from VIP in my project and when I route user to another scene open the scene twice ,my router is like bellow? if your need more detail say me. Thank's
#objc protocol ListLanguageRoutingLogic
{
func routeToStartPage(segue: UIStoryboardSegue?)
}
protocol LangSelectedDataPassing
{
var dataStore: SelectLanguageDataStore? { get }
}
class RouterSelectLanguage: NSObject, ListLanguageRoutingLogic, LangSelectedDataPassing
{
weak var viewControllerSelectLanguage: ViewControllerSelectLanguage?
var dataStore: SelectLanguageDataStore?
func routeToStartPage(segue: UIStoryboardSegue?) {
print("BBB")
let destinationVC = viewControllerSelectLanguage?.storyboard?.instantiateViewController(withIdentifier: "ViewControllerStartPage") as! ViewControllerStartPage
var destinationDS = destinationVC.router!.dataStore!
passDataToStartPage(source: dataStore!, destination: &destinationDS)
navigateToStartPage(source: viewControllerSelectLanguage!, destination: destinationVC)
}
// MARK: Navigation
func navigateToStartPage(source: ViewControllerSelectLanguage, destination: ViewControllerStartPage)
{
source.show(destination, sender: nil)
}
// MARK: Passing data
func passDataToStartPage(source: SelectLanguageDataStore, destination: inout StartPageDataStore)
{
print("CCC")
let selectedRow = viewControllerSelectLanguage?.tblView.indexPathForSelectedRow?.row
destination.langSelected = source.langs?[selectedRow!]
}
}
And:
Resolved my problem, I edited my source code like bellow and good work now:
#objc protocol ListLanguageRoutingLogic
{
func routeToStartPage(segue: UIStoryboardSegue?)
}
protocol LangSelectedDataPassing
{
var dataStore: SelectLanguageDataStore? { get }
}
class RouterSelectLanguage: NSObject, ListLanguageRoutingLogic, LangSelectedDataPassing
{
weak var viewControllerSL: ViewControllerSelectLanguage?
var dataStore: SelectLanguageDataStore?
func routeToStartPage(segue: UIStoryboardSegue?) {
let destinationVC = segue!.destination as! ViewControllerStartPage
var destinationDS = destinationVC.startPageRouter!.dataStore!
passDataToStartPage(source: dataStore!, destination: &destinationDS)
}
// MARK: Passing data
func passDataToStartPage(source: SelectLanguageDataStore, destination: inout StartPageDataStore)
{
let selectedRow = viewControllerSL?.tblView.indexPathForSelectedRow?.row
destination.langSelected = source.langs?[selectedRow!]
}
}
I am new to MVC design pattern. I created "DataModel" it will make an API call, create data, and return data to the ViewController using Delegation and "DataModelItem" that will hold all data. How to call a DataModel init function in "requestData" function. Here is my code:
protocol DataModelDelegate:class {
func didRecieveDataUpdata(data:[DataModelItem])
func didFailUpdateWithError(error:Error)
}
class DataModel: NSObject {
weak var delegate : DataModelDelegate?
func requestData() {
}
private func setDataWithResponse(response:[AnyObject]){
var data = [DataModelItem]()
for item in response{
if let tableViewModel = DataModelItem(data: item as? [String : String]){
data.append(tableViewModel)
}
}
delegate?.didRecieveDataUpdata(data: data)
}
}
And for DataModelItem:
class DataModelItem{
var name:String?
var id:String?
init?(data:[String:String]?) {
if let data = data, let serviceName = data["name"] , let serviceId = data["id"] {
self.name = serviceName
self.id = serviceId
}
else{
return nil
}
}
}
Controller:
class ViewController: UIViewController {
private let dataSource = DataModel()
override func viewDidLoad() {
super.viewDidLoad()
dataSource.delegate = self
}
override func viewWillAppear(_ animated: Bool) {
dataSource.requestData()
}
}
extension ViewController : DataModelDelegate{
func didRecieveDataUpdata(data: [DataModelItem]) {
print(data)
}
func didFailUpdateWithError(error: Error) {
print("error: \(error.localizedDescription)")
}
}
How to implement simple MVC design pattern in Swift?
As a generic answer, in iOS development you're already doing this implicitly! Dealing with storyboard(s) implies the view layer and controlling the logic of how they work and how they are connected to the model is done by creating view controller, that's the default flow.
For your case, let's clarify a point which is: according to the standard MVC, by default the responsible layer for calling an api should be -logically- the view controller. However for the purpose of modularity, reusability and avoiding to create massive view controllers we can follow the approach that you are imitate, that doesn't mean that its the model responsibility, we can consider it a secondary helper layer (MVC-N for instance), which means (based on your code) is DataModel is not a model, its a "networking" layer and DataModelItem is the actual model.
How to call a DataModel init function in "requestData" function
It seems to me that it doesn't make scene. What do you need instead is an instance from DataModel therefore you could call the desired method.
In the view controller:
let object = DataModel()
object.delegate = self // if you want to handle it in the view controller itself
object.requestData()
I am just sharing my answer here and I am using a codable. It will be useful for anyone:
Model:
import Foundation
struct DataModelItem: Codable{
struct Result : Codable {
let icon : String?
let name : String?
let rating : Float?
let userRatingsTotal : Int?
let vicinity : String?
enum CodingKeys: String, CodingKey {
case icon = "icon"
case name = "name"
case rating = "rating"
case userRatingsTotal = "user_ratings_total"
case vicinity = "vicinity"
}
}
let results : [Result]?
}
NetWork Layer :
import UIKit
protocol DataModelDelegate:class {
func didRecieveDataUpdata(data:[String])
func didFailUpdateWithError(error:Error)
}
class DataModel: NSObject {
weak var delegate : DataModelDelegate?
var theatreNameArray = [String]()
var theatreVicinityArray = [String]()
var theatreiconArray = [String]()
func requestData() {
Service.sharedInstance.getClassList { (response, error) in
if error != nil {
self.delegate?.didFailUpdateWithError(error: error!)
} else if let response = response{
self.setDataWithResponse(response: response as [DataModelItem])
}
}
}
private func setDataWithResponse(response:[DataModelItem]){
for i in response[0].results!{
self.theatreNameArray.append(i.name!)
self.theatreVicinityArray.append(i.vicinity!)
self.theatreiconArray.append(i.icon!)
}
delegate?.didRecieveDataUpdata(data: theatreNameArray)
print("TheatreName------------------------->\(self.theatreNameArray)")
print("TheatreVicinity------------------------->\(self.theatreVicinityArray)")
print("Theatreicon------------------------->\(self.theatreiconArray)")
}
}
Controller :
class ViewController: UIViewController {
private let dataSource = DataModel()
override func viewDidLoad() {
super.viewDidLoad()
dataSource.delegate = self
}
override func viewWillAppear(_ animated: Bool) {
dataSource.requestData()
}
}
extension ViewController : DataModelDelegate{
func didRecieveDataUpdata(data: [DataModelItem]) {
print(data)
}
func didFailUpdateWithError(error: Error) {
print("error: \(error.localizedDescription)")
}
}
APIManager :
class Service : NSObject{
static let sharedInstance = Service()
func getClassList(completion: (([DataModelItem]?, NSError?) -> Void)?) {
guard let gitUrl = URL(string: "") else { return }
URLSession.shared.dataTask(with: gitUrl) { (data, response
, error) in
guard let data = data else { return }
do {
let decoder = JSONDecoder()
let gitData = try decoder.decode(DataModelItem.self, from: data)
completion!([gitData],nil)
} catch let err {
print("Err", err)
completion!(nil,err as NSError)
}
}.resume()
}
}
I would recommend using a singleton instance for DataModel, since this would be a class you would be invoking from many points in your application.
You may refer its documentation at :
Managing Shared resources using singleton
With this you wont need to initialise this class instance every time you need to access data.
I'm using MVVM, Clean Architecture and RxSwift in my project. There is a view controller that has a child UIView that is created from a separate .xib file on the fly (since it is used in multiple scenes). Thus there are two viewmodels, the UIViewController's view model and the UIView's. Now, there is an Rx event in the child viewmodel that should be observed by the parent and then it will call some of its and its viewmodel's functions. The code is like this:
MyPlayerViewModel:
class MyPlayerViewModel {
var eventShowUp: PublishSubject<Void> = PublishSubject<Void>()
var rxEventShowUp: Observable<Void> {
return eventShowUp
}
}
MyPlayerView:
class MyPlayerView: UIView {
var viewModel: MyPlayerViewModel?
setup(viewModel: MyPlayerViewModel) {
self.viewModel = viewModel
}
}
MyPlayerSceneViewController:
class MyPlayerSceneViewController: UIViewController {
#IBOutlet weak var myPlayerView: MyPlayerView!
#IBOutlet weak var otherView: UIView!
var viewModel: MyPlayerSceneViewModel
fileprivate var disposeBag : DisposeBag = DisposeBag()
override func viewDidLoad() {
super.viewDidLoad()
self.myPlayerView.viewModel.rxEventShowUp.subscribe(onNext: { [weak self] in
self?.viewModel.doOnShowUp()
self?.otherView.isHidden = true
})
}
}
As you can see, currently, I am exposing the myPlayerView's viewModel to the public so the parent can observe the event on it. Is this the right way to do it? If not, is there any other suggestion about the better way? Thanks.
In general, nothing bad to expose view's stuff to its view controller but do you really need two separate view models there? Don't you mix viewModel and model responsibilities?
Some thoughts:
Model shouldn't subclass UIView.
You should avoid creating own subjects in a view model. It doesn't create events by itself, it only processes input and exposes results.
I encourage you to get familiar with Binder and Driver.
Here is the code example:
struct PlayerModel {
let id: Int
let name: String
}
class MyPlayerSceneViewModel {
struct Input {
let eventShowUpTrigger: Observable<Void>
}
struct Output {
let someUIAction: Driver<PlayerModel>
}
func transform(input: Input) -> Output {
let someUIAction = input.eventShowUpTrigger
.flatMapLatest(fetchPlayerDetails) // Transform input
.asDriver(onErrorJustReturn: PlayerModel(id: -1, name: "unknown"))
return Output(someUIAction: someUIAction)
}
private func fetchPlayerDetails() -> Observable<PlayerModel> {
return Observable.just(PlayerModel(id: 1, name: "John"))
}
}
class MyPlayerView: UIView {
var eventShowUp: Observable<Void> {
return Observable.just(()) // Expose some UI trigger
}
var playerBinding: Binder<PlayerModel> {
return Binder(self) { target, player in
target.playerNameLabel.text = player.name
}
}
let playerNameLabel = UILabel()
}
class MyPlayerSceneViewController: UIViewController {
#IBOutlet weak var myPlayerView: MyPlayerView!
private var viewModel: MyPlayerSceneViewModel!
private var disposeBag: DisposeBag = DisposeBag()
override func viewDidLoad() {
super.viewDidLoad()
setupBindings()
}
private func setupBindings() {
let input = MyPlayerSceneViewModel.Input(eventShowUpTrigger: myPlayerView.eventShowUp)
let output = viewModel.transform(input: input)
// Drive manually
output
.someUIAction
.map { $0.name }
.drive(myPlayerView.playerNameLabel.rx.text)
.disposed(by: disposeBag)
// or to exposed binder
output
.someUIAction
.drive(myPlayerView.playerBinding)
.disposed(by: disposeBag)
}
}