Небольшое вступление
Всем 404! Большинство IOS-разработчиков не понимают как работать с Apple-MVC. Из-за чего появляется необоснованная критика, маленькие проекты стараются писать на архитектурах для этого не предназначенных, ViewController'ы становятся огромными, не читаемыми и тд.
В этой статье я покажу простой способ как облегчить жизнь себе, жизнь себе, заглянувшему в проект через 3 месяца, и, конечно, всем ViewController'aм. Для кого он подойдет? Для начинающих разработчиков или разработчиков с небольшими проектами, которые в итоге получатся с чистой и красивой структурой.
Где же все тонут?
Для начала давайте посмотрим на всеми любимое стандартное красивое MVC.
А теперь посмотрим на то, что осуществляется большинством.
Классно? Нет! Поэтому мы так делать не будем, а реализуем первый вариант.
Делаем структуру красоте
Создаем обычный Single View App. В поле выбора языка я ставлю Swift, больше ничего не включая: ни тестов, ни CoreDat'ы.
Я буду показывать на примере создания витрины магазина, т.к. нам нужно затронуть все части MVC, будем брать данные из model в controller распределять и показывать во view.
Окей, задача есть, пустой проект есть. Теперь нырнем в проект, я распределю все по папкам и создам пустые файлы, чтобы вы понимали где они должны лежать в дальнейшем. Также прикрепил storyboard структуру, чтобы не тратить время в дальнейшем. Не забудьте указать reuse id у cell, я написал ProductCell.
Красиво? Конечно красиво. Продолжаем…
Немного магии swift
Сейчас можно и к реализации приступить. Начнем с простого, view. Нам нужно создать все IBOutlets, и настройки нашего представления.
// Начинаем с импорта библиотек, зачастую во view нам будет необходим только UIKit.
import UIKit
class ShopView: UIView{
// Подвязываем нашу переменную к объекту в storyboard.
@IBOutlet weak var tableView: UITableView!
// Создаем функции для настройки нашей view.
func configure(){
// Здесь могут быть любые параметры, как установка цвета текста, так и установка высоты строки таблицы...
tableView.estimatedRowHeight = 50
tableView.rowHeight = 100
}
}
Так будет выглядеть наша view, в configure() функции я лишь задаю настройки, но view нужно использовать и для анимаций, и для смены названия title'а кнопок и тд… Так примитивно скажете? Но почему тогда так мало используют это преимущество?! Вот так я например использую анимации в controller'e: view.hideBorders(self) и все, а view уже сделает красивую анимацию.
// Передаем ссылку на view из контроллера.
func hideBorders(view: UIView){
UIView.animate(withDuration: 0.3,
delay: 0,
options: .curveEaseInOut,
animations: {
// И уже внутри анимации меняем высоту при помощи constraint.
self.createViewHeighConstraint.constant = view.bounds.height * 0.6
// Изменяем такие параметры, как округление краев, видимость тени и тд.
self.editButton.layer.cornerRadius = 20
self.postView.layer.shadowOpacity = 0
self.postView.layer.cornerRadius = 0
view.layoutIfNeeded()
}, completion: nil)
}
Огонь! Но вернемся к нашему проекту, время реализовать model. И так у нас будут товары, и пусть будут их группы.
// Хорошим тоном считается, что в model не должно быть никаких данных о View.
// Да, не всегда это получается, но в нашем случае достаточно импортировать Foundation.
import Foundation
// Теперь нам нужно сделать наш продукт, воспользуемся структурой.
public struct Product {
public let name: String
}
// Подумаем немного о будущем, у нас могут быть разные продукты, создадим их группы.
public struct ProductGroup {
public let products: [Product]
public let title: String
}
// Магия swift позволяет нам декорировать(расширять) структуры с помощью extension.
extension ProductGroup{
// Создадим функцию, позволяющую нам получить массив овощей(структур Product).
public static func vegetables() -> ProductGroup{
// Собираем массив овощей.
let products = [
Product(name: "Огурцы"),
Product(name: "Помидоры"),
Product(name: "Капуста")
]
// Возвращаем наш массив с названием "Овощи".
return ProductGroup(products: products, title: "Овощи")
}
}
Замечательно, наша модель готова, осталось собрать контроллер, давайте преступим. первым делом нужно будет создать две наших главных сущности модель и вью. Затем я вынесу настройки контроллера в private extension и передам данные из model в нашу таблицу.
// В controller'e у нас могут быть разные библиотеки, но сегодня нам потребуется только UIKit.
import UIKit
class ShopController: UIViewController {
// Создаем переменную-модель и получаем в нее овощи.
private var shopModel = ProductGroup.vegetables()
// Дальше создаем вычисляемое свойство (объяснение длинновато для комментария смотри внизу под кодом)
private var shopView: ShopView! {
guard isViewLoaded else { return nil }
return (view as! ShopView)
}
// Помните мы делали функцию настройки view, вот здесь и применяем ее чтобы при загрузке controller'a наша view тоже настроилась.
override func viewDidLoad() {
super.viewDidLoad()
// Тем самым настройка view вообще не занимает места в контроллере.
configure()
}
}
private extension ShopController{
// Но делегаты есть делегаты их присваиваем и оставляем в контроллере.
func configure(){
shopView.tableView.delegate = self
shopView.tableView.dataSource = self
}
}
// Теперь самое время реализовать методы делегатов.
extension ShopController: UITableViewDataSource, UITableViewDelegate{
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
// Возвращаем кол-во ячеек по кол-ву элементов в модели
return shopModel.products.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
// Помните я говорил, не забудьте reuse Id установить, так вот он нужен именно здесь.
let cell = tableView.dequeueReusableCell(withIdentifier: "ProductCell")
// Создаем магическим образом label в cell и присваиваем ей название продукта.
cell?.textLabel?.text = shopModel.products[indexPath.row].name
return cell!
}
}
Обещал обьяснить про view, не забыли?) Наша shopView — это вычисляемое свойство. Здесь мы проверяем isViewLoaded, чтобы не вызвать непреднамеренную загрузку view при доступе к этому свойству. Если представление уже загружено, мы принудительно преобразуем его в ShopView.
Вот и все, наше mvc готово! Остается нажать command+R и увидеть три ячейки.
Что мы имеем в итоге?
- Простая, понятная архитектура
- Равномерная нагрузка на model, view, controller
- Легкая переиспользуемость view
- Легкая расширяемость нашего проекта
Заключение
Очень надеюсь что эта статья поможет новичкам держать свои проекты в чистоте, не теряться в коде, а концентрироваться на задачах. Всем добра и меньше багов ^^
Izulle
Пока еще пример слишком минимальный. Добавьте получение списка продуктов с сервера.
С отображением чего-то разумного пока список грузится.
Список продуктов пусть будет длинным и грузится порциями.
Добавьте пулл-то-рефреш.
Еще добавьте второе окно с описанием конкретного продукта, которое показывается при нажатии на ячейку. Содержимое тоже пусть скачивается с сервера ведь цена продукта должна быть актуальной.
Ну и пусть названия овощей пишутся зеленым, фруктов оранжевым, круп — серым.
borisdipner Автор
Да я уже думал об показе такого подхода в действии. Спасибо за ваш пример.
house2008
Еще динамическую высоту ячеек, диплинки, обработку ошибок, добавление продуктов в favourites, фильтр продуктов, iOS 13 DiffableDataSource и потом сменить MVC на что-то другое ). Интересно SwiftUI вообще ложится на MVC?
borisdipner Автор
Ну это не сильно увеличит проект на самом деле. А про SwiftUI интересное предложение. Ещё его не смотрел так углублённо. Я все жду WWDC, чтобы посмотреть планы Apple на него. Если они покажут серьезные намеренья такие как когда-то были со Swift. То однозначно нужно смотреть и углублённо изучать. Если нет то ещё торопиться не стоит :D