MVVM disponível em extensões invadidas



Por muitos anos consecutivos, eu, entre outras coisas, estive envolvido na configuração do MVVM em meus projetos de trabalho e não em outros. Fiz isso com grande entusiasmo em projetos do Windows em que o padrão é nativo. Com um entusiasmo digno de uma melhor utilização, fiz isso em projetos iOS em que o MVVM simplesmente não se enraíza.



, , , - , .



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