Verfügbares MVVM für gehackte Erweiterungen



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



, , :



,
  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