✅ MVVM 패턴이란?
- Model-View-ViewModel로 구성된 아키텍처 패턴
✅ 구성요소
- 데이터를 처리하는 모델(Model)
- 사용자에게 보여지는 UI인 뷰(View)
- 뷰에 바인딩되어 모델과 뷰 사이를 이어주는 뷰-모델(View Model)
⭐ Model과 View뿐만 아니라 Binding을 통하여 View와 View Model 간의 의존성까지 최소한 형태로, 데이터 처리 로직과 UI 간 상호 영향이 적어 모듈화를 통해 재사용성을 높이고 및 역할별로 Unit Test가 용이해진다.
🟥 바인딩 (Binding)
- MVVM (Model-View-ViewModel) 패턴에서 바인딩(Binding)은 View와 ViewModel 사이의 데이터와 이벤트를 자동으로 동기화하는 메커니즘이다.
- 바인딩을 통해 ViewModel의 상태가 변경되면 View가 자동으로 업데이트되고, View에서 발생한 이벤트가 ViewModel에 전달되어 상태가 변경된다.
✅ 바인딩의 개념을 이해하기 위해, 다음과 같은 주요 측면을 고려해야 한다.
- 단방향 바인딩 (One-Way Binding):
- ViewModel에서 View로 데이터를 바인딩한다.
- ViewModel의 데이터가 변경되면 View가 자동으로 업데이트된다.
- 양방향 바인딩 (Two-Way Binding):
- View와 ViewModel이 상호 바인딩된다.
- View의 입력이 변경되면 ViewModel의 데이터도 자동으로 업데이트되고, 그 반대의 경우도 마찬가지이다.
✅ Swift와 UIKit에서는 바인딩을 직접 구현해야 한다.
✅ 아래 예제는 Combine을 사용하여 UIKit에서 바인딩을 구현한 예제이다.
1️⃣ Model
- user 라는 구조체로, 사용자 데이터를 나타낸다.
struct User {
let name: String
let age: Int
}
2️⃣ ViewModel
- UserViewModel은 @Published 속성을 사용하여 User 데이터를 관리하며, updateUser 메서드로 데이터를 업데이트한다.
import Foundation
import Combine
class UserViewModel {
@Published var user: User
init(user: User) {
self.user = user
}
func updateUser(name: String, age: Int) {
user.name = name
user.age = age
}
}
3️⃣ View
- UserListView는 사용자 인터페이스 요소를 관리한다.
- 텍스트 필드와 레이블을 포함하며, 레이아웃 제약 조건을 설정한다.
import UIKit
class UserListView: UIView {
let nameTextField: UITextField = {
let textField = UITextField()
textField.borderStyle = .roundedRect
textField.placeholder = "Name"
return textField
}()
let ageTextField: UITextField = {
let textField = UITextField()
textField.borderStyle = .roundedRect
textField.placeholder = "Age"
textField.keyboardType = .numberPad
return textField
}()
let greetingLabel: UILabel = {
let label = UILabel()
label.textAlignment = .center
label.numberOfLines = 0
return label
}()
override init(frame: CGRect) {
super.init(frame: frame)
setupUI()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
setupUI()
}
private func setupUI() {
addSubview(nameTextField)
addSubview(ageTextField)
addSubview(greetingLabel)
nameTextField.translatesAutoresizingMaskIntoConstraints = false
ageTextField.translatesAutoresizingMaskIntoConstraints = false
greetingLabel.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
nameTextField.topAnchor.constraint(equalTo: topAnchor, constant: 20),
nameTextField.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 20),
nameTextField.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -20),
ageTextField.topAnchor.constraint(equalTo: nameTextField.bottomAnchor, constant: 20),
ageTextField.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 20),
ageTextField.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -20),
greetingLabel.topAnchor.constraint(equalTo: ageTextField.bottomAnchor, constant: 20),
greetingLabel.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 20),
greetingLabel.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -20),
greetingLabel.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -20)
])
}
}
4️⃣ ViewController
- UserListViewController는 UIViewController를 상속받아, UserViewModel과 UserListView를 연결하고, 데이터 바인딩 및 사용자 상호작용을 처리한다.
- Combine 프레임워크를 사용하여 UserViewModel의 변경 사항을 구독하고, 텍스트 필드의 변경 사항을 ViewModel에 전달한다.
import UIKit
import Combine
class UserListViewController: UIViewController {
private var viewModel: UserViewModel!
private var userListView: UserListView!
private var cancellables: Set<AnyCancellable> = []
override func loadView() {
userListView = UserListView()
view = userListView
}
override func viewDidLoad() {
super.viewDidLoad()
viewModel = UserViewModel(user: User(name: "Alice", age: 30))
bindViewModel()
setupActions()
}
private func bindViewModel() {
viewModel.$user
.receive(on: RunLoop.main)
.sink { [weak self] user in
self?.userListView.nameTextField.text = user.name
self?.userListView.ageTextField.text = "\(user.age)"
self?.userListView.greetingLabel.text = "Hello, \(user.name)! You are \(user.age) years old."
}
.store(in: &cancellables)
}
private func setupActions() {
userListView.nameTextField.addTarget(self, action: #selector(nameTextFieldChanged), for: .editingChanged)
userListView.ageTextField.addTarget(self, action: #selector(ageTextFieldChanged), for: .editingChanged)
}
@objc private func nameTextFieldChanged() {
guard let name = userListView.nameTextField.text else { return }
viewModel.updateUser(name: name, age: viewModel.user.age)
}
@objc private func ageTextFieldChanged() {
guard let ageText = userListView.ageTextField.text, let age = Int(ageText) else { return }
viewModel.updateUser(name: viewModel.user.name, age: age)
}
}
5️⃣ SceneDelegate (iOS 13 이상)
import UIKit
class SceneDelegate: UIResponder, UIWindowSceneDelegate {
var window: UIWindow?
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
guard let windowScene = (scene as? UIWindowScene) else { return }
window = UIWindow(windowScene: windowScene)
window?.rootViewController = UINavigationController(rootViewController: UserListViewController())
window?.makeKeyAndVisible()
}
...
}
🟥 다른 예시
- 간단한 "할 일 목록" 애플리케이션을 예로 들어본다.
- 이 애플리케이션에서는 사용자가 할 일 항목을 추가하고, 목록에 표시되는 것을 목표로 한다.
1️⃣ Model
- TodoItem 구조체는 할 일 항목을 나타낸다.
struct TodoItem {
let title: String
let isCompleted: Bool
}
2️⃣ ViewModel
- ViewModel은 모델 데이터를 관리하고, UI 업데이트를 위한 바인딩을 제공한다.
- TodoViewModel은 @Published 속성을 사용하여 할 일 목록을 관리하며, addTodoItem 메서드로 새로운 할 일 항목을 추가한다.
import Foundation
import Combine
class TodoViewModel: ObservableObject {
@Published var todoItems: [TodoItem] = []
func addTodoItem(title: String) {
let newItem = TodoItem(title: title, isCompleted: false)
todoItems.append(newItem)
}
}
3️⃣ View
- 할 일 항목을 추가하고 목록을 표시하는 뷰이다.
- TodoListView는 사용자 인터페이스 요소를 관리한다.
- 텍스트 필드, 버튼, 테이블 뷰를 포함하며, 레이아웃 제약 조건을 설정한다.
import UIKit
class TodoListView: UIView {
let tableView = UITableView()
let addButton: UIButton = {
let button = UIButton(type: .system)
button.setTitle("Add Todo", for: .normal)
return button
}()
let titleTextField: UITextField = {
let textField = UITextField()
textField.placeholder = "Todo title"
textField.borderStyle = .roundedRect
return textField
}()
override init(frame: CGRect) {
super.init(frame: frame)
setupUI()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
setupUI()
}
private func setupUI() {
addSubview(titleTextField)
addSubview(addButton)
addSubview(tableView)
titleTextField.translatesAutoresizingMaskIntoConstraints = false
addButton.translatesAutoresizingMaskIntoConstraints = false
tableView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
titleTextField.topAnchor.constraint(equalTo: topAnchor, constant: 20),
titleTextField.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 20),
titleTextField.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -20),
addButton.topAnchor.constraint(equalTo: titleTextField.bottomAnchor, constant: 10),
addButton.centerXAnchor.constraint(equalTo: centerXAnchor),
tableView.topAnchor.constraint(equalTo: addButton.bottomAnchor, constant: 20),
tableView.leadingAnchor.constraint(equalTo: leadingAnchor),
tableView.trailingAnchor.constraint(equalTo: trailingAnchor),
tableView.bottomAnchor.constraint(equalTo: bottomAnchor)
])
}
}
4️⃣ ViewController
- ViewController는 ViewModel과 View를 연결한다.
- TodoListViewController는 UIViewController를 상속받아, TodoViewModel과 TodoListView를 연결하고, 데이터 바인딩 및 사용자 상호작용을 처리한다.
- Combine 프레임워크를 사용하여 TodoViewModel의 변경 사항을 구독하고, 버튼 클릭 이벤트를 ViewModel에 전달한다.
import UIKit
import Combine
class TodoListViewController: UIViewController {
private var viewModel: TodoViewModel!
private var todoListView: TodoListView!
private var cancellables: Set<AnyCancellable> = []
override func loadView() {
todoListView = TodoListView()
view = todoListView
}
override func viewDidLoad() {
super.viewDidLoad()
viewModel = TodoViewModel()
bindViewModel()
setupActions()
todoListView.tableView.dataSource = self
}
private func bindViewModel() {
viewModel.$todoItems
.receive(on: RunLoop.main)
.sink { [weak self] _ in
self?.todoListView.tableView.reloadData()
}
.store(in: &cancellables)
}
private func setupActions() {
todoListView.addButton.addTarget(self, action: #selector(addButtonTapped), for: .touchUpInside)
}
@objc private func addButtonTapped() {
guard let title = todoListView.titleTextField.text, !title.isEmpty else { return }
viewModel.addTodoItem(title: title)
todoListView.titleTextField.text = ""
}
}
extension TodoListViewController: UITableViewDataSource {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return viewModel.todoItems.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "TodoCell") ?? UITableViewCell(style: .default, reuseIdentifier: "TodoCell")
let todoItem = viewModel.todoItems[indexPath.row]
cell.textLabel?.text = todoItem.title
return cell
}
}
'Swift' 카테고리의 다른 글
ObservableObject와 @Published (0) | 2024.06.03 |
---|---|
Combine (MVVM 패턴) (0) | 2024.05.30 |
MVVM 패턴 (Model - View - ViewModel) (0) | 2024.05.30 |
고차함수 (Map, Filter, Reduce) (0) | 2024.05.26 |
MVC 패턴 (Model - View - Controller) (0) | 2024.05.25 |