Прежде чем перейти к 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, если вы думаете, что я что-то упускаю, не стесняйтесь обращаться ко мне. Кроме того, если моя статья помогла вам в обучении, подписывайтесь на меня, это действительно мотивирует меня писать больше.