데이터 바인딩을 하지 않는 경우와 바인딩을 하는 경우를 나누어서 간단한 예제를 만들어보았다.
// Model
struct Babys {
let name: String
let sex: String
let age: Int
let memo: String
let image: String
init(name: String, sex: String, age: Int, memo: String, image: String) {
self.name = name
self.sex = sex
self.age = age
self.memo = memo
self.image = image
}
}
✅ 아이 정보를 정의한 간단한 모델이다.
// Data
struct DummyData {
static let baby: [Babys] = [
Babys(name: "안재율", sex: "남아", age: 1, memo: "재율이 입니다.", image: "Jaeyul1"),
Babys(name: "안나율", sex: "여아", age: 1, memo: "나율이 입니다.", image: "Nayul1"),
Babys(name: "안재율2", sex: "남아", age: 1, memo: "재율이 입니다.2", image: "Jaeyul2")
]
}
✅ 오늘 예제에 활용할 더미데이터이다.
class ViewController: UIViewController {
let babyNameLabel: UILabel = {
let label = UILabel()
label.text = "Unknown"
label.textColor = .black
label.backgroundColor = .clear
label.font = .boldSystemFont(ofSize: 15)
label.translatesAutoresizingMaskIntoConstraints = false
return label
}()
let babySexLabel: UILabel = {
let label = UILabel()
label.text = "Unknown"
label.textColor = .black
label.backgroundColor = .clear
label.font = .systemFont(ofSize: 12)
label.translatesAutoresizingMaskIntoConstraints = false
return label
}()
let babyAgeLabel: UILabel = {
let label = UILabel()
label.text = "Unknown"
label.textColor = .black
label.backgroundColor = .clear
label.font = .systemFont(ofSize: 12)
label.translatesAutoresizingMaskIntoConstraints = false
return label
}()
let babyMemoLabel: UILabel = {
let label = UILabel()
label.text = "Unknown"
label.textColor = .black
label.backgroundColor = .clear
label.font = .systemFont(ofSize: 12)
label.translatesAutoresizingMaskIntoConstraints = false
label.textAlignment = .center
return label
}()
let babyImageView: UIImageView = {
let imageView = UIImageView()
imageView.image = UIImage(named: "")
imageView.backgroundColor = .blue
imageView.translatesAutoresizingMaskIntoConstraints = false
return imageView
}()
let nextButton: UIButton = {
let button = UIButton()
button.setTitle("Next", for: .normal)
button.setTitleColor(.white, for: .normal)
button.backgroundColor = .black
button.layer.cornerRadius = 10
button.translatesAutoresizingMaskIntoConstraints = false
return button
}()
override func viewDidLoad() {
super.viewDidLoad()
self.setView()
self.setAutoLayout()
}
private func setView() {
self.view.backgroundColor = .white
self.view.addSubview(self.babyImageView)
self.view.addSubview(self.babyNameLabel)
self.view.addSubview(self.babySexLabel)
self.view.addSubview(self.babyAgeLabel)
self.view.addSubview(self.babyMemoLabel)
self.view.addSubview(self.nextButton)
}
private func setAutoLayout() {
self.babyImageView.centerXAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.centerXAnchor).isActive = true
self.babyImageView.centerYAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.centerYAnchor).isActive = true
self.babyImageView.widthAnchor.constraint(equalToConstant: 300).isActive = true
self.babyImageView.heightAnchor.constraint(equalToConstant: 200).isActive = true
self.babyNameLabel.topAnchor.constraint(equalTo: self.babyImageView.bottomAnchor, constant: 10).isActive = true
self.babyNameLabel.leadingAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.leadingAnchor, constant: 50).isActive = true
self.babySexLabel.topAnchor.constraint(equalTo: self.babyImageView.bottomAnchor, constant: 10).isActive = true
self.babySexLabel.leadingAnchor.constraint(equalTo: self.babyNameLabel.trailingAnchor, constant: 10).isActive = true
self.babyAgeLabel.topAnchor.constraint(equalTo: self.babyImageView.bottomAnchor, constant: 10).isActive = true
self.babyAgeLabel.leadingAnchor.constraint(equalTo: self.babySexLabel.trailingAnchor, constant: 10).isActive = true
self.babyMemoLabel.topAnchor.constraint(equalTo: self.babyNameLabel.bottomAnchor, constant: 20).isActive = true
self.babyMemoLabel.leadingAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.leadingAnchor, constant: 50).isActive = true
self.babyMemoLabel.trailingAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.trailingAnchor, constant: -50).isActive = true
self.nextButton.centerXAnchor.constraint(equalTo: self.view.centerXAnchor).isActive = true
self.nextButton.bottomAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.bottomAnchor, constant: -150).isActive = true
}
}
✅ 사용자에게 보여질 뷰이다.
데이터 바인딩을 하지 않는 경우
// ViewModel
class BabyViewModel {
private var index: Int = 0
let baby: [Babys]
init(baby: [Babys]) {
self.baby = baby
}
var image: String {
return baby[0].image
}
var name: String {
return "\(baby[0].name)"
}
var sex: String {
return "\(baby[0].sex)"
}
var age: Int {
return baby[0].age
}
var memo: String {
return "\(baby[0].memo)"
}
}
extension BabyViewModel {
func configure(_ view: ViewController) {
view.babyImageView.image = UIImage(named: baby[index].image)
view.babyNameLabel.text = baby[index].name
view.babySexLabel.text = baby[index].sex
view.babyAgeLabel.text = "\(baby[index].age)"
view.babyMemoLabel.text = baby[index].memo
}
func nextButtonTapped(_ view: ViewController) {
index = (index+1 < baby.count) ? index+1 : 0
view.babyImageView.image = UIImage(named: baby[index].image)
view.babyNameLabel.text = baby[index].name
view.babySexLabel.text = baby[index].sex
view.babyAgeLabel.text = "\(baby[index].age)"
view.babyMemoLabel.text = baby[index].memo
}
}
private var index: Int = 0
➡️ 배열의 인덱스를 담고있는 index 변수
let baby: [Babys]
init(baby: [Babys]) {
self.baby = baby
}
var image: String {
return baby[0].image
}
var name: String {
return "\(baby[0].name)"
}
var sex: String {
return "\(baby[0].sex)"
}
var age: Int {
return baby[0].age
}
var memo: String {
return "\(baby[0].memo)"
}
➡️ 모델에서 실제로 뷰에 보여지기 위해 데이터를 가공해준다.
뷰모델 생성시 생성자를 통해 baby 값을 받아와 저장한다.
위의 코드는 별도의 추가 가공 과정은 없고 단순히 모델을 가지고 뷰에 표현하기 위한 뷰모델이다.
extension BabyViewModel {
func configure(_ view: ViewController) {
view.babyImageView.image = UIImage(named: baby[index].image)
view.babyNameLabel.text = baby[index].name
view.babySexLabel.text = baby[index].sex
view.babyAgeLabel.text = "\(baby[index].age)"
view.babyMemoLabel.text = baby[index].memo
}
func nextButtonTapped(_ view: ViewController) {
index = (index+1 < baby.count) ? index+1 : 0
view.babyImageView.image = UIImage(named: baby[index].image)
view.babyNameLabel.text = baby[index].name
view.babySexLabel.text = baby[index].sex
view.babyAgeLabel.text = "\(baby[index].age)"
view.babyMemoLabel.text = baby[index].memo
}
}
➡️ configure함수는 해당 뷰 UI에 모델을 가지고 표현하는 함수이다.
➡️ nextButtonTapped함수는 Next 버튼을 눌렀을 경우 다음 아기 정보를 출력하는 함수이다.
// View
private let viewModel: BabyViewModel = BabyViewModel(baby: DummyData.baby)
override func viewDidLoad() {
super.viewDidLoad()
self.setView()
self.setAutoLayout()
self.viewModel.configure(self)
}
@objc private func buttonTapped(_ sender: UIButton) {
self.viewModel.nextButtonTapped(self)
}
➡️ 다시 뷰로 넘어와서 viewModel을 생성해준다. 해당 뷰모델의 configure함수를 통해 뷰에 가공한 데이터를 출력한다.
➡️ button을 눌렀을 경우 동작하기 위해 buttonTapped를 정의하고 해당 버튼에 addTarget을 해준다.
// Observable
class Observable<T> {
typealias Listner = (T) -> Void
var listner: Listner?
var value: T {
didSet {
self.listner?(value)
}
}
init(_ value: T) {
self.value = value
}
func bind(listner: Listner?) {
self.listner = listner
listner?(value)
}
}
데이터 바인딩을 위한 Observable 클래스이다. 이전글에서 설명했으니 여기서 다루지는 않겠다.
데이터 바인딩을 한 경우 MVVM
// ViewModel
class ViewModel {
let babyData: [Babys] = DummyData.baby
private var index: Int = 0
let babyName: Observable<String> = Observable("Unknown")
let babySex: Observable<String> = Observable("Unknown")
let babyAge: Observable<Int> = Observable(0)
let babyImage: Observable<String> = Observable("Unknown")
let babyMemo: Observable<String> = Observable("Unknown")
init() {
self.babyName.value = babyData[index].name
self.babySex.value = babyData[index].sex
self.babyAge.value = babyData[index].age
self.babyImage.value = babyData[index].image
self.babyMemo.value = babyData[index].memo
}
}
extension ViewModel {
func nextButtonTapped() {
print("호출")
print(babyData.count)
index = (index+1 < babyData.count) ? index+1 : 0
print(index)
self.babyName.value = babyData[index].name
self.babySex.value = babyData[index].sex
self.babyAge.value = babyData[index].age
self.babyImage.value = babyData[index].image
self.babyMemo.value = babyData[index].memo
}
}
let babyData: [Babys] = DummyData.baby
private var index: Int = 0
➡️ 더미 데이터 변수 babyData와 배열의 index에 활용할 index 변수이다. babyData 변수는 Babys 모델을 채택하고 있다.
let babyName: Observable<String> = Observable("Unknown")
let babySex: Observable<String> = Observable("Unknown")
let babyAge: Observable<Int> = Observable(0)
let babyImage: Observable<String> = Observable("Unknown")
let babyMemo: Observable<String> = Observable("Unknown")
➡️ Observable 객체를 생성한다. 제네릭 타입이므로 타입을 명시하고 초기값을 포함하여 생성한다.
init() {
self.babyName.value = babyData[index].name
self.babySex.value = babyData[index].sex
self.babyAge.value = babyData[index].age
self.babyImage.value = babyData[index].image
self.babyMemo.value = babyData[index].memo
}
➡️ 뷰모델 생성시 생성자를 통해 더미 데이터 값을 가져와 저장한다.
우리는 value프로퍼티는 Observable에서 정의했다.
ViewModel 프로퍼티들은 Observable 클래스의 인스턴스이므로 value프로퍼티 값을 초기화할 수 있다.
만약 더미 데이터 값이 없다면 위의 객체 생성 단계에서 지정한 초기 값으로 설정 된다.
위의 코드는 별도의 추가 가공 과정은 없고 단순히 모델을 가지고 뷰에 표현하기 위한 뷰모델이다.
extension ViewModel {
func nextButtonTapped() {
index = (index+1 < babyData.count) ? index+1 : 0
self.babyName.value = babyData[index].name
self.babySex.value = babyData[index].sex
self.babyAge.value = babyData[index].age
self.babyImage.value = babyData[index].image
self.babyMemo.value = babyData[index].memo
}
}
➡️ nextButtonTapped함수는 Next 버튼을 눌렀을 경우 다음 아기 정보를 출력하는 함수이다.
value프로퍼티의 프로퍼티 옵저버 didSet을 통해 이벤트를 감지하면 어떠한 동작을 실행한다.
만약 button을 클릭하면 index 값에 변화가 있을 것이고 didSet이 이 이벤트(index 값의 변화)를 감지하여 동작을 실행할 것이다.
여기서 동작은 인덱스 값이 변화함에 따라 value값에 해당 인덱스의 값이 저장된다.
//View
private let viewModel: ViewModel = ViewModel()
override func viewDidLoad() {
super.viewDidLoad()
self.setView()
self.setAutoLayout()
bind()
}
private func bind() {
viewModel.babyName.bind { name in
self.babyNameLabel.text = name
}
viewModel.babyImage.bind { image in
self.babyImageView.image = UIImage(named: image)
}
viewModel.babySex.bind { sex in
self.babySexLabel.text = sex
}
viewModel.babyAge.bind { age in
self.babyAgeLabel.text = "\(age)"
}
viewModel.babyMemo.bind { memo in
self.babyMemoLabel.text = memo
}
}
@objc private func buttonTapped(_ sender: UIButton) {
viewModel.nextButtonTapped()
}
private let viewModel: ViewModel = ViewModel()
➡️ 뷰로 넘어와서 viewModel 객체를 생성한다.
private func bind() {
viewModel.babyName.bind { name in
self.babyNameLabel.text = name
}
viewModel.babyImage.bind { image in
self.babyImageView.image = UIImage(named: image)
}
viewModel.babySex.bind { sex in
self.babySexLabel.text = sex
}
viewModel.babyAge.bind { age in
self.babyAgeLabel.text = "\(age)"
}
viewModel.babyMemo.bind { memo in
self.babyMemoLabel.text = memo
}
}
✅ 데이터 바인딩을 하지 않을 경우와 가장 큰 차이점은 이 부분이다.
데이터 바인딩을 하지 않을 경우 이전 예제의 경우와 같이 configure함수와 해당 뷰 UI에 모델을 가지고 표현하는 함수를 구현해야했다. 하지만 데이터 바인딩을 할 경우 Observable에서 정의한 bind를 view에서 호출하여 표현한다.
➡️ 가장 중요한 bind()이다. 우리는 bind라는 메소드를 Observable에서 정의했다.
bind는 호출 시 동작을 listner에 저장하고 동작을 실행하는 함수인데, 그 일련의 동작을 뷰에서 정의해주면 된다.
'iOS > Design Pattern' 카테고리의 다른 글
[iOS/Design Pattern] Clean Architecture를 알아보자. (0) | 2024.08.26 |
---|---|
[iOS/Design Pattern] 코디네이터 패턴 (0) | 2024.08.02 |
[iOS/Design Pattern] MVVM에 대한 나의 고찰 (0) | 2024.05.01 |
[iOS/Design Pattern] MVVM 패턴 이해해보기 (0) | 2024.01.05 |
[iOS/Design Pattern] 예제로 알아보는 MVVM패턴 - 1 (0) | 2023.08.07 |