As an iOS developer, designing the architecture of your application is one of the most important tasks you will face. The architecture you choose can have a significant impact on the maintainability, scalability, and performance of your application.
In this blog post, we will discuss four popular architectures used in iOS projects:
Model-View-Controller (MVC)
Model-View-ViewModel (MVVM)
View-Interactor-Presenter-Entity-Router (VIPER)
Clean Architecture.
Model-View-Controller (MVC)
MVC is a classic architecture that has been around for a long time and is widely used in iOS development. The MVC architecture separates the application into three main components: Model, View, and Controller.
Model: Represents the data and business logic of the application.
View: Represents the user interface (UI) of the application.
Controller: Mediates between the Model and the View.
Here is an example of how the MVC architecture can be implemented in code:
class Person {
var name: String
var age: Int
init(name: String, age: Int) {
self.name = name
self.age = age
}
}
class PersonViewController: UIViewController {
var person: Person?
var nameLabel: UILabel
var ageLabel: UILabel
override func viewDidLoad() {
super.viewDidLoad()
nameLabel.text = person?.name
ageLabel.text = "\(person?.age)"
}
}
In this example, the Person class represents the Model. The PersonViewController class is the implementation of the View and the Controller.
One of the drawbacks of the MVC architecture is that it can lead to massive ViewControllers that are difficult to maintain. The Controller is responsible for both mediating between the Model and the View and handling user input. This can result in code that is tightly coupled and difficult to test.
Model-View-ViewModel (MVVM)
MVVM is a relatively new architecture that was introduced by Microsoft in 2005. MVVM is an evolution of the MVC architecture and is designed to address some of its drawbacks. MVVM separates the application into three main components: Model, View, and ViewModel.
Model: Represents the data and business logic of the application.
View: Represents the user interface (UI) of the application.
ViewModel: Acts as a bridge between the Model and the View.
Here is an example of how the MVVM architecture can be implemented in code:
class Person {
var name: String
var age: Int
init(name: String, age: Int) {
self.name = name
self.age = age
}
}
class PersonViewModel {
var person: Person?
var name: String {
return person?.name ?? ""
}
var age: String {
return "\(person?.age)"
}
}
class PersonViewController: UIViewController {
var viewModel: PersonViewModel?
var nameLabel: UILabel
var ageLabel: UILabel
override func viewDidLoad() {
super.viewDidLoad()
nameLabel.text = viewModel?.name
ageLabel.text = viewModel?.age
}
}
In this example, the Person class represents the Model. The PersonViewModel class represents the ViewModel. The PersonViewController class is the implementation of the View.
One of the benefits of the MVVM architecture is that it can lead to more maintainable and testable code. The ViewModel acts as a bridge between the Model and the View, which makes it easier to test the business logic of the application separately from the UI.
View-Interactor-Presenter-Entity-Router (VIPER)
VIPER is a newer architecture that was introduced by the iOS team at Uber. VIPER is designed to be highly modular and scalable, making it an excellent choice for large and complex applications. VIPER separates the application into five main components: View, Interactor, Presenter, Entity, and Router.
View: Represents the user interface (UI) of the application.
Interactor: Contains the business logic of the application.
Presenter: Mediates between the Interactor and the View.
Entity: Represents the data of the application.
Router: Handles navigation between screens.
Here is an example of how the VIPER architecture can be implemented in code:
// View
protocol PersonViewProtocol: AnyObject {
func setName(_ name: String)
func setAge(_ age: Int)
}
class PersonViewController: UIViewController, PersonViewProtocol {
var presenter: PersonPresenterProtocol?
var nameLabel: UILabel
var ageLabel: UILabel
func setName(_ name: String) {
nameLabel.text = name
}
func setAge(_ age: Int) {
ageLabel.text = "\(age)"
}
override func viewDidLoad() {
super.viewDidLoad()
presenter?.viewDidLoad()
}
}
// Interactor
protocol PersonInteractorProtocol: AnyObject {
func getPerson()
}
class PersonInteractor: PersonInteractorProtocol {
var presenter: PersonPresenterProtocol?
var person: Person?
func getPerson() {
person = Person(name: "John Doe", age: 30)
presenter?.didGetPerson(person)
}
}
// Presenter
protocol PersonPresenterProtocol: AnyObject {
func viewDidLoad()
func didGetPerson(_ person: Person?)
}
class PersonPresenter: PersonPresenterProtocol {
weak var view: PersonViewProtocol?
var interactor: PersonInteractorProtocol?
func viewDidLoad() {
interactor?.getPerson()
}
func didGetPerson(_ person: Person?) {
guard let person = person else { return }
view?.setName(person.name)
view?.setAge(person.age)
}
}
// Entity
class Person {
var name: String
var age:Int
init(name: String, age: Int) {
self.name = name
self.age = age
}
}
// Router
protocol PersonRouterProtocol: AnyObject {
func showNextScreen()
}
class PersonRouter: PersonRouterProtocol {
weak var viewController: UIViewController?
func showNextScreen() {
let nextViewController = NextViewController()
viewController?.navigationController?.pushViewController(nextViewController, animated: true)
}
}
In this example, the Person class represents the Entity. The PersonViewProtocol protocol represents the View. The PersonViewController class is the implementation of the View, and it communicates with the Presenter through the PersonPresenterProtocol protocol. The PersonPresenter class represents the Presenter, which communicates with the Interactor through the PersonInteractorProtocol protocol. The PersonInteractor class represents the Interactor. Finally, the PersonRouter class represents the Router, which is responsible for navigating between different screens of the application
One of the benefits of the VIPER architecture is that it provides a clear separation of concerns, which makes it easier to maintain and test code. Each component has a specific responsibility, which reduces the amount of code that needs to be modified when changes are made to the application.
Clean Architecture
Clean Architecture is a software architecture developed by Robert C. Martin (a.k.a Uncle Bob) that emphasizes the separation of concerns in software development. Clean Architecture is designed to make the software more testable, maintainable, and scalable. Clean Architecture is based on four main layers: Entities, Use Cases, Interface Adapters, and Frameworks & Drivers.
Entities: Contains the core business logic and data of the application.
Use Cases: Contains the application-specific business rules.
Interface Adapters: Contains the adapters that communicate between the application and the external world.
Frameworks & Drivers: Contains the user interface and external libraries that communicate with the application.
Here is an example of how the Clean Architecture can be implemented in code:
// Entities
struct User {
var id: Int
var name: String
}
// Use Cases
protocol UserUseCaseProtocol {
func getUsers(completion: @escaping ([User]) -> Void)
}
class UserUseCase: UserUseCaseProtocol {
var userRepository: UserRepositoryProtocolinit(userRepository: UserRepositoryProtocol) {
self.userRepository = userRepository
}
func getUsers(completion: @escaping ([User]) -> Void) {
userRepository.getUsers(completion: completion)
}
}
// Interface Adapters
protocol UserRepositoryProtocol {
func getUsers(completion: @escaping ([User]) -> Void)
}
class UserRepository: UserRepositoryProtocol {
var apiClient: APIClientProtocolinit(apiClient: APIClientProtocol) {
self.apiClient = apiClient
}
func getUsers(completion: @escaping ([User]) -> Void) {
apiClient.getUsers(completion: completion)
}
}
protocol APIClientProtocol {
func getUsers(completion: @escaping ([User]) -> Void)
}
class APIClient: APIClientProtocol {
func getUsers(completion: @escaping ([User]) -> Void) {
// Make API call and return results
completion([User(id: 1, name: "John Doe"), User(id: 2, name: "Jane Smith")])
}
}
// Frameworks & Drivers
class UserViewController: UIViewController {
var userUseCase: UserUseCaseProtocol?
var users: [User] = []
override func viewDidLoad() {
super.viewDidLoad()
userUseCase?.getUsers(completion: { [weak self] users in self?.users = users
// Update UI with users data
})
}
}
In this example, the User struct represents the Entity. The UserUseCase class represents the Use Case. The UserRepository class represents the Interface Adapter. The APIClient class represents the Frameworks & Drivers. The UserViewController class represents the user interface.
Clean Architecture allows for maximum flexibility and scalability, as each layer has a clearly defined responsibility. The architecture also allows for the addition or removal of layers as necessary. The architecture is designed to allow developers to focus on the core business logic and data of the application, without worrying about the external world or the user interface.
Conclusion
According to a survey conducted by JetBrains in 2021, 48% of iOS developers reported using the MVC pattern, while 35% reported using the MVVM pattern. VIPER and Clean Architecture were less widely used, with only 4% and 2% of developers reporting using them, respectively.
It's worth noting that the popularity of different patterns can vary depending on the specific industry or domain of the app being developed. For example, enterprise apps may be more likely to use Clean Architecture due to its emphasis on modularity and maintainability, while consumer-facing apps may be more likely to use MVC or MVVM due to their focus on user interface design.
Ultimately, the choice of architecture depends on the specific needs of the project and the preferences of the development team. Each pattern has its strengths and weaknesses, and it's up to the developers to choose the one that best suits their needs.
Comments