How to use the MVC architecture to avoid controller overload?
sobes.tech AI
Answer from AI
MVC (Model-View-Controller) in iOS often leads to "fat" controllers due to placing business logic and presentation logic within them. To address this issue, the following approaches can be used:
-
Delegating presentation logic to View: Creating custom
UIViewor table/collection view cells that handle data display and basic user actions (e.g., button presses inside the cell). The controller only configures the View with the necessary data.class MyCustomView: UIView { private let label = UILabel() override init(frame: CGRect) { super.init(frame: frame) addSubview(label) // Setting up constraints, etc. } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } func configure(with text: String) { label.text = text } // Handling UIControlEvents, e.g., via addTarget } -
Delegating business logic to Model or separate Service Objects: Creating classes responsible for specific operations (e.g., network requests, database interactions, data validation). The controller delegates these tasks to those objects.
class UserService { func fetchUsers(completion: @escaping ([User]?, Error?) -> Void) { // Network request logic } } class ProfileViewController: UIViewController { private let userService = UserService() override func viewDidLoad() { super.viewDidLoad() userService.fetchUsers { users, error in // Update UI based on received data or error } } } -
Using Data Source and Delegate Protocols: Implementing
UITableViewDataSource,UICollectionViewDataSource,UITableViewDelegate,UICollectionViewDelegateprotocols in separate classes rather than in the controller itself. The controller only owns instances of these classes.class UsersTableViewDataSource: NSObject, UITableViewDataSource { private var users: [User] = [] func update(users: [User]) { self.users = users } func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { return users.count } func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { let cell = tableView.dequeueReusableCell(withIdentifier: "UserCell", for: indexPath) let user = users[indexPath.row] cell.textLabel?.text = user.name return cell } } class UsersViewController: UIViewController { @IBOutlet weak var tableView: UITableView! private let dataSource = UsersTableViewDataSource() override func viewDidLoad() { super.viewDidLoad() tableView.dataSource = dataSource // Load data and pass to dataSource.update(users:) } } -
Applying Coordinator Pattern: Creating separate objects responsible for navigation between screens. Controllers only know that they need to transition to the next step, but not which controller will be next.
protocol AppCoordinator: AnyObject { func start() func showUserDetails(user: User) } class MainAppCoordinator: AppCoordinator { private let navigationController: UINavigationController init(navigationController: UINavigationController) { self.navigationController = navigationController } func start() { let usersVC = UsersViewController() // Setting delegate, dataSource, etc. navigationController.pushViewController(usersVC, animated: false) } func showUserDetails(user: User) { let userDetailsVC = UserDetailsViewController(user: user) navigationController.pushViewController(userDetailsVC, animated: true) } }
These approaches help unload the controller, making it more "thin" and focused on managing the View's lifecycle and coordinating interactions between Model and View, rather than executing all application logic.