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
Related
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)"
}
}
}
My app crashes when I click a cell in my tableView of recent posts. The click is supposed to segue me to the MainTextView which has the postReplyButton. The segue worked until I started experimenting with creating comments for the posts.
Here is the MainTextView code:
import Foundation
import UIKit
import Firebase
class MainTextView: UIViewController {
#IBOutlet weak var titleText: UILabel!
#IBOutlet weak var mainText: UILabel!
#IBOutlet weak var commentPlaceHolder: UILabel!
#IBOutlet weak var newCommentLabel: UITextView!
var delegate:NewPostVCDelegate?
#IBAction func postReplyButton() {
// Firebase code here
let postRef = Database.database().reference().child("posts").childByAutoId()
let postObject = [
"comment": newCommentLabel.text,
"timestamp": [".sv": "timestamp"]
] as [String : Any]
postRef.setValue(postObject, withCompletionBlock: { error, ref in
if error == nil {
self.delegate!.didUploadPost(withID: ref.key!)
self.dismiss(animated: true, completion: nil)
} else {
// Handle error
}
})
newCommentLabel.text = String()
commentPlaceHolder.isHidden = false
}
var post: Post?
// MARK: - View Controller LifeCycle
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
self.setMain()
}
override func viewDidLoad() {
super.viewDidLoad()
newCommentLabel.delegate = self as! UITextViewDelegate
}
private func setMain() {
guard let post = self.post else {
return
}
titleText.text = post.text
mainText.text = post.title
}
func textViewDidChange(_commentView: UITextView) {
commentPlaceHolder.isHidden = !newCommentLabel.text.isEmpty
}
}
For reference, here is my Post class code:
import Foundation
class Post {
var id:String
var title: String
var text:String
var createdAt:Date
var comment: [String] = []
init(id: String, title: String,text:String, timestamp:Double, comment: [String] = []) {
self.id = id
self.title = title
self.text = text
self.createdAt = Date(timeIntervalSince1970: timestamp / 1000)
}
static func parse(_ key:String, data:[String:Any]) -> Post? {
if let title = data["text"] as? String,
let text = data["title"] as? String,
let timestamp = data["timestamp"] as? Double {
return Post(id: key, title: title, text: text, timestamp:timestamp, comment: [])
}
return nil
}
}
I suspect the issue may be with the delegate, which was declared as such in my NewPostViewController:
protocol NewPostVCDelegate {
func didUploadPost(withID id:String)
}
I have tried troubleshooting the storyboard, but everything seems to be in place. Is there an issue of the reuse of the protocol or perhaps the change of adding comments to the Post class itself? Maybe the issue is that I do not in fact want to upload a new post, but really I just want to add a comment to an existing post. If this is the case, how would I change the delegate or create a new one? I can provide more detail if needed. Thank you for your help.
This usually happens if you have an IBOutlet that was created previously with the same postReplyButton name. To check if your app has any other Outlet with the same name go to the Search section in your project and search for postReplyButton and see if you get multiple results for that name. If you do then click on the one which you don't need and delete it from the properties section.
If you have any Outlet which has a bad connection you will see something like this in the properties sections when you click on any one of the search result for postReplyButton
If that does not work then try renaming the Outlet entirely and see if that fixes the problem.
EDITED:
For your issue that you mentioned in the comments try this.
Instead of casting your newCommentLabel as an optional type of UITextViewDelegate just extend your viewController to conform to UITextViewDelegate. This should solve the issue.
class MainTextView: UIViewController, UITextViewDelegate {
override func viewDidLoad() {
super.viewDidLoad()
newCommentLabel.delegate = self
}
}
Once you add UITextViewDelegate to your viewController you will no longer get the warning in viewDidLoad to cast newCommentLabel as an optional of type UITextViewDelegate.
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 have viewModel:
class EditFoodViewViewModel {
private var food: Food
var foodImage = Variable<NSData>(NSData())
init(food: Food) {
self.food = food
self.foodImage.value = food.image!
}
}
And ViewController:
class EditFoodViewController: UIViewController {
public var food: EditFoodViewViewModelType?
#IBOutlet weak var foodThumbnailImageView: UIImageView!
override func viewDidLoad() {
super.viewDidLoad()
guard let foodViewModel = food else { return }
foodViewModel.foodImage.asObservable().bind(to: foodThumbnailImageView.rx.image).disposed(by: disposeBag)
}
}
In the last line of viewController (where my UIImageView) a get error:
Generic parameter 'Self' could not be inferred
How to solve my problem? How to set image to imageView with rxSwift?
Almost invariably, when you see the error: "Generic parameter 'Self' could not be inferred", it means that the types are wrong. In this case you are trying to bind an Observable<NSData> to an Observable<Image?>.
There's a few other issues with your code as well.
it is very rare that a Subject type should be defined with the var keyword and this is not one of those rare times. Your foodImage should be a let not a var.
Variable has been deprecated; don't use it. In this case, you don't even need a subject at all.
NSData is also inappropriate in modern Swift. Use Data instead.
Based on what you have shown here, I would expect your code to look more like this:
class EditFoodViewViewModel: EditFoodViewViewModelType {
let foodImage: Observable<UIImage?>
init(food: Food) {
self.foodImage = Observable.just(UIImage(data: food.image))
}
}
class EditFoodViewController: UIViewController {
#IBOutlet weak var foodThumbnailImageView: UIImageView!
public var food: EditFoodViewViewModelType?
private let disposeBag = DisposeBag()
override func viewDidLoad() {
super.viewDidLoad()
guard let foodViewModel = food else { return }
foodViewModel.foodImage
.bind(to: foodThumbnailImageView.rx.image)
.disposed(by: disposeBag)
}
}
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)
}
}