Most articles about MVVM describe when the model is changed, for example when new data is made available and we need to update the UI, the Model notifies the View Model.
But I don’t get how Model communicate with View Model to notify about its change.
In the code below, I used property observer to bind View and ViewModel. And I know that I can change my Model by assigning new value like self.person.value.name = name in ViewModel.
I read a lot of articles about mvvm and think I wrote an appropriate example as follows, but even in this code I cannot get the concept that Model notifies its change to ViewModel. In my code below, does model notifies viewmodel about its change? Can you explain with examples?
class Observable<T> {
var value: T {
didSet {
self.listener?(value)
}
}
var listener: ((T) -> Void)?
init(_ value: T) {
self.value = value
}
func subscribe(listener: #escaping (T) -> Void) {
listener(value)
self.listener = listener
}
}
struct Person {
var name: String
var age: Int
}
struct MyViewModel {
var person: Observable<Person>
init(person: Person) {
self.person = Observable(person)
}
func changePersonName(with name: String) {
person.value.name = name
}
}
class ViewController: UIViewController {
#IBOutlet weak var infoLabel: UILabel!
let viewModel = MyViewModel(person: Person(name: “Mike“, age: 100))
override func viewDidLoad() {
viewModel.person.subscribe { [weak self] person in
self?.infoLabel.text = person.name + “& " + "\(person.age)"
}
}
}
Related
I have a data manager that encapsulates a collection of objects. I want to listen to changes in that manager, as well as changes in collection objects. I came up with the solution using PassthroughSubject and sink, but I am pretty new to Combine and wondering is it correct and is there a better way to do that.
import Combine
class Item {
var data = false {
didSet {
self.subject.send()
}
}
let subject = PassthroughSubject<Void, Never>()
}
class DataManager {
private(set) var items = [Item]() {
didSet {
self.subject.send()
}
}
let subject = PassthroughSubject<Void, Never>()
func addItem(_ item: Item) {
self.items.append(item)
item.subject.sink { [weak self] in
self?.subject.send()
}
}
}
var item = Item()
var manager = DataManager()
manager.subject.sink {
print("Received Update")
}
manager.addItem(item) // Received Update
item.data = false // Received Update
item.data = true // Received Update
If you have control over the stored items, making them all structures should work. Arrays are structures, so will trigger the didSet when changed. Structures inside of arrays should change the value of the array and cause didSet to trigger for the array. Classes will not because the reference value of the class never changes. The current stance is that you should use structures over classes unless you have a good reason to use a class. Swift documentation for more info.
The other option is to do what you are already doing and make all of the classes conform to some protocol like BindableObject, then monitor didChange for each object.
Currently though you are not handling cancelation when an item is removed from the array. You should subscribe the didChange of DataManager to the didChange of every element. Then take the resultant AnyCancellable and add it to a dictionary keyed under the the item. Then once that item is removed from the array you should remove the associated AnyCancellable which will cancel the subscription.
For the newest version of SwiftUI, I will pass down the objectWillChange.send function to each item in the #Published array. Then, for each property of each item, I will call the update handler in the willSet property change handler.
Here's an example:
import Combine
final class User {
let prepareForUpdate: (() -> Void)?
var name: String {
willSet {
prepareForUpdate?()
}
}
init(prepareForUpdate: (() -> Void)? = nil, name: String) {
self.prepareForUpdate = prepareForUpdate
self.name = name
}
}
final class UserStore: ObservableObject {
#Published var users: [User]
init(_ users: [User] = []) {
self.users = users
}
func addUser(name: String) {
// Pass in our objectWillChange.send to the User object to listen for updates
let user = User(prepareForUpdate: objectWillChange.send, name: name)
users.append(user)
return user
}
}
Using this method, the view will be updated whenever a User in the users array is changed.
I want to learn classic MVP architecture pattern and for this try to implement Weather app on Swift. I learned in theory how it should work but practically stuck on basic understanding. For now I have a model:
Model
class WeatherModel: Codable {
var name: String?
var main: Main?
}
class Main: Codable {
var temperature: Float?
var pressure: Int?
var humidity: Int?
private enum CodingKeys: String, CodingKey {
case temperature = "temp"
case pressure
case humidity
}
View
final class WeatherViewController: UIViewController {
#IBOutlet weak var cityTextField: UITextField!
#IBOutlet weak var temperatureLabel: UILabel!
#IBOutlet weak var pressureLabel: UILabel!
#IBOutlet weak var humidityLabel: UILabel!
private var presenter: WeatherPresenter!
// MARK: - Lifecycle
override func viewDidLoad() {
super.viewDidLoad()
presenter = WeatherPresenter()
cityTextField.delegate = self
presenter.delegate = self
}
#IBAction func buttonClicked(_ sender: Any) {
let city = cityTextField.text
if let city = city {
presenter.loadWeatherFor(city: city)
}
}
}
extension WeatherViewController: WeatherPresenterProtocol {
// MARK: - WeatherPresenterProtocol
func showWeather(data: WeatherModel) {
if let temperature = data.main?.temperature {
self.temperatureLabel.text = String(temperature)
}
if let pressure = data.main?.pressure {
self.pressureLabel.text = Constants.pressure + String(pressure)
}
if let humidity = data.main?.humidity {
self.humidityLabel.text = Constants.humidity + String(humidity)
}
}
}
Presenter
protocol WeatherPresenterProtocol: class {
func showWeather(data: WeatherModel) // ?
}
final class WeatherPresenter {
var model: WeatherModel!
weak var delegate: WeatherPresenterProtocol?
func loadWeatherFor(city: String) {
Network.shared.getWeather(city) { [weak self] (weather, error) in
DispatchQueue.main.async {
self?.model = weather
}
}
}
}
In Presenter I receive data from a Network Service but I can't understand how to update View with this data (how to implement protocol in Presenter) because View shouldn't know about model but in my case it will know( Any idea to implement classic MVP will be appreciated!
And second question: how to implement protocol in Presenter to get Model (as it is shown in the picture I've taken from https://www.youtube.com/watch?v=qzTeyxIW_ow)
Change
func showWeather(data: WeatherModel)
to
func showWeather(temperature: String, pressure: String, humidity: String)
Move the construction of these strings from the View to the Presenter. This way, the View remains ignorant of the model.
The Presenter should not speak directly to the full View, only to the protocol.
You can try this, in the MVP the presenter will take care of updating the view, so modify your presenter to have the view property. Here the view holds nothing but the WeatherController instance.
In terms of the MVP, the UIViewController subclasses are in fact the Views and not the Presenters.
class WeatherPresenter : WeatherViewPresenter {
unowned let view: WeatherView
let weather: WeatherModel
required init(view: WeatherView, weather: WeatherModel) {
self.view = view
self.weather = weather
}
func updateWeatherView() {
//...update properties on your weather view
self.view.setTemperature(self.weather.temperature)
}
}
For example,To update the temperature label, have a protocol to set the temperature value. You can have multiple methods to set multiple properties or have one for all of your UI elements in weather view.
protocol WeatherView: class {
func setTemperature(temp: String)
//same for pressure, humid etc
}
Now write a protocol for weather presenter
protocol WeatherViewPresenter {
init(view: WeatherView, weather: WeatherModel)
func updateWeatherView()
}
Now that you have the presenter setup for weather class, you would use it like
class WeatherViewController : UIViewController, WeatherView {
var presenter: WeatherViewPresenter!
override func viewDidLoad() {
super.viewDidLoad()
let model = //your weather model
presenter = WeatherPresenter(view: self, weather: model)
}
func setTemperature(temp: String) {
self.temperatureLabel.text = temp
}
}
After you get the data from weather, just call self.presenter.updateWeatherView() to update the weather view.
For more detailed info, please refer
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)
}
}
I have been facing an issue with binding UITextField or button with observables in viewModel.
class VM {
var emailObservable: Observable<String?> = Observable.just("")
}
I have this observable for email in my viewModel and in controller. When i try to bind my textfield with it, it gives me error
Cannot invoke 'bind' with an argument list of type '(to: Observable)'.
But when i replace the observables with Variable, it works fine.
Can someone please help me with this. I found answers which mainly include passing the observable in the init method of viewModel, but i don't want to pass it in the init method.
This is the link i found for binding but it is through init method.
How to bind rx_tap (UIButton) to ViewModel?
Instead of
emailTextfield.rx.text.asObservable().bind(to: viewModel.emailObservable).disposed(by: disposeBag)
use this code
viewModel.emailObservable.bind(to: noteField.rx.text).disposed(by: disposeBag)
Probably, you want to make two way binding, so read more about it here
I think here what you looking for:
final class ViewModel {
private let bag = DisposeBag()
let string = BehaviorSubject<String>(value: "")
init() {
string.asObservable().subscribe(onNext: { string in
print(string)
})
.disposed(by: bag)
}
}
final class ViewController: UIViewController {
#IBOutlet weak var textField: UITextField!
private let bag = DisposeBag()
private var viewModel: ViewModel!
override func viewDidLoad() {
super.viewDidLoad()
viewModel = ViewModel()
textField.rx.text
.orEmpty
.bind(to: viewModel.string)
.disposed(by: bag)
}
}
Note, as #MaximVolgin mentioned Variable is deprecated in RxSwift 4, so you can use BehaviorSubject or other that's up to you.
UPD.
Implementation with Observable only.
final class ViewModel {
private let bag = DisposeBag()
var string = "" {
didSet {
print(string)
}
}
init(stringObservable: Observable<String>) {
stringObservable.subscribe(onNext: { string in
self.string = string
})
.disposed(by: bag)
}
}
final class ViewController: UIViewController {
#IBOutlet weak var textField: UITextField!
private let bag = DisposeBag()
private var viewModel: ViewModel!
override func viewDidLoad() {
super.viewDidLoad()
viewModel = ViewModel(stringObservable: textField.rx.text.orEmpty.asObservable())
}
}
As you can see, your solution can be implemented using Observable, not Variable or any kind of Subject. Also should be mentioned that in most cases this is not the final logic (just bind textField or whatever to some variable). There can be some validation, enable/disable, etc. logic. For this cases RxSwift provide Driver. Also nice example about differences in using Observable and Driver for one project can be found here (by RxSwift).
Method .bind(to:) binds to an Observer, not Observable.
Variable (deprecated in RxSwift v4) is a special-purpose Subject.
Subjects are by definition both Observer and Observable.
This is what .bind(to:) does inside -
public func bind<O: ObserverType>(to observer: O) -> Disposable where O.E == E {
return self.subscribe(observer)
}
UPDATE:
How to avoid passing observables in .init() of VM:
// inside VM:
fileprivate let observableSwitch: BehaviorSubject<Observable<MyValue>>
fileprivate let myValueObservable = observableSwitch.switchLatest()
// instead of passing in init:
public func switch(to observable: Observable<MyValue>) {
self.observableSwitch.onNext(observable)
}
Take a subject of variable type in ViewModel class:
class ViewModel{
//MARK: - local Variables
var emailText = Variable<String?>("")
}
Now create object of viewmodel class in viewController class and bind this emailtext variable to textfield in viewcontroller.Whenever textfield text will change then it emailText of viewmodel gets value.
txtfield.rx.text
.bindTo(viewModel.emailText).addDisposableTo(disposeBag)
Try this,
override func viewDidLoad() {
super.viewDidLoad()
_ = userNameTextField.rx.text.map { $0 ?? "" }.bind(to: viewModel.userName)
}
in viewModel class,
class ViewModel{
var userName: Variable<String> = Variable("")
}
I am trying to implement MVVM Architecture pattern using Boxing. I have done it simply by Adding the Boxing Class:
class Dynamic<T> {
typealias Listener = (T) -> Void
var listener: Listener?
func bind(listener: Listener?) {
self.listener = listener
}
func bindAndFire(listener: Listener?) {
self.listener = listener
listener?(value)
}
var value: T {
didSet {
listener?(value)
}
}
init(_ v: T) {
value = v
}}
And then In the ViewController I have referenced a ViewModel, this is my View Controller:
class SignUpViewController: UIViewController {
// UI Outlets
#IBOutlet weak var emailLoginTextField: FloatLabelTextField!
#IBOutlet weak var passwordLoginTextField: FloatLabelTextField!
var viewModel = AuthenticationViewModel()
override func viewDidLoad() {
super.viewDidLoad()
viewModel.user.email.bind{
self.emailLoginTextField.text = $0
}
}}
And This is my View Model:
class AuthenticationViewModel{
let defaults = UserDefaults.standard
let serviceManager = ServiceManager()
var user = User()
func signupUser(email : String?, password: String?){
let parameters : [String:String] = ["email":emailField, "password": password!, "system": "ios"]
serviceManager.initWithPOSTConnection(server: Utitlites.getServerName(), parameters: parameters, methodName: "/api/user/register", completion: { (responseData , errorMessage) -> Void in
let json = (responseData as AnyObject) as! JSON
print(json)
if ErrorHandling.handleErrorMessage(responseData: responseData).0 == true {
self.defaults.set("userId", forKey: json["user"]["id"].stringValue)
//self.userId.value = json["user"]["id"].stringValue
self.user = User(json: json)
}
})
}}
And this is my Model:
class User{
var id = Dynamic("")
var name = Dynamic("")
var email = Dynamic("")
init(){
}
init(json: JSON){
id.value = json["user"]["id"].stringValue
email.value = json["user"]["email"].stringValue
}}
My Question is:
MVVM Architecture wise, is it right to access the model using this line in the ViewController:
viewModel.user.email.bind{
self.emailLoginTextField.text = $0
}
Because I can see now that the View is accessing the Model which I think is not what MVVM Stands for. I need someone to clarify
The best practice to go about this (imo) and according to this raywanderlich video at 31:18 is to actually set the Model to be private, your VC doesn't need to know about it at all, only the ViewModel.
After that, set getters for the Model in the ViewModel like this:
var id: Dynamic<String> = Dynamic("")
var name: Dynamic<String> = Dynamic("")
var email: Dynamic<String> = Dynamic("")
And then, in your ViewModel also, set the User object to have a didSet notifier that will update the ViewModel's data accordingly:
private var user = User() {
didSet {
id = user.id
name = user.name
email = user.email
}
}
Now, you can access these properties only from the ViewModel instead of the Model directly:
viewModel.email.bind{
self.emailLoginTextField.text = $0
}
Oh, and don't forget to set the properties on the Model to be just regular strings ;)