iOS/Swift

MVVM 패턴 - Binding 개념

밤새는 탐험가89 2024. 5. 30. 07:27

 

 

✅ 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
    }
}

'iOS > 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