// // LocationsListViewController.swift // Locations // // Created by Javier Cicchelli on 08/04/2023. // Copyright © 2023 Röck+Cöde. All rights reserved. // import Combine import Core import UIKit class LocationsListViewController: BaseViewController { // MARK: Properties private let viewModel: LocationsListViewModeling private var cancellables: Set = [] // MARK: Outlets private lazy var error = ErrorMessageView() private lazy var loading = LoadingSpinnerView() private lazy var table = { let table = UITableView(frame: .zero, style: .plain) table.dataSource = self table.delegate = self table.translatesAutoresizingMaskIntoConstraints = false table.register(LocationViewCell.self, forCellReuseIdentifier: LocationViewCell.identifier) return table }() // MARK: Initialisers init(viewModel: LocationsListViewModeling) { self.viewModel = viewModel super.init() } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } // MARK: UIViewController override func viewDidLoad() { super.viewDidLoad() setupBar() setupView() bindViewModel() viewModel.loadLocations() } } // MARK: - UITableViewDataSource extension LocationsListViewController: UITableViewDataSource { // MARK: Functions func numberOfSections(in tableView: UITableView) -> Int { viewModel.numberOfLocationSections } func tableView( _ tableView: UITableView, numberOfRowsInSection section: Int ) -> Int { viewModel.numberOfLocations(in: section) } func tableView( _ tableView: UITableView, cellForRowAt indexPath: IndexPath ) -> UITableViewCell { guard let cell = tableView.dequeueReusableCell( withIdentifier: LocationViewCell.identifier, for: indexPath ) as? LocationViewCell else { return .init() } let entity = viewModel.location(at: indexPath) cell.update( iconName: entity.source == .remote ? "network" : "house", name: entity.name, latitude: entity.latitude, longitude: entity.longitude ) return cell } } // MARK: - UITableViewDelegate extension LocationsListViewController: UITableViewDelegate { // MARK: Functions func tableView( _ tableView: UITableView, didSelectRowAt indexPath: IndexPath ) { viewModel.openWikipedia(at: indexPath) tableView.deselectRow(at: indexPath, animated: true) } } // MARK: - Helpers private extension LocationsListViewController { // MARK: Functions func setupBar() { navigationController?.navigationBar.prefersLargeTitles = true navigationController?.navigationBar.tintColor = .red navigationItem.rightBarButtonItem = .init( title: "Add", style: .plain, target: self, action: #selector(addLocationPressed) ) title = "Locations" } func setupView() { view.addSubview(table) view.addSubview(error) view.addSubview(loading) error.onRetry = { self.viewModel.loadLocations() } NSLayoutConstraint.activate([ error.widthAnchor.constraint(equalToConstant: 300), view.centerXAnchor.constraint(equalTo: error.centerXAnchor), view.centerYAnchor.constraint(equalTo: error.centerYAnchor), view.centerXAnchor.constraint(equalTo: loading.centerXAnchor), view.centerYAnchor.constraint(equalTo: loading.centerYAnchor), view.bottomAnchor.constraint(equalTo: table.bottomAnchor), view.leadingAnchor.constraint(equalTo: table.leadingAnchor), view.topAnchor.constraint(equalTo: table.topAnchor), view.trailingAnchor.constraint(equalTo: table.trailingAnchor), ]) } func bindViewModel() { viewModel .viewStatusPublisher .receive(on: RunLoop.main) .sink { viewStatus in self.navigationItem.rightBarButtonItem?.isEnabled = viewStatus == .loaded self.error.isHidden = viewStatus != .error self.loading.isHidden = viewStatus != .loading self.table.isHidden = viewStatus != .loaded if viewStatus == .loaded { self.table.reloadData() } } .store(in: &cancellables) viewModel .locationsDidChangePublisher .sink(receiveValue: { [weak self] updates in var movedToIndexPaths = [IndexPath]() self?.table.performBatchUpdates({ for update in updates { switch update { case let .section(sectionUpdate): switch sectionUpdate { case let .inserted(index): self?.table.insertSections([index], with: .automatic) case let .deleted(index): self?.table.deleteSections([index], with: .automatic) } case let .object(objectUpdate): switch objectUpdate { case let .inserted(at: indexPath): self?.table.insertRows(at: [indexPath], with: .automatic) case let .deleted(from: indexPath): self?.table.deleteRows(at: [indexPath], with: .automatic) case let .updated(at: indexPath): self?.table.reloadRows(at: [indexPath], with: .automatic) case let .moved(from: source, to: target): self?.table.moveRow(at: source, to: target) movedToIndexPaths.append(target) } } } }, completion: { done in self?.table.reloadRows(at: movedToIndexPaths, with: .automatic) }) }) .store(in: &cancellables) } @objc func addLocationPressed() { viewModel.openLocationsAdd() } }