2
\$\begingroup\$

I'm making an application that has a UIViewController with a UITableView in order to show the favorite users that the user of the app have added. They are being stored in UserDefaults, for which I have created a UserDefaultsManager struct to handle its logic.

I had the following ViewController:

import UIKit
class FavoriteListViewController: UIViewController {
 // MARK: - Private Properties
 
 private let tableView = UITableView()
 private var favorites: [User] = []
 
 
 // MARK: - Lifecycle
 
 override func viewDidLoad() {
 super.viewDidLoad()
 configureViewController()
 getFavorites()
 configureTableView()
 }
 
 override func viewWillAppear(_ animated: Bool) {
 super.viewWillAppear(animated)
 getFavorites()
 }
 
 // MARK: - Private Methods
 
 private func configureViewController() {
 view.backgroundColor = .systemBackground
 title = "favorites".localized()
 navigationController?.navigationBar.prefersLargeTitles = true
 }
 
 private func configureTableView() {
 view.addSubview(tableView)
 tableView.frame = view.bounds
 print(tableView.frame)
 tableView.rowHeight = 80
 
 tableView.delegate = self
 tableView.dataSource = self
 
 tableView.register(GFFavoriteCell.self, forCellReuseIdentifier: GFFavoriteCell.reuseId)
 }
 
 private func getFavorites() {
 UserDefaultsManager.shared.retrieveFavorites { [weak self] result in
 guard let self = self else { return }
 
 switch result {
 case .success(let favorites):
 DispatchQueue.main.async {
 self.updateUI(with: favorites)
 }
 
 case .failure(let error):
 DispatchQueue.main.async {
 self.presentGFAlert(title: error.title, message: error.description)
 }
 }
 }
 }
 
 private func updateUI(with favorites: [User]) {
 if favorites.isEmpty {
 view.showEmptyStateView(with: "FavoritesListViewController.empty_state".localized())
 } else {
 self.favorites = favorites
 self.tableView.reloadData()
 self.view.bringSubviewToFront(self.tableView)
 }
 }
 
}
// MARK: - Delegates
extension FavoriteListViewController: UITableViewDataSource, UITableViewDelegate {
 
 func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
 return favorites.count
 }
 
 func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
 let cell = tableView.dequeueReusableCell(withIdentifier: GFFavoriteCell.reuseId) as! GFFavoriteCell
 var favorite = favorites[indexPath.row]
 favorite.avatarImage = try? ImageManager.retrieveImage(for: favorite.username)
 cell.set(favorite: favorite)
 
 return cell
 }
 
 func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
 let favorite = favorites[indexPath.row]
 let destinationVC = UserListViewController(username: favorite.username)
 navigationController?.pushViewController(destinationVC, animated: true)
 }
 
 func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCell.EditingStyle, forRowAt indexPath: IndexPath) {
 guard editingStyle == .delete else {
 return
 }
 
 UserDefaultsManager.shared.update(with: favorites[indexPath.row], actionType: .remove) { [weak self] error in
 guard let self = self else { return }
 if let error = error {
 DispatchQueue.main.async {
 self.presentGFAlert(title: error.title, message: error.description)
 }
 return
 }
 
 DispatchQueue.main.async {
 self.favorites.remove(at: indexPath.row)
 tableView.deleteRows(at: [indexPath], with: .left)
 }
 }
 }
 
 func tableView(_ tableView: UITableView, viewForFooterInSection section: Int) -> UIView? {
 UIView(frame: .zero)
 }
 
}

I have refactored it and broken it down into the files listed below:

FavoriteListDataSource

import UIKit
class FavoriteListDataSource: NSObject, UITableViewDataSource {
 // MARK: Public Properties
 
 var favoriteRepository: FavoriteRepository {
 return FavoriteRepository()
 }
 private weak var delegate: FavoriteListDelegate?
 
 // MARK: - Initializers
 
 init(delegate: FavoriteListDelegate?) {
 self.delegate = delegate
 }
 
 // MARK: - Protocol Methods
 
 func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
 return favoriteRepository.favorites.count
 }
 
 func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
 let cell = tableView.dequeueReusableCell(withIdentifier: GFFavoriteCell.reuseId) as! GFFavoriteCell
 var favorite = favoriteRepository.favorite(at: indexPath.row)
 favorite.avatarImage = try? ImageManager.retrieveImage(for: favorite.username)
 cell.set(favorite: favorite)
 
 return cell
 }
 
 func tableView(
 _ tableView: UITableView,
 commit editingStyle: UITableViewCell.EditingStyle,
 forRowAt indexPath: IndexPath
 ) {
 UserDefaultsManager.shared.update(
 with: favoriteRepository.favorite(at: indexPath.row),
 actionType: .remove
 ) { [weak self] error in
 
 guard let self = self else { return }
 if let error = error {
 DispatchQueue.main.async {
 self.delegate?.showError(title: error.title, message: error.description)
 }
 return
 }
 
 DispatchQueue.main.async {
 tableView.deleteRows(at: [indexPath], with: .left)
 self.delegate?.handleEmptyView()
 }
 }
 }
 
}

FavoriteListDelegate

import UIKit
protocol FavoriteListDelegate: AnyObject {
 func didSelectRow(at index: Int)
 func handleEmptyView()
 func showError(title: String, message: String)
}

FavoriteListView

import UIKit
class FavoriteListView: UIView {
 
 // MARK: Private Properties
 
 private let tableView = UITableView()
 private var dataSource: FavoriteListDataSource!
 private weak var delegate: FavoriteListDelegate?
 // MARK: - Initializers
 
 init(delegate: FavoriteListDelegate) {
 super.init(frame: UIScreen.main.bounds)
 
 self.delegate = delegate
 dataSource = FavoriteListDataSource(delegate: delegate)
 
 backgroundColor = .systemBackground
 configureTableView()
 }
 
 required init?(coder: NSCoder) {
 fatalError("init(coder:) has not been implemented")
 }
 
 // MARK: - Public Methods
 
 func reloadData() {
 tableView.reloadData()
 }
 
 // MARK: Private Methods
 
 private func configureTableView() {
 addSubview(tableView)
 
 tableView.frame = bounds
 tableView.rowHeight = 80
 
 tableView.dataSource = dataSource
 tableView.delegate = self
 
 tableView.register(GFFavoriteCell.self, forCellReuseIdentifier: GFFavoriteCell.reuseId)
 }
 
}
extension FavoriteListView: UITableViewDelegate {
 
 func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
 delegate?.didSelectRow(at: indexPath.row)
 }
 
 // Hides the empty cells from the TableView
 func tableView(_ tableView: UITableView, viewForFooterInSection section: Int) -> UIView? {
 UIView(frame: .zero)
 }
 
}

FavoriteListViewController

import UIKit
class FavoriteListViewController: UIViewController, HasCustomView {
 // MARK: Protocol Typealias
 
 typealias CustomView = FavoriteListView
 
 // MARK: Private Properties
 
 private var favoriteRepository: FavoriteRepository {
 return FavoriteRepository()
 }
 private var favoriteListView: FavoriteListView!
 
 // MARK: - Lifecycle
 
 override func loadView() {
 favoriteListView = FavoriteListView(delegate: self)
 view = favoriteListView
 }
 
 override func viewDidLoad() {
 super.viewDidLoad()
 configureViewController()
 }
 
 override func viewWillAppear(_ animated: Bool) {
 super.viewWillAppear(animated)
 favoriteRepository.refresh()
 handleEmptyStateView()
 }
 
 // MARK: - Private Methods
 
 private func configureViewController() {
 title = "favorites".localized()
 navigationController?.navigationBar.prefersLargeTitles = true
 }
 
 private func handleEmptyStateView() {
 if favoriteRepository.favorites.isEmpty {
 view.showEmptyStateView(with: "FavoriteList.empty_state".localized())
 } else {
 favoriteListView.reloadData()
 view.hideEmptyStateView()
 }
 }
 
}
// MARK: - Delegates
extension FavoriteListViewController: FavoriteListDelegate {
 
 func didSelectRow(at index: Int) {
 let favorite = favoriteRepository.favorite(at: index)
 let destinationVC = UserListViewController(username: favorite.username)
 navigationController?.pushViewController(destinationVC, animated: true)
 }
 
 func handleEmptyView() {
 handleEmptyStateView()
 }
 
 func showError(title: String, message: String) {
 presentGFAlert(title: title, message: message)
 }
 
}

FavoriteRepository

class FavoriteRepository {
 
 // MARK: Properties
 
 private(set) var favorites: [User] = []
 
 // MARK: - Initializers
 
 init() {
 fetchFavorites()
 }
 // MARK: - Public Methods
 
 func favorite(at index: Int) -> User {
 return favorites[index]
 }
 
 func refresh() {
 fetchFavorites()
 }
 
 // MARK: Private Methods
 
 func fetchFavorites() {
 UserDefaultsManager.shared.retrieveFavorites { [weak self] result in
 guard let self = self else { return }
 
 switch result {
 case .success(let favorites):
 self.favorites = favorites
 
 case .failure:
 self.favorites = []
 }
 }
 }
 
}

I'm aware that there is much more code than before, which is something that I would be fine with as long as I follow the SOLID principles and the code scales appropiately, but I have the feeling that some things are not quite right. For example, I have a favoriteRepository computed property in the ViewController and in the DataSource in order to respond appropiately to the case where the user removes a favorite user directly from the list by sliding the cell and tapping on delete. I'm not sure if that's the best way to handle that because both classes are accessing the same repository from a different object and I have to recur to computed properties to resolve that.

As I said, that's only an example, but I suspect that there are a few more things that might be improved. I would be very grateful to read any criticism of my code in order to refactor it the proper way as I'm trying to get over massive ViewControllers.

It might be worth noting that I don't use the DataSource in any more ViewControllers.

asked Mar 6, 2023 at 9:17
\$\endgroup\$

0

You must log in to answer this question.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.