For many years in a row, among other things, I was involved in configuring MVVM in my working and not very working projects. I've done it with great enthusiasm in Windows projects where the pattern is native. With an enthusiasm worthy of a better use, I've done this in iOS projects where MVVM just doesn't take root.
, , , - , .
- β , , MVVM . : , MVC MVVM .
, MVVM , , . MVVM. , Microsoft, , . , . , - , .
, , :
- . .
- . , .
- . , SOLID, SOLID.
- . , .
, , .
MVVM , . , . - MVVM , . , «» .
, β . , , , .
OrdersVC | - . , iOS β -. , -. |
OrdersView | OrdersVC. β VC View , . OrdersView β , , , |
OrdersVM | OrdersVC, , . OrdersProvider - |
Order | , , . |
OrderCell | UITableView, |
OrderVM | OrderCell. Order, |
OrdersProvider | , β , , β . |
.
, MVVM , , iOS, MVC, - . , , β View, iOS .
: , View, , .
.
, View ViewModel, : -, . . MVVM, : View viewModel
:
protocol IHaveViewModel: AnyObject {
associatedtype ViewModel
var viewModel: ViewModel? { get set }
func viewModelChanged(_ viewModel: ViewModel)
}
I interface. -, , . , .
, viewModel
. - , viewModelChanged(_:)
, . IHaveViewModel
OrderCell β OrderVM
:
final class OrderCell: UITableViewCell, IHaveViewModel {
var viewModel: OrderVM? {
didSet {
guard let viewModel = viewModel else { return }
viewModelChanged(viewModel)
}
}
func viewModelChanged(_ viewModel: OrderVM) {
textLabel?.text = viewModel.name
}
}
, , textLabel
. , :
final class OrderVM {
let order: Order
var name: String {
return "\(order.name) #\(order.id)"
}
init(order: Order) {
self.order = order
}
}
, viewModel
, , . OrderCell
, :
-
tableView(_:cellForRowAt:)
dequeueReusableCell(withIdentifier:for:)
UITableViewCell
. -
IHaveViewModel
,viewModel
-. - , , 2, .
- Protocol 'IHaveViewModel' can only be used as a generic constraint because it has Self or associated type requirements.
, (type erasure). . , β (shadow type erasure). ? , :
protocol IHaveAnyViewModel: AnyObject {
var anyViewModel: Any? { get set }
}
, . IHaveViewModel
, :
protocol IHaveViewModel: IHaveAnyViewModel {
associatedtype ViewModel
var viewModel: ViewModel? { get set }
func viewModelChanged(_ viewModel: ViewModel)
}
OrderCell
:
final class OrderCell: UITableViewCell, IHaveViewModel {
typealias ViewModel = OrderVM
var anyViewModel: Any? {
didSet {
guard let viewModel = anyViewModel as? ViewModel else { return }
viewModelChanged(viewModel)
}
}
var viewModel: ViewModel? {
get {
return anyViewModel as? ViewModel
}
set {
anyViewModel = newValue
}
}
func viewModelChanged(_ viewModel: ViewModel) {
textLabel?.text = viewModel.name
}
}
anyViewModel
, , . IHaveAnyViewModel
-. viewModel
, -, , , viewModelChanged(_:)
.
, MVVM : . , - IHaveViewModel
, , , . , , IHaveViewModel
.
(extensions) : . , , , , .
, IHaveViewModel
, extensions must not contain stored properties:
extension IHaveViewModel {
var anyViewModel: Any? // :(
}
, , . .
, , . , extensions must not contain stored properties , , , . , , Objective-C-. , , :
private var viewModelKey: UInt8 = 0
extension IHaveViewModel {
var anyViewModel: Any? {
get {
return objc_getAssociatedObject(self, &viewModelKey)
}
set {
let viewModel = newValue as? ViewModel
objc_setAssociatedObject(self,
&viewModelKey,
viewModel,
.OBJC_ASSOCIATION_RETAIN_NONATOMIC)
if let viewModel = viewModel {
viewModelChanged(viewModel)
}
}
var viewModel: ViewModel? {
get {
return anyViewModel as? ViewModel
}
set {
anyViewModel = newValue
}
}
func viewModelChanged(_ viewModel: ViewModel) {
}
}
, . : objc_getAssociatedObject
objc_setAssociatedObject
, .
, . , viewModelKey
. OrderCell
:
final class OrderCell: UITableViewCell, IHaveViewModel {
typealias ViewModel = OrderVM
func viewModelChanged(_ viewModel: OrderVM) {
textLabel?.text = viewModel.name
}
}
, , , . Objective-C- β . , .
( )
IHaveViewModel
OrdersVC β OrdersVM
. - :
final class OrdersVM {
var orders: [OrderVM] = []
private var ordersProvider: OrdersProvider
init(ordersProvider: OrdersProvider) {
self.ordersProvider = ordersProvider
}
func loadOrders() {
ordersProvider.loadOrders() { [weak self] model in
self?.orders = model.map { OrderVM(order: $0) }
}
}
}
OrdersVM
OrdersProvider
. OrdersProvider
loadOrders(completion:)
:
struct Order {
let name: String
let id: Int
}
final class OrdersProvider {
func loadOrders(completion: @escaping ([Order]) -> Void) {
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
completion((0...99).map { Order(name: "Order", id: $0) })
}
}
}
, , -:
final class OrdersVC: UIViewController, IHaveViewModel {
typealias ViewModel = OrdersVM
private lazy var tableView = UITableView()
override func viewDidLoad() {
super.viewDidLoad()
tableView.dataSource = self
tableView.register(OrderCell.self, forCellReuseIdentifier: "order")
view.addSubview(tableView)
viewModel?.loadOrders()
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
tableView.frame = view.bounds
}
func viewModelChanged(_ viewModel: OrdersVM) {
tableView.reloadData()
}
}
viewDidLoad()
loadOrders()
-, . - viewModelChanged(_:)
, . :
extension OrdersVC: UITableViewDataSource {
func tableView(_ tableView: UITableView,
numberOfRowsInSection section: Int) -> Int {
return viewModel?.orders.count ?? 0
}
func tableView(_ tableView: UITableView,
cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "order",
for: indexPath)
if let cell = cell as? IHaveAnyViewModel {
cell.anyViewModel = viewModel?.orders[indexPath.row]
}
return cell
}
}
, IHaveAnyViewModel
, , - . . , :
let viewModel = OrdersVM(ordersProvider: OrdersProvider())
let viewController = OrdersVC()
viewController.viewModel = viewModel
OrdersVC
, . , , , .
, loadOrders(completion:)
, viewDidLoad()
, , reloadData()
orders
. , β -.
MVVM , ViewModel View. View β , . - , View - . View, - , . View, , , ViewModel View.
iOS- : . , MVVM Rx. MVVM . .NET β β INotifyPropertyChanged, ViewModel, β View.
, , . , , . . , , . RxSwift , Combine β iOS 13.
, , , , iOS .NET. ViewModel.
.NET β «», : , c -. , , , , - ViewController, View.
Swift : , , NotificationCenter. , , . :
final class Weak<T: AnyObject> {
private let id: ObjectIdentifier?
private(set) weak var value: T?
var isAlive: Bool {
return value != nil
}
init(_ value: T?) {
self.value = value
if let value = value {
id = ObjectIdentifier(value)
} else {
id = nil
}
}
}
, , . - nil
, ObjectIdentifier
, Hashable
:
extension Weak: Hashable {
static func == (lhs: Weak<T>, rhs: Weak<T>) -> Bool {
return lhs.id == rhs.id
}
func hash(into hasher: inout Hasher) {
if let id = id {
hasher.combine(id)
}
}
}
Weak<T>
, :
final class Event<Args> {
//
private var handlers: [Weak<AnyObject>: (Args) -> Void] = [:]
func subscribe<Subscriber: AnyObject>(
_ subscriber: Subscriber,
handler: @escaping (Subscriber, Args) -> Void) {
//
let key = Weak<AnyObject>(subscriber)
// ,
handlers = handlers.filter { $0.key.isAlive }
//
handlers[key] = {
[weak subscriber] args in
// ,
//
guard let subscriber = subscriber else { return }
handler(subscriber, args)
}
}
func unsubscribe(_ subscriber: AnyObject) {
// ,
let key = Weak<AnyObject>(subscriber)
handlers[key] = nil
}
func raise(_ args: Args) {
//
let aliveHandlers = handlers.filter { $0.key.isAlive }
//
aliveHandlers.forEach { $0.value(args) }
}
}
, , , . Weak<T>
, , , , β .
, , . Event<Args>
subscribe(_:handler:)
unsubscribe(_:)
. ( -) - , raise(_:)
.
, Void
:
extension Event where Args == Void {
func subscribe<Subscriber: AnyObject>(
_ subscriber: Subscriber,
handler: @escaping (Subscriber) -> Void) {
subscribe(subscriber) { this, _ in
handler(this)
}
}
func raise() {
raise(())
}
}
. , - :
let event = Event<Void>()
event.raise() // - ,
. , , weak self
, :
event.subscribe(self) { this in
this.foo() //
}
, :
event.unsubscribe(self) //
! , . , , MVVM . .
OrdersVM
OrdersVC
, - . , , -, , . Objective-C-, :
private var changedEventKey: UInt8 = 0
protocol INotifyOnChanged {
var changed: Event<Void> { get }
}
extension INotifyOnChanged {
var changed: Event<Void> {
get {
if let event = objc_getAssociatedObject(self,
&changedEventKey) as? Event<Void> {
return event
} else {
let event = Event<Void>()
objc_setAssociatedObject(self,
&changedEventKey,
event,
.OBJC_ASSOCIATION_RETAIN_NONATOMIC)
return event
}
}
}
}
INotifyOnChanged
- changed
. INotifyOnChanged
IHaveViewModel
: - viewModelChanged(_:)
:
extension IHaveViewModel {
var anyViewModel: Any? {
get {
return objc_getAssociatedObject(self, &viewModelKey)
}
set {
(anyViewModel as? INotifyOnChanged)?.changed.unsubscribe(self)
let viewModel = newValue as? ViewModel
objc_setAssociatedObject(self,
&viewModelKey,
viewModel,
.OBJC_ASSOCIATION_RETAIN_NONATOMIC)
if let viewModel = viewModel {
viewModelChanged(viewModel)
}
(viewModel as? INotifyOnChanged)?.changed.subscribe(self) { this in
if let viewModel = viewModel {
this.viewModelChanged(viewModel)
}
}
}
}
}
, , :
final class OrdersVM: INotifyOnChanged {
var orders: [OrderVM] = []
private var ordersProvider: OrdersProvider
init(ordersProvider: OrdersProvider) {
self.ordersProvider = ordersProvider
}
func loadOrders() {
ordersProvider.loadOrders() { [weak self] model in
self?.orders = model.map { OrderVM(name: $0.name) }
self?.changed.raise() // !
}
}
}
, β Weak<T>
, Event<Args>
, INotifyOnChanged
, β , -: changed.raise()
.
raise()
, , , , viewModelChanged(_:)
, , .
One More Thing:
INotifyOnChanged
changed
- . , , β , , β View - ViewModel? , - myPropertyChanged
, β .
, Apple?
property wrapper, , wrappedValue
, , @propertyWrapper
. , , «» projectedValue
. , , , , :
@propertyWrapper
struct Observable<T> {
let projectedValue = Event<T>()
init(wrappedValue: T) {
self.wrappedValue = wrappedValue
}
var wrappedValue: T {
didSet {
projectedValue.raise(wrappedValue)
}
}
}
Observable
. projectedValue
. , wrappedValue
. , , didSet
.
Observable<T>
, :
@Observable
var orders: [OrderVM] = []
, :
private var _orders = Observable<[OrderVM]>(wrappedValue: [])
var orders: [OrderVM] {
get { _orders.wrappedValue }
set { _orders.wrappedValue = newValue }
}
var $orders: Event<[OrderVM]> {
get { _orders.projectedValue }
}
, , orders
, wrappedValue
, $orders
, projectedValue
. projectedValue
β , orders
:
viewModel.$orders.subscribe(self) { this, orders in
this.update(with: orders)
}
! 15 Published Combine Apple, .
, Objective-C-. , , MVVM iOS. , , , .NET. iOS-, shadow type erasure property wrappers projected value.