Seit vielen Jahren in Folge bin ich unter anderem an der Einrichtung von MVVM in meinen Arbeitsprojekten beteiligt. Ich habe es mit großer Begeisterung in Windows-Projekten gemacht, in denen das Muster nativ ist. Mit einer Begeisterung, die einer besseren Verwendung würdig ist, habe ich dies in iOS-Projekten getan, in denen MVVM einfach keine Wurzeln schlägt.
, , , - , .
- — , , 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.