var value: T func observe(_ closure: @escaping (_ old: T, _ new: T) -> Void) -> ObserverProtocol func observeNewAndCall(_ closure: @escaping (_ new: T) -> Void) -> ObserverProtocol
self.observers.append(self.viewModel.items.observe { [weak self] (_, newItems) in self?.state = newItems.isEmpty ? .zeroCase(type: .empty) : .normal self?.collectionView.reloadSections(IndexSet(integer: 0)) })
self.viewModel.items
property, and when a change occurs, the handler executes the business logic. For example, it updates the view state and reloads the collection view with new elements. final class FriendsListViewController: UIViewController { struct ViewConfig { let backgroundColor: UIColor let cornerRadius: CGFloat } private var infoView: FriendsListView! private let viewModel: FriendsListViewModelProtocol private let viewConfig: ViewConfig init(viewModel: FriendsListViewModelProtocol, viewConfig: ViewConfig) { self.viewModel = viewModel self.viewConfig = viewConfig super.init(nibName: nil, bundle: nil) } required init?(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } override func viewDidLoad() { super.viewDidLoad() self.setupContainerView() } private func setupContainerView() { self.view.backgroundColor = self.viewConfig.backgroundColor let infoView = FriendsListView( frame: .zero, viewModel: self.viewModel, viewConfig: .defaultConfig) infoView.backgroundColor = self.viewConfig.backgroundColor self.view.addSubview(infoView) self.infoView = infoView infoView.translatesAutoresizingMaskIntoConstraints = false infoView.leadingAnchor.constraint(equalTo: self.view.leadingAnchor).isActive = true infoView.trailingAnchor.constraint(equalTo: self.view.trailingAnchor).isActive = true infoView.topAnchor.constraint(equalTo: self.view.topAnchor).isActive = true infoView.bottomAnchor.constraint(equalTo: self.view.bottomAnchor).isActive = true } // …. }
extension FriendsListViewController.ViewConfig { static var defaultConfig: FriendsListViewController.ViewConfig { return FriendsListViewController.ViewConfig(backgroundColor: .white, cornerRadius: 16) } }
setupContainerView
method, which is called only once from viewDidLoad at the moment when the view is already created and loaded but not yet drawn on the screen, that is, all necessary elements (subviews) are simply added to the view hierarchy, and then markup is applied (layout) and styles. final class FriendsListPresenter: FriendsListPresenterProtocol { // … func presentFriendsList(from presentingViewController: UIViewController) { let controller = Class.createFriendsListViewController( presentingViewController: presentingViewController, headerViewModel: self.headerViewModel, contentViewModel: self.contentViewModel) controller.modalPresentationStyle = .overCurrentContext controller.modalTransitionStyle = .crossDissolve presentingViewController.present(controller, animated: true, completion: nil) } private class func createFriendsListViewController( presentingViewController: UIViewController, headerViewModel: FriendsListHeaderViewModelProtocol, contentViewModel: FriendsListContentViewModelProtocol) -> FriendsListContainerViewController { let dismissViewControllerBlock: VoidBlock = { [weak presentingViewController] in presentingViewController?.dismiss(animated: true, completion: nil) } let infoViewModel = FriendsListViewModel( headerViewModel: headerViewModel, contentViewModel: contentViewModel) let containerViewModel = FriendsListContainerViewModel(onOutsideContentTapAction: dismissViewControllerBlock) let friendsListViewController = FriendsListViewController( viewModel: infoViewModel, viewConfig: .defaultConfig) let controller = FriendsListContainerViewController( contentViewController: friendsListViewController, viewModel: containerViewModel, viewConfig: .defaultConfig) return controller } }
let friendsListViewController = FriendsListViewController( viewModel: infoViewModel, viewConfig: .defaultConfig)
self.view.addSubview(infoView) self.infoView = infoView infoView.translatesAutoresizingMaskIntoConstraints = false infoView.leadingAnchor.constraint(equalTo: self.view.leadingAnchor).isActive = true infoView.trailingAnchor.constraint(equalTo: self.view.trailingAnchor).isActive = true infoView.topAnchor.constraint(equalTo: self.view.topAnchor).isActive = true infoView.bottomAnchor.constraint(equalTo: self.view.bottomAnchor).isActive = true
infoView
inside the parent view (superview), at coordinates (0, 0) relative to the original size of the superview. NSLayoutConstraint.constraints( withVisualFormat: "V:|-(\(topSpace))-[headerView(headerHeight@200)]-[collectionView(collectionViewHeight@990)]|", options: [], metrics: metrics, views: views)
FriendsListHeaderView
- displays information about friends and the "Close" button.FriendsListContentView
- displays a list of friends with clickable cells, the content is dynamically loaded when it reaches the end of the list.FriendsListView
is a container for the two previous views. func presentFriendsList(from presentingViewController: UIViewController) { let controller = Class.createFriendsListViewController( presentingViewController: presentingViewController, headerViewModel: self.headerViewModel, contentViewModel: self.contentViewModel) controller.modalPresentationStyle = .overCurrentContext controller.modalTransitionStyle = .crossDissolve presentingViewController.present(controller, animated: true, completion: nil) }
protocol FriendsListHeaderViewModelProtocol { var friendsCountIcon: UIImage? { get } var closeButtonIcon: UIImage? { get } var friendsCount: Observable<String> { get } var onCloseAction: VoidBlock? { get set } }
final class FriendsListHeaderDemoViewModel: FriendsListHeaderViewModelProtocol { var friendsCountIcon: UIImage? = UIImage(named: "ic_friends_count") var closeButtonIcon: UIImage? = UIImage(named: "ic_close_cross") var friendsCount: Observable<String> var onCloseAction: VoidBlock? init() { let friendsCountString = "\(Int.random(min: 1, max: 5000))" self.friendsCount = Observable(friendsCountString) } }
final class FriendsListHeaderViewModel: FriendsListHeaderViewModelProtocol { let friendsCountIcon: UIImage? let closeButtonIcon: UIImage? let friendsCount: Observable<String> = Observable("0") var onCloseAction: VoidBlock? private let dataProvider: FriendsListDataProviderProtocol private var observers: [ObserverProtocol] = [] init(dataProvider: FriendsListDataProviderProtocol, friendsCountIcon: UIImage?, closeButtonIcon: UIImage?) { self.dataProvider = dataProvider self.friendsCountIcon = friendsCountIcon self.closeButtonIcon = closeButtonIcon self.setupDataObservers() } private func setupDataObservers() { self.observers.append(self.dataProvider.totalItemsCount.observeNewAndCall { [weak self] (newCount) in self?.friendsCount.value = "\(newCount)" }) } }
private func presentRealFriendsList(sender: Any) { let avatarPlaceholderImage = UIImage(named: "avatar-placeholder") let itemFactory = FriendsListItemFactory(avatarPlaceholderImage: avatarPlaceholderImage) let dataProvider = FriendsListDataProvider(itemFactory: itemFactory) let viewModelFactory = FriendsListViewModelFactory(dataProvider: dataProvider) var headerViewModel = viewModelFactory.makeHeaderViewModel() headerViewModel.onCloseAction = { [weak self] in self?.dismiss(animated: true, completion: nil) } let contentViewModel = viewModelFactory.makeContentViewModel() let presenter = FriendsListPresenter( headerViewModel: headerViewModel, contentViewModel: contentViewModel) presenter.presentFriendsList(from: self) }
Source: https://habr.com/ru/post/421559/
All Articles