Прежде чем перейти к MVVM, давайте сначала узнаем немного о MVC. MVC расшифровывается как Model-View-Controller. Теперь позвольте мне разбить эти три компонента для вас.

Модель:

Что это такое ? Модель — это не что иное, как данные в вашем приложении. На изображении ниже вы можете увидеть новостное приложение. Мы отображаем несколько историй в приложении, теперь заголовок, описание, дата и даже изображение — не что иное, как модель. Модель — это логика, которая обрабатывает данные, передаваемые между базой данных и вашим приложением.

Просмотр:

Что такое представление? Ну, я открываю WhatsApp, нахожу друга из моего списка друзей, пишу сообщение с клавиатурой, отображаемой на моем Iphone, нажимаю кнопку отправки. Так был ли это мой мобильный телефон, с которым я взаимодействовал? Да! Так мобильный вид? Нет! Вид — это то, что отображается на экране вашего мобильного телефона.

Все эти параметры поиска, новый чат, камера и т. д. представляют собой представление, отображаемое для нас, поэтому, когда вы нажимаете любой из этих параметров, вы взаимодействуете с представлением. Когда вы открыли WhatsApp и отправили сообщение, все это время вы взаимодействовали с представлением WhatsApp, которое помогло вам выполнить желаемую операцию.

Контроллер:

Когда вы взаимодействовали с представлением, вы выполняли желаемую операцию. Для вас это было представление, которое делало все на самом деле, это был контроллер, который выполнял все ваши запросы на основе вашего взаимодействия с представлением.

Например, если вы обожжете палец, обожженный палец пошлет сигнал в ваш мозг, а мозг передаст сигнал вашему телу, чтобы выполнить какие-то действия, чтобы избавиться от боли. Теперь представьте, что у вас нет мозга, вы почувствуете боль? Нет! ваше тело по-прежнему принадлежит им, но если у них нет мозга, вы не сможете ничего чувствовать или выполнять какие-либо действия. Поэтому я не думаю, что было бы неправильно, если бы я сказал, что «Контроллер» — это мозг без контроллера, независимо от того, что вы делаете с представлением, ваше приложение не сможет выполнять какие-либо действия.

Изображение, показанное выше, представляет собой какао-версию MVC. Там, где есть и традиционная версия для MVC.

На изображении выше модель ничего не отправляет контроллеру. Контроллер обновляет модель в соответствии с запросом пользователя, затем модель уведомляет представление, и представление получает измененное состояние модели из самой модели. Это называется традиционным шаблоном MVC.

Что не так с MVC?

Ну, честно говоря, я не думаю, что с MVC что-то не так, есть много приложений, созданных с помощью MVC-архитектора, и у них все еще есть миллионы пользователей, и они работают нормально.

Даже когда я начал программировать, я создал свое первое приложение с MVC-архитектором. Единственный ответ, который вы найдете на этот вопрос, заключается в том, что код контроллеров становится огромным, и в конце дня его становится трудно тестировать. Но я считаю, что если вы сделаете реализацию правильно, вы тоже не столкнетесь с этой проблемой. И выбор правильного шаблона зависит от проекта, но мы не можем обвинять MVC.

Что такое MVVM?

MVVM расшифровывается как Model View ViewModel в этом шаблоне, представление не может напрямую взаимодействовать с моделью, как бы ViewModel не взаимодействовала как с представлением, так и с моделью.

Наша ViewModel выполняет много работы, она заботится о бизнес-логике, операциях с базой данных, вызовах API и т. д.

Всегда помните две вещи при реализации MVVM

  • ViewModel не является заменой ViewController.
  • Не сбрасывайте все от ViewControllers до ViewModels

Выше приведено изображение, описывающее поток для MVVM. Таким образом, нет прямой связи представления с моделью.

Если говорить об этих паттернах, то можно говорить днями, но я думаю, что для этой статьи этих знаний достаточно.

Что мы создаем?

Мы будем создавать новостное приложение с использованием NewYork Times API. Зарегистрируйтесь здесь и получите свой API для этого проекта. Я использую Top Stories API.

Это то, что мы строим. Давайте посмотрим на наш ответ API.

В этом ответе корень — это словарь, а «результаты» — это массив словаря. Нам нужно получить «заголовок», «аннотацию» и URL-адрес из мультимедиа для отображения изображения.

Давайте создадим класс модели.

public struct TopStoriesResponse : Codable {
let results : [Results]
}
public struct Results : Codable {
let title : String
let abstract : String
let multimedia : [Media]?
}
public struct Media : Codable {
let url : String
}

Здесь мы создали наш модельный класс, чтобы убедиться, что написание ваших констант внутри структуры такое же, как и в ответе API.

Давайте создадим расширение URL, которое поможет нам загрузить json. Мы создадим это расширение в новом классе, и этот класс будет внутри папки с именем «Расширения».

import Foundation
import RxSwift
import RxCocoa
struct Resource<T:Decodable>{
let url : URL
}
extension URLRequest {
static func loadRequest<T>(resource : Resource<T>) -> Observable<T>{
return Observable.just(resource.url)
.flatMap{ url -> Observable<Data> in
let request =  URLRequest(url: url)
return URLSession.shared.rx.data(request:request)
}
.map{ data -> T in
return try JSONDecoder().decode(T.self, from: data)
}
}
}

Сначала мы создали структуру для Resource, которая имеет универсальный тип, и этот универсальный тип должен соответствовать декодируемому.

struct Resource<T:Decodable>{
let url : URL
}

Затем мы создаем расширение для запроса URL, и под этим расширением есть статическая функция, и эта статическая функция принимает ресурс в качестве параметра и возвращает наблюдаемое из общего.

extension URLRequest {
static func loadRequest<T>(resource : Resource<T>) -> Observable<T>{

Затем мы возвращаем наблюдаемый URL-адрес ресурса внутри функции и с помощью оператора преобразования flatMap преобразуем наблюдаемый URL-адрес ресурса в наблюдаемый данных. Если вы не знакомы с flatMap, вы можете подробнее прочитать о нем здесь.

return Observable.just(resource.url)
.flatMap{ url -> Observable<Data> in

Как только мы получим наблюдаемые данные, пришло время создать URL-запрос и начать извлекать данные из API.

let request =  URLRequest(url: url)
return URLSession.shared.rx.data(request:request)
}

Вы видите .rx в приведенном выше коде, он исходит от RxSwift. Как только мы выполним запрос URL, пришло время преобразовать данные в общие с помощью карты, а затем декодировать их.

.map{ data -> T in
return try JSONDecoder().decode(T.self, from: data)
}

Хорошо! теперь мы запросили данные, которые мы получили и расшифровали. Давайте перейдем к нашей ViewModel.

struct TopStoriesListViewModel {
let topStoriesList = PublishSubject<[Results]>()
}
extension TopStoriesListViewModel {
func fetchItems(disposeBag : DisposeBag) {
let resource = Resource<TopStoriesResponse>.init(url: URL(string: "https://api.nytimes.com/svc/topstories/v2/home.json?api-key=wZ6nNutGYhiI7LDJQUCTva0k88twlGep")!)
URLRequest.loadRequest(resource: resource).subscribe(onNext : { result in
let results = result.results
topStoriesList.onNext(results)
topStoriesList.onCompleted()
}).disposed(by: disposeBag)
}
}

В верхней части нашего класса ViewModel мы создали структуру, содержащую свойство «topSotriesList», которое является предметом публикации массива результатов типа. Помните массив результатов из нашего класса Model? да это оно.

struct TopStoriesListViewModel {
let topStoriesList = PublishSubject<[Results]>()
}

Затем мы создали расширение для этой структуры, которое содержит функцию. Эта функция принимает параметр типа disposeBag и инициализирует структуру «Ресурс» из класса расширения URL-адреса и передает URL-адрес, то есть наш полный URL-адрес API. Где, как и в структуре Resource, мы передаем нашу модель как тип, поскольку она является декодируемой, и это то, что мы хотим декодировать.

В нашей функции URLRequest loadJson везде, где встречается буква «T», будет заменена нашей моделью.

extension TopStoriesListViewModel {
func fetchItems(disposeBag : DisposeBag) {
let resource = Resource<TopStoriesResponse>.init(url: URL(string: "https://api.nytimes.com/svc/topstories/v2/home.json?api-key=wZ6nNutGYhiI7LDJQUCTva0k88twlGep")!)

Затем мы вызываем URLRequest расширение loadJson, которое мы создали, поскольку loadJson возвращал нам наблюдаемое универсального типа, поэтому мы подписываемся на него.

URLRequest.loadRequest(resource: resource).subscribe(onNext : { result in

он возвращает результат типа TopStoriesResponse из нашего модельного класса. Этот TopStoriesResponse является корнем нашего ответа API, но нас интересует массив результатов из ответа. Таким образом, возвращаемый результат = TopStoriesResponse & TopStoriesResponse имеет результат свойства, который имеет тип массива результатов. [Полученные результаты].

Таким образом, наше свойство TopStoriesListViewModel topStoriesList, которое является PublishSubject, будет наблюдаться из результата.

let results = result.results
topStoriesList.onNext(results)
topStoriesList.onCompleted()
}).disposed(by: disposeBag)
}
}

как только он заметил результат из result.results, мы больше не хотим, чтобы он наблюдал, поэтому мы передаем .onComplete. Далее мы удаляем подписчика.

Теперь пришло время связать нашу ViewModel с нашим представлением. Наше представление состоит из tableView.

//
//  ViewController.swift
//  RxNewYorkTimes
//
//  Created by omair khan on 15/12/2021.
//
import UIKit
import SDWebImage
import RxSwift
import RxCocoa
class ViewController: UIViewController {
// TableView
private let tableView: UITableView = {
let tableView = UITableView()
tableView.register(TopNewsTableViewCell.self, forCellReuseIdentifier: "cell")
tableView.translatesAutoresizingMaskIntoConstraints = false
return tableView
}()
let disposeBag = DisposeBag()
private var viewModel = TopStoriesListViewModel()
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
title = "Top Stories"
setUpTableView()
populateData()
}
// MARK: TableView setup
func setUpTableView(){
/*
- Add as Subview
- Add constraints
- tableView Row Height
*/
self.view.addSubview(tableView)
self.tableView.topAnchor.constraint(equalTo: self.view.topAnchor, constant: (self.navigationController?.navigationBar.frame.height)!).isActive = true
self.tableView.leadingAnchor.constraint(equalTo: self.view.leadingAnchor, constant: 0).isActive = true
self.tableView.trailingAnchor.constraint(equalTo: self.view.trailingAnchor, constant: 0).isActive = true
self.tableView.bottomAnchor.constraint(equalTo: self.view.bottomAnchor, constant: 0).isActive = true
tableView.rowHeight = 150
}
//MARK: Populate Data
func populateData(){
// fetch items
self.viewModel.fetchItems(disposeBag: disposeBag)
// Bind Data
self.viewModel.topStoriesList.bind(to: self.tableView.rx.items(cellIdentifier: "cell", cellType: TopNewsTableViewCell.self)){ row,item,cell in
cell.abbstractLabel.text = item.abstract
cell.titleLabel.text = item.title
cell.myImageView.sd_setImage(with: URL(string: item.multimedia?[0].url ?? ""), placeholderImage: UIImage(named: "NY"), options: .continueInBackground, completed: nil)
}
}
}

Сначала мы создали tableView и установили его в нашем основном представлении. Затем мы создали disposeBag и экземпляр TopStoriesListViewModel().

let disposeBag = DisposeBag()
private var viewModel = TopStoriesListViewModel()

Теперь наше основное внимание должно быть сосредоточено на функции «populateData», которую мы вызываем в нашем ViewDidLoad.

Давайте разберем эту функцию, чтобы понять, что происходит.

Внутри этой функции первое, что мы делаем, — это извлекаем данные, вызывающие функцию, из нашей ViewModel.

func populateData(){
// fetch items
self.viewModel.fetchItems(disposeBag: disposeBag)

Далее мы связываем данные с нашим tableView.

self.viewModel.topStoriesList.bind(to: self.tableView.rx.items(cellIdentifier: "cell", cellType: TopNewsTableViewCell.self)){ row,item,cell in

viewModel является экземпляром TopStoriesListViewModel, где topStoiresList является предметом публикации в TopStoriesListViewModel, тогда мы вызываем метод .bind и привязываем его к нашему tableView, 'rx' исходит из RxSwift, элементы привязываются к tableViewCell, используйте идентификатор ячейки, который вы упомянули в вашем коде и пользовательский класс ячейки в «cellType».

Это вернет (строка, элемент и ячейка), элемент - это элементы, которые вы извлекли, т.е. результаты из класса модели, строка - это строка tableView, а ячейка представляет собой ячейку tableView, которая теперь назначает данные вашему представлению ячейки tableView.

cell.abbstractLabel.text = item.abstract
cell.titleLabel.text = item.title
cell.myImageView.sd_setImage(with: URL(string: item.multimedia?[0].url ?? ""), placeholderImage: UIImage(named: "NY"), options: .continueInBackground, completed: nil)

Для изображения мы используем библиотеку SdWebImage, которая поможет нам загрузить изображение с URL-адреса, полученного из json, и отобразить его в нашем ImageView.

Вы видите, что без назначения каких-либо делегатов или источников данных или соответствия протоколам tableViewDataSource и Delegate нам удалось связать данные и отобразить их для наших конечных пользователей. В этом сила RxSwift.

Вы можете скачать полный код этого приложения здесь.

После этой статьи я не думаю, что буду писать о RxSwift. Я рассмотрел все, что мог, в RxSwift, если вы думаете, что я что-то упускаю, не стесняйтесь обращаться ко мне. Кроме того, если моя статья помогла вам в обучении, подписывайтесь на меня, это действительно мотивирует меня писать больше.