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.