Affordable MVVM on hacked extensions



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, , . , . , - , .



, , :



,
  1. . .
  2. . , .
  3. . , SOLID, SOLID.
  4. . , .


, , .









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 , :



  1. tableView(_:cellForRowAt:) dequeueReusableCell(withIdentifier:for:) UITableViewCell.
  2. IHaveViewModel, viewModel -.
  3. , , 2, .
  4. 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.






Swift Playground.




All Articles