안녕하세요🐶
빈지식 채우기의 비니🙋🏻♂️ 입니다.
이전에! MVVM에서의 필수적인 첫 번째 요소인 Command 패턴에 대해 알아보았습니다.
이번 시간에는 두 번째 필수 요소인 Data Binding에 대해 알아보도록 하죠!
아래 포스팅을 보고 오시면 더욱 이해가 쉬울 수 있습니다!
[디자인패턴] MVC, MVP, MVVM
안녕하세요🐶빈지식 채우기의 비니🙋🏻♂️ 입니다. 바로 오늘의 주제에 대해 아래 대화로 알아보도록 하죠! 👨🏻💼 : 안녕하세요 여러분~ 우리가 유지보수와 개발 효율 상승을 위해
beanistory.tistory.com
[디자인패턴] 커맨드 패턴 ( Command Pattern )
안녕하세요🐶빈지식 채우기의 비니🙋🏻♂️ 입니다. 오늘도 역시 학생과 선생님의 대화로 주제를 알아보도록 하겠습니다. 👨🏻💼 : 비니 학생, 혹시 MVVM 패턴에서 가장 중요한 요소에
beanistory.tistory.com
1. Data Binding
두 개의 데이터 소스 ( View, View Model ) 를 함께 연결하고 동기화 상태를 유지하는 일반적인 기술
바로 예제를 통해 알아볼건데,
여기서는 View Model (데이터 변경), View ( 데이터 변경 체크 및 업데이트 ) 로 만들 예정입니다.
여러 방법 중 Closure, Observable, Combine 를 이용한 예제를 준비했습니다.
2. 시계 예제
MVVM 패턴으로 간단한 시계를 만들어 봅니다.
Closure, Observable, Combine 세 가지 방법으로 Data Binding 을 알아보도록 하겠습니다.
2-1. 기본 설정
프로젝트 구조는 다음과 같습니다.
- Controller : View Model 데이터 변화를 감지해 업데이트를 진행하는 View Controller
- View Model : Model과 함께 데이터 변화를 담당
- Model : Data Binding에 사용되는 데이터 모델 정의
- Observable : Observable 사용하기 위한 관찰자 정의
// 모델 (Model)
class Clock {
static var currentTime: (() -> String) = {
let today = Date()
let hours = Calendar.current.component(.hour, from: today)
let minutes = Calendar.current.component(.minute, from: today)
let minStr = String(format: "%02d", minutes)
let seconds = Calendar.current.component(.second, from: today)
let secStr = String(format: "%02d", seconds)
return "\(hours):\(minStr):\(secStr)"
}
}
// 뷰 컨트롤러
class ClockViewController: UIViewController {
@IBOutlet weak var closureLobel: UILabel!
@IBOutlet weak var ObservableLabel: UILabel!
@IBOutlet weak var combineLabelPupblished: UILabel!
@IBOutlet weak var combieLabelSubject: UILabel!
override func viewDidLoad() {
super.viewDidLoad()
startTimer()
}
// 매 초마다 시간을 업데이트
private func startTimer() {
// 매 초마다 뷰 모델 CheckTime 함수 호출
Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { [weak self] _ in
}
}
}
2-2. Closure
먼저 Closure을 사용하여 가장 간단한 Data Binding을 구현해보도록 하겠습니다.
//
// ClockViewModel.swift
//
import Foundation
// 뷰 모델 정의
class ClockViewModel {
// 프로퍼티 옵저버
// 매 초마다 CheckTime 호출 -> closureTime 값 저장
// 저장될떄마다 didSet 호출
var closureTime: String {
didSet {
didChangeTime?(self)
}
}
// 생성 시 초기 시간을 노출
init() {
closureTime = Clock.currentTime()
}
// 매 초마다 호출 -> closureTime 값 저장
func checkTime() {
closureTime = Clock.currentTime()
}
}
- 매 초마다 CheckTime() 이 호출 되면서 closureTime 값이 변경된다.
- 프로퍼티 옵저버가 이를 인지해서 뷰의 데이터를 업데이트 한다. ( Data Binding )
//
// ClockViewController.swift
//
import Foundation
import UIKit
class ClockViewController: UIViewController {
@IBOutlet weak var closureLobel: UILabel!
@IBOutlet weak var ObservableLabel: UILabel!
@IBOutlet weak var combineLabelPupblished: UILabel!
@IBOutlet weak var combieLabelSubject: UILabel!
// 뷰 모델 객체 정의
private var viewModel = ClockViewModel()
override func viewDidLoad() {
super.viewDidLoad()
setBindings()
startTimer()
}
// 매 초마다 시간을 업데이트
private func startTimer() {
// 매 초마다 뷰 모델 CheckTime 함수 호출
Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { [weak self] _ in
self?.viewModel.checkTime()
}
}
func setBindings() {
// 뷰 모델 클로저 변수에 수행될 동작을 저장
viewModel.didChangeTime = { [weak self] viewModel in
self?.closureLobel.text = viewModel.closureTime
}
}
}
- setBindings 를 통해 didChangeTime을 정의한다 ( 수행되는 동작을 정의 )
2-3. Observable
변화를 관찰하며 동작하는 Observable 클래스를 생성한다.
//
// Observable.swift
//
import Foundation
class ClockObservable<T> {
typealias Obs = (T) -> Void
var value: T? {
didSet {
self.observable?(value!)
}
}
// 클로저를 통해 동작을 담아줄 변수를 생성
private var observable: (Obs)?
init(_ value: T?) {
self.value = value
}
func bind(_ listener: @escaping Obs) {
self.observable = listener
}
}
- value 값이 변경하면 프로퍼티 옵저버를 이용하여 listener(observable) 동작 수행
//
// ClockViewModel.swift
//
import Foundation
import Combine
// 뷰 모델 정의
class ClockViewModel {
var observableTime: ClockObservable<String> = ClockObservable("Observable")
// 생성 시 초기 시간을 노출
init() {
observableTime.value = Clock.currentTime()
}
// 매 초마다 호출 -> closureTime 값 저장
func checkTime() {
observableTime.value = Clock.currentTime()
}
}
- 매 초마다 observable 클래스의 value 값이 변경된다.
- ClockObservable 클래스의 프로퍼티 옵저버가를 통해 listener(observable) 동작을 수행한다
//
// ClockViewController.swift
//
import Foundation
import UIKit
class ClockViewController: UIViewController {
@IBOutlet weak var closureLobel: UILabel!
@IBOutlet weak var ObservableLabel: UILabel!
@IBOutlet weak var combineLabelPupblished: UILabel!
@IBOutlet weak var combieLabelSubject: UILabel!
// 뷰 모델 객체 정의
private var viewModel = ClockViewModel()
override func viewDidLoad() {
super.viewDidLoad()
setBindings()
startTimer()
}
// 매 초마다 시간을 업데이트
private func startTimer() {
// 매 초마다 뷰 모델 CheckTime 함수 호출
Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { [weak self] _ in
self?.viewModel.checkTime()
}
}
func setBindings() {
// 클로저 내부 동작을 Observable에 저장
viewModel.observableTime.bind { [weak self] time in
self?.ObservableLabel.text = time
}
}
}
- 매 초마다 checkTime() 함수를 호출하여 ClockObservable 클래스의 value 값을 변경한다.
- bind 를 통해 수행되는 동작을 정의한다.
2-4. Combine
Combine 을 통해서도 Data Binding 을 구현해볼 수 있다.
아래 포스팅을 참고하면 이해하기 더 쉬울 것이다.
[Combine] Combine 이란?
안녕하세요🐶 빈지식 채우기의 비니🙋🏻♂️입니다. 요즘 RxSwift 공부를 통해 비동기 프로그래밍에 대해 블로그도 작성하고 있는데, First-Party 인 Combine 이라는 것이 있더라구요! . 마찬가지
beanistory.tistory.com
Combine 을 작업할 때,
Published 방식과 Subject 방식 두 방법으로 작업을 해보았다.
//
// ClockViewModel.swift
//
import Foundation
import Combine
// 뷰 모델 정의
class ClockViewModel {
// Subject를 통해 데이터를 제공
var combineTimeSubject = CurrentValueSubject<String, Never>("Combine")
// @Published를 통해 데이터를 제공
// 연산자 $을 통해 Published 속성에 접근
@Published var combineTimePublished: String = "Combine"
// 생성 시 초기 시간을 노출
init() {
combineTimeSubject.value = Clock.currentTime()
combineTimePublished = Clock.currentTime()
}
// 매 초마다 호출 -> closureTime 값 저장
func checkTime() {
combineTimeSubject.value = Clock.currentTime()
combineTimePublished = Clock.currentTime()
}
}
- Subject 와 Published 가 적용된 변수를 각각 선언한다.
//
// ClockViewController.swift
//
import Foundation
import UIKit
import Combine
class ClockViewController: UIViewController {
@IBOutlet weak var closureLobel: UILabel!
@IBOutlet weak var ObservableLabel: UILabel!
@IBOutlet weak var combineLabelPupblished: UILabel!
@IBOutlet weak var combieLabelSubject: UILabel!
// Combine Published 취소 저장 객체 정의
private var cancellables: Set<AnyCancellable> = []
// Combine Subject Sink 동적 정의
var combineSink: AnyCancellable?
override func viewDidLoad() {
super.viewDidLoad()
setBindings()
startTimer()
}
// 매 초마다 시간을 업데이트
private func startTimer() {
// 매 초마다 뷰 모델 CheckTime 함수 호출
Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { [weak self] _ in
self?.viewModel.checkTime()
}
}
func setBindings() {
// Combine 로직 수행 ( Subject 사용 )
combineSink = viewModel.combineTimeSubject.sink { [weak self] value in
self?.combieLabelSubject.text = value
}
// Combine 로직 수행 ( Publihsed 사용 )
viewModel.$combineTimePublished
.compactMap { String($0) } // compactMap 을 통해 데이터 가공을 한다 ( 옵셔널 제거 )
.assign(to: \.text, on: combineLabelPupblished) // assign 방식은 keyPath 형식으로 객체 접근, Label에 값 없데이트
.store(in: &cancellables) // 취소할 동적을 저장
}
}
- Subject의 경우, sink 라는 메서드로 값의 변화를 감지해 Closure 로 정의된 동작을 수행한다.
- 해당 소스의 경우 combineSink 라는 변수를 선언했는데, sink로 선언된 동작을 저장하기 위해서이다.
- Published 의 경우, compactMap 과 assign 을 통해 데이터를 가공하여 뷰를 업데이트를 한다.
2-5. 결과
다음과 같이 결과가 나오는 것을 확인할 수 있습니다.
이 세 가지의 모든 방법의 공통점은 아래와 같다.
직접 View 가 View Model의 값에 접근하지 않고 값의 변화를 감지해 유동적으로 업데이트 한다.
이것이 Data Binding 이다.
감사합니다.
참고
- https://www.youtube.com/watch?v=6mTwA7da0Vk
- https://www.youtube.com/watch?v=shQMRnTNM2E
- https://ios-daniel-yang.tistory.com/entry/iOSSwift-Data-Binding%EC%97%90-%EB%8C%80%ED%95%98%EC%97%AC-%EC%95%8C%EC%95%84%EB%B3%B4%EC%9E%90-Closure-Observable-Combine-MVVM
'iOS📱 > Common' 카테고리의 다른 글
[디자인패턴] 커맨드 패턴 ( Command Pattern ) (0) | 2025.02.05 |
---|---|
[디자인패턴] MVC, MVP, MVVM (0) | 2025.02.05 |
[ iOS ] GCD 4편 - GCD ( Grand Central Dispatch ) (0) | 2023.01.19 |
[ iOS ] GCD 3편 - Serial vs Concurrent (0) | 2023.01.18 |
[ iOS ] GCD 2편 - Sync vs Async (2) | 2023.01.12 |